@cabane/companion 0.6.173 → 0.6.175

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.
package/dist/cli.js CHANGED
@@ -2509,7 +2509,7 @@ var init_pump = __esm({
2509
2509
  });
2510
2510
 
2511
2511
  // packages/agent-runtime/src/turn-control/descriptions.ts
2512
- var MAX_ASK_QUESTION_BYTES, MAX_ASK_HEADLINE_BYTES, MAX_ASK_ITEMS, MAX_ASK_OPTION_BYTES, ASK_OPTION_NOT_A_STRING, MIN_ASK_OPTIONS, MAX_ASK_OPTIONS, MAX_WAKE_NOTE_BYTES, MAX_WITHDRAW_REASON_BYTES, MAX_SKIP_REASON_BYTES, TURN_CONTROL_TOOLS, TURN_CONTROL_TOOL_DESCRIPTIONS, TURN_CONTROL_FIELD_DESCRIPTIONS;
2512
+ var MAX_ASK_QUESTION_BYTES, MAX_ASK_HEADLINE_BYTES, MAX_ASK_ITEMS, MAX_ASK_OPTION_BYTES, ASK_OPTION_NOT_A_STRING, MIN_ASK_OPTIONS, MAX_ASK_OPTIONS, ASK_TARGET_PATTERN, ASK_TARGET_INVALID, MAX_WAKE_NOTE_BYTES, MAX_WITHDRAW_REASON_BYTES, MAX_SKIP_REASON_BYTES, TURN_CONTROL_TOOLS, TURN_CONTROL_TOOL_DESCRIPTIONS, TURN_CONTROL_FIELD_DESCRIPTIONS;
2513
2513
  var init_descriptions = __esm({
2514
2514
  "packages/agent-runtime/src/turn-control/descriptions.ts"() {
2515
2515
  "use strict";
@@ -2520,6 +2520,8 @@ var init_descriptions = __esm({
2520
2520
  ASK_OPTION_NOT_A_STRING = "An ask option is a plain string label.";
2521
2521
  MIN_ASK_OPTIONS = 2;
2522
2522
  MAX_ASK_OPTIONS = 4;
2523
+ ASK_TARGET_PATTERN = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|@?[A-Za-z0-9-]{2,32})$/;
2524
+ ASK_TARGET_INVALID = "An ask target is a user id or an @handle.";
2523
2525
  MAX_WAKE_NOTE_BYTES = 2e3;
2524
2526
  MAX_WITHDRAW_REASON_BYTES = 400;
2525
2527
  MAX_SKIP_REASON_BYTES = 500;
@@ -2531,7 +2533,7 @@ var init_descriptions = __esm({
2531
2533
  "withdraw_ask"
2532
2534
  ];
2533
2535
  TURN_CONTROL_TOOL_DESCRIPTIONS = {
2534
- ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster; to bring in an agent, name it with \`@handle\` in your reply.
2536
+ ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster, by their \`@handle\`; to bring in an agent, name it with \`@handle\` in your reply.
2535
2537
  - Form: one question (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} \`{ headline, body?, options? }\`; never both. Decisions that arise together go in ONE call, in the order you want them answered. A second \`ask\` is a separate item, never an edit; raise one only for a genuinely new question.
2536
2538
  - \`headline\`: one sentence ending in \`?\`. Body (\`question\` or each item's \`body\`): a sentence or two of framing, not a report; inline Markdown only, no lists or headings. \`options\`: ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers (always for a yes/no go-ahead), each a distinct course of action you can carry out without asking back.
2537
2539
  - Voice, in headline, body and options: "I" for yourself, never your own name; name everyone else who decides or acts, as on your roster. Never "you", "your", "we", "us", "our" or their contractions, nor "he", "she" or "they" for a named actor. Each option reads correctly alone. A quotation stays accurate: say who said it.
@@ -2546,7 +2548,7 @@ var init_descriptions = __esm({
2546
2548
  };
2547
2549
  TURN_CONTROL_FIELD_DESCRIPTIONS = {
2548
2550
  ask: {
2549
- targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
2551
+ targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
2550
2552
  question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`.`,
2551
2553
  headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the person reads first in For you and on the chip \u2014 one scannable question, no elaboration (that goes in \`question\`). Strongly encouraged.`,
2552
2554
  options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
@@ -8508,7 +8510,7 @@ function createTurnControlMcpServer(acts, skipState) {
8508
8510
  ASK_TOOL,
8509
8511
  TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
8510
8512
  {
8511
- targetUserId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
8513
+ targetUserId: z17.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
8512
8514
  question: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
8513
8515
  headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
8514
8516
  options: z17.array(z17.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
@@ -12115,11 +12117,60 @@ init_config();
12115
12117
  init_bundled_binaries();
12116
12118
  init_cabane_command();
12117
12119
  init_config();
12120
+ import { homedir as homedir6 } from "os";
12121
+ import { join as join7 } from "path";
12122
+
12123
+ // src/device-state.ts
12124
+ var HARNESS_STATE_LABEL = {
12125
+ checking: "checking",
12126
+ connected: "connected",
12127
+ needs_sign_in: "needs sign-in",
12128
+ limited: "limited",
12129
+ problem: "problem",
12130
+ off: "off",
12131
+ gone: "gone"
12132
+ };
12133
+
12134
+ // src/harness-state.ts
12135
+ init_config();
12136
+ function harnessEntry(runtime, state) {
12137
+ switch (state.state) {
12138
+ case "usable":
12139
+ return {
12140
+ runtime,
12141
+ state: "connected",
12142
+ reason: state.via,
12143
+ version: state.version,
12144
+ ...state.url ? { url: state.url } : {}
12145
+ };
12146
+ case "signed_out":
12147
+ return { runtime, state: "needs_sign_in", reason: "signed_out", version: state.version };
12148
+ case "not_running":
12149
+ return { runtime, state: "problem", reason: "not_running", url: state.url };
12150
+ case "off":
12151
+ return { runtime, state: "off" };
12152
+ case "problem":
12153
+ return { runtime, state: "problem", reason: checkReason(state.check) };
12154
+ }
12155
+ }
12156
+ function harnessEntriesFromDetection(detection) {
12157
+ return HARNESS_RUNTIMES.map((runtime) => harnessEntry(runtime, detection[runtime]));
12158
+ }
12159
+ function checkReason(check) {
12160
+ switch (check.status) {
12161
+ case "bundle_missing":
12162
+ return check.bundled;
12163
+ case "check_failed":
12164
+ return `check_failed:${check.reason}`;
12165
+ default:
12166
+ return check.status;
12167
+ }
12168
+ }
12169
+
12170
+ // src/harness-copy.ts
12118
12171
  init_install_kind();
12119
12172
  init_install_record();
12120
12173
  init_term();
12121
- import { homedir as homedir6 } from "os";
12122
- import { join as join7 } from "path";
12123
12174
  var HARNESS_LABELS = {
12124
12175
  "claude-code": "Claude Code",
12125
12176
  codex: "Codex",
@@ -12174,23 +12225,23 @@ function agentRows(detection, opts = {}) {
12174
12225
  });
12175
12226
  }
12176
12227
  function agentRowText(runtime, state, opts = {}) {
12177
- const connected = opts.connected ?? true ? " \u2014 connected" : "";
12228
+ const word = HARNESS_STATE_LABEL[harnessEntry(runtime, state).state];
12178
12229
  switch (state.state) {
12179
12230
  case "usable": {
12180
12231
  const version = state.version ? ` (${state.version})` : "";
12181
- if (state.via === "api_key") return `API key configured${version}${connected}`;
12182
- if (state.via === "container") return `connected \u2014 turns run in Docker`;
12183
- if (state.via === "serve") return `running at ${state.url ?? ""}${connected}`;
12184
- return `signed in${version}${connected}`;
12232
+ if (state.via === "api_key") return `${word} \u2014 API key configured${version}`;
12233
+ if (state.via === "container") return `${word} \u2014 turns run in Docker`;
12234
+ if (state.via === "serve") return `${word} \u2014 running at ${state.url ?? ""}`;
12235
+ return `${word} \u2014 signed in${version}`;
12185
12236
  }
12186
12237
  case "signed_out":
12187
- return opts.signInHint && runtime !== "opencode" ? `not signed in \u2014 run ${loginCommand(runtime)}` : "not signed in";
12238
+ return opts.signInHint && runtime !== "opencode" ? `${word} \u2014 run ${loginCommand(runtime)}` : word;
12188
12239
  case "not_running":
12189
- return "not running";
12240
+ return `${word} \u2014 not running`;
12190
12241
  case "off":
12191
- return "turned off in config";
12242
+ return `${word} \u2014 turned off in config`;
12192
12243
  case "problem":
12193
- return runtime === "opencode" ? "not running" : unusableReason(runtime, state.check) ?? "not signed in";
12244
+ return runtime === "opencode" ? `${word} \u2014 not running` : `${word} \u2014 ${unusableReason(runtime, state.check) ?? "could not be checked"}`;
12194
12245
  }
12195
12246
  }
12196
12247
  var AGENT_CHOICES = [
@@ -12279,7 +12330,7 @@ function nothingUsableLines(detection) {
12279
12330
  if (reason) lines2.push(reason);
12280
12331
  }
12281
12332
  }
12282
- lines2.push(`Cabane can run while you sign in to a coding tool.`);
12333
+ lines2.push(`Cabane can run while you sign in to a harness.`);
12283
12334
  return lines2;
12284
12335
  }
12285
12336
  function indent(line) {
@@ -13458,7 +13509,7 @@ function turnFailureCopy(reason, runtime) {
13458
13509
  if (reason?.startsWith("runtime_incomplete:"))
13459
13510
  return `The agent installation is incomplete. ${repairAction()} Then restart the companion.`;
13460
13511
  if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
13461
- return "The agent is unavailable on this device. Enable it here or choose another connector in Cabane.";
13512
+ return "The harness is off on this device. Enable it here, or choose another harness or device in Cabane.";
13462
13513
  if (reason?.startsWith("model_unavailable:"))
13463
13514
  return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
13464
13515
  if (reason?.startsWith("prepare_failed:"))
@@ -13837,7 +13888,6 @@ init_install_kind();
13837
13888
  init_src();
13838
13889
  var ConnectorHealthStore = class {
13839
13890
  byRuntime = /* @__PURE__ */ new Map();
13840
- sources = /* @__PURE__ */ new Map();
13841
13891
  // Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
13842
13892
  // classified failure sets `limited`(+reset) / `auth_expired` with the reason as
13843
13893
  // `last_error`; anything else (a user Stop, a timeout, an unclassified error)
@@ -13846,7 +13896,6 @@ var ConnectorHealthStore = class {
13846
13896
  recordSettle(runtime, outcome) {
13847
13897
  if (outcome.ok) {
13848
13898
  this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
13849
- this.sources.set(runtime, "turn");
13850
13899
  return;
13851
13900
  }
13852
13901
  const failure = decodeFailureReason(outcome.errorReason);
@@ -13858,26 +13907,6 @@ var ConnectorHealthStore = class {
13858
13907
  limitedUntil: quota.limitedUntil,
13859
13908
  lastError: outcome.errorReason ?? null
13860
13909
  });
13861
- this.sources.set(runtime, "turn");
13862
- }
13863
- // Record what the companion's own sign-in check found for a connected
13864
- // runtime at `start` or `connect`, so the heartbeat carries "sign-in needed"
13865
- // before any turn fails. Signed out seeds `auth_expired` unless a turn has
13866
- // already spoken; usable clears only a seed of our own.
13867
- recordLocalCheck(runtime, usable) {
13868
- const source = this.sources.get(runtime);
13869
- if (!usable) {
13870
- if (source === "turn" && this.byRuntime.get(runtime)?.quotaState !== "ok") return;
13871
- this.byRuntime.set(runtime, {
13872
- quotaState: "auth_expired",
13873
- limitedUntil: null,
13874
- lastError: encodeFailureReason({ kind: "auth_expired" })
13875
- });
13876
- this.sources.set(runtime, "local");
13877
- return;
13878
- }
13879
- if (source !== "local") return;
13880
- this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
13881
13910
  }
13882
13911
  // The store's current opinion for one runtime, or undefined when it has
13883
13912
  // no observation. Read by the dispatcher at idle-timeout fire: an idle reap
@@ -16362,6 +16391,9 @@ var CompanionSupervisor = class {
16362
16391
  // Omitted until the first observed failure or heal, so the server's
16363
16392
  // status-less manifest synthesis still runs for a fresh companion.
16364
16393
  ...connectorReports.length > 0 ? { connectors: connectorReports } : {},
16394
+ // The complete harness inventory in the machine's words, once detection
16395
+ // has run; omitted before it, which the server reads as "nothing yet".
16396
+ ...this.detection ? { harnesses: harnessEntriesFromDetection(this.detection) } : {},
16365
16397
  ...opencodeModels !== null ? { models: opencodeModels } : {},
16366
16398
  ...this.updater ? { update: this.updater.report() } : {}
16367
16399
  });
@@ -17314,7 +17346,6 @@ var CompanionSupervisor = class {
17314
17346
  ...this.probeOpencode ? { probeOpencode: this.probeOpencode } : {}
17315
17347
  });
17316
17348
  this.detection = detection;
17317
- this.seedSignInHealth(previous, detection);
17318
17349
  this.logBundledTransitions();
17319
17350
  this.harnessVersions = {
17320
17351
  claudeCode: detectedVersion(detection["claude-code"]),
@@ -17372,23 +17403,6 @@ var CompanionSupervisor = class {
17372
17403
  const bare = advertisedRuntimes(this.detection).filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
17373
17404
  return [...health, ...bare];
17374
17405
  }
17375
- // Seed connector health from what detection saw of a sign-in, so Cabane says
17376
- // "sign-in needed" before a turn fails. Only a LAPSE seeds it — an agent that
17377
- // was usable at an earlier look and is signed out now — because a device
17378
- // that never signed in to Codex has nothing to report about Codex. Usable
17379
- // again clears a seed of our own.
17380
- seedSignInHealth(previous, detection) {
17381
- for (const runtime of ["claude-code", "codex"]) {
17382
- const now = detection[runtime];
17383
- if (now.state === "usable") {
17384
- if (now.via !== "container") this.connectorHealth.recordLocalCheck(runtime, true);
17385
- continue;
17386
- }
17387
- if (now.state === "signed_out" && previous?.[runtime].state === "usable") {
17388
- this.connectorHealth.recordLocalCheck(runtime, false);
17389
- }
17390
- }
17391
- }
17392
17406
  // Beat NOW and wait for it to land. `start` calls this before asking the
17393
17407
  // person anything, so the browser's connect step already shows what this
17394
17408
  // machine has rather than sitting blank while the terminal waits on an answer.
@@ -18361,7 +18375,7 @@ async function startScript(opts, interactive) {
18361
18375
  }
18362
18376
  if (!isDevicePaired()) {
18363
18377
  refuse([
18364
- `This device isn't set up yet. Run ${cabane("setup")} to set up a coding tool and pair it.`
18378
+ `This device isn't set up yet. Run ${cabane("setup")} to sign in to a harness and pair it.`
18365
18379
  ]);
18366
18380
  return;
18367
18381
  }
@@ -18812,7 +18826,7 @@ async function setupScript(opts, deps) {
18812
18826
  const detection = await detect(cfg, running, deps);
18813
18827
  ui.intro("Cabane setup");
18814
18828
  const usable = anyUsable(detection);
18815
- ui.step(["Coding tools on this device", ...agentLines(detection, { signInHint: !usable })]);
18829
+ ui.step(["Harnesses on this device", ...agentLines(detection, { signInHint: !usable })]);
18816
18830
  if (!usable && hasProblem(detection)) {
18817
18831
  ui.cancel(`Fix the above, then run ${cabane("setup")} again.`);
18818
18832
  process.exitCode = 1;
@@ -19212,7 +19226,7 @@ function strayStatusLines(running, find = findStrayCompanions) {
19212
19226
  }
19213
19227
  async function agentLines2(cfg, running) {
19214
19228
  const detection = running ?? await detectHarnesses(cfg);
19215
- return agentRows(detection, { connected: false }).map((agent) => row2(agent.label, agent.text));
19229
+ return agentRows(detection).map((agent) => row2(agent.label, agent.text));
19216
19230
  }
19217
19231
  function sandboxStatusLines(cfg) {
19218
19232
  const rows = agentSandboxRows(cfg);
package/dist/runtime.js CHANGED
@@ -1860,7 +1860,7 @@ function turnFailureCopy(reason, runtime) {
1860
1860
  if (reason?.startsWith("runtime_incomplete:"))
1861
1861
  return `The agent installation is incomplete. ${repairAction()} Then restart the companion.`;
1862
1862
  if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
1863
- return "The agent is unavailable on this device. Enable it here or choose another connector in Cabane.";
1863
+ return "The harness is off on this device. Enable it here, or choose another harness or device in Cabane.";
1864
1864
  if (reason?.startsWith("model_unavailable:"))
1865
1865
  return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
1866
1866
  if (reason?.startsWith("prepare_failed:"))
@@ -3468,6 +3468,8 @@ var MAX_ASK_OPTION_BYTES = 200;
3468
3468
  var ASK_OPTION_NOT_A_STRING = "An ask option is a plain string label.";
3469
3469
  var MIN_ASK_OPTIONS = 2;
3470
3470
  var MAX_ASK_OPTIONS = 4;
3471
+ var ASK_TARGET_PATTERN = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|@?[A-Za-z0-9-]{2,32})$/;
3472
+ var ASK_TARGET_INVALID = "An ask target is a user id or an @handle.";
3471
3473
  var MAX_WAKE_NOTE_BYTES = 2e3;
3472
3474
  var MAX_WITHDRAW_REASON_BYTES = 400;
3473
3475
  var MAX_SKIP_REASON_BYTES = 500;
@@ -3479,7 +3481,7 @@ var TURN_CONTROL_TOOLS = [
3479
3481
  "withdraw_ask"
3480
3482
  ];
3481
3483
  var TURN_CONTROL_TOOL_DESCRIPTIONS = {
3482
- ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster; to bring in an agent, name it with \`@handle\` in your reply.
3484
+ ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster, by their \`@handle\`; to bring in an agent, name it with \`@handle\` in your reply.
3483
3485
  - Form: one question (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} \`{ headline, body?, options? }\`; never both. Decisions that arise together go in ONE call, in the order you want them answered. A second \`ask\` is a separate item, never an edit; raise one only for a genuinely new question.
3484
3486
  - \`headline\`: one sentence ending in \`?\`. Body (\`question\` or each item's \`body\`): a sentence or two of framing, not a report; inline Markdown only, no lists or headings. \`options\`: ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers (always for a yes/no go-ahead), each a distinct course of action you can carry out without asking back.
3485
3487
  - Voice, in headline, body and options: "I" for yourself, never your own name; name everyone else who decides or acts, as on your roster. Never "you", "your", "we", "us", "our" or their contractions, nor "he", "she" or "they" for a named actor. Each option reads correctly alone. A quotation stays accurate: say who said it.
@@ -3494,7 +3496,7 @@ var TURN_CONTROL_TOOL_DESCRIPTIONS = {
3494
3496
  };
3495
3497
  var TURN_CONTROL_FIELD_DESCRIPTIONS = {
3496
3498
  ask: {
3497
- targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
3499
+ targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
3498
3500
  question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`.`,
3499
3501
  headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the person reads first in For you and on the chip \u2014 one scannable question, no elaboration (that goes in \`question\`). Strongly encouraged.`,
3500
3502
  options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
@@ -7759,7 +7761,6 @@ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
7759
7761
  // src/connector-health.ts
7760
7762
  var ConnectorHealthStore = class {
7761
7763
  byRuntime = /* @__PURE__ */ new Map();
7762
- sources = /* @__PURE__ */ new Map();
7763
7764
  // Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
7764
7765
  // classified failure sets `limited`(+reset) / `auth_expired` with the reason as
7765
7766
  // `last_error`; anything else (a user Stop, a timeout, an unclassified error)
@@ -7768,7 +7769,6 @@ var ConnectorHealthStore = class {
7768
7769
  recordSettle(runtime, outcome) {
7769
7770
  if (outcome.ok) {
7770
7771
  this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
7771
- this.sources.set(runtime, "turn");
7772
7772
  return;
7773
7773
  }
7774
7774
  const failure = decodeFailureReason(outcome.errorReason);
@@ -7780,26 +7780,6 @@ var ConnectorHealthStore = class {
7780
7780
  limitedUntil: quota.limitedUntil,
7781
7781
  lastError: outcome.errorReason ?? null
7782
7782
  });
7783
- this.sources.set(runtime, "turn");
7784
- }
7785
- // Record what the companion's own sign-in check found for a connected
7786
- // runtime at `start` or `connect`, so the heartbeat carries "sign-in needed"
7787
- // before any turn fails. Signed out seeds `auth_expired` unless a turn has
7788
- // already spoken; usable clears only a seed of our own.
7789
- recordLocalCheck(runtime, usable) {
7790
- const source = this.sources.get(runtime);
7791
- if (!usable) {
7792
- if (source === "turn" && this.byRuntime.get(runtime)?.quotaState !== "ok") return;
7793
- this.byRuntime.set(runtime, {
7794
- quotaState: "auth_expired",
7795
- limitedUntil: null,
7796
- lastError: encodeFailureReason({ kind: "auth_expired" })
7797
- });
7798
- this.sources.set(runtime, "local");
7799
- return;
7800
- }
7801
- if (source !== "local") return;
7802
- this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
7803
7783
  }
7804
7784
  // The store's current opinion for one runtime, or undefined when it has
7805
7785
  // no observation. Read by the dispatcher at idle-timeout fire: an idle reap
@@ -9101,7 +9081,7 @@ function createTurnControlMcpServer(acts, skipState) {
9101
9081
  ASK_TOOL,
9102
9082
  TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
9103
9083
  {
9104
- targetUserId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
9084
+ targetUserId: z17.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
9105
9085
  question: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
9106
9086
  headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
9107
9087
  options: z17.array(z17.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
@@ -12765,6 +12745,41 @@ function runtimeIssuesFor(gates) {
12765
12745
  return issues;
12766
12746
  }
12767
12747
 
12748
+ // src/harness-state.ts
12749
+ function harnessEntry(runtime, state) {
12750
+ switch (state.state) {
12751
+ case "usable":
12752
+ return {
12753
+ runtime,
12754
+ state: "connected",
12755
+ reason: state.via,
12756
+ version: state.version,
12757
+ ...state.url ? { url: state.url } : {}
12758
+ };
12759
+ case "signed_out":
12760
+ return { runtime, state: "needs_sign_in", reason: "signed_out", version: state.version };
12761
+ case "not_running":
12762
+ return { runtime, state: "problem", reason: "not_running", url: state.url };
12763
+ case "off":
12764
+ return { runtime, state: "off" };
12765
+ case "problem":
12766
+ return { runtime, state: "problem", reason: checkReason(state.check) };
12767
+ }
12768
+ }
12769
+ function harnessEntriesFromDetection(detection) {
12770
+ return HARNESS_RUNTIMES.map((runtime) => harnessEntry(runtime, detection[runtime]));
12771
+ }
12772
+ function checkReason(check) {
12773
+ switch (check.status) {
12774
+ case "bundle_missing":
12775
+ return check.bundled;
12776
+ case "check_failed":
12777
+ return `check_failed:${check.reason}`;
12778
+ default:
12779
+ return check.status;
12780
+ }
12781
+ }
12782
+
12768
12783
  // src/harness-versions.ts
12769
12784
  var EMPTY = { claudeCode: null, opencode: null, codex: null };
12770
12785
  function parseVersionToken(raw) {
@@ -14299,6 +14314,9 @@ var CompanionSupervisor = class {
14299
14314
  // Omitted until the first observed failure or heal, so the server's
14300
14315
  // status-less manifest synthesis still runs for a fresh companion.
14301
14316
  ...connectorReports.length > 0 ? { connectors: connectorReports } : {},
14317
+ // The complete harness inventory in the machine's words, once detection
14318
+ // has run; omitted before it, which the server reads as "nothing yet".
14319
+ ...this.detection ? { harnesses: harnessEntriesFromDetection(this.detection) } : {},
14302
14320
  ...opencodeModels !== null ? { models: opencodeModels } : {},
14303
14321
  ...this.updater ? { update: this.updater.report() } : {}
14304
14322
  });
@@ -15251,7 +15269,6 @@ var CompanionSupervisor = class {
15251
15269
  ...this.probeOpencode ? { probeOpencode: this.probeOpencode } : {}
15252
15270
  });
15253
15271
  this.detection = detection;
15254
- this.seedSignInHealth(previous, detection);
15255
15272
  this.logBundledTransitions();
15256
15273
  this.harnessVersions = {
15257
15274
  claudeCode: detectedVersion(detection["claude-code"]),
@@ -15309,23 +15326,6 @@ var CompanionSupervisor = class {
15309
15326
  const bare = advertisedRuntimes(this.detection).filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
15310
15327
  return [...health, ...bare];
15311
15328
  }
15312
- // Seed connector health from what detection saw of a sign-in, so Cabane says
15313
- // "sign-in needed" before a turn fails. Only a LAPSE seeds it — an agent that
15314
- // was usable at an earlier look and is signed out now — because a device
15315
- // that never signed in to Codex has nothing to report about Codex. Usable
15316
- // again clears a seed of our own.
15317
- seedSignInHealth(previous, detection) {
15318
- for (const runtime of ["claude-code", "codex"]) {
15319
- const now = detection[runtime];
15320
- if (now.state === "usable") {
15321
- if (now.via !== "container") this.connectorHealth.recordLocalCheck(runtime, true);
15322
- continue;
15323
- }
15324
- if (now.state === "signed_out" && previous?.[runtime].state === "usable") {
15325
- this.connectorHealth.recordLocalCheck(runtime, false);
15326
- }
15327
- }
15328
- }
15329
15329
  // Beat NOW and wait for it to land. `start` calls this before asking the
15330
15330
  // person anything, so the browser's connect step already shows what this
15331
15331
  // machine has rather than sitting blank while the terminal waits on an answer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.173",
3
+ "version": "0.6.175",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",