@huynle/hub 0.11.0 → 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 (102) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +19 -5
  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 +240 -83
  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/mcp.ts +20 -4
  12. package/packages/cli/src/team-intake-mcp.ts +2 -1
  13. package/packages/cli/src/teams-mcp.ts +7 -3
  14. package/packages/codex/package.json +1 -1
  15. package/packages/codex/src/sessions.ts +14 -3
  16. package/packages/node/package.json +1 -1
  17. package/packages/preset/package.json +1 -1
  18. package/packages/protocol/package.json +1 -1
  19. package/packages/protocol/schema/api/v1/_root.json +119 -0
  20. package/packages/protocol/schema/api/v1/account.json +419 -0
  21. package/packages/protocol/schema/api/v1/decisions.json +635 -0
  22. package/packages/protocol/schema/api/v1/discovery.json +110 -0
  23. package/packages/protocol/schema/api/v1/events.json +679 -0
  24. package/packages/protocol/schema/api/v1/session-actions.json +660 -0
  25. package/packages/protocol/schema/api/v1/sessions.json +2055 -0
  26. package/packages/protocol/schema/api/v1/speech.json +492 -0
  27. package/packages/protocol/schema/api/v1/unspecified.json +282 -0
  28. package/packages/protocol/schema/app-delivery/v1.schema.json +67 -5
  29. package/packages/protocol/schema/app-front/v1.schema.json +7 -3
  30. package/packages/protocol/src/api-v1.ts +8 -0
  31. package/packages/protocol/src/index.ts +2 -0
  32. package/packages/protocol/src/teams.ts +13 -1
  33. package/packages/runtime-openshell/package.json +1 -1
  34. package/packages/runtime-sdk/package.json +1 -1
  35. package/packages/sdk/package.json +1 -1
  36. package/packages/sdk/src/app-front.ts +2 -2
  37. package/packages/sdk/src/ask-fields.ts +76 -0
  38. package/packages/sdk/src/client.ts +11 -2
  39. package/packages/sdk/src/envelope.ts +3 -2
  40. package/packages/sdk/src/index.ts +1 -0
  41. package/packages/sdk/src/team-intake.ts +6 -0
  42. package/packages/sdk/src/teams.ts +2 -0
  43. package/packages/sdk/src/types.ts +36 -4
  44. package/packages/sdk/src/wake.ts +9 -4
  45. package/packages/spoke-sdk/package.json +1 -1
  46. package/src/central/agent/agent-limits.ts +185 -0
  47. package/src/central/agent/app-decisions.ts +6 -5
  48. package/src/central/agent/decisions.ts +130 -9
  49. package/src/central/agent/delivery.ts +134 -22
  50. package/src/central/agent/errors.ts +2 -1
  51. package/src/central/agent/events.ts +20 -2
  52. package/src/central/agent/launch-engine.ts +6 -3
  53. package/src/central/agent/message-limits.ts +5 -2
  54. package/src/central/agent/message-routes.ts +33 -6
  55. package/src/central/agent/messages.ts +7 -0
  56. package/src/central/agent/placement.ts +4 -2
  57. package/src/central/agent/read-routes.ts +5 -0
  58. package/src/central/agent/routes.ts +2 -0
  59. package/src/central/agent/subscriptions.ts +13 -4
  60. package/src/central/api-v1.ts +115 -0
  61. package/src/central/artifacts/store.ts +4 -2
  62. package/src/central/concierge/board-results.ts +2 -1
  63. package/src/central/concierge/notifier.ts +4 -2
  64. package/src/central/config.ts +20 -6
  65. package/src/central/health/classify.ts +209 -0
  66. package/src/central/http.ts +285 -23
  67. package/src/central/recording/index.ts +2 -1
  68. package/src/central/recording/schema.ts +2 -1
  69. package/src/central/registry.ts +34 -3
  70. package/src/central/teams/budgets.ts +144 -0
  71. package/src/central/teams/hop-stalls.ts +14 -6
  72. package/src/central/teams/intake.ts +38 -6
  73. package/src/central/teams/involvement.ts +95 -0
  74. package/src/central/teams/item-decisions.ts +60 -0
  75. package/src/central/teams/item-eligibility.ts +35 -0
  76. package/src/central/teams/item-stalls.ts +87 -0
  77. package/src/central/teams/loops.ts +40 -0
  78. package/src/central/teams/progress.ts +154 -0
  79. package/src/central/teams/routes.ts +2 -2
  80. package/src/central/teams/store.ts +21 -5
  81. package/src/central/teams/validation.ts +17 -6
  82. package/src/central/workspaces/coordinator.ts +3 -1
  83. package/src/spoke/hub-tool.ts +26 -11
  84. package/web/dist/assets/{ArtifactPage-C5MMqXLN.js → ArtifactPage-TbFJi59n.js} +1 -1
  85. package/web/dist/assets/{ArtifactPane-CXSCmhyJ.js → ArtifactPane-7D-M-Ich.js} +1 -1
  86. package/web/dist/assets/{ArtifactWorkbench-S0RxNGWy.js → ArtifactWorkbench-DlCLEjut.js} +1 -1
  87. package/web/dist/assets/{Diagram-BGmyRb-R.js → Diagram-PvOPetOI.js} +1 -1
  88. package/web/dist/assets/{Library-CFmMbXEa.js → Library-Cdi83pOV.js} +2 -2
  89. package/web/dist/assets/{ProfilesDestination-7jpOc2HM.js → ProfilesDestination-cryC7P6j.js} +1 -1
  90. package/web/dist/assets/TeamsPage-B4xrlGhp.js +1 -0
  91. package/web/dist/assets/Watchtower-60B1JX_J.js +2 -0
  92. package/web/dist/assets/index-DA0gKIzv.js +33 -0
  93. package/web/dist/assets/live-B2IfKm37.js +5 -0
  94. package/web/dist/assets/{live-BUofsQpz.css → live-De-kd3eW.css} +1 -1
  95. package/web/dist/assets/{renderer2d-B2Nh39UN.js → renderer2d-CsZnF3_o.js} +1 -1
  96. package/web/dist/assets/{renderer3d-CTYbljHH.js → renderer3d-BIMA0wFn.js} +2 -2
  97. package/web/dist/assets/{safe-area-CkhEclGo.js → safe-area-dtrnw9NF.js} +1 -1
  98. package/web/dist/index.html +1 -1
  99. package/web/dist/assets/TeamsPage-C1NGrUR-.js +0 -1
  100. package/web/dist/assets/Watchtower-DnvlJaU0.js +0 -2
  101. package/web/dist/assets/index-Da8SKXMw.js +0 -33
  102. package/web/dist/assets/live-BmqLASSo.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.11.0",
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 } : {}
@@ -1200,7 +1275,7 @@ class HubClient {
1200
1275
  // packages/sdk/src/envelope.ts
1201
1276
  var LABEL_MAX = 120;
1202
1277
  var ANSI = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|\u001b[P^_][^\u001b]*(?:\u001b\\)?|\u001b[ -/]*[0-~]?|\u009b[0-?]*[ -/]*[@-~]/g;
1203
- 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;
1204
1279
  function sanitizeText(text) {
1205
1280
  return text.replace(/\r\n?/g, `
1206
1281
  `).replace(ANSI, "").replace(CONTROL, "");
@@ -1247,13 +1322,16 @@ function renderEnvelope(input) {
1247
1322
  var WAKE_SKIP_TEXT = {
1248
1323
  wake_false: "sent without a wake",
1249
1324
  subagent: "subagents are never woken",
1250
- hop_cap: "hop limit reached (8 agent messages without a human turn)",
1251
- 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"
1252
1329
  };
1253
- function wakeSkipNote({ delivery, woke, wakeSkipped }) {
1330
+ function wakeSkipNote({ delivery, woke, wakeSkipped, wakeLimit }) {
1254
1331
  if (woke !== false || !wakeSkipped || delivery === "steer" || delivery === "turn")
1255
1332
  return "";
1256
- 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}`;
1257
1335
  }
1258
1336
  // packages/cli/src/http.ts
1259
1337
  var EXIT = {
@@ -4868,7 +4946,7 @@ function readConfig(file, machineName) {
4868
4946
  return defaults(machineName);
4869
4947
  }
4870
4948
  }
4871
- var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4949
+ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4872
4950
  function readRawConfig(file) {
4873
4951
  let text;
4874
4952
  try {
@@ -4884,7 +4962,7 @@ function readRawConfig(file) {
4884
4962
  } catch {
4885
4963
  parsed = undefined;
4886
4964
  }
4887
- if (!isObject(parsed))
4965
+ if (!isObject2(parsed))
4888
4966
  throw new Error(`${file} is not a JSON object; fix or remove it first`);
4889
4967
  return parsed;
4890
4968
  }
@@ -5318,7 +5396,7 @@ class ProfileError extends RuntimeError {
5318
5396
  this.code = code;
5319
5397
  }
5320
5398
  }
5321
- var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5399
+ var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5322
5400
  var show = (v) => JSON.stringify(v)?.slice(0, 80) ?? String(v);
5323
5401
  var builtinDefault = () => ({ name: DEFAULT_PROFILE, providers: [], endpoints: [], allowGitRemote: false });
5324
5402
  function egressHost(v) {
@@ -5342,7 +5420,7 @@ function egressEndpoint(v) {
5342
5420
  return;
5343
5421
  host = v.slice(0, colon);
5344
5422
  port = Number(v.slice(colon + 1));
5345
- } else if (isObject2(v)) {
5423
+ } else if (isObject3(v)) {
5346
5424
  host = v.host;
5347
5425
  port = v.port;
5348
5426
  }
@@ -5399,9 +5477,9 @@ function normalizeOpenShellProfiles(raw) {
5399
5477
  const problems = [];
5400
5478
  const profiles = { [DEFAULT_PROFILE]: builtinDefault() };
5401
5479
  const unsupported = {};
5402
- if (raw !== undefined && !isObject2(raw))
5480
+ if (raw !== undefined && !isObject3(raw))
5403
5481
  problems.push("openshell.profiles must be an object of profile name to profile");
5404
- let entries = Object.entries(isObject2(raw) ? raw : {});
5482
+ let entries = Object.entries(isObject3(raw) ? raw : {});
5405
5483
  if (entries.length > PROFILES_MAX) {
5406
5484
  problems.push(`openshell.profiles: at most ${PROFILES_MAX} profiles; the rest are ignored`);
5407
5485
  entries = entries.slice(0, PROFILES_MAX);
@@ -5411,7 +5489,7 @@ function normalizeOpenShellProfiles(raw) {
5411
5489
  problems.push(`openshell.profiles: ${show(name)} is not a profile name (lowercase letters, digits, . _ -; at most ${NAME_MAX})`);
5412
5490
  continue;
5413
5491
  }
5414
- if (!isObject2(value)) {
5492
+ if (!isObject3(value)) {
5415
5493
  problems.push(`openshell.profiles.${name} must be an object`);
5416
5494
  continue;
5417
5495
  }
@@ -5845,7 +5923,7 @@ var HARNESS = /^[a-z0-9][a-z0-9._-]{0,63}$/;
5845
5923
  function nodeConfigPath(env = process.env, home = homedir2()) {
5846
5924
  return path9.join(path9.dirname(configPath(env, home)), NODE_CONFIG_FILE);
5847
5925
  }
5848
- var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5926
+ var isObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5849
5927
  var whole = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
5850
5928
  var show2 = (v) => JSON.stringify(v)?.slice(0, 80) ?? String(v);
5851
5929
  function configPathOf(raw, options) {
@@ -5867,7 +5945,7 @@ var isRoot = (p) => p === "/" || /^[A-Z]:\/$/.test(p);
5867
5945
  function normalizeNodeConfig(raw, options = {}) {
5868
5946
  const problems = [];
5869
5947
  let r = {};
5870
- if (isObject3(raw))
5948
+ if (isObject4(raw))
5871
5949
  r = raw;
5872
5950
  else if (raw !== undefined)
5873
5951
  problems.push("node.json must hold a JSON object");
@@ -5959,7 +6037,7 @@ function loopbackOrigin(raw) {
5959
6037
  return url.origin;
5960
6038
  }
5961
6039
  function normalizeOpenCode(raw, problems, options) {
5962
- if (!isObject3(raw)) {
6040
+ if (!isObject4(raw)) {
5963
6041
  problems.push("opencode must be an object");
5964
6042
  return;
5965
6043
  }
@@ -5970,7 +6048,7 @@ function normalizeOpenCode(raw, problems, options) {
5970
6048
  const list = Array.isArray(raw.servers) ? raw.servers : [];
5971
6049
  list.forEach((entry, i) => {
5972
6050
  const field = `opencode.servers[${i}]`;
5973
- if (!isObject3(entry))
6051
+ if (!isObject4(entry))
5974
6052
  return void problems.push(`${field} must be an object {url, passwordFile, directories?}`);
5975
6053
  const url = loopbackOrigin(entry.url);
5976
6054
  if (!url)
@@ -5998,7 +6076,7 @@ function normalizeOpenCode(raw, problems, options) {
5998
6076
  return { ...directories ? { directories } : {}, servers, ...timeoutSeconds !== undefined ? { timeoutSeconds } : {} };
5999
6077
  }
6000
6078
  function normalizeHarnesses(raw, problems) {
6001
- if (!isObject3(raw)) {
6079
+ if (!isObject4(raw)) {
6002
6080
  problems.push("harnesses must be an object of harness names to true or false");
6003
6081
  return;
6004
6082
  }
@@ -6014,7 +6092,7 @@ function normalizeHarnesses(raw, problems) {
6014
6092
  return out;
6015
6093
  }
6016
6094
  function normalizeOpenShell(raw, problems) {
6017
- if (!isObject3(raw)) {
6095
+ if (!isObject4(raw)) {
6018
6096
  problems.push("openshell must be an object");
6019
6097
  return;
6020
6098
  }
@@ -6256,10 +6334,13 @@ var DECISION_FLAGS = {
6256
6334
  deadline: "string",
6257
6335
  "on-timeout": "string",
6258
6336
  tier: "string",
6259
- about: "multi"
6337
+ about: "multi",
6338
+ choice: "multi",
6339
+ kind: "string"
6260
6340
  };
6261
- var URGENCIES = ["low", "normal", "high"];
6262
- var TIERS = ["confirm", "tap"];
6341
+ var URGENCIES2 = ["low", "normal", "high"];
6342
+ var KINDS2 = ["question", "plan", "merge", "release"];
6343
+ var TIERS2 = ["confirm", "tap"];
6263
6344
  var STATES = ["open", "answered", "expired", "closed"];
6264
6345
  var DECISION_STATES = STATES;
6265
6346
  function optionArg(value) {
@@ -6275,6 +6356,37 @@ function optionArg(value) {
6275
6356
  throw new UsageError(`--option ${id}: the label is empty`);
6276
6357
  return { id, label, ...safe ? { safe: true } : {} };
6277
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
+ }
6278
6390
  var oneOf2 = (value, allowed, flag) => {
6279
6391
  if (value !== undefined && !allowed.includes(value))
6280
6392
  throw new UsageError(`${flag} is one of ${allowed.join(", ")}`);
@@ -6291,13 +6403,19 @@ function decisionOptions(flags, to) {
6291
6403
  if (why !== undefined && recommend === undefined)
6292
6404
  throw new UsageError("--why explains --recommend");
6293
6405
  const deadline = str(flags, "deadline");
6294
- const urgency = oneOf2(str(flags, "urgency"), URGENCIES, "--urgency");
6295
- 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");
6296
6408
  const onTimeout = str(flags, "on-timeout");
6297
6409
  const options = flags.option;
6298
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");
6299
6415
  return {
6300
6416
  ...options ? { options: options.map(optionArg) } : {},
6417
+ ...choices ? { choices: choices.map(choiceArg) } : {},
6418
+ ...kind ? { kind } : {},
6301
6419
  ...recommend !== undefined ? { recommendation: { option: recommend, ...why !== undefined ? { why } : {} } } : {},
6302
6420
  ...urgency ? { urgency } : {},
6303
6421
  ...tier ? { tier } : {},
@@ -6311,6 +6429,10 @@ function stateArg(value) {
6311
6429
  }
6312
6430
  var iso2 = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
6313
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
+ }
6314
6436
  const options = decision.options.map((o) => `${o.id} ${oneLine(o.label, 80)}${o.safe ? " (safe)" : ""}`).join(" · ");
6315
6437
  const r = decision.recommendation;
6316
6438
  const recommended = r ? `recommended: ${r.option}${r.why ? ` (${oneLine(r.why, 200)})` : ""}` : "";
@@ -6319,7 +6441,8 @@ function choicesText(decision) {
6319
6441
  function stateText(decision) {
6320
6442
  if (decision.state === "answered") {
6321
6443
  const a = decision.answer;
6322
- 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)" : ""}`;
6323
6446
  }
6324
6447
  if (decision.state === "open" && decision.deadline !== undefined)
6325
6448
  return `open (deadline ${iso2(decision.deadline)})`;
@@ -6496,7 +6619,7 @@ class ToolArgError extends Error {
6496
6619
 
6497
6620
  // packages/cli/src/artifacts-mcp.ts
6498
6621
  var MCP_INLINE_TEXT_BYTES = 256 * 1024;
6499
- var KINDS = ["markdown", "code", "image", "table", "chart", "dashboard", "html", "app", "file"];
6622
+ var KINDS3 = ["markdown", "code", "image", "table", "chart", "dashboard", "html", "app", "file"];
6500
6623
  var DEFAULT_MEDIA = { markdown: "text/markdown", html: "text/html", table: "text/csv", chart: "application/json", dashboard: "application/json" };
6501
6624
  var PATH3 = "/api/agent/artifacts";
6502
6625
  var STRING = { type: "string" };
@@ -6592,7 +6715,7 @@ var artifactTools = [
6592
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.",
6593
6716
  properties: {
6594
6717
  title: STRING,
6595
- kind: { type: "string", enum: KINDS },
6718
+ kind: { type: "string", enum: KINDS3 },
6596
6719
  description: STRING,
6597
6720
  visibility: { type: "string", enum: ["private", "workspace"] },
6598
6721
  workspaceID: STRING,
@@ -6604,8 +6727,8 @@ var artifactTools = [
6604
6727
  run: async (hub, args, signal) => {
6605
6728
  const title = need(args, "title");
6606
6729
  const kind = need(args, "kind");
6607
- if (!KINDS.includes(kind))
6608
- 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(", ")}`);
6609
6732
  const body = files(args, kind);
6610
6733
  const description = optional2(args, "description");
6611
6734
  const visibility = optional2(args, "visibility");
@@ -6640,7 +6763,7 @@ var STRING2 = { type: "string" };
6640
6763
  var ID3 = { type: "string", description: "The artifact's ID (art_…)." };
6641
6764
  var JOB = { type: "string", description: "The job's ID (job_…)." };
6642
6765
  var REVISION = { type: "integer", description: "The job revision you read (from artifact_job)." };
6643
- var KINDS2 = ["ask", "propose-edit", "run-action"];
6766
+ var KINDS4 = ["ask", "propose-edit", "run-action"];
6644
6767
  var need2 = (args, key) => {
6645
6768
  const value = args[key];
6646
6769
  if (typeof value !== "string" || !value.trim())
@@ -6717,7 +6840,7 @@ var artifactJobTools = [
6717
6840
  id: ID3,
6718
6841
  baseRevisionID: { type: "string", description: "The revision the job is about (propose-edit: the latest)." },
6719
6842
  instruction: { type: "string", description: "What to do (plain text, at most 8000 characters)." },
6720
- kind: { type: "string", enum: KINDS2 },
6843
+ kind: { type: "string", enum: KINDS4 },
6721
6844
  action: { type: "string", description: "run-action: one of the artifact's declared actions." },
6722
6845
  machine: { type: "string", description: "Launch a handler session on this machine…" },
6723
6846
  repo: { type: "string", description: "…in this absolute repository path." },
@@ -6732,8 +6855,8 @@ var artifactJobTools = [
6732
6855
  readOnly: false,
6733
6856
  run: async (hub, args, signal) => {
6734
6857
  const kind = optional3(args, "kind");
6735
- if (kind && !KINDS2.includes(kind))
6736
- 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(", ")}`);
6737
6860
  const action = optional3(args, "action");
6738
6861
  const apply = optional3(args, "apply");
6739
6862
  const key = optional3(args, "key");
@@ -6905,7 +7028,7 @@ function notAfterOf(args) {
6905
7028
  }
6906
7029
  throw new ToolArgError(`"notAfter" must be epoch milliseconds or an ISO 8601 date-time like 2027-01-15T12:00:00Z`);
6907
7030
  }
6908
- 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` : ""}`;
6909
7032
  function intakeText(i) {
6910
7033
  const from = i.source.kind === "user" ? "the user" : i.source.kind === "app" ? `app ${i.source.tokenID}` : `session ${i.source.sessionID}`;
6911
7034
  return [
@@ -6919,7 +7042,8 @@ function intakeText(i) {
6919
7042
  ...i.criteria?.length ? ["done means:", ...i.criteria.map((c) => `- ${sanitizeText(c)}`)] : [],
6920
7043
  ...i.ref ? [`ref: ${sanitizeText(i.ref)}`] : [],
6921
7044
  ...i.reason ? [`reason: ${sanitizeText(i.reason)}`] : [],
6922
- ...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}` : ""}`] : []
6923
7047
  ].join(`
6924
7048
  `);
6925
7049
  }
@@ -7189,7 +7313,7 @@ var STRINGS = { type: "array", items: STRING6 };
7189
7313
  var TEAM2 = { type: "string", description: "The team: its ID (team_…) or its name." };
7190
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." };
7191
7315
  var IDEMPOTENCY = { type: "string", description: "Idempotency-Key: the same key and arguments within 24 h answer the first result again." };
7192
- 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)." };
7193
7317
  var DIRECTION = { type: "string", enum: ["one-way", "two-way"], description: "two-way: `to` may also contact `from` with the same kinds." };
7194
7318
  var ROLE_FIELDS = {
7195
7319
  label: { ...STRING6, description: "A display label (confers no authority)." },
@@ -7202,12 +7326,14 @@ var ROLE_FIELDS = {
7202
7326
  maxMembers: { type: "integer", minimum: 1, maximum: 16 }
7203
7327
  };
7204
7328
  var ROLE = { type: "object", properties: { name: { ...STRING6, description: "A slug: lowercase letters, digits, dashes." }, ...ROLE_FIELDS }, required: ["name"] };
7205
- var EDGE = { type: "object", properties: { from: STRING6, to: STRING6, kinds: KINDS3, direction: DIRECTION }, required: ["from", "to", "kinds", "direction"] };
7329
+ var EDGE = { type: "object", properties: { from: STRING6, to: STRING6, kinds: KINDS5, direction: DIRECTION }, required: ["from", "to", "kinds", "direction"] };
7206
7330
  var SPEC_FIELDS = {
7207
7331
  title: { ...STRING6, description: "1–120 characters." },
7208
7332
  description: STRING6,
7209
7333
  mode: { type: "string", enum: ["open", "restricted"], description: "open: every member may msg/ask every other member. restricted: only the drawn paths (edges)." },
7210
- hopLimit: { type: "integer", minimum: 1, maximum: 100, description: "Agent-only wake hop limit between this team's seats; may not exceed the hub-wide agents.hopLimit. Unset uses the global limit." },
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." },
7211
7337
  roles: { type: "array", items: ROLE, description: "1–32 roles." },
7212
7338
  entryRole: { ...STRING6, description: "The role that receives submitted work (e.g. pm) and supervises members." },
7213
7339
  edges: { type: "array", items: EDGE, description: "Communication paths between roles (restricted mode)." },
@@ -7332,6 +7458,8 @@ function specLines(spec) {
7332
7458
  return [
7333
7459
  `mode: ${spec.mode}${spec.mode === "open" ? " (every member may msg/ask every other)" : " (only the paths below)"}`,
7334
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`] : [],
7335
7463
  `entry role: ${spec.entryRole}`,
7336
7464
  "roles:",
7337
7465
  ...spec.roles.map(role),
@@ -7501,7 +7629,7 @@ var teamTools = [
7501
7629
  need6(args, "name");
7502
7630
  need6(args, "title");
7503
7631
  const key = optional6(args, "idempotencyKey");
7504
- const spec = picked(args, ["name", "title", "description", "mode", "hopLimit", "roles", "entryRole", "edges", "tags", "metadata", "projects"]);
7632
+ const spec = picked(args, ["name", "title", "description", "mode", "hopLimit", "itemBudget", "loop", "roles", "entryRole", "edges", "tags", "metadata", "projects"]);
7505
7633
  const { team, created } = await hub.teams.create({ ...spec, edges: spec.edges ?? [] }, { signal, ...key ? { idempotencyKey: key } : {} });
7506
7634
  return [`${created ? "created" : "already created (same key)"}: ${teamLine(team)}`, ...specLines(team)].join(`
7507
7635
  `);
@@ -7517,7 +7645,7 @@ var teamTools = [
7517
7645
  idempotent: true,
7518
7646
  run: async (hub, args, signal) => {
7519
7647
  const expectedRevision = revision(args);
7520
- const patch = picked(args, ["title", "description", "mode", "hopLimit", "roles", "entryRole", "edges", "tags", "metadata", "projects"]);
7648
+ const patch = picked(args, ["title", "description", "mode", "hopLimit", "itemBudget", "loop", "roles", "entryRole", "edges", "tags", "metadata", "projects"]);
7521
7649
  if (Object.keys(patch).length === 0)
7522
7650
  throw new ArgError("give at least one field to change");
7523
7651
  const id = need6(args, "team");
@@ -7527,7 +7655,7 @@ var teamTools = [
7527
7655
  },
7528
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 })),
7529
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 })),
7530
- 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: KINDS3, direction: DIRECTION }, ["from", "to", "kinds", "direction"], (teams, team, args, 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) => {
7531
7659
  const kinds = strings3(args, "kinds");
7532
7660
  if (!kinds?.length)
7533
7661
  throw new ArgError(`"kinds" is a list of msg and/or ask`);
@@ -8214,7 +8342,8 @@ var sendOptions = (args) => {
8214
8342
  const wake = optionalBool(args, "wake");
8215
8343
  const urgent = optionalBool(args, "urgent");
8216
8344
  const notAfter = optionalNotAfter(args);
8217
- 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 } : {} };
8218
8347
  };
8219
8348
  var whyOption = (args) => {
8220
8349
  const why = optional8(args, "why");
@@ -8337,6 +8466,7 @@ function messageText({ message, answer }) {
8337
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.";
8338
8467
  var STRING9 = { type: "string" };
8339
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)." },
8340
8470
  wake: { type: "boolean", description: "false: admit the message without starting a turn." },
8341
8471
  urgent: { type: "boolean", description: "Steer into the recipient's running turn." },
8342
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)." },
@@ -8951,6 +9081,13 @@ var TOOLS = [
8951
9081
  ...operationTools,
8952
9082
  ...teamTools,
8953
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
+ },
8954
9091
  {
8955
9092
  name: "whoami",
8956
9093
  description: "Who this hub credential is: the app token of `hub login` (or the session of HUB_SESSION_TOKEN) and its user.",
@@ -9031,10 +9168,11 @@ var TOOLS = [
9031
9168
  },
9032
9169
  {
9033
9170
  name: "ask",
9034
- 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.`,
9035
9172
  properties: {
9036
9173
  to: TO,
9037
9174
  text: { ...STRING9, description: "The question." },
9175
+ ...DECISION_ASK_PROPERTIES,
9038
9176
  wait: { type: "number", minimum: 0, maximum: WAIT_POLL_MAX_S, description: `Seconds to wait for the answer (0–${WAIT_POLL_MAX_S}).` },
9039
9177
  expires: { type: ["number", "string"], description: "When the ask expires: seconds, or 30s, 5m, 2h, 1d (1 minute to 7 days; default 24 h)." },
9040
9178
  ...SEND,
@@ -9047,7 +9185,13 @@ var TOOLS = [
9047
9185
  const text = need10(args, "text");
9048
9186
  const wait = optionalWait(args);
9049
9187
  const expires = optionalExpires(args);
9050
- 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 };
9051
9195
  if (optionalBool(args, "all")) {
9052
9196
  if (wait !== undefined)
9053
9197
  throw new ArgError3(`"wait" cannot be combined with "all"`);
@@ -9311,7 +9455,7 @@ var clean = (text, token) => {
9311
9455
  var toolResult = (text, isError) => ({ content: [{ type: "text", text }], isError });
9312
9456
  var ok2 = (id, result) => ({ jsonrpc: "2.0", id, result });
9313
9457
  var rpcError = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
9314
- var isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9458
+ var isObject5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9315
9459
  var isId = (value) => typeof value === "string" || typeof value === "number" && Number.isFinite(value);
9316
9460
  function createMcpServer(options) {
9317
9461
  const initialize = (params) => {
@@ -9326,7 +9470,7 @@ function createMcpServer(options) {
9326
9470
  if (!tool)
9327
9471
  return rpcError(id, RPC.invalidParams, typeof params.name === "string" ? `unknown tool: ${params.name}` : "params.name (the tool) is required");
9328
9472
  const args = params.arguments ?? {};
9329
- if (!isObject4(args))
9473
+ if (!isObject5(args))
9330
9474
  return rpcError(id, RPC.invalidParams, "params.arguments must be an object");
9331
9475
  let token;
9332
9476
  const controller = new AbortController;
@@ -9357,7 +9501,7 @@ function createMcpServer(options) {
9357
9501
  inFlight.clear();
9358
9502
  },
9359
9503
  async handle(message) {
9360
- if (!isObject4(message))
9504
+ if (!isObject5(message))
9361
9505
  return rpcError(null, RPC.invalidRequest, Array.isArray(message) ? "batches are not supported" : "not a JSON-RPC message");
9362
9506
  const hasId = "id" in message;
9363
9507
  const id = isId(message.id) ? message.id : null;
@@ -9368,12 +9512,12 @@ function createMcpServer(options) {
9368
9512
  if (typeof message.method !== "string")
9369
9513
  return rpcError(id, RPC.invalidRequest, "method must be a string");
9370
9514
  if (!hasId) {
9371
- notify(message.method, isObject4(message.params) ? message.params : {});
9515
+ notify(message.method, isObject5(message.params) ? message.params : {});
9372
9516
  return;
9373
9517
  }
9374
9518
  if (!isId(message.id))
9375
9519
  return rpcError(null, RPC.invalidRequest, "id must be a string or a number");
9376
- const params = isObject4(message.params) ? message.params : {};
9520
+ const params = isObject5(message.params) ? message.params : {};
9377
9521
  switch (message.method) {
9378
9522
  case "initialize":
9379
9523
  return ok2(id, initialize(params));
@@ -9692,7 +9836,7 @@ import { arch as osArch, platform as osPlatform } from "node:os";
9692
9836
  // packages/node/package.json
9693
9837
  var package_default2 = {
9694
9838
  name: "@huynle/hub-node",
9695
- version: "0.11.0",
9839
+ version: "0.11.1",
9696
9840
  private: true,
9697
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).",
9698
9842
  license: "MIT",
@@ -11147,7 +11291,7 @@ import path14 from "node:path";
11147
11291
  // packages/runtime-openshell/package.json
11148
11292
  var package_default3 = {
11149
11293
  name: "@huynle/hub-runtime-openshell",
11150
- version: "0.11.0",
11294
+ version: "0.11.1",
11151
11295
  private: true,
11152
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).",
11153
11297
  license: "MIT",
@@ -12887,7 +13031,7 @@ function ask(socketPath, req, options = {}) {
12887
13031
  var ENV_NAME3 = /^[A-Z_][A-Z0-9_]*$/;
12888
13032
  var DROPPED = ["tool_response"];
12889
13033
  var quote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
12890
- var isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
13034
+ var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
12891
13035
  function flags(argv) {
12892
13036
  const rewake = argv.includes("--rewake");
12893
13037
  const at = argv.indexOf("--wait");
@@ -12925,16 +13069,16 @@ async function runHook(io) {
12925
13069
  const launch = io.env.HUB_CLAUDE_LAUNCH;
12926
13070
  const req = { v: WIRE_VERSION, kind: "hook", input: sent, ...launch ? { launch } : {}, ...rewake ? { rewake: true } : {}, ...waitMs !== undefined ? { waitMs } : {} };
12927
13071
  const reply = await ask(io.socketPath, req, io.timeoutMs !== undefined ? { timeoutMs: io.timeoutMs } : {});
12928
- if (!isObject5(reply))
13072
+ if (!isObject6(reply))
12929
13073
  return 0;
12930
- if (isObject5(reply.env))
13074
+ if (isObject6(reply.env))
12931
13075
  writeEnvFile(io.env.CLAUDE_ENV_FILE, reply.env);
12932
13076
  if (rewake && reply.exit === 2) {
12933
13077
  if (typeof reply.stderr === "string")
12934
13078
  io.stderr(reply.stderr);
12935
13079
  return 2;
12936
13080
  }
12937
- if (isObject5(reply.output))
13081
+ if (isObject6(reply.output))
12938
13082
  io.stdout(`${JSON.stringify(reply.output)}
12939
13083
  `);
12940
13084
  return 0;
@@ -13828,7 +13972,7 @@ function ensurePrivateDir(dir) {
13828
13972
  import { Socket as Socket2 } from "node:net";
13829
13973
  var MCP_END = { hub: "end" };
13830
13974
  var ACT_TOOLS = new Set(["approve", "deny", "answer", "prompt", "stop"]);
13831
- var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
13975
+ var isObject7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
13832
13976
  function createMcpRouter(options) {
13833
13977
  const credentials = (pick, what) => () => {
13834
13978
  const sessionID = options.sessionID();
@@ -13841,9 +13985,9 @@ function createMcpRouter(options) {
13841
13985
  const act = createMcpServer({ version: options.version, credentials: credentials(options.actToken, "hub session act token") });
13842
13986
  return {
13843
13987
  async handle(message) {
13844
- 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))
13845
13989
  return act.handle(message);
13846
- if (isObject6(message) && !("id" in message))
13990
+ if (isObject7(message) && !("id" in message))
13847
13991
  act.handle(message);
13848
13992
  return shell.handle(message);
13849
13993
  },
@@ -14687,9 +14831,10 @@ class ClaudeSessions {
14687
14831
  this.#settle(s, undefined, true);
14688
14832
  s.stopRequested = false;
14689
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;
14690
14835
  const turnID = s.turnID;
14691
14836
  s.busy = false;
14692
- 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}` } : {} } } });
14693
14838
  return {};
14694
14839
  }
14695
14840
  #child(s, input, start = true) {
@@ -15105,7 +15250,7 @@ import path23 from "node:path";
15105
15250
  // packages/claude-code/package.json
15106
15251
  var package_default4 = {
15107
15252
  name: "@huynle/hub-claude-code",
15108
- version: "0.11.0",
15253
+ version: "0.11.1",
15109
15254
  private: true,
15110
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).",
15111
15256
  license: "MIT",
@@ -15394,7 +15539,7 @@ import path26 from "node:path";
15394
15539
  // packages/preset/package.json
15395
15540
  var package_default5 = {
15396
15541
  name: "@huynle/hub-preset",
15397
- version: "0.11.0",
15542
+ version: "0.11.1",
15398
15543
  private: true,
15399
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).",
15400
15545
  license: "MIT",
@@ -15919,7 +16064,7 @@ import { openSync as openSync6, closeSync as closeSync6, readFileSync as readFil
15919
16064
  // packages/codex/package.json
15920
16065
  var package_default6 = {
15921
16066
  name: "@huynle/hub-codex",
15922
- version: "0.11.0",
16067
+ version: "0.11.1",
15923
16068
  private: true,
15924
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).",
15925
16070
  license: "MIT",
@@ -16269,6 +16414,12 @@ var DEFAULT_TITLE = "Codex session";
16269
16414
  var TOOL_ITEMS = new Set(["commandExecution", "fileChange", "mcpToolCall", "dynamicToolCall", "webSearch"]);
16270
16415
  var APPROVALS = { "item/commandExecution/requestApproval": "shell", "item/fileChange/requestApproval": "edit" };
16271
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
+ }
16272
16423
  var isObj3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
16273
16424
  var str6 = (v) => typeof v === "string" && v !== "" ? v : undefined;
16274
16425
  var cut5 = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
@@ -16656,7 +16807,8 @@ class CodexSessions {
16656
16807
  return;
16657
16808
  const status = typeof turn.status === "string" ? turn.status : "completed";
16658
16809
  const error = isObj3(turn.error) && typeof turn.error.message === "string" ? cut5(turn.error.message, HARNESS_ERROR_MAX) : undefined;
16659
- 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 } : {} };
16660
16812
  if (thread.turnID === turnID)
16661
16813
  thread.turnID = undefined;
16662
16814
  thread.ended.set(turnID, end);
@@ -16669,7 +16821,7 @@ class CodexSessions {
16669
16821
  if (status === "interrupted")
16670
16822
  this.#emit({ type: "session.execution.interrupted", data });
16671
16823
  else if (status === "failed")
16672
- 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 } : {} } } : {} } });
16673
16825
  else
16674
16826
  this.#emit({ type: "session.execution.succeeded", data });
16675
16827
  thread.items.clear();
@@ -18672,7 +18824,7 @@ function mergeConfigText(text, format, sets) {
18672
18824
  return txFail("manual_edit_required", `the config cannot be edited losslessly (${error instanceof Error ? error.message : "unreadable"}); make the edit by hand`);
18673
18825
  }
18674
18826
  const tree = toValue(current, cst);
18675
- if (!isObject7(tree))
18827
+ if (!isObject8(tree))
18676
18828
  return txFail("conflict", "the config is not a JSON object");
18677
18829
  const changed = applySet(tree, set);
18678
18830
  if (!changed.ok)
@@ -18687,7 +18839,7 @@ function mergeConfigText(text, format, sets) {
18687
18839
  }
18688
18840
  return txOk(current);
18689
18841
  }
18690
- var isObject7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
18842
+ var isObject8 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
18691
18843
  var same = (a, b) => canonicalJSON(a) === canonicalJSON(b);
18692
18844
  var clone = (v) => JSON.parse(JSON.stringify(v));
18693
18845
  function segments(pointer) {
@@ -18711,7 +18863,7 @@ function applySet(root, set) {
18711
18863
  if (index === undefined)
18712
18864
  return txFail("conflict", `${set.pointer}: no array element ${seg}`);
18713
18865
  next = current[index];
18714
- } else if (isObject7(current)) {
18866
+ } else if (isObject8(current)) {
18715
18867
  next = Object.hasOwn(current, seg) ? current[seg] : undefined;
18716
18868
  if (next === undefined) {
18717
18869
  next = segs[i + 1] === "-" ? [] : {};
@@ -18739,7 +18891,7 @@ function applySet(root, set) {
18739
18891
  current[index] = value;
18740
18892
  return txOk(true);
18741
18893
  }
18742
- if (!isObject7(current))
18894
+ if (!isObject8(current))
18743
18895
  return txFail("conflict", `${set.pointer}: the parent is not an object or array`);
18744
18896
  if (Object.hasOwn(current, last) && same(current[last], value))
18745
18897
  return txOk(false);
@@ -18908,7 +19060,7 @@ var LEGACY_PLUGIN_PACKAGE = "@huynle/opencode-voice";
18908
19060
  var HUB_MCP_NAME = "hub";
18909
19061
  var MCP_SUBCOMMAND = "mcp";
18910
19062
  var HUB_PROGRAMS = new Set(["hub", "hubctl"]);
18911
- var isObject8 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
19063
+ var isObject9 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
18912
19064
  var basename = (p) => p.slice(p.lastIndexOf("/") + 1);
18913
19065
  var WINDOWS_PATH = /^[A-Za-z]:[\\/]/;
18914
19066
  var programName = (p) => {
@@ -18943,12 +19095,12 @@ function classifyPluginSpec(spec) {
18943
19095
  return { source: "custom" };
18944
19096
  return;
18945
19097
  }
18946
- 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;
18947
19099
  var parse3 = (text) => {
18948
19100
  const parsed = parseJsonc(text);
18949
19101
  if (!parsed.ok)
18950
19102
  return txFail("manual_edit_required", `the OpenCode config cannot be edited losslessly (${parsed.message}); make the edit by hand`);
18951
- if (!isObject8(parsed.value))
19103
+ if (!isObject9(parsed.value))
18952
19104
  return txFail("conflict", "the OpenCode config is not a JSON object");
18953
19105
  return txOk(parsed.value);
18954
19106
  };
@@ -18964,7 +19116,7 @@ function inspectPlugin(config, desired, findings) {
18964
19116
  const spec = specOf(entry);
18965
19117
  const kind = spec === undefined ? undefined : classifyPluginSpec(spec);
18966
19118
  if (kind)
18967
- found.push({ pointer: `/${key}/${i}`, spec, object: isObject8(entry), ...kind });
19119
+ found.push({ pointer: `/${key}/${i}`, spec, object: isObject9(entry), ...kind });
18968
19120
  });
18969
19121
  }
18970
19122
  if (found.length === 0)
@@ -18995,17 +19147,17 @@ function inspectPlugin(config, desired, findings) {
18995
19147
  findings.push({ code: "plugin-drift", message: `the Hub plugin is pinned to ${entry.version}; it becomes ${pluginSpec(desired)}` });
18996
19148
  return txOk({ state: "drift", ...base, object: entry.object });
18997
19149
  }
18998
- 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;
18999
19151
  function inspectMcp(config, desired, findings) {
19000
19152
  const mcp = config.mcp;
19001
19153
  if (mcp === undefined)
19002
19154
  return txOk({ state: "absent" });
19003
- if (!isObject8(mcp))
19155
+ if (!isObject9(mcp))
19004
19156
  return txFail("conflict", '"mcp" in the OpenCode config is not an object; fix it by hand');
19005
19157
  const servers = mcp.servers;
19006
19158
  if (servers === undefined)
19007
19159
  return txOk({ state: "absent" });
19008
- if (!isObject8(servers))
19160
+ if (!isObject9(servers))
19009
19161
  return txFail("conflict", '"mcp.servers" in the OpenCode config is not an object; fix it by hand');
19010
19162
  const elsewhere = Object.entries(servers).find(([name, server]) => name !== HUB_MCP_NAME && isHubMcp(server));
19011
19163
  if (elsewhere)
@@ -26474,19 +26626,22 @@ Identity
26474
26626
  hub role release <role[.project]> [--session <addr>]
26475
26627
 
26476
26628
  Messaging (text "-" reads it from stdin)
26477
- 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]
26478
26630
  tell: fire and forget; --not-after: never delivered after that time
26479
- 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]
26480
26632
  tracked until answered, declined or expired; --wait alone waits until
26481
26633
  it expires; exit 0 answered, 1 declined or no answer yet, 8 expired;
26482
26634
  --key (Idempotency-Key): a retry with the same key and text answers
26483
- 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)
26484
26637
  hub reply <msgID> "text" [--no-wake] goes to the real sender
26485
26638
  hub decline <msgID> --why "reason"
26486
26639
  hub ask @me "question" [--option id=label[:safe]]... [--recommend id [--why "…"]] [--urgency low|normal|high]
26487
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]
26488
26642
  opens a Decision for the user (answered in the console; the answer
26489
- 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
26490
26645
  hub decisions list [--state open|answered|expired|closed] the Decisions you asked
26491
26646
  hub inbox asks to answer, your open asks, recent messages, mutes
26492
26647
  hub mute <addr> · hub unmute <addr> refuse (or accept again) messages from a session
@@ -26659,11 +26814,13 @@ var whyOf = (flags) => {
26659
26814
  var sendFlags = (flags) => {
26660
26815
  const notAfter = str(flags, "not-after");
26661
26816
  const key = str(flags, "key");
26817
+ const item = str(flags, "item");
26662
26818
  return {
26663
26819
  ...flags["no-wake"] === true ? { wake: false } : {},
26664
26820
  ...flags.urgent === true ? { urgent: true } : {},
26665
26821
  ...notAfter === undefined ? {} : { notAfter: notAfterArg(notAfter, "--not-after") },
26666
- ...key !== undefined ? { idempotencyKey: key } : {}
26822
+ ...key !== undefined ? { idempotencyKey: key } : {},
26823
+ ...item !== undefined ? { item } : {}
26667
26824
  };
26668
26825
  };
26669
26826
  var nodeIo = (io) => ({
@@ -26787,7 +26944,7 @@ var COMMANDS = {
26787
26944
  },
26788
26945
  msg: {
26789
26946
  args: ["<addr>", "<text>"],
26790
- 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" },
26791
26948
  async handle(context) {
26792
26949
  const { flags, args } = context.parsed;
26793
26950
  const text = await textArg(args[1], context.io.stdin);
@@ -26799,7 +26956,7 @@ var COMMANDS = {
26799
26956
  },
26800
26957
  ask: {
26801
26958
  args: ["<addr>", "<text>"],
26802
- 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 },
26803
26960
  async handle(context) {
26804
26961
  const { flags, args } = context.parsed;
26805
26962
  const decision = decisionOptions(flags, args[0]);