@huynle/hub 0.10.1 → 0.11.1

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 (129) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +36 -8
  3. package/package.json +1 -1
  4. package/packages/claude-code/package.json +1 -1
  5. package/packages/claude-code/src/sessions.ts +4 -1
  6. package/packages/cli/dist/hub-cli.js +851 -466
  7. package/packages/cli/package.json +1 -1
  8. package/packages/cli/src/cli.ts +12 -7
  9. package/packages/cli/src/decisions.ts +52 -4
  10. package/packages/cli/src/format.ts +9 -0
  11. package/packages/cli/src/grants-mcp.ts +65 -0
  12. package/packages/cli/src/mcp-operations.ts +49 -0
  13. package/packages/cli/src/mcp.ts +26 -4
  14. package/packages/cli/src/subscriptions-mcp.ts +76 -0
  15. package/packages/cli/src/team-intake-mcp.ts +2 -1
  16. package/packages/cli/src/teams-mcp.ts +9 -3
  17. package/packages/codex/package.json +1 -1
  18. package/packages/codex/src/sessions.ts +14 -3
  19. package/packages/node/package.json +1 -1
  20. package/packages/preset/package.json +1 -1
  21. package/packages/protocol/package.json +1 -1
  22. package/packages/protocol/schema/api/v1/_root.json +119 -0
  23. package/packages/protocol/schema/api/v1/account.json +419 -0
  24. package/packages/protocol/schema/api/v1/decisions.json +635 -0
  25. package/packages/protocol/schema/api/v1/discovery.json +110 -0
  26. package/packages/protocol/schema/api/v1/events.json +679 -0
  27. package/packages/protocol/schema/api/v1/session-actions.json +660 -0
  28. package/packages/protocol/schema/api/v1/sessions.json +2055 -0
  29. package/packages/protocol/schema/api/v1/speech.json +492 -0
  30. package/packages/protocol/schema/api/v1/unspecified.json +282 -0
  31. package/packages/protocol/schema/app-delivery/v1.schema.json +67 -5
  32. package/packages/protocol/schema/app-front/v1.schema.json +7 -3
  33. package/packages/protocol/src/api-v1.ts +8 -0
  34. package/packages/protocol/src/index.ts +2 -0
  35. package/packages/protocol/src/teams.ts +16 -0
  36. package/packages/protocol/src/voice-recording.ts +39 -2
  37. package/packages/runtime-openshell/image/entry.sh +3 -1
  38. package/packages/runtime-openshell/image/start.sh +3 -1
  39. package/packages/runtime-openshell/package.json +1 -1
  40. package/packages/runtime-sdk/package.json +1 -1
  41. package/packages/sdk/package.json +1 -1
  42. package/packages/sdk/src/app-front.ts +2 -2
  43. package/packages/sdk/src/ask-fields.ts +76 -0
  44. package/packages/sdk/src/client.ts +24 -2
  45. package/packages/sdk/src/envelope.ts +3 -2
  46. package/packages/sdk/src/index.ts +1 -0
  47. package/packages/sdk/src/team-intake.ts +6 -0
  48. package/packages/sdk/src/teams.ts +4 -0
  49. package/packages/sdk/src/types.ts +36 -4
  50. package/packages/sdk/src/wake.ts +9 -4
  51. package/packages/spoke-sdk/package.json +1 -1
  52. package/src/central/agent/agent-limits.ts +185 -0
  53. package/src/central/agent/app-decisions.ts +6 -5
  54. package/src/central/agent/decisions.ts +136 -9
  55. package/src/central/agent/delivery.ts +164 -22
  56. package/src/central/agent/errors.ts +2 -1
  57. package/src/central/agent/events.ts +20 -2
  58. package/src/central/agent/grant-routes.ts +14 -1
  59. package/src/central/agent/grants.ts +23 -0
  60. package/src/central/agent/launch-engine.ts +9 -6
  61. package/src/central/agent/message-limits.ts +6 -0
  62. package/src/central/agent/message-routes.ts +33 -6
  63. package/src/central/agent/messages.ts +7 -0
  64. package/src/central/agent/placement.ts +4 -2
  65. package/src/central/agent/read-routes.ts +5 -0
  66. package/src/central/agent/routes.ts +13 -0
  67. package/src/central/agent/subscriptions.ts +15 -4
  68. package/src/central/api-v1.ts +115 -0
  69. package/src/central/artifacts/store.ts +4 -2
  70. package/src/central/concierge/board-results.ts +2 -1
  71. package/src/central/concierge/notifier.ts +4 -2
  72. package/src/central/config.ts +24 -3
  73. package/src/central/health/classify.ts +209 -0
  74. package/src/central/http.ts +380 -36
  75. package/src/central/recording/export.ts +57 -3
  76. package/src/central/recording/index.ts +236 -17
  77. package/src/central/recording/ingest.ts +11 -2
  78. package/src/central/recording/routes.ts +61 -7
  79. package/src/central/recording/schema.ts +68 -3
  80. package/src/central/recording/settings.ts +19 -5
  81. package/src/central/recording/store.ts +19 -0
  82. package/src/central/recording/taps.ts +45 -1
  83. package/src/central/registry.ts +34 -3
  84. package/src/central/speech/openai.ts +4 -4
  85. package/src/central/speech/openrouter.ts +4 -4
  86. package/src/central/speech/provider.ts +32 -1
  87. package/src/central/speech/service.ts +6 -5
  88. package/src/central/teams/budgets.ts +144 -0
  89. package/src/central/teams/hop-stalls.ts +80 -0
  90. package/src/central/teams/intake.ts +38 -6
  91. package/src/central/teams/involvement.ts +95 -0
  92. package/src/central/teams/item-decisions.ts +60 -0
  93. package/src/central/teams/item-eligibility.ts +35 -0
  94. package/src/central/teams/item-stalls.ts +87 -0
  95. package/src/central/teams/loops.ts +40 -0
  96. package/src/central/teams/policy.ts +10 -0
  97. package/src/central/teams/progress.ts +154 -0
  98. package/src/central/teams/routes.ts +2 -0
  99. package/src/central/teams/seats.ts +3 -3
  100. package/src/central/teams/staffing.ts +5 -0
  101. package/src/central/teams/store.ts +20 -1
  102. package/src/central/teams/validation.ts +16 -2
  103. package/src/central/workspaces/coordinator.ts +3 -1
  104. package/src/spoke/hub-tool.ts +26 -11
  105. package/web/dist/assets/{ArtifactPage-CsPjam8D.js → ArtifactPage-TbFJi59n.js} +1 -1
  106. package/web/dist/assets/{ArtifactPane-B9weO8L6.js → ArtifactPane-7D-M-Ich.js} +1 -1
  107. package/web/dist/assets/{ArtifactWorkbench-DzKhMs3l.js → ArtifactWorkbench-DlCLEjut.js} +1 -1
  108. package/web/dist/assets/{Diagram-CuRIffka.js → Diagram-PvOPetOI.js} +1 -1
  109. package/web/dist/assets/{Library-rv5DGtrm.js → Library-Cdi83pOV.js} +2 -2
  110. package/web/dist/assets/{ProfilesDestination-DUuQ9jmd.js → ProfilesDestination-cryC7P6j.js} +1 -1
  111. package/web/dist/assets/TeamsPage-B4xrlGhp.js +1 -0
  112. package/web/dist/assets/Watchtower-60B1JX_J.js +2 -0
  113. package/web/dist/assets/{artifacts-But87Jfo.js → artifacts-DlQEYmDl.js} +1 -1
  114. package/web/dist/assets/index-DA0gKIzv.js +33 -0
  115. package/web/dist/assets/{index-rW6LKQj1.css → index-Dyq-jBeE.css} +1 -1
  116. package/web/dist/assets/live-B2IfKm37.js +5 -0
  117. package/web/dist/assets/{live-BUofsQpz.css → live-De-kd3eW.css} +1 -1
  118. package/web/dist/assets/{pane-layout-BW0RdX5a.js → pane-layout-BAuu_pA3.js} +1 -1
  119. package/web/dist/assets/{renderer2d-DyANQ1WS.js → renderer2d-CsZnF3_o.js} +1 -1
  120. package/web/dist/assets/{renderer3d-vYYKFAJq.js → renderer3d-BIMA0wFn.js} +2 -2
  121. package/web/dist/assets/{safe-area-B2Bk__FK.js → safe-area-dtrnw9NF.js} +1 -1
  122. package/web/dist/assets/{silero-worker-e802lB5T.js → silero-worker-CC2etKS3.js} +1 -1
  123. package/web/dist/assets/{source-C-De8L3s.js → source-GYayxUxS.js} +1 -1
  124. package/web/dist/assets/{toast-CXTwTvDw.js → toast-q818mjUF.js} +1 -1
  125. package/web/dist/index.html +5 -5
  126. package/web/dist/assets/TeamsPage-D_17MkvN.js +0 -1
  127. package/web/dist/assets/Watchtower-DlrNOJ7X.js +0 -2
  128. package/web/dist/assets/index-bX8B6NLo.js +0 -33
  129. package/web/dist/assets/live-BGLA-peO.js +0 -5
@@ -7,7 +7,7 @@ import { createInterface } from "readline";
7
7
  // packages/cli/package.json
8
8
  var package_default = {
9
9
  name: "@huynle/hub-cli",
10
- version: "0.10.1",
10
+ version: "0.11.1",
11
11
  private: true,
12
12
  description: "The hub CLI (`hub` / `hubctl`) for agents and scripts (internal; bundled into @huynle/hub, not published on its own).",
13
13
  license: "MIT",
@@ -306,6 +306,12 @@ function showText(s) {
306
306
  if (s.roles?.length)
307
307
  field("roles", s.roles.map((r) => `@${at(r)}`).join(" "));
308
308
  field("now", s.now);
309
+ if (s.health && s.health.kind !== "ok") {
310
+ const h = s.health;
311
+ const reset = typeof h.resetAt === "number" && Number.isFinite(h.resetAt) && Math.abs(h.resetAt) <= 8640000000000000 ? new Date(h.resetAt).toISOString() : undefined;
312
+ const facts = [h.kind, reset ? `resets ${reset}` : undefined, h.attempt !== undefined ? `attempt ${h.attempt}` : undefined].filter(Boolean).join(", ");
313
+ field("health", h.detail ? `${facts} — ${oneLine(h.detail, 200)}` : facts);
314
+ }
309
315
  if (s.children)
310
316
  field("subagents", s.children);
311
317
  if (s.online === false && s.lastSeen !== undefined)
@@ -419,6 +425,74 @@ function parseAddress(addr) {
419
425
  const scope = project ? { project } : {};
420
426
  return role ? { kind: "role", role: match[1], ...scope } : { kind: "name", name: match[1], ...scope };
421
427
  }
428
+ // packages/sdk/src/ask-fields.ts
429
+ var URGENCIES = ["low", "normal", "high"];
430
+ var TIERS = ["confirm", "tap"];
431
+ var KINDS = ["question", "plan", "merge", "release"];
432
+ var DECISION_ASK_PROPERTIES = {
433
+ options: {
434
+ type: "array",
435
+ items: { type: "object", properties: { id: { type: "string" }, label: { type: "string" }, safe: { type: "boolean" } }, required: ["id", "label"] },
436
+ description: "ask @me: buttons the user taps, [{id, label, safe?}] (at most 8; id: lowercase letters, digits, - or _)."
437
+ },
438
+ recommendation: {
439
+ type: "object",
440
+ properties: { option: { type: "string" }, why: { type: "string" } },
441
+ required: ["option"],
442
+ description: "ask @me: the option you recommend ({option: an option id, why})."
443
+ },
444
+ urgency: { type: "string", enum: [...URGENCIES], description: "ask @me: low, normal (default) or high." },
445
+ tier: { type: "string", enum: [...TIERS], description: "ask @me: confirm (default: any of the user's devices) or tap (only the signed-in console)." },
446
+ kind: { type: "string", enum: [...KINDS], description: "ask @me: what it is about: question (default), plan, merge or release." },
447
+ choices: {
448
+ type: "array",
449
+ items: {
450
+ type: "object",
451
+ properties: {
452
+ id: { type: "string" },
453
+ label: { type: "string" },
454
+ options: { type: "array", items: { type: "object", properties: { id: { type: "string" }, label: { type: "string" }, recommended: { type: "boolean" } }, required: ["id", "label"] } }
455
+ },
456
+ required: ["id", "label", "options"]
457
+ },
458
+ description: 'ask @me: several points in one ask, instead of options: [{id: "D1", label, options: [{id, label, recommended?}]}], 1–12 groups of 2–6 options, at most one recommended each; the user picks one option per group and you get one "D1: label [a]" line per group.'
459
+ }
460
+ };
461
+ var DECISION_ASK_FIELDS = Object.keys(DECISION_ASK_PROPERTIES);
462
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
463
+ function decisionAskFields(args) {
464
+ const out = {};
465
+ const { options, recommendation, urgency, tier, kind, choices } = args;
466
+ const objects = (value, field, shape) => {
467
+ if (!Array.isArray(value) || !value.every(isObject))
468
+ throw new Error(`"${field}" must be a list of ${shape}`);
469
+ return value;
470
+ };
471
+ if (options !== undefined)
472
+ out.options = objects(options, "options", "{id, label, safe?}");
473
+ if (recommendation !== undefined) {
474
+ if (!isObject(recommendation) || typeof recommendation.option !== "string")
475
+ throw new Error(`"recommendation" must be {option, why?}`);
476
+ out.recommendation = recommendation;
477
+ }
478
+ const oneOf = (value, field, allowed) => {
479
+ if (value === undefined)
480
+ return;
481
+ if (typeof value !== "string" || !allowed.includes(value))
482
+ throw new Error(`"${field}" is one of ${allowed.join(", ")}`);
483
+ out[field] = value;
484
+ };
485
+ oneOf(urgency, "urgency", URGENCIES);
486
+ oneOf(tier, "tier", TIERS);
487
+ oneOf(kind, "kind", KINDS);
488
+ if (choices !== undefined) {
489
+ const shape = "{id, label, options: [{id, label, recommended?}]}";
490
+ for (const group of objects(choices, "choices", shape))
491
+ objects(group.options, "choices", shape);
492
+ out.choices = choices;
493
+ }
494
+ return out;
495
+ }
422
496
  // packages/sdk/src/errors.ts
423
497
  class HubError extends Error {
424
498
  status;
@@ -843,7 +917,7 @@ var segment = (value) => encodeURIComponent(value).replace(/%40/g, "@");
843
917
  var bareRole = (role) => role.replace(/^@@/, "");
844
918
  var decisionFields = (options) => {
845
919
  const out = {};
846
- for (const key of ["options", "recommendation", "urgency", "tier", "deadline", "onTimeout", "about"])
920
+ for (const key of ["options", "recommendation", "choices", "kind", "urgency", "tier", "deadline", "onTimeout", "about"])
847
921
  if (options[key] !== undefined)
848
922
  out[key] = options[key];
849
923
  return out;
@@ -852,6 +926,7 @@ var whyField = (options) => options.why !== undefined ? { why: options.why } : {
852
926
  var signalOf = (options) => options.signal ? { signal: options.signal } : {};
853
927
  var idempotent = (options) => ({ ...signalOf(options), ...options.idempotencyKey ? { headers: { "idempotency-key": options.idempotencyKey } } : {} });
854
928
  var sendFields = (options) => ({
929
+ ...options.item !== undefined ? { item: options.item } : {},
855
930
  ...options.wake !== undefined ? { wake: options.wake } : {},
856
931
  ...options.urgent !== undefined ? { urgent: options.urgent } : {},
857
932
  ...options.notAfter !== undefined ? { notAfter: options.notAfter } : {}
@@ -1162,6 +1237,15 @@ class HubClient {
1162
1237
  grants(options = {}) {
1163
1238
  return this.call("GET", `${AGENT2}/grants`, signalOf(options));
1164
1239
  }
1240
+ heldGrants(options = {}) {
1241
+ return this.call("GET", `${AGENT2}/grants/mine`, signalOf(options));
1242
+ }
1243
+ heldGrant(id, options = {}) {
1244
+ return this.call("GET", `${AGENT2}/grants/mine/${segment(id)}`, signalOf(options));
1245
+ }
1246
+ revokeHeldGrant(id, options = {}) {
1247
+ return this.call("DELETE", `${AGENT2}/grants/mine/${segment(id)}`, signalOf(options));
1248
+ }
1165
1249
  grant(id, options = {}) {
1166
1250
  return this.call("GET", `${AGENT2}/grants/${segment(id)}`, signalOf(options));
1167
1251
  }
@@ -1191,7 +1275,7 @@ class HubClient {
1191
1275
  // packages/sdk/src/envelope.ts
1192
1276
  var LABEL_MAX = 120;
1193
1277
  var ANSI = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|\u001b[P^_][^\u001b]*(?:\u001b\\)?|\u001b[ -/]*[0-~]?|\u009b[0-?]*[ -/]*[@-~]/g;
1194
- var CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028-\u202e\u2060-\u206f\ufeff]/g;
1278
+ var CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028-\u202e\u2060-\u206f\ufeff\p{Cf}]/gu;
1195
1279
  function sanitizeText(text) {
1196
1280
  return text.replace(/\r\n?/g, `
1197
1281
  `).replace(ANSI, "").replace(CONTROL, "");
@@ -1238,13 +1322,16 @@ function renderEnvelope(input) {
1238
1322
  var WAKE_SKIP_TEXT = {
1239
1323
  wake_false: "sent without a wake",
1240
1324
  subagent: "subagents are never woken",
1241
- hop_cap: "hop limit reached (8 agent messages without a human turn)",
1242
- budget: "wake budget spent (30 per 10 minutes)"
1325
+ hop_cap: "hop limit reached (too many agent messages without a human turn)",
1326
+ budget: "wake budget spent",
1327
+ item_budget: "item wake budget spent",
1328
+ item_loop: "this pair is looping on the item without progress; the user decides"
1243
1329
  };
1244
- function wakeSkipNote({ delivery, woke, wakeSkipped }) {
1330
+ function wakeSkipNote({ delivery, woke, wakeSkipped, wakeLimit }) {
1245
1331
  if (woke !== false || !wakeSkipped || delivery === "steer" || delivery === "turn")
1246
1332
  return "";
1247
- return ` — wake skipped: ${WAKE_SKIP_TEXT[wakeSkipped] ?? wakeSkipped}`;
1333
+ const detail = wakeLimit !== undefined && Number.isFinite(wakeLimit) && wakeLimit >= 1 ? wakeSkipped === "hop_cap" ? `hop limit reached (${wakeLimit} agent messages without a human turn)` : wakeSkipped === "budget" ? `wake budget spent (${wakeLimit} per 10 minutes)` : wakeSkipped === "item_budget" ? `item wake budget spent (${wakeLimit} wakes)` : undefined : undefined;
1334
+ return ` — wake skipped: ${detail ?? WAKE_SKIP_TEXT[wakeSkipped] ?? wakeSkipped}`;
1248
1335
  }
1249
1336
  // packages/cli/src/http.ts
1250
1337
  var EXIT = {
@@ -4325,6 +4412,19 @@ var RECORDING_LIMITS = {
4325
4412
  hubFlushMs: 2000,
4326
4413
  hubFlushEvents: 100
4327
4414
  };
4415
+ var DEV_LIMITS = {
4416
+ clipsPerRecording: 200,
4417
+ clipPreMs: 3000,
4418
+ clipPostMs: 2000,
4419
+ clipBytes: 192 * 1024,
4420
+ clipGapMs: 2000,
4421
+ repliesPerRecording: 2000,
4422
+ replyBytes: 1024 * 1024,
4423
+ userHeadroomBytes: 50 * 1024 * 1024,
4424
+ recordingHeadroomBytes: 15 * 1024 * 1024,
4425
+ tracesPerMinute: 30,
4426
+ logsPerMinute: 20
4427
+ };
4328
4428
  // packages/protocol/src/app-front.ts
4329
4429
  var ACCESS_TOKEN_TTL_MS = 60 * 60000;
4330
4430
  var REFRESH_TOKEN_IDLE_MS = 30 * 24 * 60 * 60000;
@@ -4846,7 +4946,7 @@ function readConfig(file, machineName) {
4846
4946
  return defaults(machineName);
4847
4947
  }
4848
4948
  }
4849
- var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4949
+ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4850
4950
  function readRawConfig(file) {
4851
4951
  let text;
4852
4952
  try {
@@ -4862,7 +4962,7 @@ function readRawConfig(file) {
4862
4962
  } catch {
4863
4963
  parsed = undefined;
4864
4964
  }
4865
- if (!isObject(parsed))
4965
+ if (!isObject2(parsed))
4866
4966
  throw new Error(`${file} is not a JSON object; fix or remove it first`);
4867
4967
  return parsed;
4868
4968
  }
@@ -5296,7 +5396,7 @@ class ProfileError extends RuntimeError {
5296
5396
  this.code = code;
5297
5397
  }
5298
5398
  }
5299
- var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5399
+ var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5300
5400
  var show = (v) => JSON.stringify(v)?.slice(0, 80) ?? String(v);
5301
5401
  var builtinDefault = () => ({ name: DEFAULT_PROFILE, providers: [], endpoints: [], allowGitRemote: false });
5302
5402
  function egressHost(v) {
@@ -5320,7 +5420,7 @@ function egressEndpoint(v) {
5320
5420
  return;
5321
5421
  host = v.slice(0, colon);
5322
5422
  port = Number(v.slice(colon + 1));
5323
- } else if (isObject2(v)) {
5423
+ } else if (isObject3(v)) {
5324
5424
  host = v.host;
5325
5425
  port = v.port;
5326
5426
  }
@@ -5377,9 +5477,9 @@ function normalizeOpenShellProfiles(raw) {
5377
5477
  const problems = [];
5378
5478
  const profiles = { [DEFAULT_PROFILE]: builtinDefault() };
5379
5479
  const unsupported = {};
5380
- if (raw !== undefined && !isObject2(raw))
5480
+ if (raw !== undefined && !isObject3(raw))
5381
5481
  problems.push("openshell.profiles must be an object of profile name to profile");
5382
- let entries = Object.entries(isObject2(raw) ? raw : {});
5482
+ let entries = Object.entries(isObject3(raw) ? raw : {});
5383
5483
  if (entries.length > PROFILES_MAX) {
5384
5484
  problems.push(`openshell.profiles: at most ${PROFILES_MAX} profiles; the rest are ignored`);
5385
5485
  entries = entries.slice(0, PROFILES_MAX);
@@ -5389,7 +5489,7 @@ function normalizeOpenShellProfiles(raw) {
5389
5489
  problems.push(`openshell.profiles: ${show(name)} is not a profile name (lowercase letters, digits, . _ -; at most ${NAME_MAX})`);
5390
5490
  continue;
5391
5491
  }
5392
- if (!isObject2(value)) {
5492
+ if (!isObject3(value)) {
5393
5493
  problems.push(`openshell.profiles.${name} must be an object`);
5394
5494
  continue;
5395
5495
  }
@@ -5823,7 +5923,7 @@ var HARNESS = /^[a-z0-9][a-z0-9._-]{0,63}$/;
5823
5923
  function nodeConfigPath(env = process.env, home = homedir2()) {
5824
5924
  return path9.join(path9.dirname(configPath(env, home)), NODE_CONFIG_FILE);
5825
5925
  }
5826
- var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5926
+ var isObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5827
5927
  var whole = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
5828
5928
  var show2 = (v) => JSON.stringify(v)?.slice(0, 80) ?? String(v);
5829
5929
  function configPathOf(raw, options) {
@@ -5845,7 +5945,7 @@ var isRoot = (p) => p === "/" || /^[A-Z]:\/$/.test(p);
5845
5945
  function normalizeNodeConfig(raw, options = {}) {
5846
5946
  const problems = [];
5847
5947
  let r = {};
5848
- if (isObject3(raw))
5948
+ if (isObject4(raw))
5849
5949
  r = raw;
5850
5950
  else if (raw !== undefined)
5851
5951
  problems.push("node.json must hold a JSON object");
@@ -5937,7 +6037,7 @@ function loopbackOrigin(raw) {
5937
6037
  return url.origin;
5938
6038
  }
5939
6039
  function normalizeOpenCode(raw, problems, options) {
5940
- if (!isObject3(raw)) {
6040
+ if (!isObject4(raw)) {
5941
6041
  problems.push("opencode must be an object");
5942
6042
  return;
5943
6043
  }
@@ -5948,7 +6048,7 @@ function normalizeOpenCode(raw, problems, options) {
5948
6048
  const list = Array.isArray(raw.servers) ? raw.servers : [];
5949
6049
  list.forEach((entry, i) => {
5950
6050
  const field = `opencode.servers[${i}]`;
5951
- if (!isObject3(entry))
6051
+ if (!isObject4(entry))
5952
6052
  return void problems.push(`${field} must be an object {url, passwordFile, directories?}`);
5953
6053
  const url = loopbackOrigin(entry.url);
5954
6054
  if (!url)
@@ -5976,7 +6076,7 @@ function normalizeOpenCode(raw, problems, options) {
5976
6076
  return { ...directories ? { directories } : {}, servers, ...timeoutSeconds !== undefined ? { timeoutSeconds } : {} };
5977
6077
  }
5978
6078
  function normalizeHarnesses(raw, problems) {
5979
- if (!isObject3(raw)) {
6079
+ if (!isObject4(raw)) {
5980
6080
  problems.push("harnesses must be an object of harness names to true or false");
5981
6081
  return;
5982
6082
  }
@@ -5992,7 +6092,7 @@ function normalizeHarnesses(raw, problems) {
5992
6092
  return out;
5993
6093
  }
5994
6094
  function normalizeOpenShell(raw, problems) {
5995
- if (!isObject3(raw)) {
6095
+ if (!isObject4(raw)) {
5996
6096
  problems.push("openshell must be an object");
5997
6097
  return;
5998
6098
  }
@@ -6234,10 +6334,13 @@ var DECISION_FLAGS = {
6234
6334
  deadline: "string",
6235
6335
  "on-timeout": "string",
6236
6336
  tier: "string",
6237
- about: "multi"
6337
+ about: "multi",
6338
+ choice: "multi",
6339
+ kind: "string"
6238
6340
  };
6239
- var URGENCIES = ["low", "normal", "high"];
6240
- var TIERS = ["confirm", "tap"];
6341
+ var URGENCIES2 = ["low", "normal", "high"];
6342
+ var KINDS2 = ["question", "plan", "merge", "release"];
6343
+ var TIERS2 = ["confirm", "tap"];
6241
6344
  var STATES = ["open", "answered", "expired", "closed"];
6242
6345
  var DECISION_STATES = STATES;
6243
6346
  function optionArg(value) {
@@ -6253,6 +6356,37 @@ function optionArg(value) {
6253
6356
  throw new UsageError(`--option ${id}: the label is empty`);
6254
6357
  return { id, label, ...safe ? { safe: true } : {} };
6255
6358
  }
6359
+ function choiceArg(value) {
6360
+ const usage = `--choice takes GROUP=Label:a=Option*,b=Option (got "${value}")`;
6361
+ const eq = value.indexOf("=");
6362
+ if (eq <= 0)
6363
+ throw new UsageError(usage);
6364
+ const id = value.slice(0, eq);
6365
+ const rest = value.slice(eq + 1);
6366
+ let split = -1;
6367
+ for (let i = rest.lastIndexOf(":");i >= 0; i = rest.lastIndexOf(":", i - 1)) {
6368
+ if (/^[^=,:\s][^=,:]*=/.test(rest.slice(i + 1))) {
6369
+ split = i;
6370
+ break;
6371
+ }
6372
+ }
6373
+ if (split < 0)
6374
+ throw new UsageError(usage);
6375
+ const label = rest.slice(0, split);
6376
+ if (!label.trim())
6377
+ throw new UsageError(`--choice ${id}: the label is empty`);
6378
+ const options = rest.slice(split + 1).split(",").map((option) => {
6379
+ const at = option.indexOf("=");
6380
+ if (at <= 0)
6381
+ throw new UsageError(usage);
6382
+ const recommended = option.endsWith("*");
6383
+ const text = option.slice(at + 1, recommended ? -1 : undefined);
6384
+ if (!text.trim())
6385
+ throw new UsageError(`--choice ${id}: option ${option.slice(0, at)} has an empty label`);
6386
+ return { id: option.slice(0, at), label: text, ...recommended ? { recommended: true } : {} };
6387
+ });
6388
+ return { id, label, options };
6389
+ }
6256
6390
  var oneOf2 = (value, allowed, flag) => {
6257
6391
  if (value !== undefined && !allowed.includes(value))
6258
6392
  throw new UsageError(`${flag} is one of ${allowed.join(", ")}`);
@@ -6269,13 +6403,19 @@ function decisionOptions(flags, to) {
6269
6403
  if (why !== undefined && recommend === undefined)
6270
6404
  throw new UsageError("--why explains --recommend");
6271
6405
  const deadline = str(flags, "deadline");
6272
- const urgency = oneOf2(str(flags, "urgency"), URGENCIES, "--urgency");
6273
- const tier = oneOf2(str(flags, "tier"), TIERS, "--tier");
6406
+ const urgency = oneOf2(str(flags, "urgency"), URGENCIES2, "--urgency");
6407
+ const tier = oneOf2(str(flags, "tier"), TIERS2, "--tier");
6274
6408
  const onTimeout = str(flags, "on-timeout");
6275
6409
  const options = flags.option;
6276
6410
  const about = flags.about;
6411
+ const choices = flags.choice;
6412
+ if (choices && options)
6413
+ throw new UsageError("--choice and --option don't mix: give one or the other");
6414
+ const kind = oneOf2(str(flags, "kind"), KINDS2, "--kind");
6277
6415
  return {
6278
6416
  ...options ? { options: options.map(optionArg) } : {},
6417
+ ...choices ? { choices: choices.map(choiceArg) } : {},
6418
+ ...kind ? { kind } : {},
6279
6419
  ...recommend !== undefined ? { recommendation: { option: recommend, ...why !== undefined ? { why } : {} } } : {},
6280
6420
  ...urgency ? { urgency } : {},
6281
6421
  ...tier ? { tier } : {},
@@ -6289,6 +6429,10 @@ function stateArg(value) {
6289
6429
  }
6290
6430
  var iso2 = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
6291
6431
  function choicesText(decision) {
6432
+ if (decision.choices?.length) {
6433
+ return decision.choices.map((g) => `${g.id} ${oneLine(g.label, 120)}: ${g.options.map((o) => `${o.id} ${oneLine(o.label, 80)}${o.recommended ? " (recommended)" : ""}`).join(" · ")}`).join(`
6434
+ `);
6435
+ }
6292
6436
  const options = decision.options.map((o) => `${o.id} ${oneLine(o.label, 80)}${o.safe ? " (safe)" : ""}`).join(" · ");
6293
6437
  const r = decision.recommendation;
6294
6438
  const recommended = r ? `recommended: ${r.option}${r.why ? ` (${oneLine(r.why, 200)})` : ""}` : "";
@@ -6297,7 +6441,8 @@ function choicesText(decision) {
6297
6441
  function stateText(decision) {
6298
6442
  if (decision.state === "answered") {
6299
6443
  const a = decision.answer;
6300
- return `answered${a?.option ? ` ${a.option}` : ""}${a?.answeredBy === "timeout" ? " (timeout)" : ""}`;
6444
+ const picks = a?.picks ? ` ${Object.entries(a.picks).map(([group, option]) => `${group}=${option}`).join(" ")}` : "";
6445
+ return `answered${a?.option ? ` ${a.option}` : ""}${picks}${a?.answeredBy === "timeout" ? " (timeout)" : ""}`;
6301
6446
  }
6302
6447
  if (decision.state === "open" && decision.deadline !== undefined)
6303
6448
  return `open (deadline ${iso2(decision.deadline)})`;
@@ -6474,7 +6619,7 @@ class ToolArgError extends Error {
6474
6619
 
6475
6620
  // packages/cli/src/artifacts-mcp.ts
6476
6621
  var MCP_INLINE_TEXT_BYTES = 256 * 1024;
6477
- var KINDS = ["markdown", "code", "image", "table", "chart", "dashboard", "html", "app", "file"];
6622
+ var KINDS3 = ["markdown", "code", "image", "table", "chart", "dashboard", "html", "app", "file"];
6478
6623
  var DEFAULT_MEDIA = { markdown: "text/markdown", html: "text/html", table: "text/csv", chart: "application/json", dashboard: "application/json" };
6479
6624
  var PATH3 = "/api/agent/artifacts";
6480
6625
  var STRING = { type: "string" };
@@ -6570,7 +6715,7 @@ var artifactTools = [
6570
6715
  description: "Create a persistent artifact with its first revision from small inline text or references to staged bytes. Needs scope artifacts:write for an app token.",
6571
6716
  properties: {
6572
6717
  title: STRING,
6573
- kind: { type: "string", enum: KINDS },
6718
+ kind: { type: "string", enum: KINDS3 },
6574
6719
  description: STRING,
6575
6720
  visibility: { type: "string", enum: ["private", "workspace"] },
6576
6721
  workspaceID: STRING,
@@ -6582,8 +6727,8 @@ var artifactTools = [
6582
6727
  run: async (hub, args, signal) => {
6583
6728
  const title = need(args, "title");
6584
6729
  const kind = need(args, "kind");
6585
- if (!KINDS.includes(kind))
6586
- throw new ToolArgError(`"kind" must be one of ${KINDS.join(", ")}`);
6730
+ if (!KINDS3.includes(kind))
6731
+ throw new ToolArgError(`"kind" must be one of ${KINDS3.join(", ")}`);
6587
6732
  const body = files(args, kind);
6588
6733
  const description = optional2(args, "description");
6589
6734
  const visibility = optional2(args, "visibility");
@@ -6618,7 +6763,7 @@ var STRING2 = { type: "string" };
6618
6763
  var ID3 = { type: "string", description: "The artifact's ID (art_…)." };
6619
6764
  var JOB = { type: "string", description: "The job's ID (job_…)." };
6620
6765
  var REVISION = { type: "integer", description: "The job revision you read (from artifact_job)." };
6621
- var KINDS2 = ["ask", "propose-edit", "run-action"];
6766
+ var KINDS4 = ["ask", "propose-edit", "run-action"];
6622
6767
  var need2 = (args, key) => {
6623
6768
  const value = args[key];
6624
6769
  if (typeof value !== "string" || !value.trim())
@@ -6695,7 +6840,7 @@ var artifactJobTools = [
6695
6840
  id: ID3,
6696
6841
  baseRevisionID: { type: "string", description: "The revision the job is about (propose-edit: the latest)." },
6697
6842
  instruction: { type: "string", description: "What to do (plain text, at most 8000 characters)." },
6698
- kind: { type: "string", enum: KINDS2 },
6843
+ kind: { type: "string", enum: KINDS4 },
6699
6844
  action: { type: "string", description: "run-action: one of the artifact's declared actions." },
6700
6845
  machine: { type: "string", description: "Launch a handler session on this machine…" },
6701
6846
  repo: { type: "string", description: "…in this absolute repository path." },
@@ -6710,8 +6855,8 @@ var artifactJobTools = [
6710
6855
  readOnly: false,
6711
6856
  run: async (hub, args, signal) => {
6712
6857
  const kind = optional3(args, "kind");
6713
- if (kind && !KINDS2.includes(kind))
6714
- throw new ToolArgError(`"kind" must be one of ${KINDS2.join(", ")}`);
6858
+ if (kind && !KINDS4.includes(kind))
6859
+ throw new ToolArgError(`"kind" must be one of ${KINDS4.join(", ")}`);
6715
6860
  const action = optional3(args, "action");
6716
6861
  const apply = optional3(args, "apply");
6717
6862
  const key = optional3(args, "key");
@@ -6883,7 +7028,7 @@ function notAfterOf(args) {
6883
7028
  }
6884
7029
  throw new ToolArgError(`"notAfter" must be epoch milliseconds or an ISO 8601 date-time like 2027-01-15T12:00:00Z`);
6885
7030
  }
6886
- var line4 = (i) => `${i.id} ${i.status}${i.progress ? `/${i.progress}` : ""} ${i.priority} r${i.revision} ${sanitizeText(i.title).replace(/\s+/g, " ")}`;
7031
+ var line4 = (i) => `${i.id} ${i.status}${i.progress ? `/${i.progress}` : ""} ${i.priority} r${i.revision} ${sanitizeText(i.title).replace(/\s+/g, " ")}${i.usage ? ` wakes ${i.usage.wakes}/${i.usage.budget.wakes} · ${i.usage.hours.toFixed(1)}/${i.usage.budget.hours}h` : ""}`;
6887
7032
  function intakeText(i) {
6888
7033
  const from = i.source.kind === "user" ? "the user" : i.source.kind === "app" ? `app ${i.source.tokenID}` : `session ${i.source.sessionID}`;
6889
7034
  return [
@@ -6897,7 +7042,8 @@ function intakeText(i) {
6897
7042
  ...i.criteria?.length ? ["done means:", ...i.criteria.map((c) => `- ${sanitizeText(c)}`)] : [],
6898
7043
  ...i.ref ? [`ref: ${sanitizeText(i.ref)}`] : [],
6899
7044
  ...i.reason ? [`reason: ${sanitizeText(i.reason)}`] : [],
6900
- ...i.lastError ? [`last routing error: ${i.lastError} (retried)`] : []
7045
+ ...i.lastError ? [`last routing error: ${i.lastError} (retried)`] : [],
7046
+ ...i.usage ? [`usage: ${i.usage.wakes}/${i.usage.budget.wakes} wakes · ${i.usage.hours.toFixed(1)}/${i.usage.budget.hours} hours (paused and blocked time counts)${i.usage.chargedTo ? `; last charged to ${i.usage.chargedTo}` : ""}`] : []
6901
7047
  ].join(`
6902
7048
  `);
6903
7049
  }
@@ -6984,35 +7130,215 @@ ${intakeText(intake)}`;
6984
7130
  }
6985
7131
  ];
6986
7132
 
6987
- // packages/cli/src/teams-mcp.ts
7133
+ // packages/cli/src/subscriptions.ts
7134
+ var SUBSCRIBE_FLAGS = { type: "multi", delivery: "string", batch: "string", expires: "string", "max-per-hour": "string", subscriber: "string", note: "string" };
7135
+ var DELIVERIES2 = ["queue", "steer", "silent"];
7136
+ var safe = (v, max = 120) => sanitizeLabel(String(v), max);
7137
+ function subscribeInput(addrs, flags) {
7138
+ if (!addrs.length)
7139
+ throw new UsageError("needs at least one address (a session, @@role, #team or children)");
7140
+ const delivery = str(flags, "delivery");
7141
+ if (delivery !== undefined && !DELIVERIES2.includes(delivery))
7142
+ throw new UsageError(`--delivery is one of ${DELIVERIES2.join(", ")}`);
7143
+ const batch = str(flags, "batch");
7144
+ const expires = str(flags, "expires");
7145
+ const max = str(flags, "max-per-hour");
7146
+ if (max !== undefined && !/^\d+$/.test(max))
7147
+ throw new UsageError("--max-per-hour takes a whole number (1–120)");
7148
+ const types = flags.type;
7149
+ const subscriber = str(flags, "subscriber");
7150
+ const note = str(flags, "note");
7151
+ return {
7152
+ to: addrs.length === 1 ? addrs[0] : [...addrs],
7153
+ ...types?.length ? { types: types.flatMap((t) => t.split(",")).map((t) => t.trim()).filter(Boolean) } : {},
7154
+ ...delivery !== undefined ? { delivery } : {},
7155
+ ...batch !== undefined ? { batch: durationArg(batch, "--batch") } : {},
7156
+ ...expires !== undefined ? { expires: durationArg(expires, "--expires") } : {},
7157
+ ...max !== undefined ? { maxPerHour: Number(max) } : {},
7158
+ ...subscriber !== undefined ? { subscriber } : {},
7159
+ ...note !== undefined ? { note } : {}
7160
+ };
7161
+ }
7162
+ var targets = (s) => s.targets.map((t) => t.kind === "children" ? "its subagents" : safe(t.addr, 80)).join(", ");
7163
+ var iso3 = (ms) => new Date(ms).toISOString().slice(0, 16) + "Z";
7164
+ function subscriptionLine(s) {
7165
+ const extra = [safe(s.delivery, 10), `batch ${Math.round(s.batchMs / 1000)}s`, `≤${s.maxPerHour}/h`, `expires ${iso3(s.expiresAt)}`, `${s.stats.delivered} delivered`, ...s.pending ? [`${s.pending} pending`] : [], ...s.stats.dropped ? [`${s.stats.dropped} dropped`] : [], ...s.stats.suppressed ? [`${s.stats.suppressed} loop-suppressed`] : []];
7166
+ return `${safe(s.id, 40)} ${safe(s.subscriber, 80)} ← ${targets(s)} [${s.types.map((t) => safe(t, 40)).join(",")}] ${extra.join(" · ")}${s.note ? ` "${safe(s.note, 200)}"` : ""}`;
7167
+ }
7168
+ var subscribedText = (b) => `subscribed ${subscriptionLine(b.subscription)}
7169
+ the subscriber gets one hub message per batch window; stop with \`hub unsubscribe ${safe(b.subscription.id, 40)}\``;
7170
+ var subscriptionsText = (b) => b.subscriptions.length ? b.subscriptions.map(subscriptionLine).join(`
7171
+ `) : "no subscriptions";
7172
+
7173
+ // packages/cli/src/subscriptions-mcp.ts
6988
7174
  var STRING4 = { type: "string" };
6989
- var STRINGS = { type: "array", items: STRING4 };
7175
+ var need4 = (args, key) => {
7176
+ const value = args[key];
7177
+ if (typeof value !== "string" || !value.trim())
7178
+ throw new ToolArgError(`"${key}" is required (a non-empty string)`);
7179
+ return value;
7180
+ };
7181
+ var optional5 = (args, key) => {
7182
+ const value = args[key];
7183
+ if (value === undefined || value === null)
7184
+ return;
7185
+ if (typeof value !== "string")
7186
+ throw new ToolArgError(`"${key}" must be a string`);
7187
+ return value.trim() || undefined;
7188
+ };
7189
+ var array = (args, key) => {
7190
+ const value = args[key];
7191
+ if (value === undefined || value === null)
7192
+ return;
7193
+ if (!Array.isArray(value) || !value.every((v) => typeof v === "string"))
7194
+ throw new ToolArgError(`"${key}" must be a list of strings`);
7195
+ return value;
7196
+ };
7197
+ var number = (args, key, min, max) => {
7198
+ const value = args[key];
7199
+ if (value === undefined || value === null)
7200
+ return;
7201
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max)
7202
+ throw new ToolArgError(`"${key}" must be a whole number from ${min} to ${max}`);
7203
+ return value;
7204
+ };
7205
+ var subscriptionTools = [
7206
+ {
7207
+ name: "subscribe",
7208
+ description: "Follow journal events for a subscriber session. A session subscribes itself; users and apps name subscriber. Apps need events:subscribe, sessions:read, relevant type scopes and messages:send unless silent. Hub checks visibility, rates and loops.",
7209
+ properties: {
7210
+ to: { type: ["string", "array"], items: STRING4, description: "One or more session/role/team addresses, or children." },
7211
+ subscriber: STRING4,
7212
+ types: { type: "array", items: STRING4 },
7213
+ delivery: { type: "string", enum: ["queue", "steer", "silent"] },
7214
+ batch: { type: "number", description: "Seconds per batch (1–3600)." },
7215
+ expires: { type: "number", description: "Seconds until expiry." },
7216
+ maxPerHour: { type: "integer", minimum: 1, maximum: 120 },
7217
+ note: STRING4
7218
+ },
7219
+ required: ["to"],
7220
+ readOnly: false,
7221
+ run: async (hub, args, signal) => {
7222
+ const to = typeof args.to === "string" && args.to.trim() ? args.to : array(args, "to");
7223
+ if (!to || Array.isArray(to) && !to.length)
7224
+ throw new ToolArgError('"to" needs at least one address');
7225
+ const delivery = optional5(args, "delivery");
7226
+ if (delivery && !["queue", "steer", "silent"].includes(delivery))
7227
+ throw new ToolArgError('"delivery" is queue, steer or silent');
7228
+ const subscriber = optional5(args, "subscriber");
7229
+ const types = array(args, "types");
7230
+ const batch = number(args, "batch", 1, 3600);
7231
+ const expires = number(args, "expires", 1, 30 * 86400);
7232
+ const maxPerHour = number(args, "maxPerHour", 1, 120);
7233
+ const note = optional5(args, "note");
7234
+ const input = { to, ...subscriber ? { subscriber } : {}, ...types ? { types } : {}, ...delivery ? { delivery } : {}, ...batch ? { batch } : {}, ...expires ? { expires } : {}, ...maxPerHour ? { maxPerHour } : {}, ...note ? { note } : {} };
7235
+ return subscribedText(await hub.subscribe(input, { signal }));
7236
+ }
7237
+ },
7238
+ {
7239
+ name: "subscriptions",
7240
+ description: "List subscriptions visible to this credential (session: its own; app: created by it; user: all). Optional session filter; route enforces scopes and tenancy.",
7241
+ properties: { session: STRING4 },
7242
+ readOnly: true,
7243
+ run: async (hub, args, signal) => subscriptionsText(await hub.subscriptions({ ...optional5(args, "session") ? { session: optional5(args, "session") } : {}, signal }))
7244
+ },
7245
+ {
7246
+ name: "unsubscribe",
7247
+ description: "Remove a subscription this credential owns. An unknown or another caller's ID answers not_found.",
7248
+ properties: { id: STRING4 },
7249
+ required: ["id"],
7250
+ readOnly: false,
7251
+ organizes: true,
7252
+ run: async (hub, args, signal) => {
7253
+ const id = need4(args, "id");
7254
+ await hub.unsubscribe(id, { signal });
7255
+ return `unsubscribed ${id}`;
7256
+ }
7257
+ }
7258
+ ];
7259
+
7260
+ // packages/cli/src/mcp-operations.ts
7261
+ var STRING5 = { type: "string" };
7262
+ var need5 = (args, key) => {
7263
+ const v = args[key];
7264
+ if (typeof v !== "string" || !v.trim())
7265
+ throw new ToolArgError(`"${key}" is required (a non-empty string)`);
7266
+ return v;
7267
+ };
7268
+ var userOnly = async (hub, signal) => {
7269
+ if ((await hub.whoami({ signal })).actor.kind !== "user")
7270
+ throw new CliError(EXIT.forbidden, "only the user can use this tool", "Use the user's device credential");
7271
+ };
7272
+ var operationTools = [
7273
+ {
7274
+ name: "session_model_set",
7275
+ description: "Set one existing session's model (providerID, modelID and optional variant), exactly as the console Model chip does. User device credential only; the console route checks ownership, catalog, availability and audit. An app or session cannot set it.",
7276
+ properties: { session: STRING5, model: { type: "object", properties: { providerID: STRING5, modelID: STRING5, variant: STRING5 }, required: ["providerID", "modelID"] } },
7277
+ required: ["session", "model"],
7278
+ readOnly: false,
7279
+ run: async (hub, args, signal) => {
7280
+ await userOnly(hub, signal);
7281
+ const model = args.model;
7282
+ if (typeof model !== "object" || model === null || Array.isArray(model))
7283
+ throw new ToolArgError('"model" is required (providerID, modelID, variant?)');
7284
+ const result = await hub.call("PUT", `/api/sessions/${encodeURIComponent(need5(args, "session"))}/model`, { body: { model }, signal });
7285
+ return JSON.stringify(result);
7286
+ }
7287
+ },
7288
+ {
7289
+ name: "machines",
7290
+ description: "The user's own enrolled machines, online status and processes. User device credential only; wraps GET /api/machines. No machine tokens or secrets.",
7291
+ properties: {},
7292
+ readOnly: true,
7293
+ run: async (hub, _args, signal) => {
7294
+ await userOnly(hub, signal);
7295
+ return JSON.stringify(await hub.call("GET", "/api/machines", { signal }));
7296
+ }
7297
+ },
7298
+ {
7299
+ name: "launch_nodes",
7300
+ description: "The user's connected launch nodes: machine, workspace roots, capacity/used/free slots, runtimes (harnesses) and labels. User device credential only; no tokens, connection IDs or mutation.",
7301
+ properties: {},
7302
+ readOnly: true,
7303
+ run: async (hub, _args, signal) => {
7304
+ await userOnly(hub, signal);
7305
+ return JSON.stringify(await hub.call("GET", "/api/agent/launch-nodes", { signal }));
7306
+ }
7307
+ }
7308
+ ];
7309
+
7310
+ // packages/cli/src/teams-mcp.ts
7311
+ var STRING6 = { type: "string" };
7312
+ var STRINGS = { type: "array", items: STRING6 };
6990
7313
  var TEAM2 = { type: "string", description: "The team: its ID (team_…) or its name." };
6991
7314
  var REVISION2 = { type: "integer", minimum: 0, description: "The team's revision you read (team_show); a stale one answers conflict with the current revision." };
6992
7315
  var IDEMPOTENCY = { type: "string", description: "Idempotency-Key: the same key and arguments within 24 h answer the first result again." };
6993
- var KINDS3 = { type: "array", items: { type: "string", enum: ["msg", "ask"] }, description: "What the path permits: msg and/or ask (replies to a permitted ask never need a path)." };
7316
+ var KINDS5 = { type: "array", items: { type: "string", enum: ["msg", "ask"] }, description: "What the path permits: msg and/or ask (replies to a permitted ask never need a path)." };
6994
7317
  var DIRECTION = { type: "string", enum: ["one-way", "two-way"], description: "two-way: `to` may also contact `from` with the same kinds." };
6995
7318
  var ROLE_FIELDS = {
6996
- label: { ...STRING4, description: "A display label (confers no authority)." },
6997
- description: STRING4,
7319
+ label: { ...STRING6, description: "A display label (confers no authority)." },
7320
+ description: STRING6,
6998
7321
  externalContact: { type: "boolean", description: "Holders may contact sessions outside all their teams (default false)." },
6999
7322
  responsibilities: { ...STRINGS, description: "Short lines (at most 16)." },
7000
- profile: { type: "object", properties: { profileID: STRING4, version: { type: "integer", minimum: 1 } }, required: ["profileID"], description: "The agent profile members should run (reported, never pinned by staffing)." },
7001
- prefs: { type: "object", properties: { harness: STRING4, model: STRING4, machineID: STRING4 }, description: "Placement preferences for launched members (advisory)." },
7323
+ profile: { type: "object", properties: { profileID: STRING6, version: { type: "integer", minimum: 1 } }, required: ["profileID"], description: "The agent profile members should run (reported, never pinned by staffing)." },
7324
+ prefs: { type: "object", properties: { harness: STRING6, model: STRING6, machineID: STRING6 }, description: "Placement preferences for launched members (advisory)." },
7002
7325
  minMembers: { type: "integer", minimum: 0, maximum: 16 },
7003
7326
  maxMembers: { type: "integer", minimum: 1, maximum: 16 }
7004
7327
  };
7005
- var ROLE = { type: "object", properties: { name: { ...STRING4, description: "A slug: lowercase letters, digits, dashes." }, ...ROLE_FIELDS }, required: ["name"] };
7006
- var EDGE = { type: "object", properties: { from: STRING4, to: STRING4, kinds: KINDS3, direction: DIRECTION }, required: ["from", "to", "kinds", "direction"] };
7328
+ var ROLE = { type: "object", properties: { name: { ...STRING6, description: "A slug: lowercase letters, digits, dashes." }, ...ROLE_FIELDS }, required: ["name"] };
7329
+ var EDGE = { type: "object", properties: { from: STRING6, to: STRING6, kinds: KINDS5, direction: DIRECTION }, required: ["from", "to", "kinds", "direction"] };
7007
7330
  var SPEC_FIELDS = {
7008
- title: { ...STRING4, description: "1–120 characters." },
7009
- description: STRING4,
7331
+ title: { ...STRING6, description: "1–120 characters." },
7332
+ description: STRING6,
7010
7333
  mode: { type: "string", enum: ["open", "restricted"], description: "open: every member may msg/ask every other member. restricted: only the drawn paths (edges)." },
7334
+ hopLimit: { type: "integer", minimum: 1, maximum: 1e4, description: "Agent-only wake hop limit between this team's seats; at most the hub's hop ceiling (agent_limits hopCeiling, default 100). The effective limit is the lower of this and the hub-wide hopLimit. Unset uses the hub-wide limit." },
7335
+ itemBudget: { type: "object", properties: { wakes: { type: "integer", minimum: 10, maximum: 5000 }, hours: { type: "integer", minimum: 1, maximum: 720 } }, required: ["wakes", "hours"], description: "User-only per-item budget override: agent wakes and hours in progress." },
7336
+ loop: { type: "object", properties: { messages: { type: "integer", minimum: 4, maximum: 200 }, windowMinutes: { type: "integer", minimum: 5, maximum: 240 } }, required: ["messages", "windowMinutes"], description: "User-only no-progress pair-message threshold and window." },
7011
7337
  roles: { type: "array", items: ROLE, description: "1–32 roles." },
7012
- entryRole: { ...STRING4, description: "The role that receives submitted work (e.g. pm) and supervises members." },
7338
+ entryRole: { ...STRING6, description: "The role that receives submitted work (e.g. pm) and supervises members." },
7013
7339
  edges: { type: "array", items: EDGE, description: "Communication paths between roles (restricted mode)." },
7014
7340
  tags: { ...STRINGS, description: "Slugs." },
7015
- metadata: { type: "object", additionalProperties: STRING4, description: "String key/value pairs (keys are slugs)." },
7341
+ metadata: { type: "object", additionalProperties: STRING6, description: "String key/value pairs (keys are slugs)." },
7016
7342
  projects: { ...STRINGS, description: "Project names the team works on." }
7017
7343
  };
7018
7344
  var LAUNCH = {
@@ -7023,11 +7349,11 @@ var LAUNCH = {
7023
7349
  type: "object",
7024
7350
  description: "Where the member works on its machine.",
7025
7351
  properties: {
7026
- repo: { ...STRING4, description: "The repository's absolute path on the machine." },
7352
+ repo: { ...STRING6, description: "The repository's absolute path on the machine." },
7027
7353
  mode: { type: "string", enum: ["worktree", "checkout", "clone"], description: "worktree (default; needs branch), checkout, or clone (needs remote)." },
7028
- branch: { ...STRING4, description: "The branch to create or check out (a safe git branch name; required for worktree)." },
7029
- base: { ...STRING4, description: "What a new branch starts from." },
7030
- remote: { ...STRING4, description: "An https or ssh git remote, without credentials (required for clone)." }
7354
+ branch: { ...STRING6, description: "The branch to create or check out (a safe git branch name; required for worktree)." },
7355
+ base: { ...STRING6, description: "What a new branch starts from." },
7356
+ remote: { ...STRING6, description: "An https or ssh git remote, without credentials (required for clone)." }
7031
7357
  },
7032
7358
  required: ["repo"]
7033
7359
  },
@@ -7035,8 +7361,8 @@ var LAUNCH = {
7035
7361
  type: "object",
7036
7362
  description: "Which machine runs it.",
7037
7363
  properties: {
7038
- machine: { ...STRING4, description: "A machine ID or name." },
7039
- labels: { type: "object", additionalProperties: STRING4, description: "Node labels to match." },
7364
+ machine: { ...STRING6, description: "A machine ID or name." },
7365
+ labels: { type: "object", additionalProperties: STRING6, description: "Node labels to match." },
7040
7366
  mode: { type: "string", enum: ["require", "prefer"], description: "require (default) or prefer the machine/labels." }
7041
7367
  }
7042
7368
  },
@@ -7044,7 +7370,7 @@ var LAUNCH = {
7044
7370
  type: "object",
7045
7371
  properties: {
7046
7372
  kind: { type: "string", enum: ["host", "openshell"], description: "host (default) or openshell (clone workspaces only)." },
7047
- profile: { ...STRING4, description: "An openshell sandbox profile (openshell only)." }
7373
+ profile: { ...STRING6, description: "An openshell sandbox profile (openshell only)." }
7048
7374
  }
7049
7375
  },
7050
7376
  end: {
@@ -7056,27 +7382,27 @@ var LAUNCH = {
7056
7382
  cleanup: { type: "string", enum: ["keep", "remove"], description: "keep (default) or remove the workspace at the end." }
7057
7383
  }
7058
7384
  },
7059
- name: { ...STRING4, description: "The session's @name, without the @." },
7385
+ name: { ...STRING6, description: "The session's @name, without the @." },
7060
7386
  roles: { ...STRINGS, description: "Roles the session claims (up to 10)." },
7061
- harness: { ...STRING4, description: "The harness (default opencode)." },
7062
- agent: { ...STRING4, description: "The harness agent." },
7063
- model: { ...STRING4, description: "provider/model." },
7064
- prompt: { ...STRING4, description: "The first prompt (up to 10000 characters)." },
7065
- intent: { ...STRING4, description: "What the session is for (one line)." },
7066
- labels: { type: "object", additionalProperties: STRING4, description: "Free-form app labels." },
7067
- agentProfile: { type: "object", properties: { profileID: STRING4, version: { type: "integer", minimum: 1 } }, required: ["profileID", "version"], description: "The profile version to pin (needs profiles:assign for an app)." }
7387
+ harness: { ...STRING6, description: "The harness (default opencode)." },
7388
+ agent: { ...STRING6, description: "The harness agent." },
7389
+ model: { ...STRING6, description: "provider/model." },
7390
+ prompt: { ...STRING6, description: "The first prompt (up to 10000 characters)." },
7391
+ intent: { ...STRING6, description: "What the session is for (one line)." },
7392
+ labels: { type: "object", additionalProperties: STRING6, description: "Free-form app labels." },
7393
+ agentProfile: { type: "object", properties: { profileID: STRING6, version: { type: "integer", minimum: 1 } }, required: ["profileID", "version"], description: "The profile version to pin (needs profiles:assign for an app)." }
7068
7394
  },
7069
7395
  required: ["workspace"]
7070
7396
  };
7071
7397
  var SCOPES = "App tokens need scope teams:read (reads) or teams:manage (changes); seats, status and staffing also need sessions:read. Session tokens read only teams they are seated in and never change definitions. Team roles confer no authority.";
7072
7398
  var ArgError = ToolArgError;
7073
- var need4 = (args, key) => {
7399
+ var need6 = (args, key) => {
7074
7400
  const value = args[key];
7075
7401
  if (typeof value !== "string" || !value.trim())
7076
7402
  throw new ArgError(`"${key}" is required (a non-empty string)`);
7077
7403
  return value;
7078
7404
  };
7079
- var optional5 = (args, key) => {
7405
+ var optional6 = (args, key) => {
7080
7406
  const value = args[key];
7081
7407
  if (value === undefined || value === null)
7082
7408
  return;
@@ -7131,6 +7457,9 @@ function specLines(spec) {
7131
7457
  const role = (r) => ` ${r.name}${r.name === spec.entryRole ? " (entry)" : ""}${r.label ? ` "${one(r.label)}"` : ""}${r.externalContact ? " external contact" : ""}${r.minMembers !== undefined || r.maxMembers !== undefined ? ` members ${r.minMembers ?? 0}–${r.maxMembers ?? 16}` : ""}${r.profile ? ` profile ${r.profile.profileID}${r.profile.version ? `@${r.profile.version}` : ""}` : ""}`;
7132
7458
  return [
7133
7459
  `mode: ${spec.mode}${spec.mode === "open" ? " (every member may msg/ask every other)" : " (only the paths below)"}`,
7460
+ ...spec.hopLimit !== undefined ? [`hop limit: ${spec.hopLimit}`] : [],
7461
+ ...spec.itemBudget ? [`item budget: ${spec.itemBudget.wakes} wakes, ${spec.itemBudget.hours} hours`] : [],
7462
+ ...spec.loop ? [`loop: ${spec.loop.messages} messages per ${spec.loop.windowMinutes} minutes`] : [],
7134
7463
  `entry role: ${spec.entryRole}`,
7135
7464
  "roles:",
7136
7465
  ...spec.roles.map(role),
@@ -7149,7 +7478,7 @@ function rosterLines(roster) {
7149
7478
  const flags = [r.vacant ? "vacant" : "", r.understaffed ? `understaffed (min ${r.minMembers})` : "", r.launching ? `${r.launching} launching` : ""].filter(Boolean);
7150
7479
  lines.push(` ${r.role}${r.entry ? " (entry)" : ""} ${r.seats.length}/${r.maxMembers} ${r.online} online${flags.length ? ` ${flags.join(", ")}` : ""}`);
7151
7480
  for (const s of r.seats)
7152
- lines.push(` ${s.staffing.id} ${s.staffing.sessionID} ${s.presence} gen ${s.staffing.generation} r${s.staffing.revision}${s.profile ? ` profile ${s.profile.status}${s.profile.matches ? "" : " (mismatch)"}` : ""}`);
7481
+ lines.push(` ${s.staffing.id} ${s.staffing.sessionID} ${s.presence}${s.stalled ? ` stalled: ${s.stalled}` : ""} gen ${s.staffing.generation} r${s.staffing.revision}${s.profile ? ` profile ${s.profile.status}${s.profile.matches ? "" : " (mismatch)"}` : ""}`);
7153
7482
  }
7154
7483
  const failed = roster.requests.filter((q) => q.state !== "staffed").slice(-5);
7155
7484
  for (const q of failed)
@@ -7166,7 +7495,7 @@ var statusTool = (verb, description, organizes) => ({
7166
7495
  idempotent: true,
7167
7496
  run: async (hub, args, signal) => {
7168
7497
  const expectedRevision = revision(args);
7169
- const { team } = await hub.teams[verb](need4(args, "team"), expectedRevision, { signal });
7498
+ const { team } = await hub.teams[verb](need6(args, "team"), expectedRevision, { signal });
7170
7499
  return `${team.revision === expectedRevision ? "unchanged" : { pause: "paused", resume: "resumed", archive: "archived", restore: "restored" }[verb]}: ${teamLine(team)}`;
7171
7500
  }
7172
7501
  });
@@ -7180,7 +7509,7 @@ var draftTool = (name, description, properties, required, edit) => ({
7180
7509
  idempotent: true,
7181
7510
  run: async (hub, args, signal) => {
7182
7511
  const expectedRevision = revision(args);
7183
- const { team, draft } = await edit(hub.teams, need4(args, "team"), args, expectedRevision, signal);
7512
+ const { team, draft } = await edit(hub.teams, need6(args, "team"), args, expectedRevision, signal);
7184
7513
  return draftText("draft saved", team, draft);
7185
7514
  }
7186
7515
  });
@@ -7191,22 +7520,22 @@ var teamTools = [
7191
7520
  properties: {
7192
7521
  state: { type: "string", enum: ["active", "archived", "all"] },
7193
7522
  tags: STRINGS,
7194
- project: STRING4,
7195
- metadata: { type: "object", additionalProperties: STRING4 },
7523
+ project: STRING6,
7524
+ metadata: { type: "object", additionalProperties: STRING6 },
7196
7525
  paused: { type: "boolean" },
7197
7526
  limit: { type: "integer", minimum: 1, maximum: 200 },
7198
- cursor: STRING4
7527
+ cursor: STRING6
7199
7528
  },
7200
7529
  readOnly: true,
7201
7530
  run: async (hub, args, signal) => {
7202
- const state = optional5(args, "state");
7531
+ const state = optional6(args, "state");
7203
7532
  if (state !== undefined && !["active", "archived", "all"].includes(state))
7204
7533
  throw new ArgError(`"state" is one of active, archived, all`);
7205
7534
  const tags = strings3(args, "tags");
7206
- const project = optional5(args, "project");
7535
+ const project = optional6(args, "project");
7207
7536
  const metadata = record(args, "metadata");
7208
7537
  const limit = int3(args, "limit", 1, 200);
7209
- const cursor = optional5(args, "cursor");
7538
+ const cursor = optional6(args, "cursor");
7210
7539
  if (args.paused !== undefined && typeof args.paused !== "boolean")
7211
7540
  throw new ArgError(`"paused" must be true or false`);
7212
7541
  const page = await hub.teams.list({
@@ -7232,7 +7561,7 @@ var teamTools = [
7232
7561
  required: ["team"],
7233
7562
  readOnly: true,
7234
7563
  run: async (hub, args, signal) => {
7235
- const { team, draft } = await hub.teams.get(need4(args, "team"), { signal });
7564
+ const { team, draft } = await hub.teams.get(need6(args, "team"), { signal });
7236
7565
  return [teamLine(team), ...specLines(team), ...draft ? [`draft pending (on v${draft.baseVersion}, saved ${when(draft.savedAt)}): see team_diff`] : []].join(`
7237
7566
  `);
7238
7567
  }
@@ -7244,7 +7573,7 @@ var teamTools = [
7244
7573
  required: ["team"],
7245
7574
  readOnly: true,
7246
7575
  run: async (hub, args, signal) => {
7247
- const { team, roster, intake } = await hub.teams.status(need4(args, "team"), { signal });
7576
+ const { team, roster, intake } = await hub.teams.status(need6(args, "team"), { signal });
7248
7577
  const lines = [teamLine(team), ...rosterLines(roster)];
7249
7578
  if (intake)
7250
7579
  lines.push(`intake: ${intake.queued} queued, ${intake.routed} routed (${intake.open} open, ${intake.inProgress} in progress, ${intake.blocked} blocked), ${intake.done} done, ${intake.failed} failed, ${intake.cancelled} cancelled${intake.oldestQueuedAt !== undefined ? `; oldest queued ${when(intake.oldestQueuedAt)}` : ""}`);
@@ -7255,12 +7584,12 @@ var teamTools = [
7255
7584
  {
7256
7585
  name: "team_history",
7257
7586
  description: `A team's retained published versions, newest first (version, digest, when). ${SCOPES}`,
7258
- properties: { team: TEAM2, cursor: STRING4 },
7587
+ properties: { team: TEAM2, cursor: STRING6 },
7259
7588
  required: ["team"],
7260
7589
  readOnly: true,
7261
7590
  run: async (hub, args, signal) => {
7262
- const cursor = optional5(args, "cursor");
7263
- const page = await hub.teams.versions(need4(args, "team"), { signal, ...cursor ? { cursor } : {} });
7591
+ const cursor = optional6(args, "cursor");
7592
+ const page = await hub.teams.versions(need6(args, "team"), { signal, ...cursor ? { cursor } : {} });
7264
7593
  return [...page.items.map((v) => `v${v.version} ${v.digest.slice(0, 12)} ${when(v.publishedAt)}`), ...page.cursor ? [`more: cursor ${page.cursor}`] : []].join(`
7265
7594
  `);
7266
7595
  }
@@ -7274,7 +7603,7 @@ var teamTools = [
7274
7603
  run: async (hub, args, signal) => {
7275
7604
  const from = int3(args, "from", 1, 1e9);
7276
7605
  const to = args.to === undefined || args.to === null || args.to === "draft" ? "draft" : int3(args, "to", 1, 1e9);
7277
- const { from: a, to: b, diff } = await hub.teams.diff(need4(args, "team"), { signal, ...from !== undefined ? { from } : {}, to });
7606
+ const { from: a, to: b, diff } = await hub.teams.diff(need6(args, "team"), { signal, ...from !== undefined ? { from } : {}, to });
7278
7607
  const part = (label, d) => ["added", "removed", "changed"].flatMap((k) => d[k]?.length ? [`${label} ${k}: ${d[k].join(", ")}`] : []);
7279
7608
  const lines = [
7280
7609
  ...diff.fields.length ? [`fields changed: ${diff.fields.join(", ")}`] : [],
@@ -7291,16 +7620,16 @@ var teamTools = [
7291
7620
  {
7292
7621
  name: "team_create",
7293
7622
  description: `Assemble a persistent team: roles (one is the entry role that receives submitted work, e.g. a project manager), open or restricted communication with one-way/two-way paths, tags, projects and metadata. Published as version 1 at once; the name is the team's address (@@role#name) and never changes. ${SCOPES}`,
7294
- properties: { name: { ...STRING4, description: "A slug (2–40 lowercase letters, digits, dashes), unique per user." }, ...SPEC_FIELDS, idempotencyKey: IDEMPOTENCY },
7623
+ properties: { name: { ...STRING6, description: "A slug (2–40 lowercase letters, digits, dashes), unique per user." }, ...SPEC_FIELDS, idempotencyKey: IDEMPOTENCY },
7295
7624
  required: ["name", "title", "mode", "roles", "entryRole"],
7296
7625
  readOnly: false,
7297
7626
  organizes: true,
7298
7627
  idempotent: true,
7299
7628
  run: async (hub, args, signal) => {
7300
- need4(args, "name");
7301
- need4(args, "title");
7302
- const key = optional5(args, "idempotencyKey");
7303
- const spec = picked(args, ["name", "title", "description", "mode", "roles", "entryRole", "edges", "tags", "metadata", "projects"]);
7629
+ need6(args, "name");
7630
+ need6(args, "title");
7631
+ const key = optional6(args, "idempotencyKey");
7632
+ const spec = picked(args, ["name", "title", "description", "mode", "hopLimit", "itemBudget", "loop", "roles", "entryRole", "edges", "tags", "metadata", "projects"]);
7304
7633
  const { team, created } = await hub.teams.create({ ...spec, edges: spec.edges ?? [] }, { signal, ...key ? { idempotencyKey: key } : {} });
7305
7634
  return [`${created ? "created" : "already created (same key)"}: ${teamLine(team)}`, ...specLines(team)].join(`
7306
7635
  `);
@@ -7316,24 +7645,24 @@ var teamTools = [
7316
7645
  idempotent: true,
7317
7646
  run: async (hub, args, signal) => {
7318
7647
  const expectedRevision = revision(args);
7319
- const patch = picked(args, ["title", "description", "mode", "roles", "entryRole", "edges", "tags", "metadata", "projects"]);
7648
+ const patch = picked(args, ["title", "description", "mode", "hopLimit", "itemBudget", "loop", "roles", "entryRole", "edges", "tags", "metadata", "projects"]);
7320
7649
  if (Object.keys(patch).length === 0)
7321
7650
  throw new ArgError("give at least one field to change");
7322
- const id = need4(args, "team");
7651
+ const id = need6(args, "team");
7323
7652
  const { team } = await hub.teams.update(id, patch, expectedRevision, { signal });
7324
7653
  return draftText("draft saved", team, (await hub.teams.draft(team.id, { signal })).draft);
7325
7654
  }
7326
7655
  },
7327
- draftTool("team_role_set", "Add a role to a team's draft, or replace the role of that name (all its fields).", { role: { ...STRING4, description: "The role's name (a slug)." }, ...ROLE_FIELDS }, ["role"], (teams, team, args, rev, signal) => teams.setRole(team, { name: need4(args, "role"), ...picked(args, Object.keys(ROLE_FIELDS)) }, rev, { signal })),
7328
- draftTool("team_role_remove", "Remove a role (not the entry role) and every path touching it from a team's draft.", { role: STRING4 }, ["role"], (teams, team, args, rev, signal) => teams.removeRole(team, need4(args, "role"), rev, { signal })),
7329
- draftTool("team_edge_set", "Draw a communication path from one role to another in a team's draft (replaces the path drawn between them).", { from: STRING4, to: STRING4, kinds: KINDS3, direction: DIRECTION }, ["from", "to", "kinds", "direction"], (teams, team, args, rev, signal) => {
7656
+ draftTool("team_role_set", "Add a role to a team's draft, or replace the role of that name (all its fields).", { role: { ...STRING6, description: "The role's name (a slug)." }, ...ROLE_FIELDS }, ["role"], (teams, team, args, rev, signal) => teams.setRole(team, { name: need6(args, "role"), ...picked(args, Object.keys(ROLE_FIELDS)) }, rev, { signal })),
7657
+ draftTool("team_role_remove", "Remove a role (not the entry role) and every path touching it from a team's draft.", { role: STRING6 }, ["role"], (teams, team, args, rev, signal) => teams.removeRole(team, need6(args, "role"), rev, { signal })),
7658
+ draftTool("team_edge_set", "Draw a communication path from one role to another in a team's draft (replaces the path drawn between them).", { from: STRING6, to: STRING6, kinds: KINDS5, direction: DIRECTION }, ["from", "to", "kinds", "direction"], (teams, team, args, rev, signal) => {
7330
7659
  const kinds = strings3(args, "kinds");
7331
7660
  if (!kinds?.length)
7332
7661
  throw new ArgError(`"kinds" is a list of msg and/or ask`);
7333
- const direction = need4(args, "direction");
7334
- return teams.setEdge(team, { from: need4(args, "from"), to: need4(args, "to"), kinds, direction }, rev, { signal });
7662
+ const direction = need6(args, "direction");
7663
+ return teams.setEdge(team, { from: need6(args, "from"), to: need6(args, "to"), kinds, direction }, rev, { signal });
7335
7664
  }),
7336
- draftTool("team_edge_remove", "Remove the path drawn from one role to another (or a two-way path between them) from a team's draft.", { from: STRING4, to: STRING4 }, ["from", "to"], (teams, team, args, rev, signal) => teams.removeEdge(team, need4(args, "from"), need4(args, "to"), rev, { signal })),
7665
+ draftTool("team_edge_remove", "Remove the path drawn from one role to another (or a two-way path between them) from a team's draft.", { from: STRING6, to: STRING6 }, ["from", "to"], (teams, team, args, rev, signal) => teams.removeEdge(team, need6(args, "from"), need6(args, "to"), rev, { signal })),
7337
7666
  {
7338
7667
  name: "team_discard_draft",
7339
7668
  description: `Drop a team's pending draft (unpublished edits are lost; the published version stays). ${SCOPES}`,
@@ -7341,7 +7670,7 @@ var teamTools = [
7341
7670
  required: ["team", "expectedRevision"],
7342
7671
  readOnly: false,
7343
7672
  idempotent: true,
7344
- run: async (hub, args, signal) => `draft discarded: ${teamLine((await hub.teams.discardDraft(need4(args, "team"), revision(args), { signal })).team)}`
7673
+ run: async (hub, args, signal) => `draft discarded: ${teamLine((await hub.teams.discardDraft(need6(args, "team"), revision(args), { signal })).team)}`
7345
7674
  },
7346
7675
  {
7347
7676
  name: "team_publish",
@@ -7351,7 +7680,7 @@ var teamTools = [
7351
7680
  readOnly: false,
7352
7681
  idempotent: true,
7353
7682
  run: async (hub, args, signal) => {
7354
- const { team, published } = await hub.teams.publish(need4(args, "team"), revision(args), { signal });
7683
+ const { team, published } = await hub.teams.publish(need6(args, "team"), revision(args), { signal });
7355
7684
  return `${published ? `published version ${team.version}` : "the draft equalled the published version; dropped"}: ${teamLine(team)}`;
7356
7685
  }
7357
7686
  },
@@ -7362,17 +7691,17 @@ var teamTools = [
7362
7691
  {
7363
7692
  name: "team_staff",
7364
7693
  description: `Staff a role: attach an existing session (sessionID, any machine) or launch a new member (launch: a launch spec as the launch tool takes). An agent needs the user's grants (roles for the session's project; launch for a repo) or the hub answers needs_user; staffing never issues grants. ${SCOPES}`,
7365
- properties: { team: TEAM2, role: STRING4, sessionID: { ...STRING4, description: "An existing session (ses_…)." }, launch: LAUNCH, idempotencyKey: IDEMPOTENCY },
7694
+ properties: { team: TEAM2, role: STRING6, sessionID: { ...STRING6, description: "An existing session (ses_…)." }, launch: LAUNCH, idempotencyKey: IDEMPOTENCY },
7366
7695
  required: ["team", "role"],
7367
7696
  readOnly: false,
7368
7697
  run: async (hub, args, signal) => {
7369
- const team = need4(args, "team");
7370
- const role = need4(args, "role");
7371
- const sessionID = optional5(args, "sessionID");
7698
+ const team = need6(args, "team");
7699
+ const role = need6(args, "role");
7700
+ const sessionID = optional6(args, "sessionID");
7372
7701
  const launch = launchOf(args);
7373
7702
  if (sessionID === undefined === (launch === undefined))
7374
7703
  throw new ArgError(`give exactly one of sessionID or launch`);
7375
- const key = optional5(args, "idempotencyKey");
7704
+ const key = optional6(args, "idempotencyKey");
7376
7705
  const options = { signal, ...key ? { idempotencyKey: key } : {} };
7377
7706
  if (sessionID !== undefined) {
7378
7707
  const r = await hub.teams.staff(team, { role, sessionID }, options);
@@ -7385,29 +7714,29 @@ var teamTools = [
7385
7714
  {
7386
7715
  name: "team_unstaff",
7387
7716
  description: `End a seat (the session leaves the role and loses the team's policy rights). Send the seat's revision (team_status). ${SCOPES}`,
7388
- properties: { team: TEAM2, staffingID: { ...STRING4, description: "The seat (stf_…)." }, expectedRevision: { ...REVISION2, description: "The seat's revision you read (team_status)." } },
7717
+ properties: { team: TEAM2, staffingID: { ...STRING6, description: "The seat (stf_…)." }, expectedRevision: { ...REVISION2, description: "The seat's revision you read (team_status)." } },
7389
7718
  required: ["team", "staffingID", "expectedRevision"],
7390
7719
  readOnly: false,
7391
7720
  idempotent: true,
7392
7721
  run: async (hub, args, signal) => {
7393
- const { staffing } = await hub.teams.unstaff(need4(args, "team"), need4(args, "staffingID"), revision(args), { signal });
7722
+ const { staffing } = await hub.teams.unstaff(need6(args, "team"), need6(args, "staffingID"), revision(args), { signal });
7394
7723
  return `unstaffed ${staffing.id} (${staffing.sessionID} as ${staffing.role})`;
7395
7724
  }
7396
7725
  },
7397
7726
  {
7398
7727
  name: "team_replace",
7399
7728
  description: `Replace a seat by an existing session (sessionID) or a new launch (launch): the old seat ends at once and the new member gets the slot's next generation. Send the seat's revision (team_status). ${SCOPES}`,
7400
- properties: { team: TEAM2, staffingID: STRING4, sessionID: STRING4, launch: LAUNCH, expectedRevision: { ...REVISION2, description: "The seat's revision you read (team_status)." } },
7729
+ properties: { team: TEAM2, staffingID: STRING6, sessionID: STRING6, launch: LAUNCH, expectedRevision: { ...REVISION2, description: "The seat's revision you read (team_status)." } },
7401
7730
  required: ["team", "staffingID", "expectedRevision"],
7402
7731
  readOnly: false,
7403
7732
  run: async (hub, args, signal) => {
7404
7733
  const expectedRevision = revision(args);
7405
- const sessionID = optional5(args, "sessionID");
7734
+ const sessionID = optional6(args, "sessionID");
7406
7735
  const launch = launchOf(args);
7407
7736
  if (sessionID === undefined === (launch === undefined))
7408
7737
  throw new ArgError(`give exactly one of sessionID or launch`);
7409
7738
  const input = sessionID !== undefined ? { expectedRevision, sessionID } : { expectedRevision, launch };
7410
- const r = await hub.teams.replace(need4(args, "team"), need4(args, "staffingID"), input, { signal });
7739
+ const r = await hub.teams.replace(need6(args, "team"), need6(args, "staffingID"), input, { signal });
7411
7740
  const next = r.staffing ? `${r.staffing.sessionID} (${r.staffing.id}, generation ${r.staffing.generation})` : `launch ${r.launch?.id ?? "?"} (request ${r.request?.id ?? "?"})`;
7412
7741
  return `replaced ${r.ended.sessionID} in ${r.ended.role} by ${next}`;
7413
7742
  }
@@ -7415,13 +7744,13 @@ var teamTools = [
7415
7744
  ];
7416
7745
 
7417
7746
  // packages/cli/src/profiles-mcp.ts
7418
- var STRING5 = { type: "string" };
7419
- var STRINGS2 = { type: "array", items: STRING5 };
7747
+ var STRING7 = { type: "string" };
7748
+ var STRINGS2 = { type: "array", items: STRING7 };
7420
7749
  var ID4 = { type: "string", description: "The profile's ID (prof_…; see profiles)." };
7421
7750
  var REVISION3 = { type: "integer", minimum: 0, description: "The profile's revision you read (profile_show); a stale one answers conflict with the current revision." };
7422
7751
  var IDEMPOTENCY2 = { type: "string", description: "Idempotency-Key: the same key and arguments within 24 h answer the first result again." };
7423
- var SKILL = { type: "object", properties: { name: { ...STRING5, description: "A slug." }, description: STRING5, body: { ...STRING5, description: "The skill's full text (loaded on demand)." } }, required: ["name", "description", "body"] };
7424
- var COMMAND3 = { type: "object", properties: { name: { ...STRING5, description: "A slug." }, description: STRING5, template: { ...STRING5, description: "Text with {{name}} parameters and {{secret:NAME}} references (never resolved by the hub)." } }, required: ["name", "description", "template"] };
7752
+ var SKILL = { type: "object", properties: { name: { ...STRING7, description: "A slug." }, description: STRING7, body: { ...STRING7, description: "The skill's full text (loaded on demand)." } }, required: ["name", "description", "body"] };
7753
+ var COMMAND3 = { type: "object", properties: { name: { ...STRING7, description: "A slug." }, description: STRING7, template: { ...STRING7, description: "Text with {{name}} parameters and {{secret:NAME}} references (never resolved by the hub)." } }, required: ["name", "description", "template"] };
7425
7754
  var CONTENT_FIELDS = {
7426
7755
  instructions: { type: ["string", "null"], description: "The profile's instructions (AGENTS.md-like text)." },
7427
7756
  model: { type: ["string", "null"], description: "provider/model the profile suggests." },
@@ -7429,21 +7758,21 @@ var CONTENT_FIELDS = {
7429
7758
  commands: { type: "array", items: COMMAND3, description: "Commands, wholesale." }
7430
7759
  };
7431
7760
  var META_FIELDS = {
7432
- title: { ...STRING5, description: "1–120 characters." },
7433
- description: STRING5,
7761
+ title: { ...STRING7, description: "1–120 characters." },
7762
+ description: STRING7,
7434
7763
  tags: { ...STRINGS2, description: "Slugs." },
7435
- metadata: { type: "object", additionalProperties: STRING5, description: "String key/value pairs (≤ 32 slug keys)." },
7764
+ metadata: { type: "object", additionalProperties: STRING7, description: "String key/value pairs (≤ 32 slug keys)." },
7436
7765
  projects: { ...STRINGS2, description: "Project names the profile is meant for (≤ 16)." }
7437
7766
  };
7438
7767
  var SCOPES2 = "App tokens need scope profiles:read (reads) or profiles:manage (changes; profile_version_create with content fields and the skill/command set/remove tools read the latest version first, so they also need profiles:read); pins need profiles:assign and sessions:read (an app also needs a prompt grant on the session). Session tokens never change profiles. A profile confers no authority.";
7439
7768
  var ArgError2 = ToolArgError;
7440
- var need5 = (args, key) => {
7769
+ var need7 = (args, key) => {
7441
7770
  const value = args[key];
7442
7771
  if (typeof value !== "string" || !value.trim())
7443
7772
  throw new ArgError2(`"${key}" is required (a non-empty string)`);
7444
7773
  return value;
7445
7774
  };
7446
- var optional6 = (args, key) => {
7775
+ var optional7 = (args, key) => {
7447
7776
  const value = args[key];
7448
7777
  if (value === undefined || value === null)
7449
7778
  return;
@@ -7459,7 +7788,7 @@ var whole2 = (args, key, min, what) => {
7459
7788
  };
7460
7789
  var revision2 = (args) => whole2(args, "expectedRevision", 0, "the revision you read (profile_show; 0 for a session with no pin)");
7461
7790
  var idempotency = (args) => {
7462
- const key = optional6(args, "idempotencyKey");
7791
+ const key = optional7(args, "idempotencyKey");
7463
7792
  return key ? { idempotencyKey: key } : {};
7464
7793
  };
7465
7794
  var picked2 = (args, keys) => Object.fromEntries(keys.filter((k) => args[k] !== undefined).map((k) => [k, args[k]]));
@@ -7492,7 +7821,7 @@ var contentTool = (name, description, properties, required, change) => ({
7492
7821
  idempotent: true,
7493
7822
  run: async (hub, args, signal) => {
7494
7823
  const expectedRevision = revision2(args);
7495
- const id = need5(args, "id");
7824
+ const id = need7(args, "id");
7496
7825
  const { latest, content } = await latestContent(hub.profiles, id, signal);
7497
7826
  return versionText(await hub.profiles.createVersion(id, { expectedRevision, content: change(content, args, latest) }, { signal }));
7498
7827
  }
@@ -7512,7 +7841,7 @@ var stateTool = (verb, description) => ({
7512
7841
  idempotent: true,
7513
7842
  run: async (hub, args, signal) => {
7514
7843
  const expectedRevision = revision2(args);
7515
- const { profile } = await hub.profiles.update(need5(args, "id"), { expectedRevision, state: verb === "archive" ? "archived" : "active" }, { signal });
7844
+ const { profile } = await hub.profiles.update(need7(args, "id"), { expectedRevision, state: verb === "archive" ? "archived" : "active" }, { signal });
7516
7845
  return `${verb === "archive" ? "archived" : "restored"}: ${profileLine(profile)}`;
7517
7846
  }
7518
7847
  });
@@ -7524,7 +7853,7 @@ var profileLifecycleTools = [
7524
7853
  required: ["id"],
7525
7854
  readOnly: true,
7526
7855
  run: async (hub, args, signal) => {
7527
- const { profile, latest } = await hub.profiles.get(need5(args, "id"), { signal });
7856
+ const { profile, latest } = await hub.profiles.get(need7(args, "id"), { signal });
7528
7857
  return [
7529
7858
  profileLine(profile),
7530
7859
  ...profile.description ? [`description: ${one2(profile.description)}`] : [],
@@ -7539,13 +7868,13 @@ var profileLifecycleTools = [
7539
7868
  {
7540
7869
  name: "profile_versions",
7541
7870
  description: `A profile's retained versions, newest first (version, digest, bytes, when, skill and command counts). ${SCOPES2}`,
7542
- properties: { id: ID4, limit: { type: "integer", minimum: 1, maximum: 200 }, cursor: STRING5 },
7871
+ properties: { id: ID4, limit: { type: "integer", minimum: 1, maximum: 200 }, cursor: STRING7 },
7543
7872
  required: ["id"],
7544
7873
  readOnly: true,
7545
7874
  run: async (hub, args, signal) => {
7546
7875
  const limit = args.limit === undefined ? undefined : whole2(args, "limit", 1, "a whole number from 1 to 200");
7547
- const cursor = optional6(args, "cursor");
7548
- const page = await hub.profiles.versions(need5(args, "id"), { signal, ...limit !== undefined ? { limit } : {}, ...cursor ? { cursor } : {} });
7876
+ const cursor = optional7(args, "cursor");
7877
+ const page = await hub.profiles.versions(need7(args, "id"), { signal, ...limit !== undefined ? { limit } : {}, ...cursor ? { cursor } : {} });
7549
7878
  const counts = (v) => v.skills !== undefined ? ` ${v.skills} skills, ${v.commands ?? 0} commands${v.instructions ? ", instructions" : ""}` : "";
7550
7879
  return [...page.items.map((v) => `v${v.version} ${v.digest.slice(0, 12)} ${v.bytes} bytes ${new Date(v.createdAt).toISOString()}${counts(v)}`), ...page.cursor ? [`more: cursor ${page.cursor}`] : []].join(`
7551
7880
  `) || "no versions";
@@ -7554,14 +7883,14 @@ var profileLifecycleTools = [
7554
7883
  {
7555
7884
  name: "profile_create",
7556
7885
  description: `Create a profile: a versioned bundle of instructions, skills and commands, published as version 1 at once. The name is unique per user. Attach it to a team role with team_role_set (profile), pin it with profile_assign. ${SCOPES2}`,
7557
- properties: { name: { ...STRING5, description: "A slug, unique per user." }, ...META_FIELDS, ...CONTENT_FIELDS, idempotencyKey: IDEMPOTENCY2 },
7886
+ properties: { name: { ...STRING7, description: "A slug, unique per user." }, ...META_FIELDS, ...CONTENT_FIELDS, idempotencyKey: IDEMPOTENCY2 },
7558
7887
  required: ["name", "title"],
7559
7888
  readOnly: false,
7560
7889
  organizes: true,
7561
7890
  idempotent: true,
7562
7891
  run: async (hub, args, signal) => {
7563
- need5(args, "name");
7564
- need5(args, "title");
7892
+ need7(args, "name");
7893
+ need7(args, "title");
7565
7894
  const meta = picked2(args, ["name", "title", "description", "tags", "metadata", "projects"]);
7566
7895
  const text = Object.entries(picked2(args, ["instructions", "model"])).filter(([, v]) => v !== null);
7567
7896
  const content = { ...Object.fromEntries(text), skills: args.skills ?? [], commands: args.commands ?? [] };
@@ -7583,7 +7912,7 @@ var profileLifecycleTools = [
7583
7912
  const patch = picked2(args, ["title", "description", "tags", "metadata", "projects"]);
7584
7913
  if (Object.keys(patch).length === 0)
7585
7914
  throw new ArgError2("give at least one field to change");
7586
- const { profile } = await hub.profiles.update(need5(args, "id"), { expectedRevision, ...patch }, { signal });
7915
+ const { profile } = await hub.profiles.update(need7(args, "id"), { expectedRevision, ...patch }, { signal });
7587
7916
  return `updated: ${profileLine(profile)}`;
7588
7917
  }
7589
7918
  },
@@ -7597,7 +7926,7 @@ var profileLifecycleTools = [
7597
7926
  idempotent: true,
7598
7927
  run: async (hub, args, signal) => {
7599
7928
  const expectedRevision = revision2(args);
7600
- const id = need5(args, "id");
7929
+ const id = need7(args, "id");
7601
7930
  const given = Object.keys(picked2(args, Object.keys(CONTENT_FIELDS)));
7602
7931
  const options = { signal, ...idempotency(args) };
7603
7932
  if (args.fromVersion !== undefined) {
@@ -7618,77 +7947,225 @@ var profileLifecycleTools = [
7618
7947
  return versionText(await hub.profiles.createVersion(id, { expectedRevision, content: next }, options));
7619
7948
  }
7620
7949
  },
7621
- contentTool("profile_skill_set", "Attach a skill to a profile, or replace the skill of that name.", { name: { ...STRING5, description: "The skill's name (a slug)." }, description: STRING5, body: { ...STRING5, description: "The skill's full text." } }, ["name", "description", "body"], (content, args) => ({
7950
+ contentTool("profile_skill_set", "Attach a skill to a profile, or replace the skill of that name.", { name: { ...STRING7, description: "The skill's name (a slug)." }, description: STRING7, body: { ...STRING7, description: "The skill's full text." } }, ["name", "description", "body"], (content, args) => ({
7622
7951
  ...content,
7623
- skills: setNamed(content.skills, { name: need5(args, "name"), description: need5(args, "description"), body: need5(args, "body") })
7952
+ skills: setNamed(content.skills, { name: need7(args, "name"), description: need7(args, "description"), body: need7(args, "body") })
7624
7953
  })),
7625
- contentTool("profile_skill_remove", "Detach a skill from a profile.", { name: { ...STRING5, description: "The skill's name." } }, ["name"], (content, args, latest) => ({
7954
+ contentTool("profile_skill_remove", "Detach a skill from a profile.", { name: { ...STRING7, description: "The skill's name." } }, ["name"], (content, args, latest) => ({
7626
7955
  ...content,
7627
- skills: removeNamed(content.skills, need5(args, "name"), "skill", latest)
7956
+ skills: removeNamed(content.skills, need7(args, "name"), "skill", latest)
7628
7957
  })),
7629
- contentTool("profile_command_set", "Attach a command to a profile, or replace the command of that name.", { name: { ...STRING5, description: "The command's name (a slug)." }, description: STRING5, template: COMMAND3.properties.template }, ["name", "description", "template"], (content, args) => ({ ...content, commands: setNamed(content.commands, { name: need5(args, "name"), description: need5(args, "description"), template: need5(args, "template") }) })),
7630
- contentTool("profile_command_remove", "Detach a command from a profile.", { name: { ...STRING5, description: "The command's name." } }, ["name"], (content, args, latest) => ({
7958
+ contentTool("profile_command_set", "Attach a command to a profile, or replace the command of that name.", { name: { ...STRING7, description: "The command's name (a slug)." }, description: STRING7, template: COMMAND3.properties.template }, ["name", "description", "template"], (content, args) => ({ ...content, commands: setNamed(content.commands, { name: need7(args, "name"), description: need7(args, "description"), template: need7(args, "template") }) })),
7959
+ contentTool("profile_command_remove", "Detach a command from a profile.", { name: { ...STRING7, description: "The command's name." } }, ["name"], (content, args, latest) => ({
7631
7960
  ...content,
7632
- commands: removeNamed(content.commands, need5(args, "name"), "command", latest)
7961
+ commands: removeNamed(content.commands, need7(args, "name"), "command", latest)
7633
7962
  })),
7634
7963
  stateTool("archive", "Archive a profile: it takes no new versions or pins; pinned sessions keep theirs; reversible with profile_restore."),
7635
7964
  stateTool("restore", "Restore an archived profile (it takes versions and pins again)."),
7636
7965
  {
7637
7966
  name: "profile_assign",
7638
7967
  description: `Pin a profile version to a session (applied through its machine; the ack tells whether it took). Send the session's pin revision (0 when it has none; see profile). Pinning confers no authority. ${SCOPES2}`,
7639
- properties: { addr: { ...STRING5, description: "The session: @name, ses_… ID or other address." }, profileID: ID4, version: { type: "integer", minimum: 1 }, expectedRevision: { ...REVISION3, description: "The session's pin revision (0 when it has none)." } },
7968
+ properties: { addr: { ...STRING7, description: "The session: @name, ses_… ID or other address." }, profileID: ID4, version: { type: "integer", minimum: 1 }, expectedRevision: { ...REVISION3, description: "The session's pin revision (0 when it has none)." } },
7640
7969
  required: ["addr", "profileID", "version", "expectedRevision"],
7641
7970
  readOnly: false,
7642
7971
  idempotent: true,
7643
7972
  run: async (hub, args, signal) => {
7644
7973
  const expectedRevision = revision2(args);
7645
- const { assignment: a } = await hub.profiles.assign(need5(args, "addr"), { profileID: need5(args, "profileID"), version: whole2(args, "version", 1, "a version number"), expectedRevision }, { signal });
7974
+ const { assignment: a } = await hub.profiles.assign(need7(args, "addr"), { profileID: need7(args, "profileID"), version: whole2(args, "version", 1, "a version number"), expectedRevision }, { signal });
7646
7975
  return `${a.sessionID} pins ${a.profileID} v${a.version} r${a.revision}${a.ack ? `, ack ${a.ack.status}${a.ack.offline ? " (offline)" : ""}` : ""}`;
7647
7976
  }
7648
7977
  },
7649
7978
  {
7650
7979
  name: "profile_unassign",
7651
7980
  description: `Remove a session's pin (what it already applied stays until it restarts). Send the pin's revision (profile_assign's answer). ${SCOPES2}`,
7652
- properties: { addr: { ...STRING5, description: "The session." }, expectedRevision: { ...REVISION3, description: "The session's pin revision." } },
7981
+ properties: { addr: { ...STRING7, description: "The session." }, expectedRevision: { ...REVISION3, description: "The session's pin revision." } },
7653
7982
  required: ["addr", "expectedRevision"],
7654
7983
  readOnly: false,
7655
7984
  idempotent: true,
7656
7985
  run: async (hub, args, signal) => {
7657
- const addr = need5(args, "addr");
7986
+ const addr = need7(args, "addr");
7658
7987
  await hub.profiles.unassign(addr, revision2(args), { signal });
7659
7988
  return `unpinned ${addr}`;
7660
7989
  }
7661
7990
  }
7662
7991
  ];
7663
7992
 
7993
+ // packages/cli/src/grants.ts
7994
+ var GRANT_FLAGS = {
7995
+ json: "json",
7996
+ holder: "string",
7997
+ project: "multi",
7998
+ allow: "multi",
7999
+ permit: "multi",
8000
+ never: "multi",
8001
+ expires: "string",
8002
+ "max-per-hour": "string",
8003
+ "allow-always": "bool"
8004
+ };
8005
+ var GRANT_REQUEST_FLAGS = { ...GRANT_FLAGS, "permission-allow": "multi", why: "string" };
8006
+ var GRANT_USER_HINT = "only the user manages grants: use the console, or `hub login` with the user's device token";
8007
+ function userOnly2(source, what) {
8008
+ if (source === "env")
8009
+ throw new CliError(EXIT.forbidden, `only the user can ${what} a grant, not a session`, GRANT_USER_HINT);
8010
+ }
8011
+ function grantError(error) {
8012
+ if (error instanceof CliError && error.exit === EXIT.forbidden)
8013
+ return new CliError(error.exit, error.message, [error.hint, GRANT_USER_HINT].filter(Boolean).join("; "), error.envelope);
8014
+ return error;
8015
+ }
8016
+ var listOf = (flags, name) => (flags[name] ?? []).flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
8017
+ async function grantSpecArg(flags, stdin) {
8018
+ return await grantArg(flags, stdin, true);
8019
+ }
8020
+ function grantRequestArg(flags, stdin) {
8021
+ return grantArg(flags, stdin, false);
8022
+ }
8023
+ async function grantArg(flags, stdin, holderRequired) {
8024
+ const json = typeof flags.json === "string" ? flags.json : undefined;
8025
+ const flagged = Object.keys(GRANT_REQUEST_FLAGS).filter((name) => name !== "json" && name !== "why" && flags[name] !== undefined);
8026
+ if (json !== undefined) {
8027
+ if (flagged.length)
8028
+ throw new UsageError(`give either --json '{…}' or ${flagged.map((f) => `--${f}`).join(", ")}`);
8029
+ return await jsonObjectArg(json, stdin, "grant");
8030
+ }
8031
+ const holder = str(flags, "holder");
8032
+ if (holder === undefined && holderRequired)
8033
+ throw new UsageError("needs --holder (ses_…, @name, @@orchestrator…, or app:<token id>)");
8034
+ const projects = listOf(flags, "project");
8035
+ if (!projects.length)
8036
+ throw new UsageError("needs --project <path> (repeatable)");
8037
+ const allow = listOf(flags, "allow");
8038
+ if (!allow.length)
8039
+ throw new UsageError("needs --allow <verbs> (approve, deny, answer, prompt, stop, roles, launch)");
8040
+ const permit = [...listOf(flags, "permit"), ...listOf(flags, "permission-allow")];
8041
+ const never = listOf(flags, "never");
8042
+ const expires = str(flags, "expires");
8043
+ const cap = str(flags, "max-per-hour");
8044
+ if (cap !== undefined && !/^\d+$/.test(cap))
8045
+ throw new UsageError("--max-per-hour is a whole number");
8046
+ return {
8047
+ ...holder !== undefined ? { holder } : {},
8048
+ scope: { projects },
8049
+ allow,
8050
+ ...permit.length || never.length ? { permissions: { ...permit.length ? { allow: permit } : {}, ...never.length ? { never } : {} } } : {},
8051
+ ...expires !== undefined ? { expires } : {},
8052
+ ...cap !== undefined ? { maxActionsPerHour: Number(cap) } : {},
8053
+ ...flags["allow-always"] === true ? { allowAlways: true } : {}
8054
+ };
8055
+ }
8056
+ var iso4 = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
8057
+ var grantLine = (g) => [g.id, g.state, g.holder, g.allow.join(","), g.scope.projects.join(","), `until ${iso4(g.expiresAt)}`].join(" ");
8058
+ var grantsText = (grants) => grants.length ? grants.map(grantLine).join(`
8059
+ `) : "no grants";
8060
+ function grantText2(g) {
8061
+ const list = (values) => values.length ? values.join(", ") : "none";
8062
+ return [
8063
+ `${g.id} ${g.state}`,
8064
+ `holder: ${g.holder}${g.holderSessionID && g.holderSessionID !== g.holder ? ` (${g.holderSessionID})` : ""}`,
8065
+ `projects: ${list(g.scope.projects)}`,
8066
+ `verbs: ${list(g.allow)}`,
8067
+ `permissions allow: ${list(g.permissions.allow)}`,
8068
+ `permissions never: ${list(g.permissions.never)}`,
8069
+ `always: ${g.allowAlways ? "yes" : "no"}`,
8070
+ `max actions per hour: ${g.maxActionsPerHour}`,
8071
+ g.revokedAt !== undefined ? `revoked: ${iso4(g.revokedAt)}` : `expires: ${iso4(g.expiresAt)}`
8072
+ ].join(`
8073
+ `);
8074
+ }
8075
+ var requestedText = (body) => `requested; waiting for the user (Decision ${body.decision.id})`;
8076
+
8077
+ // packages/cli/src/grants-mcp.ts
8078
+ var STRING8 = { type: "string" };
8079
+ var GRANT = { type: "object", description: "The grant spec accepted by POST /api/agent/grants: holder (optional for a request), scope.projects, allow, permissions, allowAlways, maxActionsPerHour and expires. The server validates all fields.", additionalProperties: true };
8080
+ var need8 = (args, key) => {
8081
+ if (typeof args[key] !== "string" || !args[key].trim())
8082
+ throw new ToolArgError(`"${key}" is required (a non-empty string)`);
8083
+ return args[key];
8084
+ };
8085
+ var spec = (args) => {
8086
+ if (typeof args.grant !== "object" || args.grant === null || Array.isArray(args.grant))
8087
+ throw new ToolArgError('"grant" is required (an object)');
8088
+ return args.grant;
8089
+ };
8090
+ var grantTools = [
8091
+ {
8092
+ name: "grant_request",
8093
+ description: "Ask the user for a grant: opens one tap Decision for @me (approve or deny). Sessions and apps with messages:send only; one open request and at most five per hour. Only the user's answer creates authority.",
8094
+ properties: { grant: GRANT, why: STRING8 },
8095
+ required: ["grant", "why"],
8096
+ readOnly: false,
8097
+ run: async (hub, args, signal) => requestedText(await hub.requestGrant(spec(args), need8(args, "why"), { signal }))
8098
+ },
8099
+ {
8100
+ name: "grant_list",
8101
+ description: "List grants (active, expired or revoked) whose holder is this session or app, including grants for roles it holds now. Apps need messages:read. A user credential has no holder: use the console or hub grant list for administration.",
8102
+ properties: {},
8103
+ readOnly: true,
8104
+ run: async (hub, _args, signal) => grantsText((await hub.heldGrants({ signal })).grants)
8105
+ },
8106
+ {
8107
+ name: "grant_show",
8108
+ description: "Show one grant only if this credential currently holds it. Apps need messages:read; another holder's ID answers not_found.",
8109
+ properties: { id: STRING8 },
8110
+ required: ["id"],
8111
+ readOnly: true,
8112
+ run: async (hub, args, signal) => grantText2((await hub.heldGrant(need8(args, "id"), { signal })).grant)
8113
+ },
8114
+ {
8115
+ name: "grant_revoke",
8116
+ description: "Irreversibly revoke a grant held by this session or app (including a held role shared with other holders). Apps need messages:send; another holder's ID answers not_found.",
8117
+ properties: { id: STRING8 },
8118
+ required: ["id"],
8119
+ readOnly: false,
8120
+ run: async (hub, args, signal) => grantText2((await hub.revokeHeldGrant(need8(args, "id"), { signal })).grant)
8121
+ },
8122
+ {
8123
+ name: "grant_create",
8124
+ description: `Create a grant directly ONLY with the user's device credential. Sessions and apps must use grant_request and wait for the user's tap. ${GRANT_USER_HINT}.`,
8125
+ properties: { grant: GRANT },
8126
+ required: ["grant"],
8127
+ readOnly: false,
8128
+ run: async (hub, args, signal) => {
8129
+ const actor = (await hub.whoami({ signal })).actor;
8130
+ if (actor.kind !== "user")
8131
+ throw new CliError(EXIT.forbidden, "only the user can create a grant, not a session or app", GRANT_USER_HINT);
8132
+ try {
8133
+ return grantText2((await hub.createGrant(spec(args), { signal })).grant);
8134
+ } catch (cause) {
8135
+ throw grantError(cause);
8136
+ }
8137
+ }
8138
+ }
8139
+ ];
8140
+
7664
8141
  // packages/cli/src/watchlist.ts
7665
- var safe = (value) => sanitizeLabel(String(value), 1024);
7666
- var list2 = (items) => items.length ? items.map(safe).join(", ") : "none";
8142
+ var safe2 = (value) => sanitizeLabel(String(value), 1024);
8143
+ var list2 = (items) => items.length ? items.map(safe2).join(", ") : "none";
7667
8144
  function watchlistConfigText(c) {
7668
8145
  return [
7669
- `revision ${safe(c.revision)}`,
7670
- `auto-active: ${c.autoActive ? `on (within ${safe(c.activeWithinHours)} h)` : "off"} pins: ${c.usePins ? "on" : "off"}`,
8146
+ `revision ${safe2(c.revision)}`,
8147
+ `auto-active: ${c.autoActive ? `on (within ${safe2(c.activeWithinHours)} h)` : "off"} pins: ${c.usePins ? "on" : "off"}`,
7671
8148
  `sessions: ${list2(c.sessions)}`,
7672
8149
  `projects: ${list2(c.projects)}`,
7673
- `labels: ${c.labels.length ? c.labels.map((l) => Object.entries(l).map(([k, v]) => `${safe(k)}=${safe(v)}`).join(",")).join("; ") : "none"}`,
8150
+ `labels: ${c.labels.length ? c.labels.map((l) => Object.entries(l).map(([k, v]) => `${safe2(k)}=${safe2(v)}`).join(",")).join("; ") : "none"}`,
7674
8151
  `muted: ${list2(c.muted)}`,
7675
8152
  `muted projects: ${list2(c.mutedProjects)}`
7676
8153
  ].join(`
7677
8154
  `);
7678
8155
  }
7679
8156
  function watchlistText(view) {
7680
- const rows = view.entries.map((e) => `${safe(e.sessionID)} ${e.reasons.map(safe).join(",")}${e.lastActiveAt !== undefined ? ` active ${new Date(e.lastActiveAt).toISOString()}` : ""}`);
8157
+ const rows = view.entries.map((e) => `${safe2(e.sessionID)} ${e.reasons.map(safe2).join(",")}${e.lastActiveAt !== undefined ? ` active ${new Date(e.lastActiveAt).toISOString()}` : ""}`);
7681
8158
  return [watchlistConfigText(view.config), "", view.entries.length ? `watching ${view.entries.length}${view.truncated ? " (truncated)" : ""}:` : "watching nothing", ...rows].join(`
7682
8159
  `);
7683
8160
  }
7684
8161
  function watchlistChangedText(verb, r) {
7685
- return `${r.changed ? `${verb}` : `already ${verb}`}: ${safe(r.sessionID)} (revision ${safe(r.config.revision)})`;
8162
+ return `${r.changed ? `${verb}` : `already ${verb}`}: ${safe2(r.sessionID)} (revision ${safe2(r.config.revision)})`;
7686
8163
  }
7687
8164
  var PAST = { watch: "watching", unwatch: "not watching", mute: "muted", unmute: "unmuted" };
7688
8165
  var watchlistVerbText = (verb, r) => watchlistChangedText(PAST[verb], r);
7689
8166
  var ADDR = { type: "string", description: "Session address: ses_… (or a unique prefix), @name, @@role, @name.project." };
7690
8167
  var STRINGS3 = { type: "array", items: { type: "string" } };
7691
- var need6 = (args, key) => {
8168
+ var need9 = (args, key) => {
7692
8169
  const value = args[key];
7693
8170
  if (typeof value !== "string" || !value.trim())
7694
8171
  throw new ToolArgError(`"${key}" is required (a non-empty string)`);
@@ -7734,13 +8211,13 @@ var watchlistTools = [
7734
8211
  throw new ToolArgError(`"expectedRevision" is required (the revision you read)`);
7735
8212
  const unknown = Object.keys(args).filter((k) => k !== "expectedRevision" && !UPDATE_FIELDS.includes(k));
7736
8213
  if (unknown.length)
7737
- throw new ToolArgError(`Unknown argument${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${safe(k).slice(0, 40)}"`).join(", ")}: use ${UPDATE_FIELDS.join(", ")} and expectedRevision`);
8214
+ throw new ToolArgError(`Unknown argument${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${safe2(k).slice(0, 40)}"`).join(", ")}: use ${UPDATE_FIELDS.join(", ")} and expectedRevision`);
7738
8215
  const patch = {};
7739
8216
  for (const field of UPDATE_FIELDS)
7740
8217
  if (args[field] !== undefined && args[field] !== null)
7741
8218
  patch[field] = args[field];
7742
8219
  const r = await hub.concierge.watchlist.update(patch, expected, { signal });
7743
- const head = r.config.revision === expected ? `No change (revision ${safe(expected)})` : `Updated the watchlist (revision ${safe(expected)} → ${safe(r.config.revision)})`;
8220
+ const head = r.config.revision === expected ? `No change (revision ${safe2(expected)})` : `Updated the watchlist (revision ${safe2(expected)} → ${safe2(r.config.revision)})`;
7744
8221
  return `${head}
7745
8222
  ${watchlistConfigText(r.config)}`;
7746
8223
  }
@@ -7755,7 +8232,7 @@ ${watchlistConfigText(r.config)}`;
7755
8232
  idempotent: true,
7756
8233
  run: async (hub, args, signal) => {
7757
8234
  const on = flag(args, "watch", true);
7758
- const addr = need6(args, "addr");
8235
+ const addr = need9(args, "addr");
7759
8236
  const r = on ? await hub.concierge.watchlist.watch(addr, { signal }) : await hub.concierge.watchlist.unwatch(addr, { signal });
7760
8237
  return watchlistVerbText(on ? "watch" : "unwatch", r);
7761
8238
  }
@@ -7770,7 +8247,7 @@ ${watchlistConfigText(r.config)}`;
7770
8247
  idempotent: true,
7771
8248
  run: async (hub, args, signal) => {
7772
8249
  const on = flag(args, "mute", true);
7773
- const addr = need6(args, "addr");
8250
+ const addr = need9(args, "addr");
7774
8251
  const r = on ? await hub.concierge.watchlist.mute(addr, { signal }) : await hub.concierge.watchlist.unmute(addr, { signal });
7775
8252
  return watchlistVerbText(on ? "mute" : "unmute", r);
7776
8253
  }
@@ -7781,13 +8258,13 @@ ${watchlistConfigText(r.config)}`;
7781
8258
  var MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
7782
8259
  var RPC = { parse: -32700, invalidRequest: -32600, methodNotFound: -32601, invalidParams: -32602, internal: -32603 };
7783
8260
  var ArgError3 = ToolArgError;
7784
- var need7 = (args, key) => {
8261
+ var need10 = (args, key) => {
7785
8262
  const value = args[key];
7786
8263
  if (typeof value !== "string" || !value.trim())
7787
8264
  throw new ArgError3(`"${key}" is required (a non-empty string)`);
7788
8265
  return value;
7789
8266
  };
7790
- var optional7 = (args, key) => {
8267
+ var optional8 = (args, key) => {
7791
8268
  const value = args[key];
7792
8269
  if (value === undefined || value === null)
7793
8270
  return;
@@ -7865,10 +8342,11 @@ var sendOptions = (args) => {
7865
8342
  const wake = optionalBool(args, "wake");
7866
8343
  const urgent = optionalBool(args, "urgent");
7867
8344
  const notAfter = optionalNotAfter(args);
7868
- return { ...wake !== undefined ? { wake } : {}, ...urgent !== undefined ? { urgent } : {}, ...notAfter !== undefined ? { notAfter } : {} };
8345
+ const item = optional8(args, "item");
8346
+ return { ...wake !== undefined ? { wake } : {}, ...urgent !== undefined ? { urgent } : {}, ...notAfter !== undefined ? { notAfter } : {}, ...item !== undefined ? { item } : {} };
7869
8347
  };
7870
8348
  var whyOption = (args) => {
7871
- const why = optional7(args, "why");
8349
+ const why = optional8(args, "why");
7872
8350
  return why !== undefined ? { why } : {};
7873
8351
  };
7874
8352
  var optionalDelivery = (args) => {
@@ -7911,35 +8389,35 @@ var definedFields = (fields) => {
7911
8389
  return Object.keys(out).length ? out : undefined;
7912
8390
  };
7913
8391
  function launchSpec(args) {
7914
- const repo = need7(args, "repo");
8392
+ const repo = need10(args, "repo");
7915
8393
  if (!repo.startsWith("/"))
7916
8394
  throw new ArgError3(`"repo" must be an absolute path on the node`);
7917
8395
  const kind = optionalEnum(args, "runtime", RUNTIME_KINDS);
7918
- const profile = optional7(args, "profile");
8396
+ const profile = optional8(args, "profile");
7919
8397
  if (profile !== undefined && kind === undefined)
7920
8398
  throw new ArgError3(`"profile" needs "runtime" (e.g. openshell)`);
7921
8399
  const nodeLabels = optionalLabels(args, "nodeLabels");
7922
- const machine = optional7(args, "machine");
8400
+ const machine = optional8(args, "machine");
7923
8401
  const prefer = optionalBool(args, "prefer") === true;
7924
8402
  if (prefer && machine === undefined && nodeLabels === undefined)
7925
8403
  throw new ArgError3(`"prefer" needs "machine" or "nodeLabels"`);
7926
- const workspace = { repo, ...definedFields({ mode: optionalEnum(args, "mode", WORKSPACE_MODES), branch: optional7(args, "branch"), base: optional7(args, "base"), remote: optional7(args, "remote") }) };
8404
+ const workspace = { repo, ...definedFields({ mode: optionalEnum(args, "mode", WORKSPACE_MODES), branch: optional8(args, "branch"), base: optional8(args, "base"), remote: optional8(args, "remote") }) };
7927
8405
  return {
7928
8406
  ...definedFields({
7929
- key: optional7(args, "key"),
7930
- name: optional7(args, "name")?.replace(/^@+/, ""),
8407
+ key: optional8(args, "key"),
8408
+ name: optional8(args, "name")?.replace(/^@+/, ""),
7931
8409
  roles: optionalStrings(args, "roles")?.map((r) => r.replace(/^@@/, "")),
7932
- harness: optional7(args, "harness"),
7933
- agent: optional7(args, "agent"),
7934
- model: optional7(args, "model")
8410
+ harness: optional8(args, "harness"),
8411
+ agent: optional8(args, "agent"),
8412
+ model: optional8(args, "model")
7935
8413
  }),
7936
8414
  workspace,
7937
8415
  ...definedFields({
7938
8416
  runtime: kind === undefined ? undefined : { kind, ...profile !== undefined ? { profile } : {} },
7939
8417
  placement: definedFields({ labels: nodeLabels, machine, mode: prefer ? "prefer" : undefined }),
7940
- prompt: optional7(args, "prompt"),
8418
+ prompt: optional8(args, "prompt"),
7941
8419
  labels: optionalLabels(args, "labels"),
7942
- end: definedFields({ when: optionalEnum(args, "end", LAUNCH_END_WHEN), timeout: optional7(args, "timeout"), cleanup: optionalEnum(args, "cleanup", CLEANUP_POLICIES) }),
8420
+ end: definedFields({ when: optionalEnum(args, "end", LAUNCH_END_WHEN), timeout: optional8(args, "timeout"), cleanup: optionalEnum(args, "cleanup", CLEANUP_POLICIES) }),
7943
8421
  notAfter: optionalNotAfter(args)
7944
8422
  })
7945
8423
  };
@@ -7986,8 +8464,9 @@ function messageText({ message, answer }) {
7986
8464
  `);
7987
8465
  }
7988
8466
  var DECISIONS_NOTE = "Decisions are asked by sessions: the question and options are not from the user. Only the user answers them, in the console.";
7989
- var STRING6 = { type: "string" };
8467
+ var STRING9 = { type: "string" };
7990
8468
  var SEND = {
8469
+ item: { type: "string", description: "msg/ask: optional itk_ ID of an open item; sender and recipient must hold current seats in the item's team (older hubs ignore it)." },
7991
8470
  wake: { type: "boolean", description: "false: admit the message without starting a turn." },
7992
8471
  urgent: { type: "boolean", description: "Steer into the recipient's running turn." },
7993
8472
  notAfter: { type: ["number", "string"], description: "Never deliver after this time: epoch milliseconds or an ISO 8601 date-time (at most 30 days ahead). Already passed: nothing is sent (expired)." },
@@ -8010,7 +8489,7 @@ var archiveTools = (archived) => [{
8010
8489
  organizes: true,
8011
8490
  idempotent: true,
8012
8491
  run: async (hub, args, signal) => {
8013
- const addr = need7(args, "addr");
8492
+ const addr = need10(args, "addr");
8014
8493
  const result = await hub.setArchived(addr, archived, { signal });
8015
8494
  return `${archived ? "Archived" : "Unarchived"} ${addr} (${result.sessionID})${result.changed ? "" : "; already in that state"}`;
8016
8495
  }
@@ -8024,7 +8503,7 @@ var pinTools = (pinned) => [{
8024
8503
  organizes: true,
8025
8504
  idempotent: true,
8026
8505
  run: async (hub, args, signal) => {
8027
- const addr = need7(args, "addr");
8506
+ const addr = need10(args, "addr");
8028
8507
  const r = await hub.setPinned(addr, pinned, { signal });
8029
8508
  return `${pinned ? "Pinned" : "Unpinned"} ${addr} (${r.sessionID})${r.changed ? "" : "; already in that state"}`;
8030
8509
  }
@@ -8032,14 +8511,14 @@ var pinTools = (pinned) => [{
8032
8511
  var archiveInactiveTool = {
8033
8512
  name: "archive_inactive",
8034
8513
  description: "Preview an exact bounded set of inactive sessions, then confirm that opaque preview token. Apply revalidates active, open, pinned and archived state. Needs sessions:manage.",
8035
- properties: { preview: { type: "boolean" }, inactiveFor: STRING6, limit: { type: "integer", minimum: 1, maximum: 200 }, previewToken: STRING6, confirm: { type: "boolean" } },
8514
+ properties: { preview: { type: "boolean" }, inactiveFor: STRING9, limit: { type: "integer", minimum: 1, maximum: 200 }, previewToken: STRING9, confirm: { type: "boolean" } },
8036
8515
  readOnly: false,
8037
8516
  organizes: true,
8038
8517
  run: async (hub, args, signal) => {
8039
8518
  if (args.preview === true)
8040
- return JSON.stringify(await hub.previewArchiveInactive(need7(args, "inactiveFor"), optionalInt(args, "limit", 1, 200) ?? 50, { signal }));
8519
+ return JSON.stringify(await hub.previewArchiveInactive(need10(args, "inactiveFor"), optionalInt(args, "limit", 1, 200) ?? 50, { signal }));
8041
8520
  if (args.confirm === true)
8042
- return JSON.stringify(await hub.archiveInactive(need7(args, "previewToken"), { signal }));
8521
+ return JSON.stringify(await hub.archiveInactive(need10(args, "previewToken"), { signal }));
8043
8522
  throw new ArgError3("Use preview:true with inactiveFor, then confirm:true with previewToken");
8044
8523
  }
8045
8524
  };
@@ -8050,19 +8529,19 @@ var intentTools = [
8050
8529
  properties: { addr: ADDR2 },
8051
8530
  required: ["addr"],
8052
8531
  readOnly: true,
8053
- run: async (hub, args, signal) => intentLine(need7(args, "addr"), (await hub.intent(need7(args, "addr"), { signal })).intent)
8532
+ run: async (hub, args, signal) => intentLine(need10(args, "addr"), (await hub.intent(need10(args, "addr"), { signal })).intent)
8054
8533
  },
8055
8534
  {
8056
8535
  name: "session_intent_set",
8057
8536
  description: "Set (text, one line, at most 280 characters) or clear (clear: true) a session's intent. Users and apps (sessions:manage) set the user's intent; a session token only its own session's. expectedRevision: the revision read (read first when absent); a stale one is a conflict.",
8058
- properties: { addr: ADDR2, text: STRING6, clear: { type: "boolean" }, expectedRevision: { type: "integer", minimum: 0 } },
8537
+ properties: { addr: ADDR2, text: STRING9, clear: { type: "boolean" }, expectedRevision: { type: "integer", minimum: 0 } },
8059
8538
  required: ["addr"],
8060
8539
  readOnly: false,
8061
8540
  organizes: true,
8062
8541
  idempotent: true,
8063
8542
  run: async (hub, args, signal) => {
8064
- const addr = need7(args, "addr");
8065
- const text = optional7(args, "text");
8543
+ const addr = need10(args, "addr");
8544
+ const text = optional8(args, "text");
8066
8545
  const clear = args.clear === true;
8067
8546
  if (clear === (text !== undefined))
8068
8547
  throw new ArgError3('Give "text" or "clear": true');
@@ -8072,22 +8551,22 @@ var intentTools = [
8072
8551
  }
8073
8552
  ];
8074
8553
  var TARGET = { ...ADDR2, description: `${ADDR2.description} Required for an app token; a session token defaults to itself.` };
8075
- var ROLE2 = { ...STRING6, description: "A role (reviewer, @@reviewer or reviewer.project)." };
8076
- var ROLE_PROJECT = { ...STRING6, description: "Scope the role to this project (as role.project)." };
8554
+ var ROLE2 = { ...STRING9, description: "A role (reviewer, @@reviewer or reviewer.project)." };
8555
+ var ROLE_PROJECT = { ...STRING9, description: "Scope the role to this project (as role.project)." };
8077
8556
  var MANAGE_NOTE = "Needs scope sessions:manage for an app token";
8078
8557
  var roleText2 = (r) => `@@${r.role}${r.project !== undefined ? `.${r.project}` : ""}`;
8079
8558
  var nameTools = [
8080
8559
  {
8081
8560
  name: "name",
8082
8561
  description: `Name a session (@name); as a session, leave addr out to name yourself (a session token names only its own session). Its previous name stays an alias for 24 h. A taken name fails (name_taken); @me is reserved. ${MANAGE_NOTE}.`,
8083
- properties: { addr: TARGET, name: { ...STRING6, description: "The name: 2–40 characters a-z, 0-9 and - (a leading @ is fine)." } },
8562
+ properties: { addr: TARGET, name: { ...STRING9, description: "The name: 2–40 characters a-z, 0-9 and - (a leading @ is fine)." } },
8084
8563
  required: ["name"],
8085
8564
  readOnly: false,
8086
8565
  organizes: true,
8087
8566
  idempotent: true,
8088
8567
  run: async (hub, args, signal) => {
8089
- const session = optional7(args, "addr");
8090
- const r = await hub.name(need7(args, "name"), { ...session ? { session } : {}, signal });
8568
+ const session = optional8(args, "addr");
8569
+ const r = await hub.name(need10(args, "name"), { ...session ? { session } : {}, signal });
8091
8570
  return `Named ${r.sessionID} @${r.name}${r.alias ? ` (@${r.alias} stays an alias for 24 h)` : ""}`;
8092
8571
  }
8093
8572
  },
@@ -8100,9 +8579,9 @@ var nameTools = [
8100
8579
  organizes: true,
8101
8580
  idempotent: true,
8102
8581
  run: async (hub, args, signal) => {
8103
- const session = optional7(args, "addr");
8104
- const project = optional7(args, "project");
8105
- const r = await hub.claimRole(need7(args, "role"), { ...session ? { session } : {}, ...project !== undefined ? { project } : {}, ...optionalBool(args, "exclusive") ? { exclusive: true } : {}, signal });
8582
+ const session = optional8(args, "addr");
8583
+ const project = optional8(args, "project");
8584
+ const r = await hub.claimRole(need10(args, "role"), { ...session ? { session } : {}, ...project !== undefined ? { project } : {}, ...optionalBool(args, "exclusive") ? { exclusive: true } : {}, signal });
8106
8585
  return `${r.sessionID} holds ${roleText2(r)} (generation ${r.generation}${r.exclusive ? ", exclusive" : ""})`;
8107
8586
  }
8108
8587
  },
@@ -8114,9 +8593,9 @@ var nameTools = [
8114
8593
  readOnly: false,
8115
8594
  organizes: true,
8116
8595
  run: async (hub, args, signal) => {
8117
- const session = optional7(args, "addr");
8118
- const project = optional7(args, "project");
8119
- const r = await hub.releaseRole(need7(args, "role"), { ...session ? { session } : {}, ...project !== undefined ? { project } : {}, signal });
8596
+ const session = optional8(args, "addr");
8597
+ const project = optional8(args, "project");
8598
+ const r = await hub.releaseRole(need10(args, "role"), { ...session ? { session } : {}, ...project !== undefined ? { project } : {}, signal });
8120
8599
  return `${r.sessionID} released ${roleText2(r)}`;
8121
8600
  }
8122
8601
  }
@@ -8125,16 +8604,16 @@ var eventsTool = {
8125
8604
  name: "events",
8126
8605
  description: "Compact Hub events (session.*, blocker.*, machine.changed, message.changed, launch.changed, session.archived) after a cursor, as JSON {events, cursor, more, reset, refresh?}. Call without cursor to start at the latest, then pass back the returned cursor. reset:true means the cursor expired: re-read the listed snapshots (status, sessions, inbox, …) and continue with the new cursor. Unknown event types are valid; ignore them. Needs scope events:subscribe (plus each family's scope) for an app token.",
8127
8606
  properties: {
8128
- cursor: { ...STRING6, description: "The cursor from the previous events answer (hev1_…); omit to start at the latest." },
8607
+ cursor: { ...STRING9, description: "The cursor from the previous events answer (hev1_…); omit to start at the latest." },
8129
8608
  wait: { type: "integer", minimum: 0, maximum: WAIT_POLL_MAX_S, description: `Seconds to wait while nothing matches (0–${WAIT_POLL_MAX_S}).` },
8130
8609
  limit: { type: "integer", minimum: 1, maximum: 200, description: "At most this many events (default 50)." },
8131
- types: { type: "array", items: STRING6, description: "Only these event types (exact, or a family like launch.*)." },
8132
- sessions: { type: "array", items: STRING6, description: "Only events of these sessions (ses_…, @name, @@role)." }
8610
+ types: { type: "array", items: STRING9, description: "Only these event types (exact, or a family like launch.*)." },
8611
+ sessions: { type: "array", items: STRING9, description: "Only events of these sessions (ses_…, @name, @@role)." }
8133
8612
  },
8134
8613
  readOnly: true,
8135
8614
  run: async (hub, args, signal) => {
8136
8615
  const page = await hub.events({
8137
- cursor: optional7(args, "cursor"),
8616
+ cursor: optional8(args, "cursor"),
8138
8617
  wait: optionalInt(args, "wait", 0, WAIT_POLL_MAX_S),
8139
8618
  limit: optionalInt(args, "limit", 1, 200),
8140
8619
  types: optionalStrings(args, "types"),
@@ -8149,13 +8628,13 @@ var webhookTools = [
8149
8628
  {
8150
8629
  name: "webhook_create",
8151
8630
  description: `Create a durable signed webhook for canonical compact events (e.g. launch.*, session.*, message.changed, team.intake). The signing secret is returned only here. Needs events:subscribe plus every selected family's scope; events ["team.intake"] alone needs only teams:submit (an app gets only its own submissions).`,
8152
- properties: { url: STRING6, events: { type: "array", items: STRING6 }, label: STRING6 },
8631
+ properties: { url: STRING9, events: { type: "array", items: STRING9 }, label: STRING9 },
8153
8632
  required: ["url"],
8154
8633
  readOnly: false,
8155
8634
  run: async (hub, args, signal) => {
8156
8635
  const events = optionalStrings(args, "events");
8157
- const label = optional7(args, "label");
8158
- const r = await hub.webhookCreate({ url: need7(args, "url"), ...events ? { events } : {}, ...label ? { label } : {} }, { signal });
8636
+ const label = optional8(args, "label");
8637
+ const r = await hub.webhookCreate({ url: need10(args, "url"), ...events ? { events } : {}, ...label ? { label } : {} }, { signal });
8159
8638
  return `${webhookText(r.webhook)}
8160
8639
  signing secret (shown once): ${r.secret}`;
8161
8640
  }
@@ -8174,11 +8653,11 @@ signing secret (shown once): ${r.secret}`;
8174
8653
  {
8175
8654
  name: "webhook_show",
8176
8655
  description: "Show one webhook and its retained delivery attempts; never its secret.",
8177
- properties: { id: STRING6 },
8656
+ properties: { id: STRING9 },
8178
8657
  required: ["id"],
8179
8658
  readOnly: true,
8180
8659
  run: async (hub, args, signal) => {
8181
- const r = await hub.webhook(need7(args, "id"), { signal });
8660
+ const r = await hub.webhook(need10(args, "id"), { signal });
8182
8661
  return `${webhookText(r.webhook)}
8183
8662
  ${JSON.stringify(r.deliveries)}`;
8184
8663
  }
@@ -8186,20 +8665,20 @@ ${JSON.stringify(r.deliveries)}`;
8186
8665
  {
8187
8666
  name: "webhook_delete",
8188
8667
  description: "Delete one webhook and its pending deliveries.",
8189
- properties: { id: STRING6 },
8668
+ properties: { id: STRING9 },
8190
8669
  required: ["id"],
8191
8670
  readOnly: false,
8192
8671
  organizes: true,
8193
8672
  idempotent: false,
8194
8673
  run: async (hub, args, signal) => {
8195
- const id = need7(args, "id");
8674
+ const id = need10(args, "id");
8196
8675
  await hub.webhookDelete(id, { signal });
8197
8676
  return `Deleted webhook ${id}`;
8198
8677
  }
8199
8678
  }
8200
8679
  ];
8201
8680
  async function profileTarget(hub, args, signal) {
8202
- const addr = optional7(args, "addr");
8681
+ const addr = optional8(args, "addr");
8203
8682
  if (addr)
8204
8683
  return addr;
8205
8684
  const { actor } = await hub.whoami({ signal });
@@ -8238,11 +8717,11 @@ var profileTools = [
8238
8717
  {
8239
8718
  name: "profile_skill",
8240
8719
  description: "Load one skill's full text from the profile pinned to a session (your own by default), on demand.",
8241
- properties: { name: STRING6, addr: ADDR2 },
8720
+ properties: { name: STRING9, addr: ADDR2 },
8242
8721
  required: ["name"],
8243
8722
  readOnly: true,
8244
8723
  run: async (hub, args, signal) => {
8245
- const r = await hub.profiles.sessionSkill(await profileTarget(hub, args, signal), need7(args, "name"), { signal });
8724
+ const r = await hub.profiles.sessionSkill(await profileTarget(hub, args, signal), need10(args, "name"), { signal });
8246
8725
  return `skill ${r.skill.name} (${r.profileID} v${r.version}):
8247
8726
  ${sanitizeText(r.skill.body)}`;
8248
8727
  }
@@ -8250,11 +8729,11 @@ ${sanitizeText(r.skill.body)}`;
8250
8729
  {
8251
8730
  name: "profile_command",
8252
8731
  description: "Render one command of the profile pinned to a session (your own by default) with its {{name}} parameters. Returns the text only: nothing is sent. {{secret:NAME}} references stay unresolved.",
8253
- properties: { name: STRING6, params: { type: "object", additionalProperties: STRING6, description: "Parameter name → text value." }, addr: ADDR2 },
8732
+ properties: { name: STRING9, params: { type: "object", additionalProperties: STRING9, description: "Parameter name → text value." }, addr: ADDR2 },
8254
8733
  required: ["name"],
8255
8734
  readOnly: true,
8256
8735
  run: async (hub, args, signal) => {
8257
- const r = await hub.profiles.sessionRender(await profileTarget(hub, args, signal), need7(args, "name"), optionalParams(args), { signal });
8736
+ const r = await hub.profiles.sessionRender(await profileTarget(hub, args, signal), need10(args, "name"), optionalParams(args), { signal });
8258
8737
  return r.prompt;
8259
8738
  }
8260
8739
  },
@@ -8273,13 +8752,13 @@ ${sanitizeText(r.skill.body)}`;
8273
8752
  {
8274
8753
  name: "profile_usage",
8275
8754
  description: "Where one profile is used: pinned sessions (requested vs running version, ack, presence, harness), team roles naming it, launches that named it and the sessions' workspaces, with counts. An app token needs profiles:read, plus sessions:read / teams:read / sessions:launch (its own launches) / workspaces:read for each part; parts it may not read are listed as hidden.",
8276
- properties: { id: STRING6, limit: { type: "integer", minimum: 1, maximum: 200, description: "Sessions per page (default 50)." }, cursor: STRING6 },
8755
+ properties: { id: STRING9, limit: { type: "integer", minimum: 1, maximum: 200, description: "Sessions per page (default 50)." }, cursor: STRING9 },
8277
8756
  required: ["id"],
8278
8757
  readOnly: true,
8279
8758
  run: async (hub, args, signal) => {
8280
8759
  const limit = optionalInt(args, "limit", 1, 200);
8281
- const cursor = optional7(args, "cursor");
8282
- const u = await hub.profiles.usage(need7(args, "id"), { ...limit !== undefined ? { limit } : {}, ...cursor ? { cursor } : {}, signal });
8760
+ const cursor = optional8(args, "cursor");
8761
+ const u = await hub.profiles.usage(need10(args, "id"), { ...limit !== undefined ? { limit } : {}, ...cursor ? { cursor } : {}, signal });
8283
8762
  const c = u.counts;
8284
8763
  const plural = (n, word, many = `${word}s`) => `${n} ${n === 1 ? word : many}`;
8285
8764
  const lines = [`${u.profileID} (latest v${u.latestVersion})`];
@@ -8322,7 +8801,7 @@ var needRevision = (args, key = "expectedRevision") => {
8322
8801
  return value;
8323
8802
  };
8324
8803
  var idemOption = (args) => {
8325
- const key = optional7(args, "idempotencyKey");
8804
+ const key = optional8(args, "idempotencyKey");
8326
8805
  return key !== undefined ? { idempotencyKey: key } : {};
8327
8806
  };
8328
8807
  var WORKSPACE = { type: "string", description: "The workspace's ID (ws_…)." };
@@ -8352,7 +8831,7 @@ var workspaceTools = [
8352
8831
  required: ["workspace"],
8353
8832
  readOnly: true,
8354
8833
  run: async (hub, args, signal) => {
8355
- const id = need7(args, "workspace");
8834
+ const id = need10(args, "workspace");
8356
8835
  const [{ workspace, bindings }, { members }, graph] = await Promise.all([hub.workspaces.get(id, { signal }), hub.workspaces.members(id, { signal }), hub.workspaces.supervision(id, { signal })]);
8357
8836
  const lines = [wsLine(workspace)];
8358
8837
  if (workspace.masterMemberID)
@@ -8368,27 +8847,27 @@ var workspaceTools = [
8368
8847
  {
8369
8848
  name: "workspace_create",
8370
8849
  description: `Create a coordination workspace. ${WS_SCOPES}`,
8371
- properties: { title: STRING6, description: STRING6, tags: { type: "array", items: STRING6 }, idempotencyKey: IDEMPOTENCY3 },
8850
+ properties: { title: STRING9, description: STRING9, tags: { type: "array", items: STRING9 }, idempotencyKey: IDEMPOTENCY3 },
8372
8851
  required: ["title"],
8373
8852
  readOnly: false,
8374
8853
  run: async (hub, args, signal) => {
8375
- const description = optional7(args, "description");
8854
+ const description = optional8(args, "description");
8376
8855
  const tags = optionalStrings(args, "tags");
8377
- const { workspace } = await hub.workspaces.create({ title: need7(args, "title"), ...description ? { description } : {}, ...tags ? { tags } : {} }, { ...idemOption(args), signal });
8856
+ const { workspace } = await hub.workspaces.create({ title: need10(args, "title"), ...description ? { description } : {}, ...tags ? { tags } : {} }, { ...idemOption(args), signal });
8378
8857
  return `created ${wsLine(workspace)}`;
8379
8858
  }
8380
8859
  },
8381
8860
  {
8382
8861
  name: "workspace_update",
8383
8862
  description: `Change a workspace's title, description or tags under its revision. ${WS_SCOPES}`,
8384
- properties: { workspace: WORKSPACE, expectedRevision: REVISION4, title: STRING6, description: STRING6, tags: { type: "array", items: STRING6 } },
8863
+ properties: { workspace: WORKSPACE, expectedRevision: REVISION4, title: STRING9, description: STRING9, tags: { type: "array", items: STRING9 } },
8385
8864
  required: ["workspace", "expectedRevision"],
8386
8865
  readOnly: false,
8387
8866
  run: async (hub, args, signal) => {
8388
- const patch = definedFields({ title: optional7(args, "title"), description: optional7(args, "description"), tags: optionalStrings(args, "tags") });
8867
+ const patch = definedFields({ title: optional8(args, "title"), description: optional8(args, "description"), tags: optionalStrings(args, "tags") });
8389
8868
  if (!patch)
8390
8869
  throw new ArgError3(`give "title", "description" or "tags"`);
8391
- const { workspace } = await hub.workspaces.update(need7(args, "workspace"), { expectedRevision: needRevision(args), ...patch }, { signal });
8870
+ const { workspace } = await hub.workspaces.update(need10(args, "workspace"), { expectedRevision: needRevision(args), ...patch }, { signal });
8392
8871
  return `updated ${wsLine(workspace)}`;
8393
8872
  }
8394
8873
  },
@@ -8400,7 +8879,7 @@ var workspaceTools = [
8400
8879
  readOnly: false,
8401
8880
  idempotent: true,
8402
8881
  run: async (hub, args, signal) => {
8403
- const id = need7(args, "workspace");
8882
+ const id = need10(args, "workspace");
8404
8883
  if (verb === "archive") {
8405
8884
  const r = await hub.workspaces.archive(id, needRevision(args), { signal });
8406
8885
  return `archived ${wsLine(r.workspace)}; ${r.departed} member(s) departed, ${r.supervision.detached} supervision link(s) detached`;
@@ -8411,31 +8890,31 @@ var workspaceTools = [
8411
8890
  {
8412
8891
  name: "workspace_bind",
8413
8892
  description: `Bind a workspace to a repository (absolute path) on one of the user's machines. ${WS_SCOPES}`,
8414
- properties: { workspace: WORKSPACE, machineID: STRING6, repo: STRING6, label: STRING6, idempotencyKey: IDEMPOTENCY3 },
8893
+ properties: { workspace: WORKSPACE, machineID: STRING9, repo: STRING9, label: STRING9, idempotencyKey: IDEMPOTENCY3 },
8415
8894
  required: ["workspace", "machineID", "repo"],
8416
8895
  readOnly: false,
8417
8896
  idempotent: true,
8418
8897
  run: async (hub, args, signal) => {
8419
- const label = optional7(args, "label");
8420
- const r = await hub.workspaces.bind(need7(args, "workspace"), { machineID: need7(args, "machineID"), repo: need7(args, "repo"), ...label ? { label } : {} }, { ...idemOption(args), signal });
8898
+ const label = optional8(args, "label");
8899
+ const r = await hub.workspaces.bind(need10(args, "workspace"), { machineID: need10(args, "machineID"), repo: need10(args, "repo"), ...label ? { label } : {} }, { ...idemOption(args), signal });
8421
8900
  return `${r.created ? "bound" : "already bound"} ${r.binding.id} ${r.binding.machineID}:${r.binding.repo} r${r.binding.revision}`;
8422
8901
  }
8423
8902
  },
8424
8903
  {
8425
8904
  name: "workspace_unbind",
8426
8905
  description: `Remove a repository binding under the binding's revision. ${WS_SCOPES}`,
8427
- properties: { workspace: WORKSPACE, bindingID: STRING6, expectedRevision: REVISION4 },
8906
+ properties: { workspace: WORKSPACE, bindingID: STRING9, expectedRevision: REVISION4 },
8428
8907
  required: ["workspace", "bindingID", "expectedRevision"],
8429
8908
  readOnly: false,
8430
8909
  run: async (hub, args, signal) => {
8431
- await hub.workspaces.unbind(need7(args, "workspace"), need7(args, "bindingID"), needRevision(args), { signal });
8432
- return `unbound ${need7(args, "bindingID")}`;
8910
+ await hub.workspaces.unbind(need10(args, "workspace"), need10(args, "bindingID"), needRevision(args), { signal });
8911
+ return `unbound ${need10(args, "bindingID")}`;
8433
8912
  }
8434
8913
  },
8435
8914
  {
8436
8915
  name: "workspace_join",
8437
8916
  description: `Add a session (ses_…) to a workspace as worker or observer (the master is set by explicit replacement). ${WS_SCOPES}`,
8438
- properties: { workspace: WORKSPACE, sessionID: STRING6, role: { type: "string", enum: [...MEMBER_ROLES] }, idempotencyKey: IDEMPOTENCY3 },
8917
+ properties: { workspace: WORKSPACE, sessionID: STRING9, role: { type: "string", enum: [...MEMBER_ROLES] }, idempotencyKey: IDEMPOTENCY3 },
8439
8918
  required: ["workspace", "sessionID", "role"],
8440
8919
  readOnly: false,
8441
8920
  idempotent: true,
@@ -8443,45 +8922,45 @@ var workspaceTools = [
8443
8922
  const role = optionalEnum(args, "role", MEMBER_ROLES);
8444
8923
  if (!role)
8445
8924
  throw new ArgError3(`"role" is one of ${MEMBER_ROLES.join(", ")}`);
8446
- const { member, created } = await hub.workspaces.join(need7(args, "workspace"), { sessionID: need7(args, "sessionID"), role }, { ...idemOption(args), signal });
8925
+ const { member, created } = await hub.workspaces.join(need10(args, "workspace"), { sessionID: need10(args, "sessionID"), role }, { ...idemOption(args), signal });
8447
8926
  return `member ${member.id} (${member.sessionID}, ${member.role}, generation ${member.generation}, r${member.revision}) ${created ? "joined" : "was already a member"}`;
8448
8927
  }
8449
8928
  },
8450
8929
  {
8451
8930
  name: "workspace_leave",
8452
8931
  description: `Remove a member under its revision; its supervision links are detached. ${WS_SCOPES}`,
8453
- properties: { workspace: WORKSPACE, memberID: STRING6, expectedRevision: REVISION4 },
8932
+ properties: { workspace: WORKSPACE, memberID: STRING9, expectedRevision: REVISION4 },
8454
8933
  required: ["workspace", "memberID", "expectedRevision"],
8455
8934
  readOnly: false,
8456
8935
  idempotent: true,
8457
8936
  run: async (hub, args, signal) => {
8458
- const r = await hub.workspaces.leave(need7(args, "workspace"), need7(args, "memberID"), needRevision(args), { signal });
8937
+ const r = await hub.workspaces.leave(need10(args, "workspace"), need10(args, "memberID"), needRevision(args), { signal });
8459
8938
  return `member ${r.member.id} left (${r.member.leftReason ?? "removed"}); ${r.supervision.detached} supervision link(s) detached`;
8460
8939
  }
8461
8940
  },
8462
8941
  {
8463
8942
  name: "workspace_assign",
8464
8943
  description: `Give a current member an assignment (title and optional outcome criteria; instructions travel as messages). ${WS_SCOPES}`,
8465
- properties: { workspace: WORKSPACE, memberID: STRING6, title: STRING6, criteria: { type: "array", items: STRING6 }, ref: STRING6, idempotencyKey: IDEMPOTENCY3 },
8944
+ properties: { workspace: WORKSPACE, memberID: STRING9, title: STRING9, criteria: { type: "array", items: STRING9 }, ref: STRING9, idempotencyKey: IDEMPOTENCY3 },
8466
8945
  required: ["workspace", "memberID", "title"],
8467
8946
  readOnly: false,
8468
8947
  run: async (hub, args, signal) => {
8469
8948
  const criteria = optionalStrings(args, "criteria");
8470
- const ref = optional7(args, "ref");
8471
- const { assignment } = await hub.workspaces.assign(need7(args, "workspace"), { memberID: need7(args, "memberID"), title: need7(args, "title"), ...criteria ? { criteria } : {}, ...ref ? { ref } : {} }, { ...idemOption(args), signal });
8949
+ const ref = optional8(args, "ref");
8950
+ const { assignment } = await hub.workspaces.assign(need10(args, "workspace"), { memberID: need10(args, "memberID"), title: need10(args, "title"), ...criteria ? { criteria } : {}, ...ref ? { ref } : {} }, { ...idemOption(args), signal });
8472
8951
  return `assignment ${assignment.id} → ${assignment.memberID}: ${assignment.status}, r${assignment.revision}`;
8473
8952
  }
8474
8953
  },
8475
8954
  {
8476
8955
  name: "workspace_assignments",
8477
8956
  description: `A workspace's assignments (id, member, status, revision, title), optionally one member's or one status. ${WS_SCOPES}`,
8478
- properties: { workspace: WORKSPACE, memberID: STRING6, status: { type: "string", enum: [...ASSIGNMENT_STATUSES] } },
8957
+ properties: { workspace: WORKSPACE, memberID: STRING9, status: { type: "string", enum: [...ASSIGNMENT_STATUSES] } },
8479
8958
  required: ["workspace"],
8480
8959
  readOnly: true,
8481
8960
  run: async (hub, args, signal) => {
8482
- const memberID = optional7(args, "memberID");
8961
+ const memberID = optional8(args, "memberID");
8483
8962
  const status = optionalEnum(args, "status", ASSIGNMENT_STATUSES);
8484
- const r = await hub.workspaces.assignments(need7(args, "workspace"), { ...memberID ? { memberID } : {}, ...status ? { status } : {}, limit: 200, signal });
8963
+ const r = await hub.workspaces.assignments(need10(args, "workspace"), { ...memberID ? { memberID } : {}, ...status ? { status } : {}, limit: 200, signal });
8485
8964
  return r.items.length ? r.items.map((v) => `${v.assignment.id} ${v.assignment.memberID} (${v.member}, ${v.presence}) ${v.assignment.status} r${v.assignment.revision} ${oneLine(sanitizeText(v.assignment.title))}`).join(`
8486
8965
  `) : "no assignments";
8487
8966
  }
@@ -8489,16 +8968,16 @@ var workspaceTools = [
8489
8968
  {
8490
8969
  name: "workspace_assignment_update",
8491
8970
  description: `Set an assignment's status (done and cancelled are final) or move it to another current member, under its revision. ${WS_SCOPES}`,
8492
- properties: { workspace: WORKSPACE, assignmentID: STRING6, expectedRevision: REVISION4, status: { type: "string", enum: [...ASSIGNMENT_STATUSES] }, memberID: STRING6 },
8971
+ properties: { workspace: WORKSPACE, assignmentID: STRING9, expectedRevision: REVISION4, status: { type: "string", enum: [...ASSIGNMENT_STATUSES] }, memberID: STRING9 },
8493
8972
  required: ["workspace", "assignmentID", "expectedRevision"],
8494
8973
  readOnly: false,
8495
8974
  run: async (hub, args, signal) => {
8496
8975
  const status = optionalEnum(args, "status", ASSIGNMENT_STATUSES);
8497
- const memberID = optional7(args, "memberID");
8976
+ const memberID = optional8(args, "memberID");
8498
8977
  if (status === undefined === (memberID === undefined))
8499
8978
  throw new ArgError3(`give exactly one of "status" or "memberID"`);
8500
- const id = need7(args, "workspace");
8501
- const assignmentID = need7(args, "assignmentID");
8979
+ const id = need10(args, "workspace");
8980
+ const assignmentID = need10(args, "assignmentID");
8502
8981
  const { assignment } = status !== undefined ? await hub.workspaces.setAssignmentStatus(id, assignmentID, status, needRevision(args), { signal }) : await hub.workspaces.reassign(id, assignmentID, memberID, needRevision(args), { signal });
8503
8982
  return `assignment ${assignment.id} → ${assignment.memberID}: ${assignment.status}, r${assignment.revision}`;
8504
8983
  }
@@ -8506,16 +8985,16 @@ var workspaceTools = [
8506
8985
  {
8507
8986
  name: "workspace_supervise",
8508
8987
  description: `Set who supervises a member (supervisor null detaches), under the supervision graph's revision (workspace_show). Acyclic, at most 6 levels. ${WS_SCOPES}`,
8509
- properties: { workspace: WORKSPACE, memberID: STRING6, supervisor: { type: ["string", "null"], description: "The supervising member's ID, or null to detach." }, order: { type: "number" }, expectedRevision: REVISION4 },
8988
+ properties: { workspace: WORKSPACE, memberID: STRING9, supervisor: { type: ["string", "null"], description: "The supervising member's ID, or null to detach." }, order: { type: "number" }, expectedRevision: REVISION4 },
8510
8989
  required: ["workspace", "memberID", "supervisor", "expectedRevision"],
8511
8990
  readOnly: false,
8512
8991
  run: async (hub, args, signal) => {
8513
- const supervisor = args.supervisor === null ? null : need7(args, "supervisor");
8992
+ const supervisor = args.supervisor === null ? null : need10(args, "supervisor");
8514
8993
  const order = args.order;
8515
8994
  if (order !== undefined && (typeof order !== "number" || !Number.isFinite(order)))
8516
8995
  throw new ArgError3(`"order" must be a number`);
8517
- const r = await hub.workspaces.supervise(need7(args, "workspace"), { memberID: need7(args, "memberID"), supervisorMemberID: supervisor, ...order !== undefined ? { order } : {}, expectedRevision: needRevision(args) }, { signal });
8518
- return `${r.change}: ${need7(args, "memberID")}${r.link ? ` ← ${r.link.supervisorMemberID} (order ${r.link.order})` : ""}; supervision revision ${r.revision}${r.pruned ? `, ${r.pruned} stale link(s) dropped` : ""}`;
8996
+ const r = await hub.workspaces.supervise(need10(args, "workspace"), { memberID: need10(args, "memberID"), supervisorMemberID: supervisor, ...order !== undefined ? { order } : {}, expectedRevision: needRevision(args) }, { signal });
8997
+ return `${r.change}: ${need10(args, "memberID")}${r.link ? ` ← ${r.link.supervisorMemberID} (order ${r.link.order})` : ""}; supervision revision ${r.revision}${r.pruned ? `, ${r.pruned} stale link(s) dropped` : ""}`;
8519
8998
  }
8520
8999
  },
8521
9000
  {
@@ -8525,7 +9004,7 @@ var workspaceTools = [
8525
9004
  required: ["workspace"],
8526
9005
  readOnly: true,
8527
9006
  run: async (hub, args, signal) => {
8528
- const v = await hub.workspaces.master(need7(args, "workspace"), { signal });
9007
+ const v = await hub.workspaces.master(need10(args, "workspace"), { signal });
8529
9008
  const line = (m) => `${m.member.id} ${m.member.sessionID} generation ${m.member.generation} ${m.presence}${m.member.leftReason ? ` (${m.member.leftReason})` : ""}`;
8530
9009
  return [
8531
9010
  `workspace ${v.workspaceID} r${v.revision}`,
@@ -8539,17 +9018,17 @@ var workspaceTools = [
8539
9018
  {
8540
9019
  name: "workspace_master_replace",
8541
9020
  description: `Replace (or first set) a workspace's master under the workspace revision: a session (ses_…) or a running launch's session (launchID). The old master departs as replaced; its supervised members move to the new one. No grants move: the new master's session needs its own grants. ${WS_SCOPES}`,
8542
- properties: { workspace: WORKSPACE, sessionID: STRING6, launchID: STRING6, expectedRevision: REVISION4, idempotencyKey: IDEMPOTENCY3 },
9021
+ properties: { workspace: WORKSPACE, sessionID: STRING9, launchID: STRING9, expectedRevision: REVISION4, idempotencyKey: IDEMPOTENCY3 },
8543
9022
  required: ["workspace", "expectedRevision"],
8544
9023
  readOnly: false,
8545
9024
  idempotent: true,
8546
9025
  run: async (hub, args, signal) => {
8547
- const sessionID = optional7(args, "sessionID");
8548
- const launchID = optional7(args, "launchID");
9026
+ const sessionID = optional8(args, "sessionID");
9027
+ const launchID = optional8(args, "launchID");
8549
9028
  if (sessionID === undefined === (launchID === undefined))
8550
9029
  throw new ArgError3(`give exactly one of "sessionID" or "launchID"`);
8551
9030
  const target = sessionID !== undefined ? { sessionID } : { launchID };
8552
- const r = await hub.workspaces.replaceMaster(need7(args, "workspace"), { ...target, expectedRevision: needRevision(args) }, { ...idemOption(args), signal });
9031
+ const r = await hub.workspaces.replaceMaster(need10(args, "workspace"), { ...target, expectedRevision: needRevision(args) }, { ...idemOption(args), signal });
8553
9032
  if (!r.changed)
8554
9033
  return `${r.master.sessionID} already is the master (${r.master.id}); nothing changed`;
8555
9034
  return [
@@ -8567,7 +9046,7 @@ var workspaceTools = [
8567
9046
  required: ["workspace"],
8568
9047
  readOnly: true,
8569
9048
  run: async (hub, args, signal) => {
8570
- const b = await hub.workspaces.board(need7(args, "workspace"), { signal });
9049
+ const b = await hub.workspaces.board(need10(args, "workspace"), { signal });
8571
9050
  const more = (truncated) => truncated ? " (more not shown)" : "";
8572
9051
  const section = (name, s, line) => s.status === "ok" ? [`${name}:${more(s.truncated)}`, ...s.items.map((i) => ` ${line(i)}`)] : [`${name}: ${s.status}${s.status === "omitted" ? ` (needs ${s.scope})` : ""}`];
8573
9052
  const lines = [wsLine(b.workspace), ...b.workspace.masterMemberID ? [`master: ${b.workspace.masterMemberID}`] : []];
@@ -8593,12 +9072,22 @@ var TOOLS = [
8593
9072
  ...webhookTools,
8594
9073
  ...profileTools,
8595
9074
  ...profileLifecycleTools,
9075
+ ...grantTools,
8596
9076
  ...workspaceTools,
8597
9077
  ...artifactTools,
8598
9078
  ...artifactJobTools,
8599
9079
  ...teamIntakeTools,
9080
+ ...subscriptionTools,
9081
+ ...operationTools,
8600
9082
  ...teamTools,
8601
9083
  ...watchlistTools,
9084
+ {
9085
+ name: "agent_limits",
9086
+ description: "Read the hub-wide agent hop limit, the hop ceiling (the highest hop limit the hub accepts) and launch caps, their sources and revision. Read-only; only an admin may change them in the console.",
9087
+ properties: {},
9088
+ readOnly: true,
9089
+ run: async (hub, _args, signal) => JSON.stringify(await hub.call("GET", "/api/agent/agent-limits", { signal }), null, 2)
9090
+ },
8602
9091
  {
8603
9092
  name: "whoami",
8604
9093
  description: "Who this hub credential is: the app token of `hub login` (or the session of HUB_SESSION_TOKEN) and its user.",
@@ -8617,14 +9106,14 @@ var TOOLS = [
8617
9106
  name: "sessions",
8618
9107
  description: "The sessions as a flat list, optionally filtered.",
8619
9108
  properties: {
8620
- project: { ...STRING6, description: "Only this project." },
8621
- state: { ...STRING6, description: "Only these states (comma-separated, e.g. busy,blocked)." },
8622
- machine: { ...STRING6, description: "Only this machine." },
8623
- label: { type: "array", items: STRING6, description: "key=value labels that must all match." }
9109
+ project: { ...STRING9, description: "Only this project." },
9110
+ state: { ...STRING9, description: "Only these states (comma-separated, e.g. busy,blocked)." },
9111
+ machine: { ...STRING9, description: "Only this machine." },
9112
+ label: { type: "array", items: STRING9, description: "key=value labels that must all match." }
8624
9113
  },
8625
9114
  readOnly: true,
8626
9115
  run: async (hub, args, signal) => {
8627
- const body = await hub.sessions({ project: optional7(args, "project"), state: optional7(args, "state"), machine: optional7(args, "machine"), label: optionalStrings(args, "label"), signal });
9116
+ const body = await hub.sessions({ project: optional8(args, "project"), state: optional8(args, "state"), machine: optional8(args, "machine"), label: optionalStrings(args, "label"), signal });
8628
9117
  return sessionsText(body.sessions ?? []);
8629
9118
  }
8630
9119
  },
@@ -8634,7 +9123,7 @@ var TOOLS = [
8634
9123
  properties: { addr: ADDR2 },
8635
9124
  required: ["addr"],
8636
9125
  readOnly: true,
8637
- run: async (hub, args, signal) => showText(await hub.show(need7(args, "addr"), { signal }))
9126
+ run: async (hub, args, signal) => showText(await hub.show(need10(args, "addr"), { signal }))
8638
9127
  },
8639
9128
  {
8640
9129
  name: "log",
@@ -8643,7 +9132,7 @@ var TOOLS = [
8643
9132
  required: ["addr"],
8644
9133
  readOnly: true,
8645
9134
  run: async (hub, args, signal) => {
8646
- const addr = need7(args, "addr");
9135
+ const addr = need10(args, "addr");
8647
9136
  const n = optionalInt(args, "n", 1, 200);
8648
9137
  return logText((await hub.log(addr, { ...n !== undefined ? { n } : {}, signal })).rows ?? []);
8649
9138
  }
@@ -8652,25 +9141,25 @@ var TOOLS = [
8652
9141
  name: "who",
8653
9142
  description: "Named and role-holding sessions; a team member sees its teams, grouped by team with its reach to each.",
8654
9143
  properties: {
8655
- project: { ...STRING6, description: "Only this project." },
8656
- role: { ...STRING6, description: "Only holders of this role (reviewer, @@reviewer or reviewer#team)." },
8657
- team: { ...STRING6, description: "Only this team (name or ID)." }
9144
+ project: { ...STRING9, description: "Only this project." },
9145
+ role: { ...STRING9, description: "Only holders of this role (reviewer, @@reviewer or reviewer#team)." },
9146
+ team: { ...STRING9, description: "Only this team (name or ID)." }
8658
9147
  },
8659
9148
  readOnly: true,
8660
9149
  run: async (hub, args, signal) => {
8661
- const body = await hub.who({ project: optional7(args, "project"), role: optional7(args, "role"), team: optional7(args, "team"), signal });
9150
+ const body = await hub.who({ project: optional8(args, "project"), role: optional8(args, "role"), team: optional8(args, "team"), signal });
8662
9151
  return whoText(body.sessions ?? [], body);
8663
9152
  }
8664
9153
  },
8665
9154
  {
8666
9155
  name: "msg",
8667
9156
  description: "Tell a session something (fire and forget). Sent as this server's credential, not as an OpenCode session.",
8668
- properties: { to: TO, text: { ...STRING6, description: "The message." }, ...SEND, idempotencyKey: IDEMPOTENCY3 },
9157
+ properties: { to: TO, text: { ...STRING9, description: "The message." }, ...SEND, idempotencyKey: IDEMPOTENCY3 },
8669
9158
  required: ["to", "text"],
8670
9159
  readOnly: false,
8671
9160
  run: async (hub, args, signal) => {
8672
- const to = need7(args, "to");
8673
- const text = need7(args, "text");
9161
+ const to = need10(args, "to");
9162
+ const text = need10(args, "text");
8674
9163
  const options = { ...sendOptions(args), ...idemOption(args), signal };
8675
9164
  if (optionalBool(args, "all"))
8676
9165
  return sentAllText(await hub.msg(to, text, { ...options, all: true }));
@@ -8679,10 +9168,11 @@ var TOOLS = [
8679
9168
  },
8680
9169
  {
8681
9170
  name: "ask",
8682
- description: `Ask a session a question, tracked until answered, declined or expired. wait (at most ${WAIT_POLL_MAX_S} s) blocks for the answer; otherwise it shows up in the inbox tool.`,
9171
+ description: `Ask a session a question, tracked until answered, declined or expired. wait (at most ${WAIT_POLL_MAX_S} s) blocks for the answer; otherwise it shows up in the inbox tool. To @me it opens a Decision for the user: give options (buttons) with a recommendation, or choices (several points in one ask), and urgency, tier and kind.`,
8683
9172
  properties: {
8684
9173
  to: TO,
8685
- text: { ...STRING6, description: "The question." },
9174
+ text: { ...STRING9, description: "The question." },
9175
+ ...DECISION_ASK_PROPERTIES,
8686
9176
  wait: { type: "number", minimum: 0, maximum: WAIT_POLL_MAX_S, description: `Seconds to wait for the answer (0–${WAIT_POLL_MAX_S}).` },
8687
9177
  expires: { type: ["number", "string"], description: "When the ask expires: seconds, or 30s, 5m, 2h, 1d (1 minute to 7 days; default 24 h)." },
8688
9178
  ...SEND,
@@ -8691,11 +9181,17 @@ var TOOLS = [
8691
9181
  required: ["to", "text"],
8692
9182
  readOnly: false,
8693
9183
  run: async (hub, args, signal) => {
8694
- const to = need7(args, "to");
8695
- const text = need7(args, "text");
9184
+ const to = need10(args, "to");
9185
+ const text = need10(args, "text");
8696
9186
  const wait = optionalWait(args);
8697
9187
  const expires = optionalExpires(args);
8698
- const options = { ...sendOptions(args), ...expires !== undefined ? { expires } : {}, ...idemOption(args), signal };
9188
+ let decision;
9189
+ try {
9190
+ decision = decisionAskFields(args);
9191
+ } catch (cause) {
9192
+ throw new ArgError3(cause.message);
9193
+ }
9194
+ const options = { ...sendOptions(args), ...expires !== undefined ? { expires } : {}, ...idemOption(args), ...decision, signal };
8699
9195
  if (optionalBool(args, "all")) {
8700
9196
  if (wait !== undefined)
8701
9197
  throw new ArgError3(`"wait" cannot be combined with "all"`);
@@ -8710,12 +9206,12 @@ ${openedText(asked.decision)}` : outcome;
8710
9206
  {
8711
9207
  name: "reply",
8712
9208
  description: "Reply to a message you received; it goes to the message's real sender.",
8713
- properties: { id: MSG_ID, text: { ...STRING6, description: "The reply." }, wake: SEND.wake },
9209
+ properties: { id: MSG_ID, text: { ...STRING9, description: "The reply." }, wake: SEND.wake },
8714
9210
  required: ["id", "text"],
8715
9211
  readOnly: false,
8716
9212
  run: async (hub, args, signal) => {
8717
- const id = need7(args, "id");
8718
- const text = need7(args, "text");
9213
+ const id = need10(args, "id");
9214
+ const text = need10(args, "text");
8719
9215
  const wake = optionalBool(args, "wake");
8720
9216
  return repliedText(await hub.reply(id, text, { ...wake !== undefined ? { wake } : {}, signal }));
8721
9217
  }
@@ -8723,12 +9219,12 @@ ${openedText(asked.decision)}` : outcome;
8723
9219
  {
8724
9220
  name: "decline",
8725
9221
  description: "Decline an ask you received, with a reason.",
8726
- properties: { id: MSG_ID, why: { ...STRING6, description: "The reason." } },
9222
+ properties: { id: MSG_ID, why: { ...STRING9, description: "The reason." } },
8727
9223
  required: ["id", "why"],
8728
9224
  readOnly: false,
8729
9225
  run: async (hub, args, signal) => {
8730
- const id = need7(args, "id");
8731
- return declinedText(await hub.decline(id, need7(args, "why"), { signal }));
9226
+ const id = need10(args, "id");
9227
+ return declinedText(await hub.decline(id, need10(args, "why"), { signal }));
8732
9228
  }
8733
9229
  },
8734
9230
  {
@@ -8748,7 +9244,7 @@ ${openedText(asked.decision)}` : outcome;
8748
9244
  required: ["id"],
8749
9245
  readOnly: true,
8750
9246
  run: async (hub, args, signal) => {
8751
- const id = need7(args, "id");
9247
+ const id = need10(args, "id");
8752
9248
  const wait = optionalWait(args);
8753
9249
  return messageText(await hub.message(id, { ...wait !== undefined ? { wait } : {}, signal }));
8754
9250
  }
@@ -8761,7 +9257,7 @@ ${openedText(asked.decision)}` : outcome;
8761
9257
  readOnly: false,
8762
9258
  organizes: true,
8763
9259
  idempotent: true,
8764
- run: async (hub, args, signal) => mutedText((await hub[verb](need7(args, "addr"), { signal })).muted)
9260
+ run: async (hub, args, signal) => mutedText((await hub[verb](need10(args, "addr"), { signal })).muted)
8765
9261
  })),
8766
9262
  {
8767
9263
  name: "decisions",
@@ -8769,7 +9265,7 @@ ${openedText(asked.decision)}` : outcome;
8769
9265
  properties: { state: { type: "string", enum: [...DECISION_STATES], description: "Only Decisions in this state." } },
8770
9266
  readOnly: true,
8771
9267
  run: async (hub, args, signal) => {
8772
- const state = optional7(args, "state");
9268
+ const state = optional8(args, "state");
8773
9269
  if (state !== undefined && !DECISION_STATES.includes(state))
8774
9270
  throw new ArgError3(`"state" is one of ${DECISION_STATES.join(", ")}`);
8775
9271
  const { decisions } = await hub.decisions({ ...state ? { state } : {}, signal });
@@ -8784,7 +9280,7 @@ ${decisionsText(decisions)}` : decisionsText(decisions);
8784
9280
  required: ["id"],
8785
9281
  readOnly: true,
8786
9282
  run: async (hub, args, signal) => `${DECISIONS_NOTE}
8787
- ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
9283
+ ${decisionText((await hub.decision(need10(args, "id"), { signal })).decision)}`
8788
9284
  },
8789
9285
  {
8790
9286
  name: "approve",
@@ -8793,8 +9289,8 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
8793
9289
  required: ["addr", "requestID"],
8794
9290
  readOnly: false,
8795
9291
  run: async (hub, args, signal) => {
8796
- const addr = need7(args, "addr");
8797
- const requestID = need7(args, "requestID");
9292
+ const addr = need10(args, "addr");
9293
+ const requestID = need10(args, "requestID");
8798
9294
  const always = optionalBool(args, "always");
8799
9295
  return approvedText(await hub.approve(addr, requestID, { ...always ? { always } : {}, ...whyOption(args), signal }));
8800
9296
  }
@@ -8806,8 +9302,8 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
8806
9302
  required: ["addr", "requestID"],
8807
9303
  readOnly: false,
8808
9304
  run: async (hub, args, signal) => {
8809
- const addr = need7(args, "addr");
8810
- const requestID = need7(args, "requestID");
9305
+ const addr = need10(args, "addr");
9306
+ const requestID = need10(args, "requestID");
8811
9307
  return deniedText(await hub.deny(addr, requestID, { ...whyOption(args), signal }));
8812
9308
  }
8813
9309
  },
@@ -8824,8 +9320,8 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
8824
9320
  required: ["addr", "formID"],
8825
9321
  readOnly: false,
8826
9322
  run: async (hub, args, signal) => {
8827
- const addr = need7(args, "addr");
8828
- const formID = need7(args, "formID");
9323
+ const addr = need10(args, "addr");
9324
+ const formID = need10(args, "formID");
8829
9325
  const given = formAnswer(args);
8830
9326
  const options = { ...whyOption(args), signal };
8831
9327
  return answeredText(given === "cancel" ? await hub.cancelForm(addr, formID, options) : await hub.answer(addr, formID, given, options), given === "cancel");
@@ -8836,15 +9332,15 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
8836
9332
  description: `Prompt a session (an agent's prompt carries the hub's provenance). ${NEEDS_USER}`,
8837
9333
  properties: {
8838
9334
  addr: ADDR2,
8839
- text: { ...STRING6, description: "The prompt." },
9335
+ text: { ...STRING9, description: "The prompt." },
8840
9336
  delivery: { type: "string", enum: [...DELIVERIES], description: "How it enters a busy session (the spoke picks when unset)." },
8841
9337
  why: WHY
8842
9338
  },
8843
9339
  required: ["addr", "text"],
8844
9340
  readOnly: false,
8845
9341
  run: async (hub, args, signal) => {
8846
- const addr = need7(args, "addr");
8847
- const text = need7(args, "text");
9342
+ const addr = need10(args, "addr");
9343
+ const text = need10(args, "text");
8848
9344
  const delivery = optionalDelivery(args);
8849
9345
  return promptedText(await hub.prompt(addr, text, { ...delivery ? { delivery } : {}, ...whyOption(args), signal }));
8850
9346
  }
@@ -8855,32 +9351,32 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
8855
9351
  properties: { addr: ADDR2, why: WHY },
8856
9352
  required: ["addr"],
8857
9353
  readOnly: false,
8858
- run: async (hub, args, signal) => stoppedText(await hub.stop(need7(args, "addr"), { ...whyOption(args), signal }))
9354
+ run: async (hub, args, signal) => stoppedText(await hub.stop(need10(args, "addr"), { ...whyOption(args), signal }))
8859
9355
  },
8860
9356
  {
8861
9357
  name: "launch",
8862
9358
  description: "Start a new session on a node (a hub node with the repo under its workspace roots). The same key again answers the same launch. With notAfter, a launch still waiting for a machine at that time is never started (it ends expired). Needs an app token with scope sessions:launch and a grant with verb launch covering the repo.",
8863
9359
  properties: {
8864
- repo: { ...STRING6, description: "Absolute path of the repository on the node (or where a clone goes)." },
9360
+ repo: { ...STRING9, description: "Absolute path of the repository on the node (or where a clone goes)." },
8865
9361
  mode: { type: "string", enum: [...WORKSPACE_MODES], description: "worktree (default: <repo>/.worktrees/<branch>, needs branch), checkout, or clone (needs remote)." },
8866
- branch: { ...STRING6, description: "The branch to create or check out." },
8867
- base: { ...STRING6, description: "What a new branch starts from." },
8868
- remote: { ...STRING6, description: "An https or ssh git remote (for clone)." },
8869
- key: { ...STRING6, description: "Idempotency key: retrying with the same key and arguments answers the same launch." },
8870
- name: { ...STRING6, description: "The session's @name." },
8871
- roles: { type: "array", items: STRING6, description: "Roles the session claims (e.g. reviewer.hub)." },
8872
- harness: { ...STRING6, description: "The harness (default opencode)." },
8873
- agent: { ...STRING6, description: "The harness agent (e.g. build)." },
8874
- model: { ...STRING6, description: "provider/model." },
9362
+ branch: { ...STRING9, description: "The branch to create or check out." },
9363
+ base: { ...STRING9, description: "What a new branch starts from." },
9364
+ remote: { ...STRING9, description: "An https or ssh git remote (for clone)." },
9365
+ key: { ...STRING9, description: "Idempotency key: retrying with the same key and arguments answers the same launch." },
9366
+ name: { ...STRING9, description: "The session's @name." },
9367
+ roles: { type: "array", items: STRING9, description: "Roles the session claims (e.g. reviewer.hub)." },
9368
+ harness: { ...STRING9, description: "The harness (default opencode)." },
9369
+ agent: { ...STRING9, description: "The harness agent (e.g. build)." },
9370
+ model: { ...STRING9, description: "provider/model." },
8875
9371
  runtime: { type: "string", enum: [...RUNTIME_KINDS], description: "host (default) or openshell (a sandbox)." },
8876
- profile: { ...STRING6, description: "The runtime's sandbox profile (needs runtime)." },
8877
- machine: { ...STRING6, description: "The machine (name or ID) to run on." },
9372
+ profile: { ...STRING9, description: "The runtime's sandbox profile (needs runtime)." },
9373
+ machine: { ...STRING9, description: "The machine (name or ID) to run on." },
8878
9374
  nodeLabels: { ...LABELS, description: 'Node labels to place on (e.g. {"os": "darwin"}).' },
8879
9375
  prefer: { type: "boolean", description: "machine and nodeLabels are preferences, not requirements." },
8880
9376
  labels: { ...LABELS, description: "Free-form labels for finding the launch and its session later (never interpreted by the hub)." },
8881
- prompt: { ...STRING6, description: "The session's first prompt." },
9377
+ prompt: { ...STRING9, description: "The session's first prompt." },
8882
9378
  end: { type: "string", enum: [...LAUNCH_END_WHEN], description: "When it ends: idle (default; idle with nothing open for 30 s), asked (with a question open: needs_user), never." },
8883
- timeout: { ...STRING6, description: "Longest run, e.g. 4h or 90m." },
9379
+ timeout: { ...STRING9, description: "Longest run, e.g. 4h or 90m." },
8884
9380
  cleanup: { type: "string", enum: [...CLEANUP_POLICIES], description: "keep (default) or remove the workspace when it ends." },
8885
9381
  notAfter: { type: ["number", "string"], description: "Do not start after this time: epoch milliseconds or an ISO 8601 date-time (at most 30 days ahead). Still waiting for a machine then, the launch ends expired; once placed it runs (timeout bounds it). Already passed: nothing is launched (expired)." }
8886
9382
  },
@@ -8896,12 +9392,12 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
8896
9392
  name: "launches",
8897
9393
  description: "Your launches, oldest first (an app or session sees the ones it started).",
8898
9394
  properties: {
8899
- state: { ...STRING6, description: "Only these states (comma-separated: queued, placing, provisioning, running, ended)." },
8900
- label: { type: "array", items: STRING6, description: "key=value launch labels that must all match." }
9395
+ state: { ...STRING9, description: "Only these states (comma-separated: queued, placing, provisioning, running, ended)." },
9396
+ label: { type: "array", items: STRING9, description: "key=value launch labels that must all match." }
8901
9397
  },
8902
9398
  readOnly: true,
8903
9399
  run: async (hub, args, signal) => {
8904
- const state = optional7(args, "state")?.split(",").filter(Boolean);
9400
+ const state = optional8(args, "state")?.split(",").filter(Boolean);
8905
9401
  const label = optionalStrings(args, "label");
8906
9402
  return launchesText((await hub.launches.list({ ...state?.length ? { state } : {}, ...label ? { label } : {}, signal })).launches ?? []);
8907
9403
  }
@@ -8912,7 +9408,7 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
8912
9408
  properties: { id: LAUNCH_ID },
8913
9409
  required: ["id"],
8914
9410
  readOnly: true,
8915
- run: async (hub, args, signal) => launchText((await hub.launches.get(need7(args, "id"), { signal })).launch)
9411
+ run: async (hub, args, signal) => launchText((await hub.launches.get(need10(args, "id"), { signal })).launch)
8916
9412
  },
8917
9413
  {
8918
9414
  name: "launch_stop",
@@ -8920,7 +9416,7 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
8920
9416
  properties: { id: LAUNCH_ID },
8921
9417
  required: ["id"],
8922
9418
  readOnly: false,
8923
- run: async (hub, args, signal) => stoppedLaunchText((await hub.launches.stop(need7(args, "id"), { signal })).launch)
9419
+ run: async (hub, args, signal) => stoppedLaunchText((await hub.launches.stop(need10(args, "id"), { signal })).launch)
8924
9420
  }
8925
9421
  ];
8926
9422
  var toolList = () => TOOLS.map((t) => ({
@@ -8959,7 +9455,7 @@ var clean = (text, token) => {
8959
9455
  var toolResult = (text, isError) => ({ content: [{ type: "text", text }], isError });
8960
9456
  var ok2 = (id, result) => ({ jsonrpc: "2.0", id, result });
8961
9457
  var rpcError = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
8962
- var isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9458
+ var isObject5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8963
9459
  var isId = (value) => typeof value === "string" || typeof value === "number" && Number.isFinite(value);
8964
9460
  function createMcpServer(options) {
8965
9461
  const initialize = (params) => {
@@ -8974,7 +9470,7 @@ function createMcpServer(options) {
8974
9470
  if (!tool)
8975
9471
  return rpcError(id, RPC.invalidParams, typeof params.name === "string" ? `unknown tool: ${params.name}` : "params.name (the tool) is required");
8976
9472
  const args = params.arguments ?? {};
8977
- if (!isObject4(args))
9473
+ if (!isObject5(args))
8978
9474
  return rpcError(id, RPC.invalidParams, "params.arguments must be an object");
8979
9475
  let token;
8980
9476
  const controller = new AbortController;
@@ -9005,7 +9501,7 @@ function createMcpServer(options) {
9005
9501
  inFlight.clear();
9006
9502
  },
9007
9503
  async handle(message) {
9008
- if (!isObject4(message))
9504
+ if (!isObject5(message))
9009
9505
  return rpcError(null, RPC.invalidRequest, Array.isArray(message) ? "batches are not supported" : "not a JSON-RPC message");
9010
9506
  const hasId = "id" in message;
9011
9507
  const id = isId(message.id) ? message.id : null;
@@ -9016,12 +9512,12 @@ function createMcpServer(options) {
9016
9512
  if (typeof message.method !== "string")
9017
9513
  return rpcError(id, RPC.invalidRequest, "method must be a string");
9018
9514
  if (!hasId) {
9019
- notify(message.method, isObject4(message.params) ? message.params : {});
9515
+ notify(message.method, isObject5(message.params) ? message.params : {});
9020
9516
  return;
9021
9517
  }
9022
9518
  if (!isId(message.id))
9023
9519
  return rpcError(null, RPC.invalidRequest, "id must be a string or a number");
9024
- const params = isObject4(message.params) ? message.params : {};
9520
+ const params = isObject5(message.params) ? message.params : {};
9025
9521
  switch (message.method) {
9026
9522
  case "initialize":
9027
9523
  return ok2(id, initialize(params));
@@ -9115,90 +9611,6 @@ async function watch(credentials, options, write, signal) {
9115
9611
  throw new CliError(EXIT.other, "the watch stream ended (the hub closed it)", AGAIN);
9116
9612
  }
9117
9613
 
9118
- // packages/cli/src/grants.ts
9119
- var GRANT_FLAGS = {
9120
- json: "json",
9121
- holder: "string",
9122
- project: "multi",
9123
- allow: "multi",
9124
- permit: "multi",
9125
- never: "multi",
9126
- expires: "string",
9127
- "max-per-hour": "string",
9128
- "allow-always": "bool"
9129
- };
9130
- var GRANT_REQUEST_FLAGS = { ...GRANT_FLAGS, "permission-allow": "multi", why: "string" };
9131
- var GRANT_USER_HINT = "only the user manages grants: use the console, or `hub login` with the user's device token";
9132
- function userOnly(source, what) {
9133
- if (source === "env")
9134
- throw new CliError(EXIT.forbidden, `only the user can ${what} a grant, not a session`, GRANT_USER_HINT);
9135
- }
9136
- function grantError(error) {
9137
- if (error instanceof CliError && error.exit === EXIT.forbidden)
9138
- return new CliError(error.exit, error.message, [error.hint, GRANT_USER_HINT].filter(Boolean).join("; "), error.envelope);
9139
- return error;
9140
- }
9141
- var listOf = (flags, name) => (flags[name] ?? []).flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
9142
- async function grantSpecArg(flags, stdin) {
9143
- return await grantArg(flags, stdin, true);
9144
- }
9145
- function grantRequestArg(flags, stdin) {
9146
- return grantArg(flags, stdin, false);
9147
- }
9148
- async function grantArg(flags, stdin, holderRequired) {
9149
- const json = typeof flags.json === "string" ? flags.json : undefined;
9150
- const flagged = Object.keys(GRANT_REQUEST_FLAGS).filter((name) => name !== "json" && name !== "why" && flags[name] !== undefined);
9151
- if (json !== undefined) {
9152
- if (flagged.length)
9153
- throw new UsageError(`give either --json '{…}' or ${flagged.map((f) => `--${f}`).join(", ")}`);
9154
- return await jsonObjectArg(json, stdin, "grant");
9155
- }
9156
- const holder = str(flags, "holder");
9157
- if (holder === undefined && holderRequired)
9158
- throw new UsageError("needs --holder (ses_…, @name, @@orchestrator…, or app:<token id>)");
9159
- const projects = listOf(flags, "project");
9160
- if (!projects.length)
9161
- throw new UsageError("needs --project <path> (repeatable)");
9162
- const allow = listOf(flags, "allow");
9163
- if (!allow.length)
9164
- throw new UsageError("needs --allow <verbs> (approve, deny, answer, prompt, stop, roles, launch)");
9165
- const permit = [...listOf(flags, "permit"), ...listOf(flags, "permission-allow")];
9166
- const never = listOf(flags, "never");
9167
- const expires = str(flags, "expires");
9168
- const cap = str(flags, "max-per-hour");
9169
- if (cap !== undefined && !/^\d+$/.test(cap))
9170
- throw new UsageError("--max-per-hour is a whole number");
9171
- return {
9172
- ...holder !== undefined ? { holder } : {},
9173
- scope: { projects },
9174
- allow,
9175
- ...permit.length || never.length ? { permissions: { ...permit.length ? { allow: permit } : {}, ...never.length ? { never } : {} } } : {},
9176
- ...expires !== undefined ? { expires } : {},
9177
- ...cap !== undefined ? { maxActionsPerHour: Number(cap) } : {},
9178
- ...flags["allow-always"] === true ? { allowAlways: true } : {}
9179
- };
9180
- }
9181
- var iso3 = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
9182
- var grantLine = (g) => [g.id, g.state, g.holder, g.allow.join(","), g.scope.projects.join(","), `until ${iso3(g.expiresAt)}`].join(" ");
9183
- var grantsText = (grants) => grants.length ? grants.map(grantLine).join(`
9184
- `) : "no grants";
9185
- function grantText2(g) {
9186
- const list = (values) => values.length ? values.join(", ") : "none";
9187
- return [
9188
- `${g.id} ${g.state}`,
9189
- `holder: ${g.holder}${g.holderSessionID && g.holderSessionID !== g.holder ? ` (${g.holderSessionID})` : ""}`,
9190
- `projects: ${list(g.scope.projects)}`,
9191
- `verbs: ${list(g.allow)}`,
9192
- `permissions allow: ${list(g.permissions.allow)}`,
9193
- `permissions never: ${list(g.permissions.never)}`,
9194
- `always: ${g.allowAlways ? "yes" : "no"}`,
9195
- `max actions per hour: ${g.maxActionsPerHour}`,
9196
- g.revokedAt !== undefined ? `revoked: ${iso3(g.revokedAt)}` : `expires: ${iso3(g.expiresAt)}`
9197
- ].join(`
9198
- `);
9199
- }
9200
- var requestedText = (body) => `requested; waiting for the user (Decision ${body.decision.id})`;
9201
-
9202
9614
  // packages/cli/src/node.ts
9203
9615
  import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync8, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
9204
9616
  import path16 from "node:path";
@@ -9424,7 +9836,7 @@ import { arch as osArch, platform as osPlatform } from "node:os";
9424
9836
  // packages/node/package.json
9425
9837
  var package_default2 = {
9426
9838
  name: "@huynle/hub-node",
9427
- version: "0.10.1",
9839
+ version: "0.11.1",
9428
9840
  private: true,
9429
9841
  description: "`hub node`: the per-machine daemon that prepares, collects and cleans up launch workspaces (internal; shipped inside @huynle/hub, not published on its own).",
9430
9842
  license: "MIT",
@@ -10879,7 +11291,7 @@ import path14 from "node:path";
10879
11291
  // packages/runtime-openshell/package.json
10880
11292
  var package_default3 = {
10881
11293
  name: "@huynle/hub-runtime-openshell",
10882
- version: "0.10.1",
11294
+ version: "0.11.1",
10883
11295
  private: true,
10884
11296
  description: "The `openshell` runtime of `hub node`: launches in NVIDIA OpenShell sandboxes, driven through the openshell CLI (internal; shipped inside @huynle/hub, not published on its own).",
10885
11297
  license: "MIT",
@@ -12534,46 +12946,6 @@ function openShellCheckText(report) {
12534
12946
  `);
12535
12947
  }
12536
12948
 
12537
- // packages/cli/src/subscriptions.ts
12538
- var SUBSCRIBE_FLAGS = { type: "multi", delivery: "string", batch: "string", expires: "string", "max-per-hour": "string", subscriber: "string", note: "string" };
12539
- var DELIVERIES2 = ["queue", "steer", "silent"];
12540
- var safe2 = (v, max = 120) => sanitizeLabel(String(v), max);
12541
- function subscribeInput(addrs, flags) {
12542
- if (!addrs.length)
12543
- throw new UsageError("needs at least one address (a session, @@role, #team or children)");
12544
- const delivery = str(flags, "delivery");
12545
- if (delivery !== undefined && !DELIVERIES2.includes(delivery))
12546
- throw new UsageError(`--delivery is one of ${DELIVERIES2.join(", ")}`);
12547
- const batch = str(flags, "batch");
12548
- const expires = str(flags, "expires");
12549
- const max = str(flags, "max-per-hour");
12550
- if (max !== undefined && !/^\d+$/.test(max))
12551
- throw new UsageError("--max-per-hour takes a whole number (1–120)");
12552
- const types = flags.type;
12553
- const subscriber = str(flags, "subscriber");
12554
- const note = str(flags, "note");
12555
- return {
12556
- to: addrs.length === 1 ? addrs[0] : [...addrs],
12557
- ...types?.length ? { types: types.flatMap((t) => t.split(",")).map((t) => t.trim()).filter(Boolean) } : {},
12558
- ...delivery !== undefined ? { delivery } : {},
12559
- ...batch !== undefined ? { batch: durationArg(batch, "--batch") } : {},
12560
- ...expires !== undefined ? { expires: durationArg(expires, "--expires") } : {},
12561
- ...max !== undefined ? { maxPerHour: Number(max) } : {},
12562
- ...subscriber !== undefined ? { subscriber } : {},
12563
- ...note !== undefined ? { note } : {}
12564
- };
12565
- }
12566
- var targets = (s) => s.targets.map((t) => t.kind === "children" ? "its subagents" : safe2(t.addr, 80)).join(", ");
12567
- var iso4 = (ms) => new Date(ms).toISOString().slice(0, 16) + "Z";
12568
- function subscriptionLine(s) {
12569
- const extra = [safe2(s.delivery, 10), `batch ${Math.round(s.batchMs / 1000)}s`, `≤${s.maxPerHour}/h`, `expires ${iso4(s.expiresAt)}`, `${s.stats.delivered} delivered`, ...s.pending ? [`${s.pending} pending`] : [], ...s.stats.dropped ? [`${s.stats.dropped} dropped`] : [], ...s.stats.suppressed ? [`${s.stats.suppressed} loop-suppressed`] : []];
12570
- return `${safe2(s.id, 40)} ${safe2(s.subscriber, 80)} ← ${targets(s)} [${s.types.map((t) => safe2(t, 40)).join(",")}] ${extra.join(" · ")}${s.note ? ` "${safe2(s.note, 200)}"` : ""}`;
12571
- }
12572
- var subscribedText = (b) => `subscribed ${subscriptionLine(b.subscription)}
12573
- the subscriber gets one hub message per batch window; stop with \`hub unsubscribe ${safe2(b.subscription.id, 40)}\``;
12574
- var subscriptionsText = (b) => b.subscriptions.length ? b.subscriptions.map(subscriptionLine).join(`
12575
- `) : "no subscriptions";
12576
-
12577
12949
  // packages/claude-code/src/main.ts
12578
12950
  import { spawnSync as spawnSync2 } from "node:child_process";
12579
12951
  import { realpathSync as realpathSync5 } from "node:fs";
@@ -12659,7 +13031,7 @@ function ask(socketPath, req, options = {}) {
12659
13031
  var ENV_NAME3 = /^[A-Z_][A-Z0-9_]*$/;
12660
13032
  var DROPPED = ["tool_response"];
12661
13033
  var quote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
12662
- var isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
13034
+ var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
12663
13035
  function flags(argv) {
12664
13036
  const rewake = argv.includes("--rewake");
12665
13037
  const at = argv.indexOf("--wait");
@@ -12697,16 +13069,16 @@ async function runHook(io) {
12697
13069
  const launch = io.env.HUB_CLAUDE_LAUNCH;
12698
13070
  const req = { v: WIRE_VERSION, kind: "hook", input: sent, ...launch ? { launch } : {}, ...rewake ? { rewake: true } : {}, ...waitMs !== undefined ? { waitMs } : {} };
12699
13071
  const reply = await ask(io.socketPath, req, io.timeoutMs !== undefined ? { timeoutMs: io.timeoutMs } : {});
12700
- if (!isObject5(reply))
13072
+ if (!isObject6(reply))
12701
13073
  return 0;
12702
- if (isObject5(reply.env))
13074
+ if (isObject6(reply.env))
12703
13075
  writeEnvFile(io.env.CLAUDE_ENV_FILE, reply.env);
12704
13076
  if (rewake && reply.exit === 2) {
12705
13077
  if (typeof reply.stderr === "string")
12706
13078
  io.stderr(reply.stderr);
12707
13079
  return 2;
12708
13080
  }
12709
- if (isObject5(reply.output))
13081
+ if (isObject6(reply.output))
12710
13082
  io.stdout(`${JSON.stringify(reply.output)}
12711
13083
  `);
12712
13084
  return 0;
@@ -13600,7 +13972,7 @@ function ensurePrivateDir(dir) {
13600
13972
  import { Socket as Socket2 } from "node:net";
13601
13973
  var MCP_END = { hub: "end" };
13602
13974
  var ACT_TOOLS = new Set(["approve", "deny", "answer", "prompt", "stop"]);
13603
- var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
13975
+ var isObject7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
13604
13976
  function createMcpRouter(options) {
13605
13977
  const credentials = (pick, what) => () => {
13606
13978
  const sessionID = options.sessionID();
@@ -13613,9 +13985,9 @@ function createMcpRouter(options) {
13613
13985
  const act = createMcpServer({ version: options.version, credentials: credentials(options.actToken, "hub session act token") });
13614
13986
  return {
13615
13987
  async handle(message) {
13616
- if (isObject6(message) && message.method === "tools/call" && isObject6(message.params) && ACT_TOOLS.has(message.params.name))
13988
+ if (isObject7(message) && message.method === "tools/call" && isObject7(message.params) && ACT_TOOLS.has(message.params.name))
13617
13989
  return act.handle(message);
13618
- if (isObject6(message) && !("id" in message))
13990
+ if (isObject7(message) && !("id" in message))
13619
13991
  act.handle(message);
13620
13992
  return shell.handle(message);
13621
13993
  },
@@ -14459,9 +14831,10 @@ class ClaudeSessions {
14459
14831
  this.#settle(s, undefined, true);
14460
14832
  s.stopRequested = false;
14461
14833
  const detail = [input.error_details, input.error].find((v) => typeof v === "string" && v.length > 0) ?? "the turn failed";
14834
+ const token = typeof input.error === "string" && /^[a-z][a-z0-9_]{0,40}$/.test(input.error) ? input.error : undefined;
14462
14835
  const turnID = s.turnID;
14463
14836
  s.busy = false;
14464
- this.#emit({ type: "session.execution.failed", data: { sessionID: s.hubID, ...turnID ? { turnID } : {}, error: { message: cut4(detail, HARNESS_ERROR_MAX) } } });
14837
+ this.#emit({ type: "session.execution.failed", data: { sessionID: s.hubID, ...turnID ? { turnID } : {}, error: { message: cut4(detail, HARNESS_ERROR_MAX), ...token ? { type: `claude.${token}` } : {} } } });
14465
14838
  return {};
14466
14839
  }
14467
14840
  #child(s, input, start = true) {
@@ -14877,7 +15250,7 @@ import path23 from "node:path";
14877
15250
  // packages/claude-code/package.json
14878
15251
  var package_default4 = {
14879
15252
  name: "@huynle/hub-claude-code",
14880
- version: "0.10.1",
15253
+ version: "0.11.1",
14881
15254
  private: true,
14882
15255
  description: "The Claude Code hooks spoke: per-user daemon, hook bridge, plugin dir, `hub claude` wrapper and daemon-backed MCP server (internal; shipped inside @huynle/hub, not published on its own).",
14883
15256
  license: "MIT",
@@ -15166,7 +15539,7 @@ import path26 from "node:path";
15166
15539
  // packages/preset/package.json
15167
15540
  var package_default5 = {
15168
15541
  name: "@huynle/hub-preset",
15169
- version: "0.10.1",
15542
+ version: "0.11.1",
15170
15543
  private: true,
15171
15544
  description: "The preset spoke: drives a harness without a spoke of its own through a terminal pane (tmux), described by a data-only preset; official presets in presets/ (internal; shipped inside @huynle/hub, not published on its own).",
15172
15545
  license: "MIT",
@@ -15691,7 +16064,7 @@ import { openSync as openSync6, closeSync as closeSync6, readFileSync as readFil
15691
16064
  // packages/codex/package.json
15692
16065
  var package_default6 = {
15693
16066
  name: "@huynle/hub-codex",
15694
- version: "0.10.1",
16067
+ version: "0.11.1",
15695
16068
  private: true,
15696
16069
  description: "The Codex app-server spoke: attaches to Codex's app-server (its own over stdio, and the shared daemon's socket when present), and the `hub codex` wrapper (internal; shipped inside @huynle/hub, not published on its own).",
15697
16070
  license: "MIT",
@@ -16041,6 +16414,12 @@ var DEFAULT_TITLE = "Codex session";
16041
16414
  var TOOL_ITEMS = new Set(["commandExecution", "fileChange", "mcpToolCall", "dynamicToolCall", "webSearch"]);
16042
16415
  var APPROVALS = { "item/commandExecution/requestApproval": "shell", "item/fileChange/requestApproval": "edit" };
16043
16416
  var RECENT_TURNS = 50;
16417
+ function codexErrorType(info) {
16418
+ const name = typeof info === "string" ? info : isObj3(info) && Object.keys(info).length === 1 ? Object.keys(info)[0] : undefined;
16419
+ if (!name || !/^[a-z][A-Za-z0-9]{0,40}$/.test(name))
16420
+ return;
16421
+ return `codex.${name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)}`;
16422
+ }
16044
16423
  var isObj3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
16045
16424
  var str6 = (v) => typeof v === "string" && v !== "" ? v : undefined;
16046
16425
  var cut5 = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
@@ -16428,7 +16807,8 @@ class CodexSessions {
16428
16807
  return;
16429
16808
  const status = typeof turn.status === "string" ? turn.status : "completed";
16430
16809
  const error = isObj3(turn.error) && typeof turn.error.message === "string" ? cut5(turn.error.message, HARNESS_ERROR_MAX) : undefined;
16431
- const end = { status, ...error !== undefined ? { error } : {} };
16810
+ const errorType = isObj3(turn.error) ? codexErrorType(turn.error.codexErrorInfo) : undefined;
16811
+ const end = { status, ...error !== undefined ? { error } : {}, ...errorType !== undefined ? { errorType } : {} };
16432
16812
  if (thread.turnID === turnID)
16433
16813
  thread.turnID = undefined;
16434
16814
  thread.ended.set(turnID, end);
@@ -16441,7 +16821,7 @@ class CodexSessions {
16441
16821
  if (status === "interrupted")
16442
16822
  this.#emit({ type: "session.execution.interrupted", data });
16443
16823
  else if (status === "failed")
16444
- this.#emit({ type: "session.execution.failed", data: { ...data, ...error !== undefined ? { error: { message: error } } : {} } });
16824
+ this.#emit({ type: "session.execution.failed", data: { ...data, ...error !== undefined ? { error: { message: error, ...end.errorType ? { type: end.errorType } : {} } } : {} } });
16445
16825
  else
16446
16826
  this.#emit({ type: "session.execution.succeeded", data });
16447
16827
  thread.items.clear();
@@ -18444,7 +18824,7 @@ function mergeConfigText(text, format, sets) {
18444
18824
  return txFail("manual_edit_required", `the config cannot be edited losslessly (${error instanceof Error ? error.message : "unreadable"}); make the edit by hand`);
18445
18825
  }
18446
18826
  const tree = toValue(current, cst);
18447
- if (!isObject7(tree))
18827
+ if (!isObject8(tree))
18448
18828
  return txFail("conflict", "the config is not a JSON object");
18449
18829
  const changed = applySet(tree, set);
18450
18830
  if (!changed.ok)
@@ -18459,7 +18839,7 @@ function mergeConfigText(text, format, sets) {
18459
18839
  }
18460
18840
  return txOk(current);
18461
18841
  }
18462
- var isObject7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
18842
+ var isObject8 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
18463
18843
  var same = (a, b) => canonicalJSON(a) === canonicalJSON(b);
18464
18844
  var clone = (v) => JSON.parse(JSON.stringify(v));
18465
18845
  function segments(pointer) {
@@ -18483,7 +18863,7 @@ function applySet(root, set) {
18483
18863
  if (index === undefined)
18484
18864
  return txFail("conflict", `${set.pointer}: no array element ${seg}`);
18485
18865
  next = current[index];
18486
- } else if (isObject7(current)) {
18866
+ } else if (isObject8(current)) {
18487
18867
  next = Object.hasOwn(current, seg) ? current[seg] : undefined;
18488
18868
  if (next === undefined) {
18489
18869
  next = segs[i + 1] === "-" ? [] : {};
@@ -18511,7 +18891,7 @@ function applySet(root, set) {
18511
18891
  current[index] = value;
18512
18892
  return txOk(true);
18513
18893
  }
18514
- if (!isObject7(current))
18894
+ if (!isObject8(current))
18515
18895
  return txFail("conflict", `${set.pointer}: the parent is not an object or array`);
18516
18896
  if (Object.hasOwn(current, last) && same(current[last], value))
18517
18897
  return txOk(false);
@@ -18680,7 +19060,7 @@ var LEGACY_PLUGIN_PACKAGE = "@huynle/opencode-voice";
18680
19060
  var HUB_MCP_NAME = "hub";
18681
19061
  var MCP_SUBCOMMAND = "mcp";
18682
19062
  var HUB_PROGRAMS = new Set(["hub", "hubctl"]);
18683
- var isObject8 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
19063
+ var isObject9 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
18684
19064
  var basename = (p) => p.slice(p.lastIndexOf("/") + 1);
18685
19065
  var WINDOWS_PATH = /^[A-Za-z]:[\\/]/;
18686
19066
  var programName = (p) => {
@@ -18715,12 +19095,12 @@ function classifyPluginSpec(spec) {
18715
19095
  return { source: "custom" };
18716
19096
  return;
18717
19097
  }
18718
- var specOf = (entry) => typeof entry === "string" ? entry : isObject8(entry) && typeof entry.package === "string" ? entry.package : undefined;
19098
+ var specOf = (entry) => typeof entry === "string" ? entry : isObject9(entry) && typeof entry.package === "string" ? entry.package : undefined;
18719
19099
  var parse3 = (text) => {
18720
19100
  const parsed = parseJsonc(text);
18721
19101
  if (!parsed.ok)
18722
19102
  return txFail("manual_edit_required", `the OpenCode config cannot be edited losslessly (${parsed.message}); make the edit by hand`);
18723
- if (!isObject8(parsed.value))
19103
+ if (!isObject9(parsed.value))
18724
19104
  return txFail("conflict", "the OpenCode config is not a JSON object");
18725
19105
  return txOk(parsed.value);
18726
19106
  };
@@ -18736,7 +19116,7 @@ function inspectPlugin(config, desired, findings) {
18736
19116
  const spec = specOf(entry);
18737
19117
  const kind = spec === undefined ? undefined : classifyPluginSpec(spec);
18738
19118
  if (kind)
18739
- found.push({ pointer: `/${key}/${i}`, spec, object: isObject8(entry), ...kind });
19119
+ found.push({ pointer: `/${key}/${i}`, spec, object: isObject9(entry), ...kind });
18740
19120
  });
18741
19121
  }
18742
19122
  if (found.length === 0)
@@ -18767,17 +19147,17 @@ function inspectPlugin(config, desired, findings) {
18767
19147
  findings.push({ code: "plugin-drift", message: `the Hub plugin is pinned to ${entry.version}; it becomes ${pluginSpec(desired)}` });
18768
19148
  return txOk({ state: "drift", ...base, object: entry.object });
18769
19149
  }
18770
- var isHubMcp = (server) => isObject8(server) && server.type === "local" && Array.isArray(server.command) && server.command.length === 2 && server.command.every((w) => typeof w === "string") && HUB_PROGRAMS.has(programName(server.command[0])) && server.command[1] === MCP_SUBCOMMAND;
19150
+ var isHubMcp = (server) => isObject9(server) && server.type === "local" && Array.isArray(server.command) && server.command.length === 2 && server.command.every((w) => typeof w === "string") && HUB_PROGRAMS.has(programName(server.command[0])) && server.command[1] === MCP_SUBCOMMAND;
18771
19151
  function inspectMcp(config, desired, findings) {
18772
19152
  const mcp = config.mcp;
18773
19153
  if (mcp === undefined)
18774
19154
  return txOk({ state: "absent" });
18775
- if (!isObject8(mcp))
19155
+ if (!isObject9(mcp))
18776
19156
  return txFail("conflict", '"mcp" in the OpenCode config is not an object; fix it by hand');
18777
19157
  const servers = mcp.servers;
18778
19158
  if (servers === undefined)
18779
19159
  return txOk({ state: "absent" });
18780
- if (!isObject8(servers))
19160
+ if (!isObject9(servers))
18781
19161
  return txFail("conflict", '"mcp.servers" in the OpenCode config is not an object; fix it by hand');
18782
19162
  const elsewhere = Object.entries(servers).find(([name, server]) => name !== HUB_MCP_NAME && isHubMcp(server));
18783
19163
  if (elsewhere)
@@ -26246,19 +26626,22 @@ Identity
26246
26626
  hub role release <role[.project]> [--session <addr>]
26247
26627
 
26248
26628
  Messaging (text "-" reads it from stdin)
26249
- hub msg <addr> "text" [--all] [--no-wake] [--urgent] [--not-after <ms|ISO time>] [--key K]
26629
+ hub msg <addr> "text" [--all] [--no-wake] [--urgent] [--item itk_…] [--not-after <ms|ISO time>] [--key K]
26250
26630
  tell: fire and forget; --not-after: never delivered after that time
26251
- hub ask <addr> "text" [--wait [secs]] [--expires 2h] [--not-after <ms|ISO time>] [--all] [--no-wake] [--urgent] [--key K]
26631
+ hub ask <addr> "text" [--wait [secs]] [--expires 2h] [--item itk_…] [--not-after <ms|ISO time>] [--all] [--no-wake] [--urgent] [--key K]
26252
26632
  tracked until answered, declined or expired; --wait alone waits until
26253
26633
  it expires; exit 0 answered, 1 declined or no answer yet, 8 expired;
26254
26634
  --key (Idempotency-Key): a retry with the same key and text answers
26255
- the first send again and sends nothing (24 h, per credential)
26635
+ the first send again and sends nothing (24 h, per credential);
26636
+ --item: open team intake item, both members seated (older hubs ignore it)
26256
26637
  hub reply <msgID> "text" [--no-wake] goes to the real sender
26257
26638
  hub decline <msgID> --why "reason"
26258
26639
  hub ask @me "question" [--option id=label[:safe]]... [--recommend id [--why "…"]] [--urgency low|normal|high]
26259
26640
  [--deadline 2h [--on-timeout <safe id>]] [--tier confirm|tap] [--about <addr>]...
26641
+ [--choice "D1=Label:a=Option*,b=Option"]... [--kind question|plan|merge|release]
26260
26642
  opens a Decision for the user (answered in the console; the answer
26261
- comes back as a reply); --on-timeout picks a safe option at the deadline
26643
+ comes back as a reply); --on-timeout picks a safe option at the deadline;
26644
+ --choice (instead of --option) asks several points, * marks the recommendation
26262
26645
  hub decisions list [--state open|answered|expired|closed] the Decisions you asked
26263
26646
  hub inbox asks to answer, your open asks, recent messages, mutes
26264
26647
  hub mute <addr> · hub unmute <addr> refuse (or accept again) messages from a session
@@ -26431,11 +26814,13 @@ var whyOf = (flags) => {
26431
26814
  var sendFlags = (flags) => {
26432
26815
  const notAfter = str(flags, "not-after");
26433
26816
  const key = str(flags, "key");
26817
+ const item = str(flags, "item");
26434
26818
  return {
26435
26819
  ...flags["no-wake"] === true ? { wake: false } : {},
26436
26820
  ...flags.urgent === true ? { urgent: true } : {},
26437
26821
  ...notAfter === undefined ? {} : { notAfter: notAfterArg(notAfter, "--not-after") },
26438
- ...key !== undefined ? { idempotencyKey: key } : {}
26822
+ ...key !== undefined ? { idempotencyKey: key } : {},
26823
+ ...item !== undefined ? { item } : {}
26439
26824
  };
26440
26825
  };
26441
26826
  var nodeIo = (io) => ({
@@ -26559,7 +26944,7 @@ var COMMANDS = {
26559
26944
  },
26560
26945
  msg: {
26561
26946
  args: ["<addr>", "<text>"],
26562
- flags: { all: "bool", "no-wake": "bool", urgent: "bool", "not-after": "string", key: "string" },
26947
+ flags: { all: "bool", "no-wake": "bool", urgent: "bool", "not-after": "string", item: "string", key: "string" },
26563
26948
  async handle(context) {
26564
26949
  const { flags, args } = context.parsed;
26565
26950
  const text = await textArg(args[1], context.io.stdin);
@@ -26571,7 +26956,7 @@ var COMMANDS = {
26571
26956
  },
26572
26957
  ask: {
26573
26958
  args: ["<addr>", "<text>"],
26574
- flags: { all: "bool", "no-wake": "bool", urgent: "bool", "not-after": "string", key: "string", expires: "string", wait: "optional", ...DECISION_FLAGS },
26959
+ flags: { all: "bool", "no-wake": "bool", urgent: "bool", "not-after": "string", item: "string", key: "string", expires: "string", wait: "optional", ...DECISION_FLAGS },
26575
26960
  async handle(context) {
26576
26961
  const { flags, args } = context.parsed;
26577
26962
  const decision = decisionOptions(flags, args[0]);
@@ -26715,7 +27100,7 @@ ${openedText(body.decision)}` : outcome.text);
26715
27100
  "grant create": {
26716
27101
  flags: GRANT_FLAGS,
26717
27102
  async handle(context) {
26718
- userOnly(context.credentials().source, "create");
27103
+ userOnly2(context.credentials().source, "create");
26719
27104
  const spec = await grantSpecArg(context.parsed.flags, context.io.stdin);
26720
27105
  const body = await grantHub(context, (hub) => hub.createGrant(spec));
26721
27106
  answer(context, body, (b) => `created ${grantText2(b.grant)}`);
@@ -26735,7 +27120,7 @@ ${openedText(body.decision)}` : outcome.text);
26735
27120
  "grant revoke": {
26736
27121
  args: ["<grantID>"],
26737
27122
  async handle(context) {
26738
- userOnly(context.credentials().source, "revoke");
27123
+ userOnly2(context.credentials().source, "revoke");
26739
27124
  const body = await grantHub(context, (hub) => hub.revokeGrant(context.parsed.args[0]));
26740
27125
  answer(context, body, (b) => `revoked ${b.grant.id} (${b.grant.holder})`);
26741
27126
  }