@staix/agent-hub 0.16.0 → 0.18.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 (96) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +2 -2
  3. package/docs/agent-notes/README.md +5 -0
  4. package/docs/agent-notes/adapters.md +20 -5
  5. package/docs/agent-notes/benchmarks.md +1 -0
  6. package/docs/agent-notes/daemon.md +5 -3
  7. package/docs/agent-notes/local-worker.md +1 -0
  8. package/docs/agent-notes/tasks.md +9 -3
  9. package/docs/agent-notes/tests.md +6 -3
  10. package/docs/dogfood.md +21 -4
  11. package/docs/memory.md +1 -1
  12. package/docs/operations.md +150 -34
  13. package/docs/profiles.md +36 -0
  14. package/docs/quickstart.md +14 -4
  15. package/docs/roles.md +1 -0
  16. package/docs/sdlc.md +39 -13
  17. package/docs/security.md +41 -9
  18. package/docs/smoke.md +51 -3
  19. package/docs/specs/2026-09-19-agent-hub-design.md +56 -10
  20. package/docs/specs/2026-09-20-upgrade-recovery-design.md +3 -1
  21. package/docs/verification/2026-10-25-0.17.0.md +74 -0
  22. package/docs/verification/2026-10-25-0.18.0.md +76 -0
  23. package/docs/verification/2026-10-25-codex-0164-reviewer-tool-call.md +52 -0
  24. package/docs/verification/2026-10-25-codex-review-live-verdict.md +35 -0
  25. package/docs/verification/2026-10-25-kimi-231-reviewer-compat.md +48 -0
  26. package/docs/verification/2026-10-25-kimi-usage-ac11.md +48 -0
  27. package/docs/verification/2026-10-25-qwen-dual-output-hub-e2e.md +23 -0
  28. package/docs/verification/2026-10-25-qwen-dual-output-t0.md +20 -0
  29. package/docs/verification/2026-10-25-remote-docker-transport.md +46 -0
  30. package/docs/verification/2026-10-25-reviewer-no-verdict-diagnostics.md +35 -0
  31. package/docs/verification/profiles/YYYY-MM-DD-general.md +46 -0
  32. package/docs/verification/profiles/YYYY-MM-DD-research.md +61 -0
  33. package/docs/verification/profiles/YYYY-MM-DD-writing.md +46 -0
  34. package/docs/verified.json +28 -19
  35. package/package.json +2 -2
  36. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  37. package/plugins/agent-hub/server.js +1115 -192
  38. package/src/adapters/codex-review-isolation.ts +10 -1
  39. package/src/adapters/compat.ts +7 -2
  40. package/src/adapters/conformance/enrollment.ts +18 -4
  41. package/src/adapters/conformance/factories.ts +4 -1
  42. package/src/adapters/conformance/kit.ts +11 -0
  43. package/src/adapters/definitions/builtin-metadata.ts +1 -1
  44. package/src/adapters/definitions/native-cli-entry-qwen.ts +32 -4
  45. package/src/adapters/definitions/native-cli-preview-qwen.ts +9 -1
  46. package/src/adapters/definitions/native-factories.ts +8 -0
  47. package/src/adapters/definitions/native-recovery.ts +8 -0
  48. package/src/adapters/definitions/qwen.ts +7 -4
  49. package/src/adapters/local-worker.ts +5 -3
  50. package/src/adapters/qwen-dual.ts +578 -0
  51. package/src/adapters/registry-types.ts +5 -1
  52. package/src/adapters/registry.ts +9 -1
  53. package/src/adapters/reviewer-mcp.ts +12 -6
  54. package/src/adapters/reviewer-print.ts +100 -17
  55. package/src/cli/console-state.ts +9 -2
  56. package/src/cli/help.ts +29 -28
  57. package/src/cli/identity.ts +87 -6
  58. package/src/cli/main.ts +190 -69
  59. package/src/cli/output.ts +7 -2
  60. package/src/cli/sdlc-output.ts +242 -0
  61. package/src/cli/task-output.ts +212 -0
  62. package/src/cli/upgrade-output.ts +57 -0
  63. package/src/cli/upgrade.ts +4 -3
  64. package/src/hub/board.ts +17 -0
  65. package/src/hub/check-runner.ts +252 -22
  66. package/src/hub/conductor.ts +5 -0
  67. package/src/hub/config-apply.ts +16 -7
  68. package/src/hub/config-edit-host.ts +1 -1
  69. package/src/hub/config-tier.ts +1 -1
  70. package/src/hub/daemon.ts +487 -94
  71. package/src/hub/dogfood-profile.ts +248 -0
  72. package/src/hub/dogfood-read.ts +77 -14
  73. package/src/hub/dogfood.ts +5 -1
  74. package/src/hub/events.ts +1 -1
  75. package/src/hub/host-executable.ts +2 -1
  76. package/src/hub/hub-tools.ts +2 -2
  77. package/src/hub/memory-plan-guard.ts +8 -33
  78. package/src/hub/operational-authority.ts +60 -0
  79. package/src/hub/operational-view.ts +141 -0
  80. package/src/hub/orphan-process.ts +69 -0
  81. package/src/hub/permission-mode.ts +1 -1
  82. package/src/hub/read-only-shell-proof.ts +80 -0
  83. package/src/hub/read-only-shell.ts +162 -0
  84. package/src/hub/reviewer-pool-config.ts +3 -2
  85. package/src/hub/reviewer-pool.ts +3 -0
  86. package/src/hub/routing.ts +23 -6
  87. package/src/hub/sdlc-merge.ts +299 -0
  88. package/src/hub/sdlc-mirror.ts +129 -0
  89. package/src/hub/sdlc.ts +58 -12
  90. package/src/hub/settings.ts +11 -5
  91. package/src/hub/snapshots.ts +60 -0
  92. package/src/hub/task-repository.ts +31 -5
  93. package/src/hub/tasks.ts +266 -28
  94. package/src/local/tools.ts +17 -4
  95. package/src/memory/brief.ts +45 -5
  96. package/src/ui/index.html +2 -1
@@ -82,7 +82,7 @@ function memoryGitEnv(env = process.env, options = {}) {
82
82
  }
83
83
  function memoryGitSync(args, options) {
84
84
  const argv = memoryGitArgv(args);
85
- return childProcess().spawnSync(argv[0], argv.slice(1), { ...options, env: memoryGitEnv(options.env ?? process.env) });
85
+ return childProcess().spawnSync(argv[0], argv.slice(1), { ...ambientGitTimeout !== undefined ? { timeout: ambientGitTimeout, killSignal: "SIGKILL" } : {}, ...options, env: memoryGitEnv(options.env ?? process.env) });
86
86
  }
87
87
  function processSignature(pid) {
88
88
  try {
@@ -118,7 +118,7 @@ var require2, processes, childProcess = () => processes ??= require2("node:child
118
118
  const parent = dirname(absolute);
119
119
  return parent === absolute ? absolute : join(canonical(parent), basename(absolute));
120
120
  }
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];
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;
122
122
  var init_platform = __esm(() => {
123
123
  require2 = createRequire(import.meta.url);
124
124
  DENY_SEGMENTS = [".maru/secrets", ".agenthub/state", ".agenthub/archive"];
@@ -310,7 +310,7 @@ var init_compat = __esm(() => {
310
310
  },
311
311
  "kimi-review": {
312
312
  label: "Kimi reviewer (kimi-code)",
313
- verified: ["2.2.0", "2.3.0"],
313
+ verified: ["2.2.0", "2.3.0", "2.3.1"],
314
314
  gate: "isolation",
315
315
  probe: ["kimi", "--version"],
316
316
  parse: (out) => plain(out.trim()),
@@ -350,6 +350,15 @@ var init_compat = __esm(() => {
350
350
  config: "qwen_cmd",
351
351
  parse: (out) => plain(out.trim()),
352
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."
353
+ },
354
+ "qwen-dual": {
355
+ label: "Qwen Code dual-output TUI",
356
+ verified: ["0.26.1"],
357
+ gate: "contract",
358
+ probe: ["qwen", "--version"],
359
+ config: "qwen_cmd",
360
+ 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."
353
362
  }
354
363
  };
355
364
  MAX_BYTES = 64 * 1024;
@@ -792,6 +801,17 @@ function parseProcessTable(text) {
792
801
  return m && !m[4].startsWith("Z") ? [{ pid: Number(m[1]), ppid: Number(m[2]), pgid: Number(m[3]), started: m[5], command: m[6] }] : [];
793
802
  });
794
803
  }
804
+ function processTable() {
805
+ for (let attempt = 0;attempt < 2; attempt++) {
806
+ try {
807
+ const r = Bun.spawnSync(PS, { ...PS_OPTIONS, stdout: "pipe", stderr: "pipe", timeout: 1e4 });
808
+ const rows = r.exitCode === 0 ? own(r.stdout.toString()) : undefined;
809
+ if (rows)
810
+ return rows;
811
+ } catch {}
812
+ }
813
+ return;
814
+ }
795
815
  async function readProcessTable() {
796
816
  for (let attempt = 0;attempt < 2; attempt++) {
797
817
  try {
@@ -1332,7 +1352,15 @@ var DEFAULT_WATCHDOG_MS = 300000;
1332
1352
 
1333
1353
  // src/adapters/reviewer-print.ts
1334
1354
  import { randomUUID as randomUUID2 } from "crypto";
1335
- var ReviewerUnavailableError, reviewTarget = (env) => {
1355
+ function reviewerNoVerdictLine(peer, diagnostic, safeRefusal, withheld = false) {
1356
+ const prefix = `[${peer}] review of task #${diagnostic.task}: no verdict exit=${diagnostic.exitCode ?? "unknown"}; `;
1357
+ if (withheld)
1358
+ return prefix + "diagnostic withheld";
1359
+ const detail = diagnostic.lastTool ? `last hub tool ${diagnostic.lastTool}; ${diagnostic.reviewCalled ? "hub_review called" : "no hub_review call"}` : "no hub_review call";
1360
+ const line = safeRefusal === undefined ? undefined : Array.from(sanitize(safeRefusal).replace(/\s+/g, " ")).filter((char) => !/\p{C}/u.test(char)).slice(0, 240).join("");
1361
+ return prefix + detail + (diagnostic.refusal === undefined && diagnostic.hostRefusal === undefined ? "" : `; refusal: ${line ?? "withheld"}`);
1362
+ }
1363
+ var ReviewerUnavailableError, ReviewerAdmissionError, REVIEWER_FALLBACK_UNCONFIRMED = "reviewer fallback unconfirmed: ", reviewTarget = (env) => {
1336
1364
  const task = reviewTask(env);
1337
1365
  if (task === undefined || env.private)
1338
1366
  return;
@@ -1356,6 +1384,13 @@ var init_reviewer_print = __esm(() => {
1356
1384
  this.detail = detail;
1357
1385
  }
1358
1386
  };
1387
+ ReviewerAdmissionError = class ReviewerAdmissionError extends Error {
1388
+ reason;
1389
+ constructor(reason) {
1390
+ super("review admission changed");
1391
+ this.reason = reason;
1392
+ }
1393
+ };
1359
1394
  ReviewerPrintPeer = class ReviewerPrintPeer extends BasePeer {
1360
1395
  opts;
1361
1396
  hubNative = true;
@@ -1374,6 +1409,29 @@ var init_reviewer_print = __esm(() => {
1374
1409
  reviewing;
1375
1410
  reviewSeq = 0;
1376
1411
  runningBinding;
1412
+ activeTrace;
1413
+ recordToolTrace(task, trace) {
1414
+ if (trace.tool !== "hub_review" && trace.tool !== "hub_task_show")
1415
+ return;
1416
+ const active = this.activeTrace, binding = this.runningBinding;
1417
+ if (!active || active.task !== task || active.generation !== this.generation || this.reviewing !== task || binding?.task !== task || this.opts.bindingCurrent?.(task, binding.binding) === false)
1418
+ return;
1419
+ if (typeof trace.refusal === "string") {
1420
+ if (active.value.hostRefusal === undefined)
1421
+ active.value.hostRefusal = trace.refusal;
1422
+ else if (active.value.hostRefusal !== trace.refusal)
1423
+ active.value.refusalUncertain = true;
1424
+ }
1425
+ this.observeTool(active.value, trace);
1426
+ }
1427
+ observeTool(value, trace) {
1428
+ if (trace.tool !== "hub_review" && trace.tool !== "hub_task_show")
1429
+ return;
1430
+ value.lastTool = trace.tool;
1431
+ value.refusal = typeof trace.refusal === "string" ? trace.refusal : undefined;
1432
+ if (trace.tool === "hub_review")
1433
+ value.reviewCalled = true;
1434
+ }
1377
1435
  reviewBinding(task) {
1378
1436
  return this.reviewing === task && this.runningBinding?.task === task ? this.runningBinding.binding : undefined;
1379
1437
  }
@@ -1510,6 +1568,17 @@ var init_reviewer_print = __esm(() => {
1510
1568
  } catch (error) {
1511
1569
  if (generation !== this.generation)
1512
1570
  return;
1571
+ if (error instanceof ReviewerAdmissionError) {
1572
+ this.opts.log?.(`[${this.id}] review of task #${task} skipped: ${error.reason === "stale_binding" ? "stale binding" : "review policy changed"}`);
1573
+ if (error.reason === "policy_changed") {
1574
+ missing.push(task);
1575
+ const target = batch.targets[index];
1576
+ if (target)
1577
+ reroute.push({ ...target, cause: "no_verdict" });
1578
+ }
1579
+ batch.next = index + 1;
1580
+ continue;
1581
+ }
1513
1582
  if (error instanceof PrintChildCleanupError) {
1514
1583
  this.opts.log?.(`[${this.id}] review of task #${task}: ${error.message}`);
1515
1584
  this.settle("needs_review", `review process group still running: ${error.message}`);
@@ -1534,6 +1603,15 @@ var init_reviewer_print = __esm(() => {
1534
1603
  if (!recorded) {
1535
1604
  missing.push(task);
1536
1605
  const target = batch.targets[index];
1606
+ const diagnostic = { ...target, task, exitCode: result.code, ...result.trace };
1607
+ try {
1608
+ if (this.opts.onNoVerdict)
1609
+ this.opts.onNoVerdict(diagnostic, () => generation === this.generation);
1610
+ else
1611
+ this.opts.log?.(reviewerNoVerdictLine(this.id, diagnostic));
1612
+ } catch {
1613
+ this.opts.log?.(reviewerNoVerdictLine(this.id, { task, exitCode: result.code, reviewCalled: false }, undefined, true));
1614
+ }
1537
1615
  if (target)
1538
1616
  reroute.push({ ...target, cause: result.quota ? "quota" : "no_verdict" });
1539
1617
  if (result.quota) {
@@ -1543,14 +1621,18 @@ var init_reviewer_print = __esm(() => {
1543
1621
  }
1544
1622
  }
1545
1623
  batch.next = index + 1;
1546
- if (result.code !== 0)
1624
+ if (recorded && result.code !== 0)
1547
1625
  this.opts.log?.(`[${this.id}] review of task #${task} exited ${result.code}`);
1548
1626
  }
1549
1627
  if (generation !== this.generation)
1550
1628
  return;
1551
1629
  this.clearWatchdog();
1552
- this.settle(failure?.state ?? (missing.length ? "needs_review" : "completed"), failure?.reason ?? (missing.length ? `no verdict recorded for task ${missing.map((id) => `#${id}`).join(", ")}` : undefined));
1553
- await this.reportFailures(reroute, generation);
1630
+ const reoffered = await this.reportFailures(reroute, generation);
1631
+ if (generation !== this.generation)
1632
+ return;
1633
+ const unresolved = missing.length > 0 && !reoffered;
1634
+ const normalNoVerdict = !failure && reroute.length > 0 && reroute.every((value) => value.cause === "no_verdict");
1635
+ this.settle(failure?.state ?? (unresolved ? "needs_review" : "completed"), failure?.reason ?? (unresolved ? `${normalNoVerdict ? REVIEWER_FALLBACK_UNCONFIRMED : ""}no verdict recorded for task ${missing.map((id) => `#${id}`).join(", ")}` : undefined));
1554
1636
  if (generation === this.generation) {
1555
1637
  this.batch = undefined;
1556
1638
  this.setState("idle");
@@ -1558,19 +1640,23 @@ var init_reviewer_print = __esm(() => {
1558
1640
  }
1559
1641
  async reportFailures(failures, generation) {
1560
1642
  const seen = new Set;
1643
+ let reoffered = true;
1561
1644
  for (const failed of failures) {
1562
1645
  if (generation !== this.generation)
1563
- return;
1646
+ return false;
1564
1647
  const identity = JSON.stringify([failed.task, failed.head, failed.flowRevision]);
1565
1648
  if (seen.has(identity))
1566
1649
  continue;
1567
1650
  seen.add(identity);
1568
1651
  try {
1569
- await this.opts.onUnavailable?.(failed, () => generation === this.generation);
1652
+ if (await this.opts.onUnavailable?.(failed, () => generation === this.generation) !== true)
1653
+ reoffered = false;
1570
1654
  } catch {
1655
+ reoffered = false;
1571
1656
  this.opts.log?.(`[${this.id}] reviewer fallback could not be recorded; inspect the task`);
1572
1657
  }
1573
1658
  }
1659
+ return reoffered;
1574
1660
  }
1575
1661
  prepare(sessionId, input, task, mcpConfig) {
1576
1662
  const abort = new AbortController;
@@ -1607,9 +1693,16 @@ var init_reviewer_print = __esm(() => {
1607
1693
  }).catch(() => {});
1608
1694
  return done;
1609
1695
  }
1610
- async review(env, task, generation) {
1696
+ assertAdmission(env, task, binding) {
1697
+ if (this.opts.bindingCurrent?.(task, binding) === false)
1698
+ throw new ReviewerAdmissionError("stale_binding");
1611
1699
  if (this.opts.admit?.(env) === false)
1612
- throw new Error("review admission changed");
1700
+ throw new ReviewerAdmissionError(this.opts.bindingCurrent?.(task, binding) === false ? "stale_binding" : "policy_changed");
1701
+ }
1702
+ async review(env, task, generation) {
1703
+ const revision = Number(env.refs?.reviewRevision);
1704
+ const binding = Object.freeze({ head: env.refs?.commit, flowRevision: Number.isSafeInteger(revision) && revision > 0 ? revision : undefined });
1705
+ this.assertAdmission(env, task, binding);
1613
1706
  let facts;
1614
1707
  try {
1615
1708
  facts = this.opts.facts?.(env);
@@ -1620,8 +1713,8 @@ var init_reviewer_print = __esm(() => {
1620
1713
 
1621
1714
  ${facts}` : ""}`;
1622
1715
  let launch;
1623
- const revision = Number(env.refs?.reviewRevision);
1624
- const binding = Object.freeze({ head: env.refs?.commit, flowRevision: Number.isSafeInteger(revision) && revision > 0 ? revision : undefined });
1716
+ const trace = { reviewCalled: false };
1717
+ this.activeTrace = { task, generation, value: trace };
1625
1718
  const mcp = this.opts.backend.taskMcp ? this.opts.taskMcp?.(task, () => generation === this.generation && this.reviewToken(task) !== undefined && this.opts.bindingCurrent?.(task, binding) !== false) : undefined;
1626
1719
  let mcpClosed = false;
1627
1720
  const closeMcp = () => {
@@ -1638,8 +1731,11 @@ ${facts}` : ""}`;
1638
1731
  const release = () => releasing ??= Promise.resolve().then(() => launch?.cleanup?.()).then(() => {});
1639
1732
  try {
1640
1733
  launch = await this.prepare(randomUUID2(), input, task, mcp?.config);
1641
- if (generation !== this.generation || this.opts.admit?.(env) === false || launch.current?.() === false)
1642
- throw new ReviewerUnavailableError("reviewer isolation or admission changed before child spawn", launch.current?.() === false ? "binary" : "unknown");
1734
+ if (generation !== this.generation)
1735
+ throw new ReviewerAdmissionError("stale_binding");
1736
+ this.assertAdmission(env, task, binding);
1737
+ if (launch.current?.() === false)
1738
+ throw new ReviewerUnavailableError("reviewer isolation changed before child spawn", "binary");
1643
1739
  this.receipt = launch.isolation;
1644
1740
  let quota = false;
1645
1741
  const child = new OwnedPrintChild({
@@ -1666,6 +1762,11 @@ ${facts}` : ""}`;
1666
1762
  }, 100);
1667
1763
  },
1668
1764
  onFrame: (event) => {
1765
+ try {
1766
+ const observed = this.opts.backend.toolTrace?.(event);
1767
+ if (observed && generation === this.generation)
1768
+ this.observeTool(trace, observed);
1769
+ } catch {}
1669
1770
  const usage = this.opts.backend.usage?.(event);
1670
1771
  if (usage)
1671
1772
  this.opts.onUsage?.(usage, task);
@@ -1687,8 +1788,10 @@ ${facts}` : ""}`;
1687
1788
  const code = await child.done;
1688
1789
  if (this.child === child)
1689
1790
  this.child = undefined;
1690
- return { code, quota };
1791
+ return { code, quota, trace };
1691
1792
  } finally {
1793
+ if (this.activeTrace?.value === trace)
1794
+ this.activeTrace = undefined;
1692
1795
  closeMcp();
1693
1796
  if (policyTimer)
1694
1797
  clearInterval(policyTimer);
@@ -1988,7 +2091,15 @@ function ownServer(text) {
1988
2091
  const env = hub.env === undefined ? {} : object2(hub.env);
1989
2092
  if (Object.values(env).some((value) => typeof value !== "string"))
1990
2093
  throw new Error("invalid hub environment");
1991
- return { command: hub.command, args: hub.args ?? [], env, enabled: true, required: true, enabled_tools: TOOLS };
2094
+ return {
2095
+ command: hub.command,
2096
+ args: hub.args ?? [],
2097
+ env,
2098
+ enabled: true,
2099
+ required: true,
2100
+ enabled_tools: TOOLS,
2101
+ tools: Object.fromEntries(TOOLS.map((name) => [name, { approval_mode: "approve" }]))
2102
+ };
1992
2103
  }
1993
2104
  async function prepare(ctx) {
1994
2105
  let root;
@@ -2234,6 +2345,16 @@ var init_codex_review_isolation = __esm(() => {
2234
2345
  codexReviewerBackend = {
2235
2346
  id: "codex",
2236
2347
  prepare,
2348
+ toolTrace(event) {
2349
+ const row = event;
2350
+ const item = row?.item;
2351
+ if (row?.type !== "item.completed" || item?.type !== "mcp_tool_call" || item.server !== "agent-hub" || item.tool !== "hub_review" && item.tool !== "hub_task_show")
2352
+ return;
2353
+ const failure = typeof item.error === "string" ? item.error : item.error?.message;
2354
+ const refused = item.result?.isError === true && Array.isArray(item.result.content) ? item.result.content.filter((value) => value?.type === "text" && typeof value.text === "string").map((value) => value.text).join(`
2355
+ `) : undefined;
2356
+ return { tool: item.tool, ...typeof failure === "string" ? { refusal: failure } : typeof refused === "string" ? { refusal: refused } : {} };
2357
+ },
2237
2358
  usage(event) {
2238
2359
  const row = event;
2239
2360
  return row?.type === "turn.completed" && row.usage ? { input_tokens: row.usage.input_tokens, output_tokens: row.usage.output_tokens, cache_read_input_tokens: row.usage.cached_input_tokens } : undefined;
@@ -4670,12 +4791,696 @@ var init_jcode = __esm(() => {
4670
4791
  };
4671
4792
  });
4672
4793
 
4673
- // src/pi/launch.ts
4794
+ // src/pi/process-signature.ts
4795
+ var init_process_signature = __esm(() => {
4796
+ init_platform();
4797
+ });
4798
+
4799
+ // src/adapters/qwen-dual.ts
4800
+ import { closeSync as closeSync4, constants as constants3, fstatSync as fstatSync4, lstatSync as lstatSync7, mkdirSync as mkdirSync5, openSync as openSync4, readSync as readSync3, realpathSync as realpathSync2, renameSync as renameSync3, writeFileSync as writeFileSync5, writeSync } from "fs";
4674
4801
  import { join as join8 } from "path";
4802
+ function qwenDualNamesStateDir(input, stateDir) {
4803
+ const needles = new Set([".agenthub/state", ".agenthub\\state", "qwen-dual", "input.jsonl", stateDir]);
4804
+ try {
4805
+ needles.add(realpathSync2(stateDir));
4806
+ } catch {}
4807
+ const seen = (value, depth) => {
4808
+ if (depth > 8)
4809
+ return true;
4810
+ if (typeof value === "string") {
4811
+ const flat = value.replaceAll("\\", "/");
4812
+ return [...needles].some((n) => n && (value.includes(n) || flat.includes(n.replaceAll("\\", "/"))));
4813
+ }
4814
+ if (Array.isArray(value))
4815
+ return value.some((item) => seen(item, depth + 1));
4816
+ if (value && typeof value === "object")
4817
+ return Object.entries(value).some(([key, item]) => seen(key, depth + 1) || seen(item, depth + 1));
4818
+ return false;
4819
+ };
4820
+ return seen(input, 0);
4821
+ }
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;
4823
+ var init_qwen_dual = __esm(() => {
4824
+ init_envelope();
4825
+ init_child_process();
4826
+ init_process_signature();
4827
+ init_compat();
4828
+ init_qwen_native();
4829
+ SUBMIT_CAP = 256 * 1024;
4830
+ READ_CAP = 1024 * 1024;
4831
+ QwenDualPeer = class QwenDualPeer extends BasePeer {
4832
+ opts;
4833
+ permissionModeState = "unmanaged";
4834
+ dir;
4835
+ inputFile;
4836
+ eventFile;
4837
+ stateFile;
4838
+ mcpFile;
4839
+ signature;
4840
+ poller;
4841
+ monitor;
4842
+ state_ = { v: 1, offset: 0, generation: 0 };
4843
+ resume = false;
4844
+ _launch;
4845
+ stopping = true;
4846
+ requestedStopReason;
4847
+ detached = false;
4848
+ sessionSeen = false;
4849
+ primed = false;
4850
+ turn;
4851
+ owner;
4852
+ lastAssistant;
4853
+ pending = new Map;
4854
+ answered = new Set;
4855
+ stalled_ = false;
4856
+ exited = false;
4857
+ pollFailures = 0;
4858
+ discarding = false;
4859
+ constructor(id, opts) {
4860
+ super(id, opts.watchdogMs);
4861
+ this.opts = opts;
4862
+ this.dir = join8(opts.stateDir, DIR);
4863
+ this.inputFile = join8(this.dir, "input.jsonl");
4864
+ this.eventFile = join8(this.dir, "events.jsonl");
4865
+ this.stateFile = join8(this.dir, "state.json");
4866
+ this.mcpFile = join8(this.dir, "mcp.json");
4867
+ this.signature = opts.signature ?? processSignature;
4868
+ }
4869
+ get launch() {
4870
+ return this._launch;
4871
+ }
4872
+ get adopted() {
4873
+ return this.adopting;
4874
+ }
4875
+ adopting = false;
4876
+ get stalled() {
4877
+ return this.stalled_;
4878
+ }
4879
+ get statusNotice() {
4880
+ if (this.detached)
4881
+ return { code: "qwen-detached", message: QWEN_DETACHED_NOTE };
4882
+ return this.stalled_ ? { code: "qwen-stalled", message: QWEN_STALLED_NOTE } : undefined;
4883
+ }
4884
+ get isDetached() {
4885
+ return this.detached;
4886
+ }
4887
+ holdsTerminal() {
4888
+ if (this.stopping || this.exited)
4889
+ return false;
4890
+ if (this.state_.ownerPid !== undefined)
4891
+ return this.ownerAlive();
4892
+ return this.sessionSeen;
4893
+ }
4894
+ get sessionId() {
4895
+ return this.state_.sessionId;
4896
+ }
4897
+ get offset() {
4898
+ return this.state_.offset;
4899
+ }
4900
+ recoveryMetadata() {
4901
+ return { ...this.state_.sessionId ? { sessionId: this.state_.sessionId } : {}, launch: { mode: "tui" } };
4902
+ }
4903
+ ensureFile(path, create) {
4904
+ const found = lstatSync7(path, { throwIfNoEntry: false });
4905
+ if (!found) {
4906
+ if (!create)
4907
+ throw new Error("Qwen dual-output file is missing");
4908
+ writeFileSync5(path, "", { mode: 384, flag: "wx" });
4909
+ return;
4910
+ }
4911
+ if (!found.isFile() || found.isSymbolicLink())
4912
+ throw new Error("Qwen dual-output path is not a plain file");
4913
+ if (typeof process.getuid === "function" && found.uid !== process.getuid())
4914
+ throw new Error("Qwen dual-output file is not owned by the person running the hub");
4915
+ if ((found.mode & 63) !== 0)
4916
+ throw new Error("Qwen dual-output file is readable or writable by others");
4917
+ }
4918
+ loadState() {
4919
+ let fd;
4920
+ try {
4921
+ fd = openSync4(this.stateFile, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
4922
+ const info = fstatSync4(fd);
4923
+ if (!info.isFile() || info.size > 4096)
4924
+ return;
4925
+ const buffer = Buffer.alloc(info.size);
4926
+ readSync3(fd, buffer, 0, info.size, 0);
4927
+ const raw = JSON.parse(buffer.toString("utf8"));
4928
+ if (raw.v !== 1 || !Number.isSafeInteger(raw.offset) || raw.offset < 0 || !Number.isSafeInteger(raw.generation))
4929
+ return;
4930
+ this.state_ = {
4931
+ v: 1,
4932
+ offset: raw.offset,
4933
+ generation: raw.generation,
4934
+ ...qwenSessionId(raw.sessionId) ? { sessionId: raw.sessionId } : {},
4935
+ ...Number.isSafeInteger(raw.ownerPid) && typeof raw.ownerSignature === "string" ? { ownerPid: raw.ownerPid, ownerSignature: raw.ownerSignature } : {}
4936
+ };
4937
+ } catch {} finally {
4938
+ if (fd !== undefined)
4939
+ try {
4940
+ closeSync4(fd);
4941
+ } catch {}
4942
+ }
4943
+ }
4944
+ saveState() {
4945
+ const temp = `${this.stateFile}.${process.pid}.tmp`;
4946
+ try {
4947
+ writeFileSync5(temp, `${JSON.stringify(this.state_)}
4948
+ `, { mode: 384 });
4949
+ renameSync3(temp, this.stateFile);
4950
+ } catch {
4951
+ this.opts.log?.(`[${this.id}] could not record its Qwen read position`);
4952
+ }
4953
+ }
4954
+ ownerAlive() {
4955
+ const { ownerPid, ownerSignature } = this.state_;
4956
+ if (ownerPid === undefined || ownerSignature === undefined)
4957
+ return false;
4958
+ return this.signature(ownerPid) === ownerSignature;
4959
+ }
4960
+ async start() {
4961
+ mkdirSync5(this.dir, { recursive: true, mode: 448 });
4962
+ const dir = lstatSync7(this.dir);
4963
+ if (!dir.isDirectory() || dir.isSymbolicLink() || typeof process.getuid === "function" && dir.uid !== process.getuid() || (dir.mode & 63) !== 0)
4964
+ throw new Error("Qwen dual-output directory is not private to the person running the hub");
4965
+ this.loadState();
4966
+ for (const path of [this.inputFile, this.eventFile])
4967
+ this.ensureFile(path, true);
4968
+ this.stopping = false;
4969
+ this.exited = false;
4970
+ if (this.ownerAlive()) {
4971
+ this.adopting = true;
4972
+ const probe = openSync4(this.eventFile, constants3.O_RDONLY | constants3.O_NOFOLLOW);
4973
+ let size = 0;
4974
+ try {
4975
+ size = fstatSync4(probe).size;
4976
+ } finally {
4977
+ closeSync4(probe);
4978
+ }
4979
+ if (size < this.state_.offset) {
4980
+ this.state_.offset = 0;
4981
+ this.state_.generation++;
4982
+ }
4983
+ this.sessionSeen = this.state_.sessionId !== undefined;
4984
+ this.primed = true;
4985
+ this.saveState();
4986
+ } else {
4987
+ this.adopting = false;
4988
+ this.resume = this.opts.sessionId !== undefined || this.state_.sessionId !== undefined;
4989
+ const resumeId = this.opts.sessionId ?? this.state_.sessionId;
4990
+ const previous = lstatSync7(this.eventFile);
4991
+ if (previous.size > 0) {
4992
+ try {
4993
+ renameSync3(this.eventFile, join8(this.dir, "events.prev.jsonl"));
4994
+ } catch {}
4995
+ this.ensureFile(this.eventFile, true);
4996
+ }
4997
+ closeSync4(openSync4(this.inputFile, constants3.O_WRONLY | constants3.O_NOFOLLOW | constants3.O_TRUNC));
4998
+ this.state_ = { v: 1, offset: 0, generation: this.state_.generation + 1, ...resumeId ? { sessionId: resumeId } : {} };
4999
+ this.sessionSeen = false;
5000
+ this.saveState();
5001
+ this.writeLaunch(resumeId);
5002
+ }
5003
+ this.poller = setInterval(() => this.poll(), this.opts.pollMs ?? 100);
5004
+ this.poller.unref?.();
5005
+ if (!this.monitor) {
5006
+ this.monitor = setInterval(() => this.checkOwner(), this.opts.ownerMs ?? 1000);
5007
+ this.monitor.unref?.();
5008
+ }
5009
+ this.poll();
5010
+ if (this.adopting && !this.detached && this.sessionSeen)
5011
+ this.setState("idle");
5012
+ }
5013
+ writeLaunch(resumeId) {
5014
+ const [bin, ...rest] = this.opts.cmd.filter((word) => word !== "--acp");
5015
+ if (!bin)
5016
+ throw new Error("qwen_cmd must name an executable");
5017
+ const servers = Object.fromEntries(this.opts.mcpServers.map((server) => [server.name, { command: server.command, args: server.args, env: server.env }]));
5018
+ writeFileSync5(this.mcpFile, JSON.stringify({ mcpServers: servers }), { mode: 384 });
5019
+ const args = [
5020
+ ...rest,
5021
+ "--input-file",
5022
+ this.inputFile,
5023
+ "--json-file",
5024
+ this.eventFile,
5025
+ "--mcp-config",
5026
+ this.mcpFile,
5027
+ "--approval-mode",
5028
+ "default",
5029
+ ...this.opts.model ? ["--model", this.opts.model] : [],
5030
+ ...resumeId ? ["-r", resumeId] : []
5031
+ ];
5032
+ this._launch = { cmd: bin, args, env: peerChildEnv(this.id, {}) };
5033
+ }
5034
+ discoverOwner() {
5035
+ if (this.stopping || this.state_.ownerPid !== undefined)
5036
+ return;
5037
+ const rows = (this.opts.table ?? processTable)();
5038
+ if (!rows)
5039
+ return;
5040
+ const flag = `--input-file ${this.inputFile}`;
5041
+ const matches = rows.filter((row) => row.pid !== process.pid && row.command.includes(flag));
5042
+ const pids = new Set(matches.map((row) => row.pid));
5043
+ const roots = matches.filter((row) => !pids.has(row.ppid));
5044
+ if (roots.length !== 1)
5045
+ return;
5046
+ const pid = roots[0].pid, signature = this.signature(pid);
5047
+ if (!signature)
5048
+ return;
5049
+ this.state_.ownerPid = pid;
5050
+ this.state_.ownerSignature = signature;
5051
+ this.saveState();
5052
+ }
5053
+ checkOwner() {
5054
+ if (this.stopping || this.exited)
5055
+ return;
5056
+ if (this.state_.ownerPid === undefined) {
5057
+ if (this.sessionSeen)
5058
+ this.discoverOwner();
5059
+ return;
5060
+ }
5061
+ if (this.ownerAlive())
5062
+ return;
5063
+ if (this.state === "offline" && !this.sessionSeen)
5064
+ return;
5065
+ this.exited = true;
5066
+ this.opts.log?.(`[${this.id}] the Qwen TUI exited`);
5067
+ this.failTurn("the Qwen terminal ended before the turn settled; inspect it before retrying");
5068
+ this.abortApprovals();
5069
+ delete this.state_.ownerPid;
5070
+ delete this.state_.ownerSignature;
5071
+ this.saveState();
5072
+ this.setState("offline");
5073
+ this.opts.onExit?.();
5074
+ }
5075
+ appendInput(record) {
5076
+ const line = `${JSON.stringify(record)}
5077
+ `;
5078
+ if (Buffer.byteLength(line) > SUBMIT_CAP)
5079
+ throw new Error("Qwen submit exceeds its size bound");
5080
+ const fd = openSync4(this.inputFile, constants3.O_WRONLY | constants3.O_APPEND | constants3.O_NOFOLLOW);
5081
+ try {
5082
+ const info = fstatSync4(fd);
5083
+ if (!info.isFile() || (info.mode & 63) !== 0 || typeof process.getuid === "function" && info.uid !== process.getuid())
5084
+ throw new Error("Qwen input file is not private");
5085
+ writeSync(fd, line);
5086
+ } finally {
5087
+ closeSync4(fd);
5088
+ }
5089
+ }
5090
+ async deliver(envs, deliveryId) {
5091
+ if (this.state !== "idle" || this.stopping || this.detached) {
5092
+ if (deliveryId)
5093
+ this.delivery({ id: deliveryId, state: "failed_safe", reason: `${this.id} is ${this.state}` });
5094
+ throw new Error(`${this.id} is ${this.state}`);
5095
+ }
5096
+ let text = this.primed || !this.opts.preamble ? renderDigest(envs, this.primed) : `${this.opts.preamble}
5097
+
5098
+ ${renderDigest(envs, false)}`;
5099
+ if (/^\s*[\/!@?]/.test(text))
5100
+ text = `[agent-hub]
5101
+ ${text}`;
5102
+ const turn = { envs, ...deliveryId ? { deliveryId } : {}, text, started: false, accepted: false };
5103
+ this.turn = turn;
5104
+ this.owner = undefined;
5105
+ this.lastAssistant = undefined;
5106
+ this.setState("busy");
5107
+ try {
5108
+ this.appendInput({ type: "submit", text });
5109
+ } catch (error) {
5110
+ this.turn = undefined;
5111
+ if (deliveryId)
5112
+ this.delivery({ id: deliveryId, state: "failed_safe", reason: "Qwen input file could not be written" });
5113
+ this.setState("idle");
5114
+ throw error instanceof Error ? error : new Error("Qwen input file could not be written");
5115
+ }
5116
+ this.primed = true;
5117
+ }
5118
+ onWatchdog() {
5119
+ if (this.state !== "busy" || this.stalled_)
5120
+ return;
5121
+ if (this.pending.size > 0) {
5122
+ this.touch();
5123
+ return;
5124
+ }
5125
+ this.stalled_ = true;
5126
+ this.opts.log?.(`[${this.id}] turn stalled; the hub cannot stop it (press Esc in the Qwen terminal)`);
5127
+ this.opts.onStalled?.(true);
5128
+ try {
5129
+ this.opts.onNotice?.(`${this.id}: ${QWEN_STALLED_NOTE}`);
5130
+ } catch {}
5131
+ }
5132
+ alive() {
5133
+ if (this.stalled_) {
5134
+ this.stalled_ = false;
5135
+ this.opts.onStalled?.(false);
5136
+ }
5137
+ if (this.state === "busy")
5138
+ this.touch();
5139
+ }
5140
+ detach(reason) {
5141
+ if (this.detached)
5142
+ return;
5143
+ this.detached = true;
5144
+ this.opts.log?.(`[${this.id}] event file ${reason}: detached`);
5145
+ this.failTurn("the Qwen event file became unreadable before the turn settled; inspect the Qwen terminal before retrying");
5146
+ this.abortApprovals();
5147
+ if (this.state !== "offline")
5148
+ this.setState("offline");
5149
+ }
5150
+ poll() {
5151
+ if (this.stopping)
5152
+ return;
5153
+ let fd;
5154
+ try {
5155
+ fd = openSync4(this.eventFile, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
5156
+ const info = fstatSync4(fd);
5157
+ if (!info.isFile())
5158
+ throw new Error("not a regular file");
5159
+ if (this.detached) {
5160
+ this.detached = false;
5161
+ this.pollFailures = 0;
5162
+ this.opts.log?.(`[${this.id}] event file readable again: reattached`);
5163
+ if (this.sessionSeen && this.state === "offline" && !this.exited)
5164
+ this.setState("idle");
5165
+ }
5166
+ let offset = this.state_.offset;
5167
+ if (info.size < offset) {
5168
+ offset = 0;
5169
+ this.discarding = false;
5170
+ this.state_.generation++;
5171
+ this.opts.log?.(`[${this.id}] event file was replaced; reading from its start`);
5172
+ }
5173
+ while (offset < info.size) {
5174
+ const want = Math.min(info.size - offset, READ_CAP), buffer = Buffer.alloc(want);
5175
+ const got = readSync3(fd, buffer, 0, want, offset);
5176
+ if (got <= 0)
5177
+ break;
5178
+ if (this.discarding) {
5179
+ const newline = buffer.subarray(0, got).indexOf(10);
5180
+ offset += newline < 0 ? got : newline + 1;
5181
+ this.state_.offset = offset;
5182
+ if (newline >= 0)
5183
+ this.discarding = false;
5184
+ continue;
5185
+ }
5186
+ const end = buffer.subarray(0, got).lastIndexOf(10) + 1;
5187
+ if (end === 0) {
5188
+ if (got < READ_CAP)
5189
+ break;
5190
+ this.opts.log?.(`[${this.id}] dropped an event line over ${READ_CAP} bytes`);
5191
+ this.discarding = true;
5192
+ offset += got;
5193
+ this.state_.offset = offset;
5194
+ continue;
5195
+ }
5196
+ offset += end;
5197
+ this.state_.offset = offset;
5198
+ for (const line of buffer.subarray(0, end).toString("utf8").split(`
5199
+ `))
5200
+ if (line.trim())
5201
+ this.parse(line);
5202
+ if (this.stopping)
5203
+ break;
5204
+ }
5205
+ this.saveState();
5206
+ } catch (error) {
5207
+ this.pollFailures++;
5208
+ if (!this.detached)
5209
+ this.detach(`could not be read (${error.code ?? "error"})`);
5210
+ } finally {
5211
+ if (fd !== undefined)
5212
+ try {
5213
+ closeSync4(fd);
5214
+ } catch {}
5215
+ }
5216
+ }
5217
+ parse(line) {
5218
+ let event;
5219
+ try {
5220
+ event = JSON.parse(line);
5221
+ } catch {
5222
+ this.opts.log?.(`[${this.id}] skipped an unreadable event line`);
5223
+ return;
5224
+ }
5225
+ if (!event || typeof event !== "object" || typeof event.type !== "string")
5226
+ return;
5227
+ try {
5228
+ this.handle(event);
5229
+ } catch (error) {
5230
+ this.opts.log?.(`[${this.id}] event handling failed: ${sanitize(error.message)}`);
5231
+ }
5232
+ }
5233
+ handle(event) {
5234
+ this.alive();
5235
+ switch (event.type) {
5236
+ case "system":
5237
+ return this.onSystem(event);
5238
+ case "user":
5239
+ return this.onUser(event);
5240
+ case "assistant":
5241
+ return this.onAssistant(event);
5242
+ case "stream_event":
5243
+ if (event.event?.type === "message_stop")
5244
+ this.onMessageStop();
5245
+ return;
5246
+ case "result":
5247
+ return this.onResult(event);
5248
+ case "control_request":
5249
+ return this.onControlRequest(event);
5250
+ case "control_response":
5251
+ return this.onControlResponse(event);
5252
+ default:
5253
+ return;
5254
+ }
5255
+ }
5256
+ onSystem(event) {
5257
+ if (event.subtype !== "session_start")
5258
+ return;
5259
+ const data = event.data ?? {};
5260
+ const id = event.session_id ?? data.session_id;
5261
+ if (!qwenSessionId(id))
5262
+ return;
5263
+ const verdict = assessVersion("qwen-dual", data.version);
5264
+ const supported = data.supported_events;
5265
+ if (!verdict.ok || data.protocol_version !== 2 || !Array.isArray(supported) || !["user", "assistant", "stream_event", "control_request", "control_response"].every((name) => supported.includes(name))) {
5266
+ this.opts.log?.(`[${this.id}] the Qwen TUI's dual-output contract is not verified; not attached`);
5267
+ try {
5268
+ this.opts.onNotice?.(`${this.id}: this Qwen's dual-output contract is not verified (${verdict.ok ? "protocol or event set" : verdict.reason}); the hub did not attach it`);
5269
+ } catch {}
5270
+ return;
5271
+ }
5272
+ this.state_.sessionId = id;
5273
+ this.sessionSeen = true;
5274
+ this.saveState();
5275
+ this.discoverOwner();
5276
+ if (!this.stopping && !this.detached && this.state === "offline")
5277
+ this.setState("idle");
5278
+ }
5279
+ promptText(event) {
5280
+ const content = event.message?.content;
5281
+ if (typeof content === "string")
5282
+ return content;
5283
+ if (!Array.isArray(content))
5284
+ return;
5285
+ const blocks = content.filter((block) => block?.type === "text" && typeof block.text === "string");
5286
+ return blocks.length ? blocks.map((block) => block.text).join("") : undefined;
5287
+ }
5288
+ onUser(event) {
5289
+ const text = this.promptText(event);
5290
+ if (text === undefined)
5291
+ return;
5292
+ const turn = this.turn;
5293
+ if (turn && !turn.started && (text.trim() === turn.text.trim() || text.trim().startsWith(turn.text.trim().slice(0, 200)))) {
5294
+ turn.started = true;
5295
+ this.owner = "hub";
5296
+ this.lastAssistant = undefined;
5297
+ if (turn.deliveryId && !turn.accepted) {
5298
+ turn.accepted = true;
5299
+ this.delivery({ id: turn.deliveryId, state: "accepted" });
5300
+ }
5301
+ return;
5302
+ }
5303
+ if (turn?.started)
5304
+ this.failTurn("the Qwen turn ended without a result (it was likely stopped in the terminal); inspect it before retrying", true);
5305
+ this.owner = "personal";
5306
+ this.lastAssistant = undefined;
5307
+ if (this.state === "idle")
5308
+ this.setState("busy");
5309
+ }
5310
+ onAssistant(event) {
5311
+ const content = event.message?.content;
5312
+ const blocks = Array.isArray(content) ? content : [];
5313
+ const text = blocks.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
5314
+ const toolUse = event.message?.stop_reason === "tool_use" || blocks.some((block) => block?.type === "tool_use");
5315
+ this.lastAssistant = { text, toolUse };
5316
+ }
5317
+ onMessageStop() {
5318
+ const last = this.lastAssistant;
5319
+ this.lastAssistant = undefined;
5320
+ if (!last || last.toolUse)
5321
+ return;
5322
+ this.endTurn(last.text);
5323
+ }
5324
+ onResult(event) {
5325
+ const text = typeof event.result === "string" ? event.result : this.lastAssistant?.text ?? "";
5326
+ this.endTurn(text);
5327
+ }
5328
+ endTurn(text) {
5329
+ const turn = this.turn, owner = this.owner;
5330
+ this.owner = undefined;
5331
+ this.lastAssistant = undefined;
5332
+ this.abortApprovals();
5333
+ if (owner === "hub" && turn?.started) {
5334
+ this.turn = undefined;
5335
+ const body = text.trim();
5336
+ if (body)
5337
+ this.onMessage?.(body, { inReplyTo: replyParent(turn.envs), to: replyAudience(turn.envs) });
5338
+ if (turn.deliveryId)
5339
+ this.delivery({ id: turn.deliveryId, state: "completed" });
5340
+ this.stalled_ = false;
5341
+ this.opts.onStalled?.(false);
5342
+ this.setState("idle");
5343
+ return;
5344
+ }
5345
+ if (owner === "personal") {
5346
+ this.stalled_ = false;
5347
+ this.opts.onStalled?.(false);
5348
+ if (!this.turn)
5349
+ this.setState("idle");
5350
+ }
5351
+ }
5352
+ failTurn(reason, notIdle = false, known = false) {
5353
+ const turn = this.turn;
5354
+ if (!turn)
5355
+ return;
5356
+ this.turn = undefined;
5357
+ this.owner = undefined;
5358
+ if (turn.deliveryId) {
5359
+ const receipt = { id: turn.deliveryId, state: known ? "completed" : "needs_review", reason };
5360
+ this.delivery(receipt);
5361
+ }
5362
+ try {
5363
+ this.opts.onNotice?.(`${this.id}: ${reason}`);
5364
+ } catch {}
5365
+ if (this.stalled_) {
5366
+ this.stalled_ = false;
5367
+ this.opts.onStalled?.(false);
5368
+ }
5369
+ if (!notIdle && this.state === "busy")
5370
+ this.setState("idle");
5371
+ }
5372
+ abortApprovals() {
5373
+ for (const { controller } of this.pending.values())
5374
+ controller.abort();
5375
+ this.pending.clear();
5376
+ }
5377
+ onControlRequest(event) {
5378
+ const { request_id: requestId, request } = event;
5379
+ if (typeof requestId !== "string" || !request || request.subtype !== "can_use_tool")
5380
+ return;
5381
+ const turn = this.turn;
5382
+ if (!turn?.started || this.owner !== "hub" || this.stopping || this.state !== "busy")
5383
+ return;
5384
+ const name = typeof request.tool_name === "string" ? request.tool_name : "";
5385
+ const input = request.input ?? {};
5386
+ const hubTool = name !== "" && this.opts.autoApprove?.(name) === true;
5387
+ if (!hubTool && qwenDualNamesStateDir({ input, path: request.blocked_path }, this.opts.guardDir ?? this.opts.stateDir)) {
5388
+ this.opts.log?.(`[${this.id}] approval refused: the call names the hub state dir`);
5389
+ return this.answer(requestId, false, turn, "a Qwen write or command that names the hub state dir was refused: the input file is a prompt channel");
5390
+ }
5391
+ let gate;
5392
+ try {
5393
+ gate = this.state_.sessionId ? this.opts.actionGate?.({ names: [name], input }, this.state_.sessionId) : undefined;
5394
+ } catch {
5395
+ gate = { title: "Action approval: the always-ask check failed; command withheld", denyOnly: true };
5396
+ }
5397
+ if (hubTool && !gate) {
5398
+ this.opts.log?.(`permission auto-approved for ${this.id}: ${name}`);
5399
+ return this.answer(requestId, true, turn);
5400
+ }
5401
+ const full = JSON.stringify(input), cut = full.length > 600;
5402
+ const title = gate ? gate.title : `${sanitize(name || "tool")}: ${cut ? `${full.slice(0, 600)} [cut, ${full.length} chars]` : full}`;
5403
+ const options = [...gate?.denyOnly ? [] : [{ optionId: "allow", name: "Allow", kind: "allow_once" }], { optionId: "deny", name: "Deny", kind: "reject_once" }];
5404
+ const controller = new AbortController;
5405
+ this.pending.set(requestId, { controller, turn });
5406
+ const tool = /^[\w.:@-]{1,80}$/.test(name) ? name : undefined;
5407
+ (async () => {
5408
+ let picked;
5409
+ try {
5410
+ picked = await this.opts.onPermission?.({ peer: this.id, title, options, ...tool ? { tool } : {}, ...gate ? { classed: true } : {} }, controller.signal);
5411
+ } catch {
5412
+ picked = undefined;
5413
+ }
5414
+ const still = this.pending.get(requestId);
5415
+ if (!still || still.controller !== controller || controller.signal.aborted || this.turn !== turn || this.stopping)
5416
+ return;
5417
+ this.pending.delete(requestId);
5418
+ this.answer(requestId, picked === "allow" && options.some((option) => option.optionId === "allow"), turn);
5419
+ })();
5420
+ }
5421
+ answer(requestId, allowed, turn, why) {
5422
+ try {
5423
+ this.appendInput({ type: "confirmation_response", request_id: requestId, allowed });
5424
+ } catch {
5425
+ this.opts.log?.(`[${this.id}] could not write an approval answer`);
5426
+ try {
5427
+ this.opts.onNotice?.(`${this.id}: the hub could not write its approval answer (the input file is unwritable); answer the prompt in the Qwen terminal`);
5428
+ } catch {}
5429
+ return;
5430
+ }
5431
+ this.answered.add(requestId);
5432
+ while (this.answered.size > ANSWERED_CAP)
5433
+ this.answered.delete(this.answered.values().next().value);
5434
+ if (allowed || this.turn !== turn)
5435
+ return;
5436
+ this.owner = undefined;
5437
+ this.abortApprovals();
5438
+ this.failTurn(why ?? "a hub deny cancelled the Qwen turn (Qwen emits nothing for it); the turn ended without an answer", false, true);
5439
+ }
5440
+ onControlResponse(event) {
5441
+ const response = event.response;
5442
+ const id = response?.request_id;
5443
+ if (typeof id !== "string")
5444
+ return;
5445
+ if (response.subtype === "error") {
5446
+ this.opts.log?.(`[${this.id}] Qwen refused an approval answer (stale or unknown request); not retried`);
5447
+ return;
5448
+ }
5449
+ const waiting = this.pending.get(id);
5450
+ if (waiting && !this.answered.has(id)) {
5451
+ this.pending.delete(id);
5452
+ waiting.controller.abort();
5453
+ this.opts.log?.(`[${this.id}] an approval was answered in the Qwen terminal; the hub's was withdrawn`);
5454
+ }
5455
+ }
5456
+ async stop(reason) {
5457
+ if (reason !== undefined)
5458
+ this.requestedStopReason = reason;
5459
+ this.stopping = true;
5460
+ if (this.poller)
5461
+ clearInterval(this.poller);
5462
+ this.poller = undefined;
5463
+ if (this.monitor)
5464
+ clearInterval(this.monitor);
5465
+ this.monitor = undefined;
5466
+ this.abortApprovals();
5467
+ const turn = this.turn;
5468
+ this.turn = undefined;
5469
+ this.owner = undefined;
5470
+ if (turn?.deliveryId)
5471
+ this.delivery({ id: turn.deliveryId, state: "needs_review", reason: reason ?? "Qwen session stopped before settlement" });
5472
+ this.saveState();
5473
+ this.setState("offline");
5474
+ }
5475
+ };
5476
+ });
5477
+
5478
+ // src/pi/launch.ts
5479
+ import { join as join9 } from "path";
4675
5480
  function buildPiLaunch(opts, inherited, extension, bridgePort, bridgeToken) {
4676
5481
  const adapter = ADAPTERS.find((adapter) => adapter.start.daemonPort === "pi-rpc");
4677
- const env = { ...peerChildEnv(opts.instance ?? adapter.kind, inherited), ...opts.hubStateDir !== undefined ? { AGENTHUB_STATE_DIR: opts.hubStateDir } : {}, PI_CODING_AGENT_DIR: join8(opts.stateDir, "pi"), AGENTHUB_PI_BRIDGE_URL: `http://127.0.0.1:${bridgePort}`, AGENTHUB_PI_BRIDGE_TOKEN: bridgeToken, AGENTHUB_PI_OWNER_TOKEN: bridgeToken, AGENTHUB_PI_RELAY_URL: opts.relay.url, AGENTHUB_PI_RELAY_TOKEN: opts.relay.token, AGENTHUB_PI_MODELS: JSON.stringify(opts.relay.models), AGENTHUB_PI_TOOLS: JSON.stringify(opts.tools), AGENTHUB_PI_MAX_STEPS: String(opts.maxSteps ?? 30) };
4678
- const args = [...opts.mode === "headless" ? ["--mode", "rpc"] : [], "--provider", "agent-hub-local", "--model", opts.model ?? modelFor(opts.backend, opts.relay.models), "--models", opts.relay.models.map((model) => `agent-hub-local/${model.id}`).join(","), "--no-builtin-tools", "--no-skills", "--no-prompt-templates", "--no-extensions", "--extension", extension, ...opts.preamble ? ["--append-system-prompt", opts.preamble] : [], "--session-dir", join8(opts.stateDir, "pi-sessions")];
5482
+ const env = { ...peerChildEnv(opts.instance ?? adapter.kind, inherited), ...opts.hubStateDir !== undefined ? { AGENTHUB_STATE_DIR: opts.hubStateDir } : {}, PI_CODING_AGENT_DIR: join9(opts.stateDir, "pi"), AGENTHUB_PI_BRIDGE_URL: `http://127.0.0.1:${bridgePort}`, AGENTHUB_PI_BRIDGE_TOKEN: bridgeToken, AGENTHUB_PI_OWNER_TOKEN: bridgeToken, AGENTHUB_PI_RELAY_URL: opts.relay.url, AGENTHUB_PI_RELAY_TOKEN: opts.relay.token, AGENTHUB_PI_MODELS: JSON.stringify(opts.relay.models), AGENTHUB_PI_TOOLS: JSON.stringify(opts.tools), AGENTHUB_PI_MAX_STEPS: String(opts.maxSteps ?? 30) };
5483
+ const args = [...opts.mode === "headless" ? ["--mode", "rpc"] : [], "--provider", "agent-hub-local", "--model", opts.model ?? modelFor(opts.backend, opts.relay.models), "--models", opts.relay.models.map((model) => `agent-hub-local/${model.id}`).join(","), "--no-builtin-tools", "--no-skills", "--no-prompt-templates", "--no-extensions", "--extension", extension, ...opts.preamble ? ["--append-system-prompt", opts.preamble] : [], "--session-dir", join9(opts.stateDir, "pi-sessions")];
4679
5484
  if (opts.sessionFile)
4680
5485
  args.push("--session", opts.sessionFile);
4681
5486
  else if (opts.sessionId)
@@ -4699,21 +5504,16 @@ var init_launch = __esm(() => {
4699
5504
  init_child_process();
4700
5505
  });
4701
5506
 
4702
- // src/pi/process-signature.ts
4703
- var init_process_signature = __esm(() => {
4704
- init_platform();
4705
- });
4706
-
4707
5507
  // src/hub/recovery-store.ts
4708
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
5508
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
4709
5509
  import { randomUUID as randomUUID3 } from "crypto";
4710
5510
  function atomicPrivateJSON(file, value) {
4711
5511
  const temporary = `${file}.${randomUUID3()}.tmp`;
4712
5512
  try {
4713
- writeFileSync5(temporary, `${JSON.stringify(value, null, 2)}
5513
+ writeFileSync6(temporary, `${JSON.stringify(value, null, 2)}
4714
5514
  `, { mode: 384, flag: "wx" });
4715
5515
  chmodSync(temporary, 384);
4716
- renameSync3(temporary, file);
5516
+ renameSync4(temporary, file);
4717
5517
  } finally {
4718
5518
  rmSync4(temporary, { force: true });
4719
5519
  }
@@ -4724,8 +5524,8 @@ var init_recovery_store = __esm(() => {
4724
5524
  });
4725
5525
 
4726
5526
  // src/pi/session.ts
4727
- import { readFileSync as readFileSync6, openSync as openSync4, closeSync as closeSync4, fsyncSync, mkdirSync as mkdirSync6 } from "fs";
4728
- import { join as join9, relative as relative2, resolve as resolve7, dirname as dirname6 } from "path";
5527
+ import { readFileSync as readFileSync6, openSync as openSync5, closeSync as closeSync5, fsyncSync, mkdirSync as mkdirSync7 } from "fs";
5528
+ import { join as join10, relative as relative2, resolve as resolve7, dirname as dirname6 } from "path";
4729
5529
  function sessionReadFailure(error) {
4730
5530
  if (error.code === "ENOENT")
4731
5531
  throw new PiSessionUnresumable("missing", error.message, { cause: error });
@@ -4735,7 +5535,7 @@ function validatePiSession(stateDir, cwd, sessionFile, sessionId) {
4735
5535
  let file, sessions;
4736
5536
  try {
4737
5537
  file = realPath(sessionFile);
4738
- sessions = realPath(join9(stateDir, "pi-sessions"));
5538
+ sessions = realPath(join10(stateDir, "pi-sessions"));
4739
5539
  } catch (error) {
4740
5540
  return sessionReadFailure(error);
4741
5541
  }
@@ -4793,11 +5593,11 @@ var init_permission_mode = __esm(() => {
4793
5593
  });
4794
5594
 
4795
5595
  // src/hub/path-policy.ts
4796
- import { accessSync, constants as constants3, lstatSync as lstatSync7, opendirSync, readlinkSync, statSync as statSync2 } from "fs";
4797
- import { dirname as dirname7, isAbsolute as isAbsolute5, join as join10, relative as relative3, resolve as resolve8, sep as sep2 } from "path";
5596
+ import { accessSync, constants as constants4, lstatSync as lstatSync8, opendirSync, readlinkSync, statSync as statSync2 } from "fs";
5597
+ import { dirname as dirname7, isAbsolute as isAbsolute5, join as join11, relative as relative3, resolve as resolve8, sep as sep2 } from "path";
4798
5598
  function existing(path) {
4799
5599
  try {
4800
- lstatSync7(path);
5600
+ lstatSync8(path);
4801
5601
  return true;
4802
5602
  } catch (error) {
4803
5603
  if (error.code === "ENOENT")
@@ -4813,7 +5613,7 @@ function policyRealPath(path) {
4813
5613
  throw new Error("path cannot be resolved");
4814
5614
  ancestor = parent;
4815
5615
  }
4816
- return join10(realPath(ancestor), relative3(ancestor, path));
5616
+ return join11(realPath(ancestor), relative3(ancestor, path));
4817
5617
  }
4818
5618
  function policyTargets(cwd, paths) {
4819
5619
  const root = realPath(cwd);
@@ -4841,14 +5641,14 @@ function directAliasTarget(path) {
4841
5641
  return;
4842
5642
  ancestor = parent;
4843
5643
  }
4844
- if (!lstatSync7(ancestor).isSymbolicLink())
5644
+ if (!lstatSync8(ancestor).isSymbolicLink())
4845
5645
  return;
4846
5646
  target = resolve8(dirname7(ancestor), readlinkSync(ancestor), relative3(ancestor, target));
4847
5647
  }
4848
5648
  return;
4849
5649
  }
4850
5650
  function aliasScope(dir, lexical, actual, includeParents, budget) {
4851
- accessSync(dir, constants3.R_OK);
5651
+ accessSync(dir, constants4.R_OK);
4852
5652
  const handle = opendirSync(dir);
4853
5653
  let matched = false;
4854
5654
  try {
@@ -4860,7 +5660,7 @@ function aliasScope(dir, lexical, actual, includeParents, budget) {
4860
5660
  budget.entries++;
4861
5661
  if (!entry.isSymbolicLink())
4862
5662
  continue;
4863
- const target = directAliasTarget(join10(dir, entry.name));
5663
+ const target = directAliasTarget(join11(dir, entry.name));
4864
5664
  if (target !== undefined && (beneath(lexical, target) || beneath(actual, target) || includeParents && (beneath(target, lexical) || beneath(target, actual))))
4865
5665
  matched = true;
4866
5666
  }
@@ -4956,7 +5756,7 @@ function globScope(root, lexical, actual, path, includeParents, budget, roots, a
4956
5756
  if (++budget.entries > 4096)
4957
5757
  throw new Error("policy glob discovery unavailable");
4958
5758
  const next = advance(pattern, states, entry.name);
4959
- if (next.size && visit(join10(dir, entry.name), next, depth + 1, chain))
5759
+ if (next.size && visit(join11(dir, entry.name), next, depth + 1, chain))
4960
5760
  return true;
4961
5761
  }
4962
5762
  } finally {
@@ -5054,7 +5854,7 @@ var init_deny = __esm(() => {
5054
5854
  import { spawn as spawn8, spawnSync as spawnSync3 } from "child_process";
5055
5855
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync4, readdirSync, rmSync as rmSync5 } from "fs";
5056
5856
  import { homedir as homedir4, tmpdir as tmpdir4 } from "os";
5057
- import { dirname as dirname9, join as join11, resolve as resolve9 } from "path";
5857
+ import { dirname as dirname9, join as join12, resolve as resolve9 } from "path";
5058
5858
  function developerDir() {
5059
5859
  const out = spawnSync3("xcode-select", ["-p"], { encoding: "utf8" });
5060
5860
  let dir = out.status === 0 ? out.stdout.trim() : "";
@@ -5075,7 +5875,7 @@ function moduleGitDirs(root) {
5075
5875
  const out = memoryGitSync(["-C", root, "rev-parse", "--git-common-dir"], { encoding: "utf8" });
5076
5876
  if (out.status !== 0)
5077
5877
  return;
5078
- const modulesRoot = join11(resolve9(root, out.stdout.trim()), "modules");
5878
+ const modulesRoot = join12(resolve9(root, out.stdout.trim()), "modules");
5079
5879
  const dirs = [];
5080
5880
  const walk = (dir, depth) => {
5081
5881
  if (depth > 8)
@@ -5089,14 +5889,14 @@ function moduleGitDirs(root) {
5089
5889
  const names = new Set(entries.map((e) => e.name));
5090
5890
  if (names.has("HEAD") && names.has("config")) {
5091
5891
  dirs.push(dir);
5092
- const nested = join11(dir, "modules");
5892
+ const nested = join12(dir, "modules");
5093
5893
  if (names.has("modules"))
5094
5894
  walk(nested, depth + 1);
5095
5895
  return;
5096
5896
  }
5097
5897
  for (const e of entries)
5098
5898
  if (e.isDirectory())
5099
- walk(join11(dir, e.name), depth + 1);
5899
+ walk(join12(dir, e.name), depth + 1);
5100
5900
  };
5101
5901
  walk(modulesRoot, 0);
5102
5902
  const entries = new Set([modulesRoot]);
@@ -5107,13 +5907,13 @@ function moduleGitDirs(root) {
5107
5907
  }
5108
5908
  function profile(cwd, network, readAllow = [], deny = [], readOnly = []) {
5109
5909
  const home = homedir4();
5110
- const inHome = (p) => p.startsWith("~/") ? join11(home, p.slice(2)) : p;
5910
+ const inHome = (p) => p.startsWith("~/") ? join12(home, p.slice(2)) : p;
5111
5911
  const root = realPath(cwd);
5112
5912
  const gitDirs = externalGitDirs(root);
5113
5913
  const moduleGit = moduleGitDirs(root);
5114
5914
  const creds = [".ssh", ".aws", ".gnupg", ".config/gh", ".config/gcloud", ".kube", ".docker", ".netrc", ".npmrc", ".omniroute", ".claude", ".codex", ".kimi-code", "Library/Keychains"];
5115
5915
  const dev = developerDir();
5116
- const readable = [root, ...HOME_READABLE.map((p) => join11(home, p)), ...readAllow.map(inHome), ...gitDirs, ...dev ? [dev] : []];
5916
+ const readable = [root, ...HOME_READABLE.map((p) => join12(home, p)), ...readAllow.map(inHome), ...gitDirs, ...dev ? [dev] : []];
5117
5917
  const subpaths = (paths) => paths.map((p) => `(subpath ${q(p)})`).join(" ");
5118
5918
  const globals = (names) => names.map((n) => `(global-name ${q(n)})`).join(" ");
5119
5919
  const proxy = typeof network === "object" ? `(allow network-outbound (remote ip ${q(`localhost:${network.proxyPort}`)}))` : undefined;
@@ -5131,14 +5931,14 @@ function profile(cwd, network, readAllow = [], deny = [], readOnly = []) {
5131
5931
  '(allow file-ioctl (regex #"^/dev/"))',
5132
5932
  ...proxy ? [proxy] : network ? ["(allow network*)"] : []
5133
5933
  ];
5134
- const places = `${creds.map((c) => `(subpath ${q(join11(home, c))})`).join(" ")} ${denyRegexes(root, deny, false).join(" ")}`;
5934
+ const places = `${creds.map((c) => `(subpath ${q(join12(home, c))})`).join(" ")} ${denyRegexes(root, deny, false).join(" ")}`;
5135
5935
  return [
5136
5936
  "(version 1)",
5137
5937
  ...start,
5138
5938
  "(deny file-write*)",
5139
5939
  ...readOnly.length ? ['(allow file-write* (literal "/dev/null"))'] : [`(allow file-write* (subpath ${q(root)}) (regex #"^/dev/") ${gitDirs.map((d) => `(subpath ${q(d)})`).join(" ")})`],
5140
5940
  `(deny file-write* ${hubWriteRegexes(root, gitDirs, moduleGit).join(" ")})`,
5141
- `(deny file-read* file-write* ${creds.map((c) => `(subpath ${q(join11(home, c))})`).join(" ")})`,
5941
+ `(deny file-read* file-write* ${creds.map((c) => `(subpath ${q(join12(home, c))})`).join(" ")})`,
5142
5942
  `(deny file-read* file-write* ${denyRegexes(root, deny).join(" ")})`,
5143
5943
  ...network ? [`(allow file-read* ${CA_BUNDLES.map((p) => `(literal ${q(p)})`).join(" ")} (regex ${q("/certifi/cacert\\.pem$")}))`, `(deny file-read* file-write* ${places})`] : []
5144
5944
  ].join(`
@@ -5152,7 +5952,7 @@ function sandboxedExec(argv, opts) {
5152
5952
  if (opts.signal?.aborted)
5153
5953
  return Promise.resolve({ code: null, output: "error: command cancelled before execution" });
5154
5954
  return new Promise((resolve) => {
5155
- const own = realPath(mkdtempSync4(join11(tmpdir4(), "ahub-cmd-")));
5955
+ const own = realPath(mkdtempSync4(join12(tmpdir4(), "ahub-cmd-")));
5156
5956
  const sandbox = `${opts.profile}
5157
5957
  (allow file-read* ${opts.readOnlyData ? "" : "file-write* "}process-exec (subpath ${q(own)}))`;
5158
5958
  const env = { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: homedir4(), LANG: process.env.LANG ?? "en_US.UTF-8", TERM: "dumb", ...opts.env, TMPDIR: `${own}/` };
@@ -5227,8 +6027,8 @@ var init_sandbox = __esm(() => {
5227
6027
  });
5228
6028
 
5229
6029
  // src/local/tools.ts
5230
- import { lstatSync as lstatSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
5231
- import { dirname as dirname10, join as join12, relative as relative4, resolve as resolve10 } from "path";
6030
+ import { lstatSync as lstatSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
6031
+ import { dirname as dirname10, join as join13, relative as relative4, resolve as resolve10 } from "path";
5232
6032
  function guardPath(ctx, path, mode) {
5233
6033
  const root = realPath(ctx.cwd);
5234
6034
  const abs = resolve10(root, path);
@@ -5241,7 +6041,7 @@ function guardPath(ctx, path, mode) {
5241
6041
  } catch {
5242
6042
  throw new Error(`${path} goes through a dangling symlink`);
5243
6043
  }
5244
- const real = join12(realExisting, relative4(existing, abs));
6044
+ const real = join13(realExisting, relative4(existing, abs));
5245
6045
  const rel = relative4(root, real);
5246
6046
  if (rel.startsWith("..") || resolve10(root, rel) !== real)
5247
6047
  throw new Error(`${path} is outside the project directory`);
@@ -5249,7 +6049,7 @@ function guardPath(ctx, path, mode) {
5249
6049
  throw new Error(`${path} is on the secrets denylist`);
5250
6050
  if (mode === "write" && isHubWritePath(rel))
5251
6051
  throw new Error(`${path} is not writable by the local worker`);
5252
- if (mode === "write" && ctx.readOnly?.length && (lstatSync8(real, { throwIfNoEntry: false })?.nlink ?? 1) > 1)
6052
+ if (mode === "write" && ctx.readOnly?.length && (lstatSync9(real, { throwIfNoEntry: false })?.nlink ?? 1) > 1)
5253
6053
  throw new Error("hardlinked write target cannot be verified with data.readonly");
5254
6054
  if (mode === "write" && matchesPolicy(root, path, ctx.readOnly ?? []))
5255
6055
  throw new Error("target is protected by data.readonly");
@@ -5320,8 +6120,8 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path), a),
5320
6120
  file = guardPath(ctx, String(a.path), "write");
5321
6121
  if (file !== approvedTarget)
5322
6122
  return "error: path target changed during approval; inspect before retrying";
5323
- mkdirSync7(dirname10(file), { recursive: true });
5324
- writeFileSync6(file, content);
6123
+ mkdirSync8(dirname10(file), { recursive: true });
6124
+ writeFileSync7(file, content);
5325
6125
  return `wrote ${a.path}`;
5326
6126
  }
5327
6127
  case "edit": {
@@ -5345,7 +6145,7 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path), a),
5345
6145
  count = text.split(String(a.old)).length - 1;
5346
6146
  if (count !== 1)
5347
6147
  return `error: file changed during approval; \`old\` must match exactly once, it matched ${count} times`;
5348
- writeFileSync6(file, text.replace(String(a.old), () => String(a.new ?? "")));
6148
+ writeFileSync7(file, text.replace(String(a.old), () => String(a.new ?? "")));
5349
6149
  return `edited ${a.path}`;
5350
6150
  }
5351
6151
  case "bash": {
@@ -5354,9 +6154,26 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path), a),
5354
6154
  return "error: empty command";
5355
6155
  if (command.length > 4000)
5356
6156
  return "error: command longer than 4000 characters; put it in a script file with write, then run that";
5357
- const refused = approvalError(await ctx.permit(`bash: ${command}`, "bash", ctx.signal, undefined, undefined, undefined, a), "error: the user did not approve this command");
5358
- if (refused)
5359
- return refused;
6157
+ const input = Object.freeze({ ...a, command });
6158
+ for (let attempt = 0;; attempt++) {
6159
+ 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");
6160
+ if (refused)
6161
+ return refused;
6162
+ if (ctx.signal?.aborted)
6163
+ return "error: turn cancelled before shell execution";
6164
+ let check = "valid";
6165
+ try {
6166
+ check = ctx.shellExecutionCheck?.(input) ?? "valid";
6167
+ } catch {
6168
+ check = "ask";
6169
+ }
6170
+ if (check === "aborted" || ctx.signal?.aborted)
6171
+ return "error: turn cancelled before shell execution";
6172
+ if (check === "valid")
6173
+ break;
6174
+ if (attempt)
6175
+ return "error: read-only shell proof remained unstable; no command was executed";
6176
+ }
5360
6177
  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 } : {} });
5361
6178
  try {
5362
6179
  ctx.onProcessExit?.(res.code);
@@ -5395,7 +6212,7 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path), a),
5395
6212
  }
5396
6213
  var APPROVAL_EXPIRED_TEXT = "error: approval expired: no person answered; do not retry this call; hand the task off or stop", ApprovalWaitStop, approvalError = (picked, denied) => picked === true ? undefined : picked === "expired" ? APPROVAL_EXPIRED_TEXT : picked === "aborted" ? "error: approval withdrawn because the turn ended; no operation was executed" : denied, lexists = (p) => {
5397
6214
  try {
5398
- lstatSync8(p);
6215
+ lstatSync9(p);
5399
6216
  return true;
5400
6217
  } catch {
5401
6218
  return false;
@@ -5420,7 +6237,7 @@ var init_tools = __esm(() => {
5420
6237
  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"]),
5421
6238
  fn("write", "Create or overwrite a file inside the project. Needs the user's approval.", { path: str, content: str }, ["path", "content"]),
5422
6239
  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"]),
5423
- 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"]),
6240
+ 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"]),
5424
6241
  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"]),
5425
6242
  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"])
5426
6243
  ];
@@ -5449,8 +6266,8 @@ var PI_CEILING_KIND = "tool-step-ceiling", PI_CEILING_UNIT = "tool-step";
5449
6266
 
5450
6267
  // src/adapters/pi.ts
5451
6268
  import { randomUUID as randomUUID4 } from "crypto";
5452
- import { existsSync as existsSync7, mkdirSync as mkdirSync8 } from "fs";
5453
- import { join as join13, resolve as resolve11 } from "path";
6269
+ import { existsSync as existsSync7, mkdirSync as mkdirSync9 } from "fs";
6270
+ import { join as join14, resolve as resolve11 } from "path";
5454
6271
  import { spawn as spawn9 } from "child_process";
5455
6272
  function ownerStillAlive(pid, signature) {
5456
6273
  const current = processSignature(pid);
@@ -5816,9 +6633,9 @@ var init_pi = __esm(() => {
5816
6633
  this.approvalAnswerEpoch = undefined;
5817
6634
  this.approvalStopReason = "";
5818
6635
  this.approvalAbortSent = false;
5819
- mkdirSync8(this.opts.stateDir, { recursive: true });
5820
- const sessions = join13(this.opts.stateDir, "pi-sessions");
5821
- mkdirSync8(sessions, { recursive: true, mode: 448 });
6636
+ mkdirSync9(this.opts.stateDir, { recursive: true });
6637
+ const sessions = join14(this.opts.stateDir, "pi-sessions");
6638
+ mkdirSync9(sessions, { recursive: true, mode: 448 });
5822
6639
  if (this.opts.sessionFile) {
5823
6640
  try {
5824
6641
  validatePiSession(this.opts.stateDir, this.opts.cwd, this.opts.sessionFile, this.opts.sessionId);
@@ -5875,7 +6692,7 @@ var init_pi = __esm(() => {
5875
6692
  return new Response("not found", { status: 404 });
5876
6693
  } });
5877
6694
  this.server = bridge;
5878
- const extension = resolve11(join13(import.meta.dir, "../pi/extension.ts"));
6695
+ const extension = resolve11(join14(import.meta.dir, "../pi/extension.ts"));
5879
6696
  const inherited = {};
5880
6697
  for (const key of ["PATH", "HOME", "USER", "SHELL", "TMPDIR", "TERM", "TERM_PROGRAM", "LANG", "LC_ALL", "LC_CTYPE", "NO_COLOR", "CODEX_HOME"])
5881
6698
  if (process.env[key])
@@ -8804,8 +9621,8 @@ var init_shipped_roles = __esm(() => {
8804
9621
 
8805
9622
  // src/hub/roles.ts
8806
9623
  import { createHash as createHash5 } from "crypto";
8807
- import { constants as constants4, closeSync as closeSync5, fstatSync as fstatSync4, lstatSync as lstatSync9, openSync as openSync5, readSync as readSync3, readdirSync as readdirSync2 } from "fs";
8808
- import { join as join14 } from "path";
9624
+ import { constants as constants5, closeSync as closeSync6, fstatSync as fstatSync5, lstatSync as lstatSync10, openSync as openSync6, readSync as readSync4, readdirSync as readdirSync2 } from "fs";
9625
+ import { join as join15 } from "path";
8809
9626
  function parseRole(text, stem, layer) {
8810
9627
  if (Buffer.byteLength(text) > 8192 || text.includes("\r") || !ID.test(stem))
8811
9628
  throw new Error("invalid role size, encoding or file id");
@@ -8852,24 +9669,24 @@ function parseRole(text, stem, layer) {
8852
9669
  return { id: stem, name: header.name, group: header.group, summary: header.summary, source: header.source, base: list("base", BASES2), classes: list("classes", CLASSES2), body, review, layer, hash: createHash5("sha256").update(text).digest("hex") };
8853
9670
  }
8854
9671
  function projectRole(file) {
8855
- const fd = openSync5(file, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
9672
+ const fd = openSync6(file, constants5.O_RDONLY | constants5.O_NOFOLLOW | constants5.O_NONBLOCK);
8856
9673
  try {
8857
- const before = fstatSync4(fd);
9674
+ const before = fstatSync5(fd);
8858
9675
  if (!before.isFile() || before.nlink !== 1 || before.size > 8192)
8859
9676
  throw new Error("role must be a bounded regular file");
8860
9677
  const bytes = Buffer.alloc(8193);
8861
9678
  let length = 0, n = 0;
8862
- while ((n = readSync3(fd, bytes, length, bytes.length - length, null)) > 0) {
9679
+ while ((n = readSync4(fd, bytes, length, bytes.length - length, null)) > 0) {
8863
9680
  length += n;
8864
9681
  if (length > 8192)
8865
9682
  throw new Error("role exceeds file cap");
8866
9683
  }
8867
- const after = fstatSync4(fd);
9684
+ const after = fstatSync5(fd);
8868
9685
  if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || length !== after.size)
8869
9686
  throw new Error("role changed during read");
8870
9687
  return bytes.subarray(0, length).toString("utf8");
8871
9688
  } finally {
8872
- closeSync5(fd);
9689
+ closeSync6(fd);
8873
9690
  }
8874
9691
  }
8875
9692
  function loadRoleCatalog(cwd) {
@@ -8884,9 +9701,9 @@ function loadRoleCatalog(cwd) {
8884
9701
  if (!cwd)
8885
9702
  return { roles, errors };
8886
9703
  try {
8887
- const config = join14(cwd, ".agenthub"), dir = join14(config, "roles");
9704
+ const config = join15(cwd, ".agenthub"), dir = join15(config, "roles");
8888
9705
  for (const path of [config, dir]) {
8889
- const stat = lstatSync9(path);
9706
+ const stat = lstatSync10(path);
8890
9707
  if (!stat.isDirectory() || stat.isSymbolicLink())
8891
9708
  throw new Error("unsafe role directory");
8892
9709
  }
@@ -8896,7 +9713,7 @@ function loadRoleCatalog(cwd) {
8896
9713
  for (const file of files) {
8897
9714
  const id = file.slice(0, -3);
8898
9715
  try {
8899
- roles.set(id, parseRole(projectRole(join14(dir, file)), id, "project"));
9716
+ roles.set(id, parseRole(projectRole(join15(dir, file)), id, "project"));
8900
9717
  } catch {
8901
9718
  errors.push({ id: ID.test(id) ? id : "invalid-file-id", reason: "invalid or unsafe project role" });
8902
9719
  }
@@ -9217,7 +10034,7 @@ var init_hub_tools = __esm(() => {
9217
10034
  tool("hub_release_list", "Read public release scope and planned/actual dates for phased planning. Configured planners only.", {}),
9218
10035
  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"]),
9219
10036
  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"]),
9220
- 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"]),
10037
+ 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"]),
9221
10038
  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"]),
9222
10039
  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"]),
9223
10040
  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"]),
@@ -9241,7 +10058,7 @@ var init_hub_tools = __esm(() => {
9241
10058
  TASK_TOOL_NAMES = new Set(TASK_TOOLS.map((t) => t.name));
9242
10059
  CONDUCTOR_TOOLS = [
9243
10060
  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.", {}),
9244
- 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"]),
10061
+ 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"]),
9245
10062
  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"]),
9246
10063
  tool("hub_task_escalate", "Escalate a task through the normal task flow, as the conductor. Requires assign capability when explicitly listed.", { id }, ["id"]),
9247
10064
  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"]),
@@ -9289,7 +10106,7 @@ var init_config = __esm(() => {
9289
10106
  });
9290
10107
 
9291
10108
  // src/hub/routing.ts
9292
- import { join as join15, resolve as resolve12 } from "path";
10109
+ import { join as join16, resolve as resolve12 } from "path";
9293
10110
  function effectiveLocalRoute(defaultRoute, classRoute, classFixedModel) {
9294
10111
  return classRoute ?? (classFixedModel === undefined ? defaultRoute : undefined);
9295
10112
  }
@@ -9300,7 +10117,7 @@ var init_routing = __esm(() => {
9300
10117
  init_spec_read();
9301
10118
  init_config();
9302
10119
  init_stage();
9303
- TEMPLATE = join15(import.meta.dir, "..", "..", "templates", "routing.toml");
10120
+ TEMPLATE = join16(import.meta.dir, "..", "..", "templates", "routing.toml");
9304
10121
  cache = new Map;
9305
10122
  });
9306
10123
 
@@ -9567,11 +10384,12 @@ var init_local_worker = __esm(() => {
9567
10384
  return `(stopped after ${maxSteps} steps) ${progress.last}`.trim();
9568
10385
  }
9569
10386
  toolContext(envs, turnSignal, pii, reply) {
10387
+ const executionGeneration = this.turn;
9570
10388
  return {
9571
10389
  cwd: this.opts.cwd,
9572
10390
  deny: this.opts.tools.deny,
9573
10391
  readOnly: this.opts.tools.readOnly,
9574
- permit: async (title, tool, _signal, canonicalTarget, _observe, originalPath, input) => {
10392
+ permit: async (title, tool, _signal, canonicalTarget, _observe, originalPath, input, policy) => {
9575
10393
  if (turnSignal.aborted)
9576
10394
  return "aborted";
9577
10395
  const generation = this.turn;
@@ -9600,7 +10418,7 @@ var init_local_worker = __esm(() => {
9600
10418
  };
9601
10419
  const onAbort = () => finish("aborted");
9602
10420
  turnSignal.addEventListener("abort", onAbort, { once: true });
9603
- this.opts.tools.permit(title, tool, turnSignal, canonicalTarget, observe, originalPath, input).then(finish, (error) => {
10421
+ this.opts.tools.permit(title, tool, turnSignal, canonicalTarget, observe, originalPath, input, policy).then(finish, (error) => {
9604
10422
  turnSignal.removeEventListener("abort", onAbort);
9605
10423
  reject(error);
9606
10424
  });
@@ -9613,6 +10431,7 @@ var init_local_worker = __esm(() => {
9613
10431
  observe({ source: "expired", answerEpoch: this.approvalAnswerEpoch ?? 0, eligibleExpiry: true });
9614
10432
  return picked;
9615
10433
  },
10434
+ shellExecutionCheck: (input) => turnSignal.aborted || executionGeneration !== this.turn ? "aborted" : this.opts.tools.shellExecutionCheck?.(input) ?? "valid",
9616
10435
  sandboxProfile: this.sandboxProfile,
9617
10436
  sandboxEnv: { ...proxyEnv(this.opts.tools.bashNetwork ?? false), AGENTHUB_PEER_ID: this.id },
9618
10437
  signal: turnSignal,
@@ -9851,7 +10670,7 @@ var init_launch2 = __esm(() => {
9851
10670
 
9852
10671
  // src/adapters/claude-task-sessions.ts
9853
10672
  import { randomUUID as randomUUID7 } from "crypto";
9854
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync8 } from "fs";
10673
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync8 } from "fs";
9855
10674
  import { dirname as dirname11 } from "path";
9856
10675
  function readState(file) {
9857
10676
  let text;
@@ -9889,7 +10708,7 @@ class ClaudeTaskSessions {
9889
10708
  }
9890
10709
  write(sessions) {
9891
10710
  const next = { version: 1, sessions };
9892
- mkdirSync9(dirname11(this.file), { recursive: true, mode: 448 });
10711
+ mkdirSync10(dirname11(this.file), { recursive: true, mode: 448 });
9893
10712
  atomicPrivateJSON(this.file, next);
9894
10713
  this.state = next;
9895
10714
  }
@@ -9934,9 +10753,9 @@ var init_claude_task_sessions = __esm(() => {
9934
10753
 
9935
10754
  // src/adapters/claude-implementer.ts
9936
10755
  import { randomUUID as randomUUID8 } from "crypto";
9937
- import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
10756
+ import { lstatSync as lstatSync11, mkdtempSync as mkdtempSync5, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
9938
10757
  import { tmpdir as tmpdir5 } from "os";
9939
- import { join as join16 } from "path";
10758
+ import { join as join17 } from "path";
9940
10759
  function claudeImplementerArgs(turn, mcp, settings, prompt) {
9941
10760
  return [
9942
10761
  "-p",
@@ -9968,10 +10787,10 @@ function claudeImplementerArgs(turn, mcp, settings, prompt) {
9968
10787
  async function proveClaudeImplementer(peer, command) {
9969
10788
  const binary = Bun.which(command[0], { PATH: process.env.PATH }) ?? command[0];
9970
10789
  const fingerprint = () => {
9971
- const file = realPath(binary), stat = lstatSync10(file);
10790
+ const file = realPath(binary), stat = lstatSync11(file);
9972
10791
  return [file, stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
9973
10792
  };
9974
- const binding = fingerprint(), home = mkdtempSync5(join16(tmpdir5(), "ahub-claude-proof-"));
10793
+ const binding = fingerprint(), home = mkdtempSync5(join17(tmpdir5(), "ahub-claude-proof-"));
9975
10794
  let version = "", timedOut = false;
9976
10795
  const child = new OwnedPrintChild({
9977
10796
  peer,
@@ -9980,7 +10799,7 @@ async function proveClaudeImplementer(peer, command) {
9980
10799
  cwd: home,
9981
10800
  input: "",
9982
10801
  envMode: "replace",
9983
- env: { PATH: process.env.PATH, HOME: home, CLAUDE_CONFIG_DIR: join16(home, ".claude") },
10802
+ env: { PATH: process.env.PATH, HOME: home, CLAUDE_CONFIG_DIR: join17(home, ".claude") },
9984
10803
  maxFrameBytes: 4096,
9985
10804
  maxOutputBytes: 4096,
9986
10805
  onLine: (line) => {
@@ -10051,7 +10870,7 @@ var init_claude_implementer = __esm(() => {
10051
10870
  if (options.mode !== undefined && options.mode !== "ask" && options.mode !== "ask-when-needed")
10052
10871
  throw new Error("Claude implementer refuses unsupported permission mode");
10053
10872
  this.mode = options.mode ?? "ask";
10054
- this.sessions = new ClaudeTaskSessions(join16(options.stateDir, claudeStateFile("claude-task-sessions", id)));
10873
+ this.sessions = new ClaudeTaskSessions(join17(options.stateDir, claudeStateFile("claude-task-sessions", id)));
10055
10874
  }
10056
10875
  get permissionModeState() {
10057
10876
  return this.mode;
@@ -10197,9 +11016,9 @@ var init_claude_implementer = __esm(() => {
10197
11016
  this.options.notify?.(`${this.id}: task #${group.task} session fresh; recorded transcript is missing`);
10198
11017
  }
10199
11018
  }
10200
- const launchId = randomUUID8(), directory = mkdtempSync5(join16(this.options.stateDir, "claude-child-")), settings = join16(directory, "settings.json");
11019
+ const launchId = randomUUID8(), directory = mkdtempSync5(join17(this.options.stateDir, "claude-child-")), settings = join17(directory, "settings.json");
10201
11020
  this.cleanup = () => rmSync6(directory, { recursive: true, force: true });
10202
- writeFileSync7(settings, sessionSettings(undefined, { script: this.options.factsHook, stateDir: this.options.stateDir, purpose: "permission" }), { mode: 384, flag: "wx" });
11021
+ writeFileSync8(settings, sessionSettings(undefined, { script: this.options.factsHook, stateDir: this.options.stateDir, purpose: "permission" }), { mode: 384, flag: "wx" });
10203
11022
  const env = {
10204
11023
  AGENTHUB_MODE: "tools",
10205
11024
  AGENTHUB_PEER_ID: this.id,
@@ -10333,6 +11152,14 @@ function createJcode(ctx) {
10333
11152
  init_jcode();
10334
11153
  return new JcodePeer(ctx.daemon.jcode);
10335
11154
  }
11155
+ function createQwenDual(ctx) {
11156
+ if (!("qwenDual" in ctx.daemon))
11157
+ throw new Error("Qwen TUI requires its dual-output transport options");
11158
+ if (ctx.permission === "read-only")
11159
+ throw new Error("Qwen has no verified read-only permission mapping");
11160
+ init_qwen_dual();
11161
+ return new QwenDualPeer(ctx.instance, ctx.daemon.qwenDual);
11162
+ }
10336
11163
  function createPi(ctx) {
10337
11164
  if (!("pi" in ctx.daemon))
10338
11165
  throw new Error("Pi requires its Pi transport options");
@@ -10384,6 +11211,7 @@ var init_native_factories = __esm(() => {
10384
11211
  DAEMON_PORTS = {
10385
11212
  "codex-appserver": (options) => ({ codex: options }),
10386
11213
  acp: (options) => ({ acp: options }),
11214
+ "qwen-dual": (options) => ({ qwenDual: options }),
10387
11215
  "owned-acp": (options) => ({ jcode: options }),
10388
11216
  "pi-rpc": (options) => ({ pi: options }),
10389
11217
  "local-worker": (options) => ({ local: options }),
@@ -10455,9 +11283,9 @@ var init_reviewer_variants = __esm(() => {
10455
11283
  });
10456
11284
 
10457
11285
  // src/adapters/agy/hooks.ts
10458
- import { closeSync as closeSync6, constants as constants5, fstatSync as fstatSync5, lstatSync as lstatSync11, openSync as openSync6, readSync as readSync4 } from "fs";
11286
+ import { closeSync as closeSync7, constants as constants6, fstatSync as fstatSync6, lstatSync as lstatSync12, openSync as openSync7, readSync as readSync5 } from "fs";
10459
11287
  import { homedir as homedir5 } from "os";
10460
- import { isAbsolute as isAbsolute6, join as join17, relative as relative5 } from "path";
11288
+ import { isAbsolute as isAbsolute6, join as join18, relative as relative5 } from "path";
10461
11289
  import { createHash as createHash6 } from "crypto";
10462
11290
  function decodeAgyHook(event, value) {
10463
11291
  if (!EVENTS2.includes(event) || !record3(value) || typeof value.conversationId !== "string" || !UUID2.test(value.conversationId) || typeof value.transcriptPath !== "string" || !isAbsolute6(value.transcriptPath) || value.transcriptPath.length > 4096 || !Array.isArray(value.workspacePaths) || !value.workspacePaths.length || value.workspacePaths.length > 32 || !value.workspacePaths.every((path) => typeof path === "string" && path.length <= 4096 && isAbsolute6(path)))
@@ -10495,19 +11323,19 @@ function readAgyTranscript(session, transcript) {
10495
11323
  if (!UUID2.test(session))
10496
11324
  return empty;
10497
11325
  const home = process.env.HOME ?? homedir5();
10498
- const candidates = ["antigravity-cli", "antigravity", "antigravity-ide"].map((surface) => join17(home, ".gemini", surface, "brain", session, ".system_generated/logs/transcript_full.jsonl"));
11326
+ const candidates = ["antigravity-cli", "antigravity", "antigravity-ide"].map((surface) => join18(home, ".gemini", surface, "brain", session, ".system_generated/logs/transcript_full.jsonl"));
10499
11327
  if (!candidates.includes(transcript))
10500
11328
  return empty;
10501
11329
  let fd;
10502
11330
  try {
10503
11331
  let cursor = home;
10504
11332
  for (const name of relative5(home, transcript).split("/")) {
10505
- cursor = join17(cursor, name);
10506
- if (lstatSync11(cursor).isSymbolicLink())
11333
+ cursor = join18(cursor, name);
11334
+ if (lstatSync12(cursor).isSymbolicLink())
10507
11335
  return empty;
10508
11336
  }
10509
- fd = openSync6(transcript, constants5.O_RDONLY | constants5.O_NOFOLLOW);
10510
- const stat = fstatSync5(fd);
11337
+ fd = openSync7(transcript, constants6.O_RDONLY | constants6.O_NOFOLLOW);
11338
+ const stat = fstatSync6(fd);
10511
11339
  if (!stat.isFile() || stat.nlink !== 1)
10512
11340
  return empty;
10513
11341
  const cap = 4 * 1024 * 1024;
@@ -10516,7 +11344,7 @@ function readAgyTranscript(session, transcript) {
10516
11344
  const buffer = Buffer.alloc(stat.size - from);
10517
11345
  let bytes = 0;
10518
11346
  while (bytes < buffer.length) {
10519
- const count = readSync4(fd, buffer, bytes, buffer.length - bytes, from + bytes);
11347
+ const count = readSync5(fd, buffer, bytes, buffer.length - bytes, from + bytes);
10520
11348
  if (!count)
10521
11349
  break;
10522
11350
  bytes += count;
@@ -10549,7 +11377,7 @@ function readAgyTranscript(session, transcript) {
10549
11377
  return empty;
10550
11378
  } finally {
10551
11379
  if (fd !== undefined)
10552
- closeSync6(fd);
11380
+ closeSync7(fd);
10553
11381
  }
10554
11382
  }
10555
11383
  var EVENTS2, UUID2, record3 = (value) => !!value && typeof value === "object" && !Array.isArray(value), AGY_FACTS_HOOK;
@@ -10628,7 +11456,7 @@ function builtinCliEntries(definitions) {
10628
11456
  }
10629
11457
  // src/adapters/definitions/native-cli-entry-claude.ts
10630
11458
  import { readFileSync as readFileSync9 } from "fs";
10631
- import { join as join18 } from "path";
11459
+ import { join as join19 } from "path";
10632
11460
  import { randomUUID as randomUUID9 } from "crypto";
10633
11461
  async function runClaudeEntry(ctx) {
10634
11462
  const { args, cwd, stateDir, cliDir, unattendedEnv, projectConfig, fail } = ctx;
@@ -10679,17 +11507,17 @@ async function runClaudeEntry(ctx) {
10679
11507
  }
10680
11508
  }
10681
11509
  let original;
10682
- for (const file of [join18(executionCwd, ".claude", "settings.local.json"), join18(executionCwd, ".claude", "settings.json"), join18(process.env.HOME ?? "", ".claude", "settings.json")]) {
11510
+ for (const file of [join19(executionCwd, ".claude", "settings.local.json"), join19(executionCwd, ".claude", "settings.json"), join19(process.env.HOME ?? "", ".claude", "settings.json")]) {
10683
11511
  try {
10684
11512
  original ??= JSON.parse(readFileSync9(file, "utf8")).statusLine;
10685
11513
  } catch {}
10686
11514
  }
10687
- const facts = claudeObservationHooks(projectConfig(), { script: join18(cliDir, "facts-hook.ts"), stateDir });
11515
+ const facts = claudeObservationHooks(projectConfig(), { script: join19(cliDir, "facts-hook.ts"), stateDir });
10688
11516
  let previousLaunch;
10689
11517
  try {
10690
- previousLaunch = JSON.parse(readFileSync9(join18(stateDir, claudeStateFile("claude-launch", peer)), "utf8"));
11518
+ previousLaunch = JSON.parse(readFileSync9(join19(stateDir, claudeStateFile("claude-launch", peer)), "utf8"));
10691
11519
  } catch {}
10692
- const launch = buildLaunch("claude", args, { unattended: unattendedEnv, statusLine: { script: join18(cliDir, "statusline-tee.ts"), stateDir, ...original ? { original } : {} }, ...facts ? { facts } : {} });
11520
+ const launch = buildLaunch("claude", args, { unattended: unattendedEnv, statusLine: { script: join19(cliDir, "statusline-tee.ts"), stateDir, ...original ? { original } : {} }, ...facts ? { facts } : {} });
10693
11521
  let result;
10694
11522
  try {
10695
11523
  let launchRecord;
@@ -11174,7 +12002,7 @@ var init_native_cli_entry_jcode = () => {};
11174
12002
 
11175
12003
  // src/adapters/definitions/native-cli-preview-claude.ts
11176
12004
  import { readFileSync as readFileSync10 } from "fs";
11177
- import { join as join19 } from "path";
12005
+ import { join as join20 } from "path";
11178
12006
  function previewClaude(ctx) {
11179
12007
  const { args, cwd, stateDir, cliDir, unattended, config } = ctx;
11180
12008
  const { buildLaunch, claudeObservationHooks } = ctx.helpers;
@@ -11182,13 +12010,13 @@ function previewClaude(ctx) {
11182
12010
  let envNames = state.envNames;
11183
12011
  let launch;
11184
12012
  let original;
11185
- for (const file of [join19(cwd, ".claude/settings.local.json"), join19(cwd, ".claude/settings.json"), join19(process.env.HOME ?? "", ".claude/settings.json")]) {
12013
+ for (const file of [join20(cwd, ".claude/settings.local.json"), join20(cwd, ".claude/settings.json"), join20(process.env.HOME ?? "", ".claude/settings.json")]) {
11186
12014
  try {
11187
12015
  original ??= JSON.parse(readFileSync10(file, "utf8")).statusLine;
11188
12016
  } catch {}
11189
12017
  }
11190
- const facts = claudeObservationHooks(config, { script: join19(cliDir, "facts-hook.ts"), stateDir });
11191
- launch = buildLaunch("claude", args, { preview: true, unattended, statusLine: { script: join19(cliDir, "statusline-tee.ts"), stateDir, ...original ? { original: { command: original.command ? REDACTED : "", ...typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}, ...typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {} } } : {} }, ...facts ? { facts } : {} });
12018
+ const facts = claudeObservationHooks(config, { script: join20(cliDir, "facts-hook.ts"), stateDir });
12019
+ launch = buildLaunch("claude", args, { preview: true, unattended, statusLine: { script: join20(cliDir, "statusline-tee.ts"), stateDir, ...original ? { original: { command: original.command ? REDACTED : "", ...typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}, ...typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {} } } : {} }, ...facts ? { facts } : {} });
11192
12020
  const safeArgs = args.map((arg, i) => {
11193
12021
  if (args[i - 1] === "--settings")
11194
12022
  return "{}";
@@ -11199,7 +12027,7 @@ function previewClaude(ctx) {
11199
12027
  return publicArg(arg);
11200
12028
  });
11201
12029
  const publicLaunch = buildLaunch("claude", safeArgs, { preview: true, unattended, statusLine: {
11202
- script: join19(cliDir, "statusline-tee.ts"),
12030
+ script: join20(cliDir, "statusline-tee.ts"),
11203
12031
  stateDir,
11204
12032
  ...original ? { original: { command: original.command ? REDACTED : "", ...typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}, ...typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {} } } : {}
11205
12033
  }, facts });
@@ -11232,7 +12060,7 @@ function previewCodex(ctx) {
11232
12060
  var init_native_cli_preview_codex = () => {};
11233
12061
 
11234
12062
  // src/adapters/definitions/native-cli-preview-kimi.ts
11235
- import { join as join20 } from "path";
12063
+ import { join as join21 } from "path";
11236
12064
  function previewKimi(ctx) {
11237
12065
  const { args, cwd, stateDir, cliDir, config } = ctx;
11238
12066
  const { childEnv, startModeOf, buildKimiLaunch, buildKimiTuiLaunch, nativeLaunchEnv, kimiMcpPlan } = ctx.helpers;
@@ -11255,14 +12083,14 @@ function previewKimi(ctx) {
11255
12083
  launch = { ...native, cmd: native.cmd === "kimi" ? "kimi" : REDACTED, args: native.args.map((arg) => arg === "--model" || arg === REDACTED ? arg : REDACTED) };
11256
12084
  envNames = Object.keys(nativeLaunchEnv("kimi", childEnv())).concat(["AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR"]);
11257
12085
  }
11258
- const { file, action, changed, tracked } = kimiMcpPlan(cwd, stateDir, join20(cliDir, "../../plugins/agent-hub/server.js"), mode);
12086
+ const { file, action, changed, tracked } = kimiMcpPlan(cwd, stateDir, join21(cliDir, "../../plugins/agent-hub/server.js"), mode);
11259
12087
  kimiMetadata = { mode, mcp: { file, action, changed, tracked } };
11260
12088
  return finishPreview(ctx, launch, envNames, unresolved, kimiMetadata);
11261
12089
  }
11262
12090
  var init_native_cli_preview_kimi = () => {};
11263
12091
 
11264
12092
  // src/adapters/definitions/native-cli-preview-pi.ts
11265
- import { join as join21, resolve as resolve15 } from "path";
12093
+ import { join as join22, resolve as resolve15 } from "path";
11266
12094
  function previewPi(ctx) {
11267
12095
  const { args, stateDir, cliDir, config } = ctx;
11268
12096
  const { relayModelIds, buildPiLaunch, startModeOf } = ctx.helpers;
@@ -11297,7 +12125,7 @@ function previewPi(ctx) {
11297
12125
  tools: [],
11298
12126
  preamble: REDACTED,
11299
12127
  maxSteps: config.pi.max_steps
11300
- }, {}, resolve15(join21(cliDir, "../pi/extension.ts")), UNRESOLVED, UNRESOLVED);
12128
+ }, {}, resolve15(join22(cliDir, "../pi/extension.ts")), UNRESOLVED, UNRESOLVED);
11301
12129
  launch = pi;
11302
12130
  envNames = Object.keys(pi.env).concat(["PATH", "HOME", "USER", "SHELL", "TMPDIR", "TERM", "TERM_PROGRAM", "LANG", "LC_ALL", "LC_CTYPE", "NO_COLOR", "CODEX_HOME"].filter((key) => process.env[key] !== undefined));
11303
12131
  if (mode === "tui")
@@ -11382,8 +12210,8 @@ var init_native_config_policy = __esm(() => {
11382
12210
  });
11383
12211
 
11384
12212
  // src/adapters/definitions/native-policy-aux.ts
11385
- import { closeSync as closeSync7, constants as constants6, fstatSync as fstatSync6, openSync as openSync7, readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
11386
- import { isAbsolute as isAbsolute7, join as join22, normalize } from "path";
12213
+ import { closeSync as closeSync8, constants as constants7, fstatSync as fstatSync7, openSync as openSync8, readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
12214
+ import { isAbsolute as isAbsolute7, join as join23, normalize } from "path";
11387
12215
  function claudeCrashPlan(_meta) {
11388
12216
  return { how: "claude: the Claude Code plugin reconnects by itself while that session is still open" };
11389
12217
  }
@@ -11428,13 +12256,13 @@ function localCrashPlan(meta) {
11428
12256
  }
11429
12257
  function claudePlugins(root, cwd) {
11430
12258
  const enabled = new Map;
11431
- for (const file of [join22(root, "settings.json"), ...cwd ? [join22(cwd, ".claude", "settings.json"), join22(cwd, ".claude", "settings.local.json")] : []]) {
12259
+ for (const file of [join23(root, "settings.json"), ...cwd ? [join23(cwd, ".claude", "settings.json"), join23(cwd, ".claude", "settings.local.json")] : []]) {
11432
12260
  const map = json(file)?.enabledPlugins;
11433
12261
  if (map && typeof map === "object")
11434
12262
  for (const [key, on] of Object.entries(map))
11435
12263
  enabled.set(key, on === true);
11436
12264
  }
11437
- const installed = json(join22(root, "plugins", "installed_plugins.json"))?.plugins ?? {};
12265
+ const installed = json(join23(root, "plugins", "installed_plugins.json"))?.plugins ?? {};
11438
12266
  const out = [];
11439
12267
  for (const [key, on] of enabled) {
11440
12268
  if (!on)
@@ -11448,7 +12276,7 @@ function claudePlugins(root, cwd) {
11448
12276
  return out;
11449
12277
  }
11450
12278
  function codexPlugins(root) {
11451
- const config = text(join22(root, "config.toml"));
12279
+ const config = text(join23(root, "config.toml"));
11452
12280
  if (config === undefined)
11453
12281
  return [];
11454
12282
  const found = new Map, disabled = new Set;
@@ -11467,7 +12295,7 @@ function codexPlugins(root) {
11467
12295
  continue;
11468
12296
  let versions = [];
11469
12297
  try {
11470
- versions = readdirSync3(join22(root, "plugins", "cache", market, plugin));
12298
+ versions = readdirSync3(join23(root, "plugins", "cache", market, plugin));
11471
12299
  } catch {}
11472
12300
  out.push({ name, version: versions.length === 1 ? version2(versions[0]) : null });
11473
12301
  }
@@ -11487,8 +12315,8 @@ function absoluteContinuityPath(value) {
11487
12315
  var str3 = (value) => typeof value === "string" && value ? value : undefined, PLUGINS, CLAUDE_ONLY, text = (file) => {
11488
12316
  let fd;
11489
12317
  try {
11490
- fd = openSync7(file, constants6.O_RDONLY | constants6.O_NONBLOCK);
11491
- const st = fstatSync6(fd);
12318
+ fd = openSync8(file, constants7.O_RDONLY | constants7.O_NONBLOCK);
12319
+ const st = fstatSync7(fd);
11492
12320
  if (!st.isFile() || st.size > 1024 * 1024)
11493
12321
  return;
11494
12322
  return readFileSync11(fd, "utf8");
@@ -11496,7 +12324,7 @@ var str3 = (value) => typeof value === "string" && value ? value : undefined, PL
11496
12324
  return;
11497
12325
  } finally {
11498
12326
  if (fd !== undefined)
11499
- closeSync7(fd);
12327
+ closeSync8(fd);
11500
12328
  }
11501
12329
  }, json = (file) => {
11502
12330
  try {
@@ -11505,7 +12333,7 @@ var str3 = (value) => typeof value === "string" && value ? value : undefined, PL
11505
12333
  } catch {
11506
12334
  return;
11507
12335
  }
11508
- }, version2 = (v) => typeof v === "string" && /^[0-9A-Za-z.+_-]{1,40}$/.test(v) ? v : null, claudePluginInventory = (ctx) => claudePlugins(ctx.env.CLAUDE_CONFIG_DIR?.trim() || join22(ctx.home, ".claude"), ctx.cwd), codexPluginInventory = (ctx) => codexPlugins(ctx.env.CODEX_HOME?.trim() || join22(ctx.home, ".codex")), codexToolSemantic = (name, _command) => name.toLowerCase() === "filechange" ? "edit" : undefined;
12336
+ }, version2 = (v) => typeof v === "string" && /^[0-9A-Za-z.+_-]{1,40}$/.test(v) ? v : null, claudePluginInventory = (ctx) => claudePlugins(ctx.env.CLAUDE_CONFIG_DIR?.trim() || join23(ctx.home, ".claude"), ctx.cwd), codexPluginInventory = (ctx) => codexPlugins(ctx.env.CODEX_HOME?.trim() || join23(ctx.home, ".codex")), codexToolSemantic = (name, _command) => name.toLowerCase() === "filechange" ? "edit" : undefined;
11509
12337
  var init_native_policy_aux = __esm(() => {
11510
12338
  PLUGINS = new Map([["claude-mem", "claude-mem"], ["ponytail", "ponytail"], ["open-code-review", "open-code-review"]]);
11511
12339
  CLAUDE_ONLY = new Map([["codex@openai-codex", "codex-plugin"]]);
@@ -11548,9 +12376,9 @@ var init_native_cli_launch_common = __esm(() => {
11548
12376
  });
11549
12377
 
11550
12378
  // src/adapters/definitions/native-cli-claude-launch.ts
11551
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, renameSync as renameSync4, unlinkSync as unlinkSync3, readFileSync as readFileSync12 } from "fs";
12379
+ import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, renameSync as renameSync5, unlinkSync as unlinkSync3, readFileSync as readFileSync12 } from "fs";
11552
12380
  import { randomUUID as randomUUID10 } from "crypto";
11553
- import { resolve as resolve16, join as join23 } from "path";
12381
+ import { resolve as resolve16, join as join24 } from "path";
11554
12382
  function claudeChannel(userArgs) {
11555
12383
  const bundle = resolve16(import.meta.dir, "../../../plugins/agent-hub/server.js");
11556
12384
  for (let i = 0;i < userArgs.length; i++) {
@@ -11612,11 +12440,11 @@ function claudeNativeLaunch(ctx) {
11612
12440
  const stateDir = ctx.facts?.stateDir ?? ctx.statusLine?.stateDir;
11613
12441
  if (!stateDir)
11614
12442
  throw new Error("Claude session settings require the hub state directory");
11615
- const file = join23(stateDir, `claude-settings-${randomUUID10()}.json`);
12443
+ const file = join24(stateDir, `claude-settings-${randomUUID10()}.json`);
11616
12444
  try {
11617
- mkdirSync10(stateDir, { recursive: true, mode: 448 });
11618
- writeFileSync8(`${file}.tmp`, settingsArgs[1], { mode: 384, flag: "wx" });
11619
- renameSync4(`${file}.tmp`, file);
12445
+ mkdirSync11(stateDir, { recursive: true, mode: 448 });
12446
+ writeFileSync9(`${file}.tmp`, settingsArgs[1], { mode: 384, flag: "wx" });
12447
+ renameSync5(`${file}.tmp`, file);
11620
12448
  } catch {
11621
12449
  try {
11622
12450
  unlinkSync3(`${file}.tmp`);
@@ -11669,7 +12497,7 @@ var init_native_cli_codex_launch = __esm(() => {
11669
12497
  });
11670
12498
 
11671
12499
  // src/hub/events.ts
11672
- import { appendFileSync, closeSync as closeSync8, existsSync as existsSync8, fstatSync as fstatSync7, openSync as openSync8, readFileSync as readFileSync13, readSync as readSync5 } from "fs";
12500
+ import { appendFileSync, closeSync as closeSync9, existsSync as existsSync8, fstatSync as fstatSync8, openSync as openSync9, readFileSync as readFileSync13, readSync as readSync6 } from "fs";
11673
12501
  function readEvents(file, since = 0) {
11674
12502
  if (!existsSync8(file))
11675
12503
  return [];
@@ -11691,13 +12519,13 @@ var init_events = () => {};
11691
12519
  // src/cli/recovery-session-file.ts
11692
12520
  import { statSync as statSync4 } from "fs";
11693
12521
  import { homedir as homedir6 } from "os";
11694
- import { join as join24 } from "path";
12522
+ import { join as join25 } from "path";
11695
12523
  function checkedSessionPath(binding, kind) {
11696
12524
  if (kind === "rollout")
11697
- return join24(binding.launch.env.CODEX_HOME ?? join24(homedir6(), ".codex"), "sessions");
11698
- const config = binding.launch.env.CLAUDE_CONFIG_DIR ?? join24(homedir6(), ".claude");
12525
+ return join25(binding.launch.env.CODEX_HOME ?? join25(homedir6(), ".codex"), "sessions");
12526
+ const config = binding.launch.env.CLAUDE_CONFIG_DIR ?? join25(homedir6(), ".claude");
11699
12527
  const slug = binding.projectRoot.replace(/[^a-zA-Z0-9]/g, "-");
11700
- return join24(config, "projects", slug, `${binding.sessionId}.jsonl`);
12528
+ return join25(config, "projects", slug, `${binding.sessionId}.jsonl`);
11701
12529
  }
11702
12530
  function recordedFileState(file) {
11703
12531
  try {
@@ -11710,7 +12538,7 @@ var init_recovery_session_file = () => {};
11710
12538
 
11711
12539
  // src/adapters/definitions/native-recovery.ts
11712
12540
  import { readdirSync as readdirSync4 } from "fs";
11713
- import { basename as basename5, join as join25 } from "path";
12541
+ import { basename as basename5, join as join26 } from "path";
11714
12542
  var rolloutState = (binding) => {
11715
12543
  try {
11716
12544
  return readdirSync4(checkedSessionPath(binding, "rollout"), { recursive: true }).some((name) => String(name).endsWith(".jsonl") && basename5(String(name)).includes(binding.sessionId));
@@ -11734,7 +12562,7 @@ var init_native_recovery = __esm(() => {
11734
12562
  if (phase !== "plan")
11735
12563
  return false;
11736
12564
  let current, started = false, turned = false;
11737
- for (const e of readEvents(join25(stateDir, "events.jsonl"))) {
12565
+ for (const e of readEvents(join26(stateDir, "events.jsonl"))) {
11738
12566
  if (e.type === "native_thread" && e.peer === binding.peer) {
11739
12567
  current = e.thread;
11740
12568
  if (e.thread === binding.sessionId && e.fresh)
@@ -11798,7 +12626,12 @@ var init_native_recovery = __esm(() => {
11798
12626
  };
11799
12627
  QWEN_RECOVERY_SESSION = {
11800
12628
  legacyTargetSupport: { file: "src/hub/peer-catalog.ts", exportName: "OPTIONAL_PEERS", refusal: "target does not support Qwen; end Qwen and make a new plan, or choose a supporting target; runtimes unchanged" },
11801
- restoreArgs: (peer) => ({ ...startupArgs(peer), ...peer.sessionId ? { "session-id": peer.sessionId } : {} })
12629
+ restoreArgs: (peer) => ({ ...startupArgs(peer), ...peer.sessionId ? { "session-id": peer.sessionId } : {} }),
12630
+ recoveryRefusal: (peer, phase = "plan") => {
12631
+ if (peer.args?.mode !== "tui")
12632
+ return;
12633
+ return phase === "restore" ? "Qwen's own terminal cannot be restored as a headless worker; end it in that terminal and make a new recovery plan" : "qwen: its own terminal has no automatic restore; end it in that terminal, then make a new recovery plan (restart it with ahub qwen --mode tui, which resumes its session)";
12634
+ }
11802
12635
  };
11803
12636
  });
11804
12637
 
@@ -11890,7 +12723,7 @@ var init_builtin_metadata = __esm(() => {
11890
12723
  headlessForms = Object.freeze(["headless"]);
11891
12724
  hubGrants = {
11892
12725
  ask: "every write, edit and shell command asks",
11893
- "ask-when-needed": "write and edit inside the project run without asking; shell and git writes still ask",
12726
+ "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",
11894
12727
  "never-ask": "every tool runs without asking, inside the hub sandbox and path guard"
11895
12728
  };
11896
12729
  CLAUDE_REVIEWER_VARIANT = {
@@ -12271,9 +13104,15 @@ var init_builtin_metadata = __esm(() => {
12271
13104
  });
12272
13105
 
12273
13106
  // src/adapters/definitions/native-cli-entry-qwen.ts
13107
+ function verifiedLaunch(value) {
13108
+ const launch = value;
13109
+ if (!launch || typeof launch.cmd !== "string" || !launch.cmd || !Array.isArray(launch.args) || !launch.args.every((word) => typeof word === "string") || !launch.env || typeof launch.env !== "object" || Array.isArray(launch.env))
13110
+ return;
13111
+ return { cmd: launch.cmd, args: launch.args, env: launch.env };
13112
+ }
12274
13113
  async function runQwenEntry(ctx) {
12275
13114
  const peer = ctx.instance ?? "qwen";
12276
- const { args, cwd, stateDir, unattendedEnv, connect, fail, takeFlags } = ctx;
13115
+ const { args, cwd, stateDir, unattendedEnv, connect, fail, takeFlags, execWithEnv } = ctx;
12277
13116
  const { launcherPreview } = ctx.helpers;
12278
13117
  if (args.includes("--print-command") || args.includes("--dry-run")) {
12279
13118
  try {
@@ -12284,28 +13123,55 @@ async function runQwenEntry(ctx) {
12284
13123
  }
12285
13124
  const { one, rest } = takeFlags(args, ["--mode", "--model", "--session-id"], []);
12286
13125
  const adapter = ctx.definition;
13126
+ const usage = "usage: ahub qwen [--mode headless|tui] [--model <alias>] [--session-id <id>]";
12287
13127
  if (one["--session-id"] !== undefined && !adapter.session.crashIdentity?.({ sessionId: one["--session-id"] }).sessionId)
12288
13128
  fail("invalid Qwen session identity");
12289
- if (rest.length || one["--mode"] !== undefined && !adapter.start.forms.includes(one["--mode"]))
12290
- fail("Qwen TUI is not offered before verification; usage: ahub qwen [--mode headless] [--model <alias>] [--session-id <id>]");
13129
+ if (rest.length || one["--mode"] !== undefined && !["headless", "tui"].includes(one["--mode"]))
13130
+ fail(usage);
13131
+ const mode = one["--mode"] === "tui" ? "tui" : "headless";
13132
+ if (mode === "tui" && ctx.instance)
13133
+ fail("extra Qwen instances require headless mode");
13134
+ if (mode === "tui" && !(process.stdin.isTTY && process.stdout.isTTY))
13135
+ fail("Qwen's TUI needs a terminal: run ahub qwen --mode tui in one, or use --mode headless");
12291
13136
  const hub = await connect();
13137
+ let launch;
12292
13138
  try {
12293
13139
  const snapshot = (await hub.request({ t: "status" })).status;
12294
13140
  const warning = retainedLaunchWarning(snapshot, peer, ctx.definition);
12295
13141
  if (warning)
12296
13142
  console.error(warning);
12297
- const reply = await hub.request({ t: "start", peer, args: { mode: "headless", ...one["--model"] ? { model: one["--model"] } : {}, ...one["--session-id"] ? { sessionId: one["--session-id"] } : {} }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 35000);
13143
+ const reply = await hub.request({ t: "start", peer, args: { mode, ...one["--model"] ? { model: one["--model"] } : {}, ...one["--session-id"] ? { sessionId: one["--session-id"] } : {} }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 35000);
12298
13144
  if (!reply.ok)
12299
13145
  fail(reply.error);
12300
- console.log(reply.already ? `${peer} is already attached` : `${peer} attached (headless); use ahub say @${peer} and the console for approvals`);
13146
+ if (mode === "tui") {
13147
+ if (reply.already)
13148
+ return console.log("qwen is already attached (its TUI is running); use that terminal");
13149
+ if (reply.adopted)
13150
+ return console.log("qwen: the hub read on from its saved position; the Qwen terminal it was driving is still running");
13151
+ launch = reply.launch;
13152
+ } else
13153
+ console.log(reply.already ? `${peer} is already attached` : `${peer} attached (headless); use ahub say @${peer} and the console for approvals`);
12301
13154
  } finally {
12302
13155
  hub.close();
12303
13156
  }
13157
+ if (mode !== "tui")
13158
+ return;
13159
+ const verified = verifiedLaunch(launch);
13160
+ if (!verified)
13161
+ return fail("Qwen TUI launch metadata was not verified");
13162
+ const person = Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("AGENTHUB_") || name === "AGENTHUB_HOME"));
13163
+ const env = { ...peerChildEnv(peer, person), ...verified.env };
13164
+ delete env.AGENTHUB_RECOVERY_OPERATION;
13165
+ delete env.AGENTHUB_UNATTENDED;
13166
+ console.error("Qwen runs in this terminal, driven by the hub through its dual-output files. Limits: a hub deny ends the turn without an answer, and a stalled turn is stopped only by pressing Esc here.");
13167
+ return execWithEnv(verified.cmd, verified.args, env);
12304
13168
  }
12305
- var init_native_cli_entry_qwen = () => {};
13169
+ var init_native_cli_entry_qwen = __esm(() => {
13170
+ init_child_process();
13171
+ });
12306
13172
 
12307
13173
  // src/adapters/definitions/native-cli-preview-qwen.ts
12308
- import { join as join26 } from "path";
13174
+ import { join as join27 } from "path";
12309
13175
  function previewQwen(ctx) {
12310
13176
  const { tool, args, cwd, stateDir, cliDir, config } = ctx;
12311
13177
  const state = previewState(args, ctx.baseEnvNames), { unresolved, value } = state;
@@ -12313,8 +13179,8 @@ function previewQwen(ctx) {
12313
13179
  let launch;
12314
13180
  const mode = value("--mode") ?? "headless", model = value("--model");
12315
13181
  const adapter = ctx.definition;
12316
- if (!adapter.start.forms.includes(mode))
12317
- throw new Error("Qwen TUI is not offered before verification");
13182
+ if (!["headless", "tui"].includes(mode))
13183
+ throw new Error("unknown Qwen mode");
12318
13184
  for (let i = 0;i < args.length; i += 2)
12319
13185
  if (!["--model", "--mode", "--session-id"].includes(args[i]) || !args[i + 1])
12320
13186
  throw new Error("unknown Qwen option");
@@ -12323,12 +13189,19 @@ function previewQwen(ctx) {
12323
13189
  throw new Error("invalid Qwen session identity");
12324
13190
  if (sessionId)
12325
13191
  unresolved.push({ field: "sessionId", reason: "requested through ACP session/load at start; preview does not load or expose it" });
13192
+ if (mode === "tui") {
13193
+ const [bin = "qwen", ...prefix] = nativeCommand(config.qwen_cmd, ["qwen"]).filter((word) => word !== "--acp");
13194
+ launch = { cmd: bin, args: [...prefix, "--input-file", "<state dir>/qwen/qwen-dual/input.jsonl", "--json-file", "<state dir>/qwen/qwen-dual/events.jsonl", "--mcp-config", "<state dir>/qwen/qwen-dual/mcp.json", "--approval-mode", "default", ...model ? ["--model", REDACTED] : [], ...sessionId ? ["-r", REDACTED] : []] };
13195
+ unresolved.push({ field: "files", reason: "the daemon prepares the input and event files (0600) when the start is admitted; preview creates nothing" });
13196
+ unresolved.push({ field: "nativeIdentity", reason: `Qwen Code (${describe("qwen-dual")}) is checked before the start and again from its session_start event; preview starts nothing` });
13197
+ return finishPreview(ctx, launch, envNames, unresolved, undefined);
13198
+ }
12326
13199
  launch = adapter.start.launch({
12327
13200
  instance: tool,
12328
13201
  form: "headless",
12329
13202
  projectRoot: cwd,
12330
13203
  stateDir,
12331
- entrypoint: join26(cliDir, "main.js"),
13204
+ entrypoint: join27(cliDir, "main.js"),
12332
13205
  userArgs: [],
12333
13206
  unattended: false,
12334
13207
  env: {},
@@ -12353,6 +13226,7 @@ var init_qwen = __esm(() => {
12353
13226
  init_native_recovery();
12354
13227
  init_acp();
12355
13228
  init_qwen_native();
13229
+ init_native_factories();
12356
13230
  QWEN_DEFINITION = {
12357
13231
  api: 1,
12358
13232
  kind: "qwen",
@@ -12371,10 +13245,11 @@ var init_qwen = __esm(() => {
12371
13245
  conductorStartable: true,
12372
13246
  hubSwitches: [],
12373
13247
  ownedFlags: [],
12374
- preflight: (request) => request.form !== undefined && request.form !== "headless" ? "Qwen TUI is not offered before its native verification; use ahub qwen --mode headless" : undefined,
13248
+ preflight: (request) => request.form !== undefined && request.form !== "headless" && request.form !== "tui" ? "unknown Qwen mode; usage: ahub qwen [--mode headless|tui]" : undefined,
13249
+ createTui: createQwenDual,
12375
13250
  launch: (context) => {
12376
13251
  if (context.form !== "headless")
12377
- throw new Error("Qwen TUI is not offered before its native verification; use ahub qwen --mode headless");
13252
+ throw new Error("the Qwen TUI is launched from the files the daemon prepares; use ahub qwen --mode tui");
12378
13253
  const launch = buildQwenLaunch(context.nativeCommand ?? ["qwen", "--acp"], context.model);
12379
13254
  const argv = [launch.cmd, ...launch.args];
12380
13255
  return { ...launch, metadata: { packageEntrypoint: context.entrypoint, argv, command: argv.map(shellQuote3).join(" "), env: {} } };
@@ -12414,7 +13289,7 @@ var init_qwen = __esm(() => {
12414
13289
  turnEnd: "adapter",
12415
13290
  budget: { account: "qwen" },
12416
13291
  capabilities: { watch: true },
12417
- cli: { command: "qwen", entry: { lifecycleAdmission: true, execute: runQwenEntry }, preview: previewQwen, usage: [["ahub qwen [--mode headless] [--model <alias>] [--session-id <id>] [--print-command]", `start optional Qwen Code ACP (${describe("qwen")}; roster.qwen, default off)`]], launchConfigKeys: ["qwen_cmd"], startWords: () => ["qwen"] },
13292
+ cli: { command: "qwen", entry: { lifecycleAdmission: true, execute: runQwenEntry }, preview: previewQwen, usage: [["ahub qwen [--mode headless|tui] [--model <alias>] [--session-id <id>] [--print-command]", `start optional Qwen Code ACP (${describe("qwen")}; roster.qwen, default off); --mode tui runs Qwen's own TUI in this terminal, driven through its dual-output files (${describe("qwen-dual")}); a hub deny ends the turn without an answer and a stalled turn is stopped with Esc in the Qwen terminal`]], launchConfigKeys: ["qwen_cmd"], startWords: () => ["qwen"] },
12418
13293
  display: QWEN_DISPLAY,
12419
13294
  hooks: { recallPlatform: "qwen" }
12420
13295
  };
@@ -12611,6 +13486,9 @@ function immutable(value, seen = new WeakSet) {
12611
13486
  immutable(member, seen);
12612
13487
  return Object.freeze(value);
12613
13488
  }
13489
+ function isFixedBuiltinDefinition(definition) {
13490
+ return definition !== undefined && fixedBuiltinDefinitions.has(definition);
13491
+ }
12614
13492
  function deriveAdapterLists(definitions) {
12615
13493
  const ordered = (field, eligible = (_definition) => true) => definitions.map((definition, index) => ({ definition, index })).filter(({ definition }) => eligible(definition)).sort((left, right) => {
12616
13494
  const a = left.definition.projection?.[field], b = right.definition.projection?.[field];
@@ -12780,7 +13658,7 @@ function assertDefinitionCoherence(definition) {
12780
13658
  fail("reviewer", "unavailable reviewer must remain metadata-only");
12781
13659
  }
12782
13660
  function assertNativeCapabilityScope(definition) {
12783
- if (fixedBuiltinDefinitions.has(definition))
13661
+ if (isFixedBuiltinDefinition(definition))
12784
13662
  return;
12785
13663
  const declared = (block, key) => block !== undefined && (key in block);
12786
13664
  const restricted = [
@@ -12788,7 +13666,10 @@ function assertNativeCapabilityScope(definition) {
12788
13666
  ["attach.admission", declared(definition.attach, "admission")],
12789
13667
  ["budget.resetSoftTokensOnResume", declared(definition.budget, "resetSoftTokensOnResume")],
12790
13668
  ["session.crashIdentity", declared(definition.session, "crashIdentity")],
13669
+ ["session.nativeRevert", declared(definition.session, "nativeRevert")],
12791
13670
  ["start.daemonPort pi-rpc", definition.start.daemonPort === "pi-rpc"],
13671
+ ["start.daemonPort qwen-dual", definition.start.daemonPort === "qwen-dual"],
13672
+ ["start.createTui", declared(definition.start, "createTui")],
12792
13673
  ["routing", definition.routing !== undefined],
12793
13674
  ["projection", definition.projection !== undefined],
12794
13675
  ["instances.nativeHome", declared(definition.instances, "nativeHome")],
@@ -13225,6 +14106,14 @@ var init_work_lists = __esm(() => {
13225
14106
  forbidden = new Set(["__proto__", "constructor", "prototype"]);
13226
14107
  });
13227
14108
 
14109
+ // node_modules/@staix/agent-mem/src/memory/candidate-pending.ts
14110
+ var CANDIDATE_AGE_MS, CANDIDATE_SHOWN_GRACE_MS, CANDIDATE_DUE_MS;
14111
+ var init_candidate_pending = __esm(() => {
14112
+ CANDIDATE_AGE_MS = 30 * 86400000;
14113
+ CANDIDATE_SHOWN_GRACE_MS = 7 * 86400000;
14114
+ CANDIDATE_DUE_MS = 7 * 86400000;
14115
+ });
14116
+
13228
14117
  // node_modules/@staix/agent-mem/src/memory/generation-sync.ts
13229
14118
  var zero;
13230
14119
  var init_generation_sync = __esm(() => {
@@ -13256,14 +14145,6 @@ var init_sync = __esm(() => {
13256
14145
  liveGroups = new Set;
13257
14146
  });
13258
14147
 
13259
- // node_modules/@staix/agent-mem/src/memory/candidate-pending.ts
13260
- var CANDIDATE_AGE_MS, CANDIDATE_SHOWN_GRACE_MS, CANDIDATE_DUE_MS;
13261
- var init_candidate_pending = __esm(() => {
13262
- CANDIDATE_AGE_MS = 30 * 86400000;
13263
- CANDIDATE_SHOWN_GRACE_MS = 7 * 86400000;
13264
- CANDIDATE_DUE_MS = 7 * 86400000;
13265
- });
13266
-
13267
14148
  // node_modules/@staix/agent-mem/src/memory/path-glob.ts
13268
14149
  var init_path_glob = __esm(() => {
13269
14150
  init_project2();
@@ -20192,7 +21073,7 @@ init_project();
20192
21073
  // src/hub/instance-config.ts
20193
21074
  init_registry();
20194
21075
  import { readFileSync as readFileSync15 } from "fs";
20195
- import { join as join29 } from "path";
21076
+ import { join as join30 } from "path";
20196
21077
 
20197
21078
  // src/adapters/external-metadata.ts
20198
21079
  import { types as types3 } from "util";
@@ -20269,8 +21150,8 @@ init_registry();
20269
21150
  // src/hub/config-trust.ts
20270
21151
  init_child_process();
20271
21152
  init_git();
20272
- import { lstatSync as lstatSync12, statSync as statSync5 } from "fs";
20273
- import { dirname as dirname12, join as join27 } from "path";
21153
+ import { lstatSync as lstatSync13, statSync as statSync5 } from "fs";
21154
+ import { dirname as dirname12, join as join28 } from "path";
20274
21155
  var MACHINE_LOCAL = [
20275
21156
  "kimi_cmd",
20276
21157
  "permission_modes",
@@ -20302,7 +21183,7 @@ function configTracked(cwd, name) {
20302
21183
  return;
20303
21184
  let opened;
20304
21185
  try {
20305
- opened = statSync5(join27(cwd, ".agenthub", name));
21186
+ opened = statSync5(join28(cwd, ".agenthub", name));
20306
21187
  } catch {}
20307
21188
  return r.stdout.split("\x00").filter(Boolean).some((row) => {
20308
21189
  const path = row.slice(row.indexOf("\t") + 1);
@@ -20311,7 +21192,7 @@ function configTracked(cwd, name) {
20311
21192
  if (!opened)
20312
21193
  return path === `.agenthub/${name}`;
20313
21194
  try {
20314
- const s = statSync5(join27(cwd, path));
21195
+ const s = statSync5(join28(cwd, path));
20315
21196
  return !!opened && s.dev === opened.dev && s.ino === opened.ino;
20316
21197
  } catch {
20317
21198
  return false;
@@ -20368,7 +21249,7 @@ function readAdapterModuleDeclarations(cwd, layers) {
20368
21249
  // src/hub/control-client.ts
20369
21250
  init_project();
20370
21251
  import { readFileSync as readFileSync14 } from "fs";
20371
- import { join as join28, resolve as resolve17 } from "path";
21252
+ import { join as join29, resolve as resolve17 } from "path";
20372
21253
  function stateDirFor(cwd) {
20373
21254
  return projectContext(cwd).stateDir;
20374
21255
  }
@@ -20376,8 +21257,8 @@ var PROTOCOL = 16;
20376
21257
  var RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, 14, 15, PROTOCOL];
20377
21258
  function readControl(stateDir) {
20378
21259
  try {
20379
- const status = JSON.parse(readFileSync14(join28(stateDir, "status.json"), "utf8"));
20380
- const token = readFileSync14(join28(stateDir, "control-token"), "utf8").trim();
21260
+ const status = JSON.parse(readFileSync14(join29(stateDir, "status.json"), "utf8"));
21261
+ const token = readFileSync14(join29(stateDir, "control-token"), "utf8").trim();
20381
21262
  if (!Number.isInteger(status.controlPort) || status.controlPort < 1 || status.controlPort > 65535 || !token)
20382
21263
  return;
20383
21264
  const startModes = status.startModes && typeof status.startModes === "object" && !Array.isArray(status.startModes) ? Object.fromEntries(Object.entries(status.startModes).filter((entry) => entry[1] === "tui" || entry[1] === "headless")) : undefined;
@@ -20561,6 +21442,37 @@ init_policy();
20561
21442
  init_screen();
20562
21443
  init_shard();
20563
21444
  init_work_lists();
21445
+ // node_modules/@staix/agent-mem/src/memory/project-import.ts
21446
+ init_bridge();
21447
+ init_private_spans();
21448
+ init_shard();
21449
+ init_store();
21450
+ var PROJECT_IMPORT_MAX_BYTES = 64 * 1024 * 1024;
21451
+ var PROJECT_IMPORT_MAX_LINE_BYTES = 64 * 1024;
21452
+ var PROJECT_IMPORT_FUTURE_SKEW_MS = 5 * 60 * 1000;
21453
+ var NOTE_KEYS = new Set(["schema", "id", "at", "machine", "op", "project", "scope", "tier", "kind", "peer", "title", "text", "task", "about", "supersedes", "promotedFrom", "source", "expires"]);
21454
+ // node_modules/@staix/agent-mem/src/memory/external-import.ts
21455
+ init_aliases();
21456
+
21457
+ // node_modules/@staix/agent-mem/src/memory/candidates.ts
21458
+ init_candidate_pending();
21459
+
21460
+ // node_modules/@staix/agent-mem/src/memory/note-add.ts
21461
+ init_store();
21462
+
21463
+ // node_modules/@staix/agent-mem/src/memory/candidates.ts
21464
+ init_home();
21465
+ init_platform();
21466
+ init_private_spans();
21467
+ var CANDIDATE_RETENTION = { count: 500, ageMs: 30 * 86400000 };
21468
+ var COUNTER_RETENTION = { count: 1e4, ageMs: 90 * 86400000 };
21469
+
21470
+ // node_modules/@staix/agent-mem/src/memory/external-import.ts
21471
+ init_candidate_pending();
21472
+ init_platform();
21473
+ init_private_spans();
21474
+ init_screen();
21475
+ var EXTERNAL_IMPORT_LIMITS = { maxFiles: 256, maxDirectories: 512, maxEntriesPerDirectory: 512, maxDepth: 16, maxFileBytes: 1024 * 1024, maxCandidatesPerRun: 100, candidateTextChars: 3000, titleChars: 200 };
20564
21476
  // node_modules/@staix/agent-mem/src/memory/cross-project.ts
20565
21477
  init_search_fts();
20566
21478
  init_capture();
@@ -20594,18 +21506,6 @@ var memoryUsage = (name = AMEM_NAMES.memory) => `usage: ${name} [list] [--tier s
20594
21506
  var MEMORY_USAGE = memoryUsage();
20595
21507
  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]`;
20596
21508
  var GOAL_USAGE = goalUsage();
20597
- // node_modules/@staix/agent-mem/src/memory/candidates.ts
20598
- init_candidate_pending();
20599
-
20600
- // node_modules/@staix/agent-mem/src/memory/note-add.ts
20601
- init_store();
20602
-
20603
- // node_modules/@staix/agent-mem/src/memory/candidates.ts
20604
- init_home();
20605
- init_platform();
20606
- init_private_spans();
20607
- var CANDIDATE_RETENTION = { count: 500, ageMs: 30 * 86400000 };
20608
- var COUNTER_RETENTION = { count: 1e4, ageMs: 90 * 86400000 };
20609
21509
 
20610
21510
  // node_modules/@staix/agent-mem/src/index.ts
20611
21511
  init_ops_contract();
@@ -20637,6 +21537,7 @@ var tool2 = (name, description, inputSchema, readOnlyHint = true) => ({
20637
21537
  });
20638
21538
  var MCP_TOOLS = [
20639
21539
  tool2("amem_search", "Search screened memory in this project. Capture results are recall, not curated evidence.", object6({ query: string(1000), limit, since: string(40), includeCapture: { type: "boolean" } }, ["query"])),
21540
+ 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"])),
20640
21541
  tool2("amem_show", "Show a screened memory entry in this project by its exact id or unique prefix.", object6({ id: string(80) }, ["id"])),
20641
21542
  tool2("amem_timeline", "Read screened memory near an entry or time in this project.", object6({ id: string(80), since: string(40), limit })),
20642
21543
  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"])),
@@ -20662,6 +21563,9 @@ var MCP_TOOLS = [
20662
21563
  tool2("amem_vault_candidates", "List screened durable curated notes eligible for a person to move to the vault. Read only.", object6({ limit })),
20663
21564
  tool2("amem_capacity", "Read this machine's bounded capture capacity proposal; applying it is person-only.", object6({}))
20664
21565
  ];
21566
+ // node_modules/@staix/agent-mem/src/memory/evidence.ts
21567
+ init_policy();
21568
+
20665
21569
  // node_modules/@staix/agent-mem/src/memory/plan-guard.ts
20666
21570
  init_store();
20667
21571
  init_project2();
@@ -20766,6 +21670,16 @@ init_generation();
20766
21670
  init_lifecycle();
20767
21671
  init_platform();
20768
21672
  init_shard();
21673
+ // node_modules/@staix/agent-mem/src/memory/consolidation-clusters.ts
21674
+ init_search_fts();
21675
+
21676
+ // node_modules/@staix/agent-mem/src/memory/consolidate.ts
21677
+ init_policy();
21678
+ init_platform();
21679
+
21680
+ // node_modules/@staix/agent-mem/src/memory/consolidation-model.ts
21681
+ init_policy();
21682
+ init_platform();
20769
21683
  // src/hub/tasks.ts
20770
21684
  init_task_code_symbols();
20771
21685
  init_test_integrity();
@@ -20781,7 +21695,7 @@ init_sdlc_metrics();
20781
21695
  // package.json
20782
21696
  var package_default = {
20783
21697
  name: "@staix/agent-hub",
20784
- version: "0.16.0",
21698
+ version: "0.18.0",
20785
21699
  description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
20786
21700
  license: "MIT",
20787
21701
  type: "module",
@@ -20798,7 +21712,7 @@ var package_default = {
20798
21712
  bun: ">=1.4.2"
20799
21713
  },
20800
21714
  dependencies: {
20801
- "@staix/agent-mem": "0.7.0"
21715
+ "@staix/agent-mem": "0.8.0"
20802
21716
  },
20803
21717
  devDependencies: {
20804
21718
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -20852,6 +21766,14 @@ init_plan_conformance();
20852
21766
  init_review_findings();
20853
21767
  init_sdlc();
20854
21768
 
21769
+ // src/hub/sdlc-merge.ts
21770
+ init_review_findings();
21771
+ init_sdlc();
21772
+
21773
+ // src/hub/sdlc-mirror.ts
21774
+ init_envelope();
21775
+ var inFlight = new Set;
21776
+
20855
21777
  // src/hub/tasks.ts
20856
21778
  init_review_findings();
20857
21779
  init_roles();
@@ -20870,13 +21792,13 @@ init_workflow();
20870
21792
  init_deliverables();
20871
21793
 
20872
21794
  // src/hub/deliverable-files.ts
20873
- import { constants as constants7, lstatSync as lstatSync13 } from "fs";
21795
+ import { constants as constants8, lstatSync as lstatSync14 } from "fs";
20874
21796
  init_tools();
20875
21797
  init_permission_mode();
20876
21798
  init_project();
20877
21799
  init_deliverables();
20878
21800
  var CHUNK = 1024 * 1024;
20879
- var NOFOLLOW = constants7.O_NOFOLLOW ?? 0;
21801
+ var NOFOLLOW = constants8.O_NOFOLLOW ?? 0;
20880
21802
 
20881
21803
  // src/hub/tasks.ts
20882
21804
  init_board();
@@ -20886,6 +21808,7 @@ init_project();
20886
21808
 
20887
21809
  // src/hub/task-repository.ts
20888
21810
  init_git();
21811
+ init_host_executable();
20889
21812
  init_project();
20890
21813
  init_spec_read();
20891
21814
  var GIT_MAX_BYTES = 16 * 1024;
@@ -21108,7 +22031,7 @@ var INSTANCE_CONFIG_FILES = ["config.json", "config.local.json"];
21108
22031
  function readInstanceConfigLayers(cwd) {
21109
22032
  return INSTANCE_CONFIG_FILES.flatMap((name) => {
21110
22033
  try {
21111
- return [{ name, raw: JSON.parse(readFileSync15(join29(cwd, ".agenthub", name), "utf8")) }];
22034
+ return [{ name, raw: JSON.parse(readFileSync15(join30(cwd, ".agenthub", name), "utf8")) }];
21112
22035
  } catch (error) {
21113
22036
  if (error.code === "ENOENT")
21114
22037
  return [];
@@ -29462,7 +30385,7 @@ class StdioServerTransport {
29462
30385
 
29463
30386
  // src/adapters/claude-channel.ts
29464
30387
  import { readFileSync as readFileSync16 } from "fs";
29465
- import { join as join30 } from "path";
30388
+ import { join as join31 } from "path";
29466
30389
  init_hub_tools();
29467
30390
  init_envelope();
29468
30391
 
@@ -29504,7 +30427,7 @@ var channel = !toolsMode && process.env.AGENTHUB_CHANNEL === "1";
29504
30427
  function roles2() {
29505
30428
  const read = (name) => {
29506
30429
  try {
29507
- return JSON.parse(readFileSync16(join30(projectRoot2, ".agenthub", name), "utf8")).roles ?? {};
30430
+ return JSON.parse(readFileSync16(join31(projectRoot2, ".agenthub", name), "utf8")).roles ?? {};
29508
30431
  } catch {
29509
30432
  return {};
29510
30433
  }
@@ -29523,7 +30446,7 @@ function sessionProfile() {
29523
30446
  for (const name of ["config.json", "config.local.json"]) {
29524
30447
  let data;
29525
30448
  try {
29526
- data = JSON.parse(readFileSync16(join30(projectRoot2, ".agenthub", name), "utf8"));
30449
+ data = JSON.parse(readFileSync16(join31(projectRoot2, ".agenthub", name), "utf8"));
29527
30450
  } catch (error) {
29528
30451
  if (error.code === "ENOENT")
29529
30452
  continue;
@@ -29550,7 +30473,7 @@ function sessionRoles() {
29550
30473
  const read = (name) => {
29551
30474
  let raw;
29552
30475
  try {
29553
- raw = JSON.parse(readFileSync16(join30(projectRoot2, ".agenthub", name), "utf8")).catalog_roles;
30476
+ raw = JSON.parse(readFileSync16(join31(projectRoot2, ".agenthub", name), "utf8")).catalog_roles;
29554
30477
  } catch {
29555
30478
  return {};
29556
30479
  }
@@ -29580,7 +30503,7 @@ var HELD = `another session is attached to the hub as "${peerId}"; this one is s
29580
30503
  var INBOX_CAP = 200;
29581
30504
  function peerHeld() {
29582
30505
  try {
29583
- const status = JSON.parse(readFileSync16(join30(stateDir, "status.json"), "utf8"));
30506
+ const status = JSON.parse(readFileSync16(join31(stateDir, "status.json"), "utf8"));
29584
30507
  const peer = status.peers?.[peerId];
29585
30508
  if (channel && peer?.toolsOnly && !peer.claiming)
29586
30509
  return false;