@floh-solutions/pharos-cli 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +154 -32
  2. package/dist/capabilities.d.ts +27 -0
  3. package/dist/capabilities.d.ts.map +1 -1
  4. package/dist/capabilities.js +144 -41
  5. package/dist/capabilities.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +62 -14
  8. package/dist/cli.js.map +1 -1
  9. package/dist/commands/delegate.d.ts +151 -6
  10. package/dist/commands/delegate.d.ts.map +1 -1
  11. package/dist/commands/delegate.js +575 -147
  12. package/dist/commands/delegate.js.map +1 -1
  13. package/dist/commands/doctor.d.ts +41 -0
  14. package/dist/commands/doctor.d.ts.map +1 -1
  15. package/dist/commands/doctor.js +345 -44
  16. package/dist/commands/doctor.js.map +1 -1
  17. package/dist/commands/pr.d.ts +56 -0
  18. package/dist/commands/pr.d.ts.map +1 -0
  19. package/dist/commands/pr.js +202 -0
  20. package/dist/commands/pr.js.map +1 -0
  21. package/dist/commands/setup.js +21 -3
  22. package/dist/commands/setup.js.map +1 -1
  23. package/dist/delegate/argus.d.ts +111 -2
  24. package/dist/delegate/argus.d.ts.map +1 -1
  25. package/dist/delegate/argus.js +99 -1
  26. package/dist/delegate/argus.js.map +1 -1
  27. package/dist/delegate/hosts.d.ts +29 -0
  28. package/dist/delegate/hosts.d.ts.map +1 -1
  29. package/dist/delegate/hosts.js +23 -0
  30. package/dist/delegate/hosts.js.map +1 -1
  31. package/dist/delegate/sessions.d.ts +174 -1
  32. package/dist/delegate/sessions.d.ts.map +1 -1
  33. package/dist/delegate/sessions.js +282 -28
  34. package/dist/delegate/sessions.js.map +1 -1
  35. package/dist/delegate/vsix.d.ts +26 -0
  36. package/dist/delegate/vsix.d.ts.map +1 -1
  37. package/dist/delegate/vsix.js +22 -5
  38. package/dist/delegate/vsix.js.map +1 -1
  39. package/package.json +3 -3
  40. package/skill/SKILL.md +32 -9
  41. package/vscode/pharos-bridge.json +1 -1
  42. package/vscode/pharos-bridge.vsix +0 -0
@@ -3,9 +3,11 @@ import { access, constants } from "node:fs/promises";
3
3
  import { homedir, platform } from "node:os";
4
4
  import { delimiter, join } from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { MARKETPLACE_NAME, PLUGIN } from "../capabilities.js";
6
7
  import { CONVERT_RELEASE } from "../convert.js";
7
- import { argusSocketPath, ArgusUnreachableError, askArgus, delegateStatusRequest, PROBE_REQUEST, readTimeoutMs, servesDelegate, systemArgusDeps, } from "../delegate/argus.js";
8
+ import { argusSocketPath, ArgusUnreachableError, askArgus, candidatesSupport, delegateCandidatesRequest, delegateStatusRequest, PROBE_REQUEST, readTimeoutMs, servesDelegate, systemArgusDeps, } from "../delegate/argus.js";
8
9
  import { findApp, HOSTS } from "../delegate/hosts.js";
10
+ import { carriedProfiles, carriedSentence, systemProbes } from "../delegate/sessions.js";
9
11
  import { BRIDGE_EXTENSION_ID, bridgeAtLeast, bundledBridge, bundledVsixPath, CLAUDE_EXTENSION_ID, CODEX_EXTENSION_ID, extensionsDirectory, installedExtensions, SESSION_BRIDGE_VERSION, } from "../delegate/vsix.js";
10
12
  import { emit, emitText } from "../output.js";
11
13
  const run = promisify(execFile);
@@ -199,22 +201,24 @@ export async function runDoctor(io, env, options, pretty, version) {
199
201
  + "`pharos setup --convert` replaces it.";
200
202
  }
201
203
  const config = configCheck(env);
202
- // **Run together, reported in order.** These three are independent of each
203
- // other and every one of them is slow for a different reason — `claude plugin
204
- // list` shells out to another CLI, `--verify` makes two Azure DevOps calls,
205
- // and `gh auth status` validates each token against github.com. Serially that
206
- // is the sum; the report is assembled afterwards, so the order a reader sees
207
- // is unchanged.
208
- const [skill, scopes, github, delegation] = await Promise.all([
204
+ // **Run together, reported in order.** These four are independent of each
205
+ // other and every one of them is slow for a different reason — the skill rows
206
+ // shell out to another CLI, `--verify` makes two Azure DevOps calls, and `gh
207
+ // auth status` validates each token against github.com. Serially that is the
208
+ // sum; the report is assembled afterwards, so the order a reader sees is
209
+ // unchanged.
210
+ const [skills, scopes, github, delegation] = await Promise.all([
209
211
  // The skill is what teaches a session this tool exists. A CLI installed
210
212
  // without it is a CLI nobody reaches for, so its absence belongs in tier 0
211
- // beside the things that make pharos work at all.
212
- skillCheck(env),
213
+ // beside the things that make pharos work at all. One row per agent that is
214
+ // actually here — the probes above have already answered that, so this asks
215
+ // nothing it can read.
216
+ skillChecks(env, checks.find((c) => c.id === "claude"), checks.find((c) => c.id === "codex")),
213
217
  options.verify ? verifyScopes(env, config.ok) : Promise.resolve([]),
214
218
  githubChecks(env, checks.find((c) => c.id === "gh"), options.verify),
215
219
  delegationChecks(env),
216
220
  ]);
217
- checks.push(skill, config, ...scopes, ...github, ...delegation);
221
+ checks.push(...skills, config, ...scopes, ...github, ...delegation);
218
222
  const result = {
219
223
  ok: checks.filter((c) => c.tier === 0).every((c) => c.ok),
220
224
  checks,
@@ -322,35 +326,90 @@ function installCommand(probe) {
322
326
  return probe.install.apt ?? null;
323
327
  }
324
328
  /**
325
- * Is the skill installed as a Claude Code plugin, and is it CURRENT?
329
+ * Is the skill installed for each agent on this machine, and is it CURRENT?
326
330
  *
327
- * Both halves matter, and the second one is the half that hides. **Publishing a
328
- * new version to the marketplace does not update an installed plugin** — found
329
- * on the author's own machine, running 1.17.0 at user scope and 1.12.0 at
330
- * project scope while 1.19.0 had been published. Seven versions of corrections
331
- * that shipped and were never read.
331
+ * ## One row per agent FOUND, and that is the whole shape (#1399)
332
332
  *
333
- * So this reports the installed version, and the reader can compare. A skill
334
- * silently behind is the same shape as a stale `pharos-convert`: present,
335
- * plausible, and answering from a world that has moved.
333
+ * The skill used to be one row about Claude Code, because Claude Code was the
334
+ * only agent that could install it. **Codex reads the same Claude-format
335
+ * `.claude-plugin/marketplace.json`** it generates its own
336
+ * `.codex-plugin/plugin.json` scoped to `./skills/pharos` and reports
337
+ * `pharos@floh-skills installed, enabled 1.28.0` — so a machine can now run two
338
+ * agents, and one of them can be several versions behind the other or missing
339
+ * the skill entirely. One row cannot hold two independent answers.
340
+ *
341
+ * So: `skill` keeps its id and now means *the skill in Claude Code*, and
342
+ * `skill-codex` is the Codex row. **Each is emitted only when its agent is
343
+ * actually on PATH**, which is the same rule the `<host>-bridge` rows follow —
344
+ * the `claude` and `codex` rows below in tier 2 are the fact for a missing
345
+ * agent, and a column of red about a tool you do not use is noise.
346
+ *
347
+ * That fixes something that was wrong before it: the old row reported `ok:
348
+ * false` whenever `claude` was absent, and it is TIER 0, so a perfectly healthy
349
+ * Codex-only machine was told pharos itself was not in working order.
350
+ *
351
+ * ## The half that hides
352
+ *
353
+ * **Publishing a new version to the marketplace does not update an installed
354
+ * plugin** — found on the author's own machine, running 1.17.0 at user scope
355
+ * and 1.12.0 at project scope while 1.19.0 had been published. Seven versions
356
+ * of corrections that shipped and were never read. Each agent has its own
357
+ * update command and they are not the same, so each row names its own.
358
+ *
359
+ * There is no comparison against "the version this CLI ships", deliberately:
360
+ * the skill's version is a field in the marketplace manifest over in
361
+ * `floh-skills`, bumped by hand at release time, and the npm package does not
362
+ * carry it. A constant copied in here would be the `CONVERT_RELEASE` hash
363
+ * written by hand — a number nobody checked, confidently wrong the first time
364
+ * somebody forgets it. What IS measurable is the two agents against each other,
365
+ * and {@link markSkillDrift} says so when they disagree.
336
366
  */
337
- async function skillCheck(env) {
338
- const base = {
339
- id: "skill",
340
- name: "The pharos skill",
367
+ async function skillChecks(env, claude, codex) {
368
+ const wanted = [];
369
+ if (claude?.ok === true)
370
+ wanted.push(claudeSkillCheck(env));
371
+ if (codex?.ok === true)
372
+ wanted.push(codexSkillCheck(env));
373
+ const rows = await Promise.all(wanted);
374
+ markSkillDrift(rows);
375
+ return rows;
376
+ }
377
+ /** What every skill row promises, whichever agent it is about. */
378
+ function skillBase(id, agent) {
379
+ return {
380
+ id,
381
+ name: `The pharos skill in ${agent}`,
341
382
  tier: 0,
342
- unlocks: "Claude Code knows to reach for pharos without being told.",
343
- without: "The CLI works, but no session knows it is there.",
383
+ unlocks: `${agent} knows to reach for pharos without being told.`,
384
+ without: `The CLI works, but no ${agent} session knows it is there.`,
344
385
  install: "pharos setup --install skill",
345
386
  };
387
+ }
388
+ /**
389
+ * The Claude Code row. Only reached when `claude` is on PATH.
390
+ *
391
+ * A failure here is therefore NOT "no plugin system" — that was the old
392
+ * meaning, and it is now the absent row. It is a `claude` that could not answer,
393
+ * which is a third state and is worded as one: reporting "not installed" about
394
+ * a question nobody managed to ask is the overclaim `gh-auth` exists to avoid.
395
+ */
396
+ async function claudeSkillCheck(env) {
397
+ const base = skillBase("skill", "Claude Code");
346
398
  try {
347
399
  const { stdout } = await run("claude", ["plugin", "list"], { timeout: 20_000, env });
348
400
  // `pharos@floh-skills` then a Version line somewhere after it. Scoped to a
349
401
  // window rather than the whole output, so another plugin's version cannot
350
402
  // be read as this one's.
351
- const at = stdout.indexOf("pharos@floh-skills");
403
+ const at = stdout.indexOf(PLUGIN);
352
404
  if (at === -1) {
353
- return { ...base, ok: false, version: null, path: null };
405
+ return {
406
+ ...base,
407
+ ok: false,
408
+ version: null,
409
+ path: null,
410
+ note: `Claude Code is here and \`${PLUGIN}\` is not among its plugins. `
411
+ + "`pharos setup --install skill` adds the marketplace and installs it.",
412
+ };
354
413
  }
355
414
  const version = /Version:\s*(\S+)/.exec(stdout.slice(at, at + 200))?.[1] ?? null;
356
415
  return {
@@ -359,23 +418,171 @@ async function skillCheck(env) {
359
418
  version,
360
419
  path: null,
361
420
  note: "Publishing a new skill does NOT update an installed one — "
362
- + "`claude plugin update pharos@floh-skills` does, and a restart loads it.",
421
+ + `\`claude plugin update ${PLUGIN}\` does, and a restart loads it.`,
363
422
  };
364
423
  }
365
- catch {
366
- // No `claude` on PATH: codex, antigravity and a plain shell all use this
367
- // CLI and have no plugin system. Not a failure, just not applicable.
424
+ catch (error) {
368
425
  return {
369
426
  ...base,
370
427
  ok: false,
371
428
  version: null,
372
429
  path: null,
373
430
  install: null,
374
- note: "No `claude` command here, so there is no plugin system to check. If you use another "
375
- + "agent, point it at the skill in the floh-skills repository.",
431
+ note: `\`claude plugin list\` could not be run: ${messageOf(error)}. So this is NOT known to `
432
+ + "be missing the question could not be asked. A Claude Code without a plugin "
433
+ + "subcommand is the likely cause.",
376
434
  };
377
435
  }
378
436
  }
437
+ /**
438
+ * The Codex row. Only reached when `codex` is on PATH.
439
+ *
440
+ * `codex plugin list --json --available --marketplace floh-skills` is the whole
441
+ * probe, and every part of that command line is load-bearing:
442
+ *
443
+ * - **`--marketplace`** narrows it to ours. Without it `--available`
444
+ * enumerates every remote marketplace Codex knows — 3712 entries on the
445
+ * machine this was measured on. Narrowed it is nine, offline, in 0.07s.
446
+ * - **`--available`** is what separates *the marketplace is configured and
447
+ * pharos is not installed from it* from *there is no floh-skills
448
+ * marketplace here at all*. Both are fixed by the same command, but they
449
+ * are different facts and a reader deserves the true one.
450
+ * - **`--json`** because the table is a table. An unknown marketplace name is
451
+ * two empty arrays and exit 0, so absence never arrives as an error.
452
+ *
453
+ * The table is still read as a fallback: a Codex old enough to have `codex
454
+ * plugin` and not `--json` would otherwise be reported as "not installed",
455
+ * which is the one answer that is worse than "could not tell".
456
+ */
457
+ async function codexSkillCheck(env) {
458
+ const base = skillBase("skill-codex", "Codex");
459
+ const state = await codexPluginState(env);
460
+ if (state.kind === "installed") {
461
+ return {
462
+ ...base,
463
+ ok: true,
464
+ version: state.version,
465
+ path: null,
466
+ note: "Publishing a new skill does NOT update an installed one, and for Codex it takes two "
467
+ + `commands rather than one: \`codex plugin marketplace upgrade ${MARKETPLACE_NAME}\` `
468
+ + `refreshes the snapshot, then \`codex plugin add ${PLUGIN}\` installs from it. `
469
+ + "Codex reads the same Claude-format marketplace manifest we publish for Claude Code.",
470
+ };
471
+ }
472
+ if (state.kind === "not-installed") {
473
+ return {
474
+ ...base,
475
+ ok: false,
476
+ version: null,
477
+ path: null,
478
+ note: `Codex has the ${MARKETPLACE_NAME} marketplace configured and has not installed `
479
+ + `\`${PLUGIN}\` from it`
480
+ + `${state.offered === null ? "" : ` — the snapshot offers ${state.offered}`}. `
481
+ + "`pharos setup --install skill` installs it.",
482
+ };
483
+ }
484
+ if (state.kind === "no-marketplace") {
485
+ return {
486
+ ...base,
487
+ ok: false,
488
+ version: null,
489
+ path: null,
490
+ note: `Codex has no ${MARKETPLACE_NAME} marketplace configured, so there is nothing to `
491
+ + "install the skill from yet. `pharos setup --install skill` adds it and installs.",
492
+ };
493
+ }
494
+ return {
495
+ ...base,
496
+ ok: false,
497
+ version: null,
498
+ path: null,
499
+ // No install command: we do not know that `pharos setup --install skill`
500
+ // can work here, and a button that fails is worse than no button. The
501
+ // likely fix is named in prose instead.
502
+ install: null,
503
+ note: `Codex is here but could not be asked: ${state.detail}. So this is NOT known to be `
504
+ + "missing. A Codex predating `codex plugin` is the likely cause; `npm i -g @openai/codex` "
505
+ + "updates it.",
506
+ };
507
+ }
508
+ async function codexPluginState(env) {
509
+ const args = ["plugin", "list", "--json", "--available", "--marketplace", MARKETPLACE_NAME];
510
+ let jsonFailure;
511
+ try {
512
+ const { stdout } = await run("codex", args, {
513
+ timeout: 20_000,
514
+ maxBuffer: 4 * 1024 * 1024,
515
+ env,
516
+ });
517
+ const parsed = JSON.parse(stdout);
518
+ const ours = (list) => (list ?? []).find((entry) => entry.pluginId === PLUGIN);
519
+ const installed = ours(parsed.installed);
520
+ if (installed !== undefined) {
521
+ return { kind: "installed", version: asVersion(installed.version) };
522
+ }
523
+ const available = ours(parsed.available);
524
+ if (available !== undefined) {
525
+ return { kind: "not-installed", offered: asVersion(available.version) };
526
+ }
527
+ return { kind: "no-marketplace" };
528
+ }
529
+ catch (error) {
530
+ jsonFailure = messageOf(error);
531
+ }
532
+ // The table. Same question, worse grammar: one row per plugin, the status
533
+ // column reading `installed, enabled` or `not installed`.
534
+ try {
535
+ const { stdout } = await run("codex", ["plugin", "list", "--marketplace", MARKETPLACE_NAME], {
536
+ timeout: 20_000,
537
+ maxBuffer: 4 * 1024 * 1024,
538
+ env,
539
+ });
540
+ const line = stdout.split("\n").find((row) => row.trimStart().startsWith(PLUGIN));
541
+ if (line === undefined)
542
+ return { kind: "no-marketplace" };
543
+ // Read only what follows the id, so a marketplace or path containing the
544
+ // word cannot be mistaken for the status.
545
+ const rest = line.slice(line.indexOf(PLUGIN) + PLUGIN.length);
546
+ if (/\bnot installed\b/.test(rest))
547
+ return { kind: "not-installed", offered: null };
548
+ if (/\binstalled\b/.test(rest)) {
549
+ return { kind: "installed", version: /(\d+\.\d+\.\d+[\w.+-]*)/.exec(rest)?.[1] ?? null };
550
+ }
551
+ return { kind: "unasked", detail: `\`codex plugin list\` printed a row this cannot read` };
552
+ }
553
+ catch (error) {
554
+ return { kind: "unasked", detail: `${jsonFailure}; and without --json: ${messageOf(error)}` };
555
+ }
556
+ }
557
+ function asVersion(value) {
558
+ return typeof value === "string" && value !== "" ? value : null;
559
+ }
560
+ /**
561
+ * Two agents, one skill, two different versions — say so on both rows.
562
+ *
563
+ * This is the comparison that is actually available here (see
564
+ * {@link skillChecks}), and it is the origin bug wearing the other hat: the
565
+ * operator's machine ran Claude Code on 1.27.0 while Codex, installed by hand
566
+ * minutes earlier, was on 1.28.0. Nothing anywhere reported it, and each row on
567
+ * its own looks entirely healthy.
568
+ *
569
+ * Exported because it is the assertable half: arranging two agents at two
570
+ * versions on a real machine is not something a test may do.
571
+ */
572
+ export function markSkillDrift(rows) {
573
+ const installed = rows.filter((row) => row.ok && row.version !== null);
574
+ if (installed.length < 2)
575
+ return;
576
+ const versions = new Set(installed.map((row) => row.version));
577
+ if (versions.size < 2)
578
+ return;
579
+ const inventory = installed.map((row) => `${row.name} has ${row.version}`).join(", ");
580
+ for (const row of installed) {
581
+ row.note = `${row.note ?? ""} One skill, two versions on this machine: ${inventory}. `
582
+ + "Update the older one — the two agents install from the same marketplace and there is "
583
+ + "no reason for them to disagree.";
584
+ }
585
+ }
379
586
  /** Configuration, read from the environment — no network unless asked. */
380
587
  function configCheck(env) {
381
588
  const missing = ["ADO_ORG", "ADO_PROJECT", "ADO_PAT"].filter((name) => (env[name] ?? "") === "");
@@ -995,13 +1202,34 @@ export async function repoAccessCheck(client, bindings, configPath) {
995
1202
  * not `osascript`). Extensions from the flavour's `extensions.json`, then its
996
1203
  * folder names. The bridge is compared to the one this CLI ships, read from
997
1204
  * `vscode/pharos-bridge.json` beside the bundled `.vsix`.
1205
+ *
1206
+ * ## …and which login a RUNNING host would start a session as (#1431)
1207
+ *
1208
+ * A VS Code started from a shell that had `CLAUDE_CONFIG_DIR` set passes it
1209
+ * to every terminal it opens, so a Claude session started there runs as that
1210
+ * login — an Argus agent account, on the machine it was measured on. Nothing
1211
+ * is wrong and nothing is refused; the row's note says so, with the fix,
1212
+ * because the alternative is finding out from a session that answers as the
1213
+ * wrong account. Terminal is asked the same question and cannot answer it on
1214
+ * macOS 26 — a platform binary's environment is not shown to `ps -E` — so its
1215
+ * row says nothing rather than something unverified.
998
1216
  */
999
1217
  async function delegationChecks(env) {
1000
- const hosts = await Promise.all(["terminal", "vscode", "vscode-insiders", "navarch"].map(async (id) => ({
1001
- id,
1002
- app: await findApp(id, env),
1003
- })));
1004
- const checks = hosts.map(({ id, app }) => hostCheck(id, app));
1218
+ const [hosts, carried] = await Promise.all([
1219
+ Promise.all(["terminal", "vscode", "vscode-insiders", "navarch"].map(async (id) => ({
1220
+ id,
1221
+ app: await findApp(id, env),
1222
+ }))),
1223
+ // `ps -E` is the BSD spelling, and every host here is a macOS app anyway.
1224
+ // A failed read is no note rather than a wrong one.
1225
+ platform() === "darwin" ? carriedProfiles(systemProbes(env, null)).catch(() => []) : Promise.resolve([]),
1226
+ ]);
1227
+ const home = env["HOME"] ?? homedir();
1228
+ const checks = hosts.map(({ id, app }) => {
1229
+ const check = hostCheck(id, app);
1230
+ const note = hostCarriedNote(carried.find((entry) => entry.host === id) ?? null, home);
1231
+ return note === "" ? check : { ...check, note: `${check.note ?? ""}${note}` };
1232
+ });
1005
1233
  const navarch = hosts.find(({ id }) => id === "navarch");
1006
1234
  if (navarch?.app != null)
1007
1235
  checks.push(await argusDelegateCheck(env));
@@ -1038,6 +1266,17 @@ async function delegationChecks(env) {
1038
1266
  *
1039
1267
  * `install` is null on purpose: the fix is a reload, and a reload ends every
1040
1268
  * live worker session. That is a person's to time, never a button.
1269
+ *
1270
+ * **A daemon that serves the verb is then asked a second question**, because
1271
+ * there are two generations of "current" and they behave differently (#1412).
1272
+ * argus #1402 added a target worker, a GitHub account and the
1273
+ * `delegate_candidates` read in one change, so one probe answers for all
1274
+ * three — and the gap matters more than a version gap usually does: `delegate`
1275
+ * IGNORES a key it does not know, so on the older daemon `--session` would be
1276
+ * discarded and the prompt typed into whichever worker the find liked. The CLI
1277
+ * refuses rather than let that happen, and this row is where somebody finds
1278
+ * out before they hit it. It stays `ok: true`: the verb works, and a red row
1279
+ * for a feature the machine has never used is a red row nobody reads.
1041
1280
  */
1042
1281
  async function argusDelegateCheck(env) {
1043
1282
  const socketPath = argusSocketPath(env);
@@ -1070,7 +1309,8 @@ async function argusDelegateCheck(env) {
1070
1309
  return {
1071
1310
  ...base,
1072
1311
  ok: true,
1073
- note: `argusd is running at ${socketPath} and serves the delegate verb.`,
1312
+ note: `argusd is running at ${socketPath} and serves the delegate verb.`
1313
+ + ` ${await targetSupportNote(systemArgusDeps(env))}`,
1074
1314
  };
1075
1315
  }
1076
1316
  return {
@@ -1083,6 +1323,54 @@ async function argusDelegateCheck(env) {
1083
1323
  + " forwarding a verb it does not itself know.",
1084
1324
  };
1085
1325
  }
1326
+ /**
1327
+ * Does this daemon read a target worker and a GitHub account? — one sentence
1328
+ * for the `argus-delegate` row.
1329
+ *
1330
+ * The probe is `delegate_candidates` with **no `directory`**, which is the
1331
+ * cheapest form of the question: a daemon that serves the op refuses
1332
+ * `bad-directory` (it validates exactly as `delegate` does), and one that does
1333
+ * not answers `unknown-op`. Either way nothing is read, nothing is written,
1334
+ * and no folder has to exist for the question to be askable — which matters,
1335
+ * because `doctor` is asked on machines with no project linked at all.
1336
+ *
1337
+ * A socket that breaks between the two reads says so rather than guessing: the
1338
+ * row above it has already reported the daemon is up, so silence here would
1339
+ * read as an answer.
1340
+ */
1341
+ export async function targetSupportNote(argus) {
1342
+ let reply;
1343
+ try {
1344
+ reply = await askArgus(argus, delegateCandidatesRequest("", "claude"), readTimeoutMs(0));
1345
+ }
1346
+ catch (error) {
1347
+ return "Whether it also serves `delegate_candidates` and `gh_account` could not be read: "
1348
+ + `${error instanceof Error ? error.message : String(error)}.`;
1349
+ }
1350
+ return candidatesSupport(reply) === "no-op"
1351
+ ? "It PREDATES `delegate_candidates` and `gh_account` (argus #1402), so `--list --host navarch`"
1352
+ + " falls back to reading `fleet_status` itself, and `--session` and `--gh-account` are refused"
1353
+ + " `argusd-outdated` there — this daemon would ignore both rather than refuse them, and type"
1354
+ + " the prompt into whichever worker its own find picked. Reloading argusd lifts that, and"
1355
+ + " ends every live worker session."
1356
+ : "It also serves `delegate_candidates` and `gh_account` (argus #1402), so `--list --host navarch`"
1357
+ + " is the daemon's own answer to its own find, and `--session` and `--gh-account` are honoured.";
1358
+ }
1359
+ /**
1360
+ * What a host row adds when the running host carries a non-default
1361
+ * `CLAUDE_CONFIG_DIR`, with its leading space — or nothing.
1362
+ *
1363
+ * The same sentence `pharos delegate` puts in its detail, from the same
1364
+ * function, plus the one thing only a machine-wide report can add: that the
1365
+ * sessions ALREADY running there are found and named, so the only surprise
1366
+ * left is the next one it starts.
1367
+ */
1368
+ export function hostCarriedNote(carried, home) {
1369
+ if (carried === null)
1370
+ return "";
1371
+ return (` Running now as pid ${carried.pid}. ${carriedSentence(carried, home)} Sessions already running there are`
1372
+ + ` still listed by \`pharos delegate --list\`, marked \`profile ${carried.profile}\`.`);
1373
+ }
1086
1374
  function hostCheck(id, app) {
1087
1375
  const spec = HOSTS[id];
1088
1376
  const unlocks = {
@@ -1142,22 +1430,35 @@ async function extensionChecks(host, env, shippedBridge) {
1142
1430
  * the one this pharos ships stays `ok:true` with a note, because `ok:false`
1143
1431
  * should mean "something you asked for does not work" rather than "there is a
1144
1432
  * newer one" — otherwise every future bump nags on every machine.
1433
+ *
1434
+ * **This row reports; it no longer has to be acted on to be fixed.** Since
1435
+ * #1394 `pharos delegate --host <flavour>` installs or upgrades the bridge on
1436
+ * its way to opening a URI, so an absent or older one is put right by the
1437
+ * first delegation. `pharos setup --install vscode-bridge` stays the way to
1438
+ * do it ahead of time — and the note says so, because a row that names no fix
1439
+ * reads as a row nobody can act on.
1145
1440
  */
1146
1441
  const bridgeStale = bridge !== undefined && !bridgeAtLeast(bridge.version, SESSION_BRIDGE_VERSION);
1442
+ /** What now fixes an absent or outdated bridge, in both the words a person needs. */
1443
+ const onTheWay = `\`pharos delegate --host ${host}\` installs it on its way to the editor, and `
1444
+ + "`pharos setup --install vscode-bridge` does it ahead of time.";
1147
1445
  const bridgeNote = bridge === undefined
1148
1446
  ? shippedBridge === null
1149
1447
  ? `${where} This pharos ships no bridge either (${bundledVsixPath(env)} is absent) — the ship todo packs it; until then \`--install vscode-bridge\` refuses by name.`
1150
- : `${where} This pharos ships ${shippedBridge}.`
1448
+ : `${where} This pharos ships ${shippedBridge}. ${onTheWay}`
1151
1449
  : bridgeStale
1152
1450
  ? `${where} Installed ${bridge.version}, which predates \`pharos delegate --session\` (${SESSION_BRIDGE_VERSION}). `
1153
1451
  + "It does not ignore the session — it refuses the whole URI, because it rejects any query key it does "
1154
1452
  + `not know — so picking a session in ${flavour} sends nothing. Delegating without --session still works. `
1155
- + `\`pharos setup --install vscode-bridge\` replaces it${shippedBridge === null ? "" : ` with ${shippedBridge}`}.`
1453
+ + (shippedBridge === null || !bridgeAtLeast(shippedBridge, SESSION_BRIDGE_VERSION)
1454
+ ? `\`pharos setup --install vscode-bridge\` replaces it${shippedBridge === null ? "" : ` with ${shippedBridge}`}.`
1455
+ : `${onTheWay} A ${flavour} window that was already open keeps ${bridge.version} until it reloads, `
1456
+ + "so the first --session delegation after the replacement is refused once and says so.")
1156
1457
  : shippedBridge === null
1157
1458
  ? `${where} Installed ${bridge.version}; this pharos ships no bridge to compare it with.`
1158
1459
  : bridge.version === shippedBridge
1159
1460
  ? `${where} Installed ${bridge.version}, which is the one this pharos ships.`
1160
- : `${where} Installed ${bridge.version}, but this pharos ships ${shippedBridge}. It serves every verb this pharos has; the URI contract may still have moved. \`pharos setup --install vscode-bridge\` replaces it.`;
1461
+ : `${where} Installed ${bridge.version}, but this pharos ships ${shippedBridge}. It serves every verb this pharos has; the URI contract may still have moved. ${onTheWay}`;
1161
1462
  return [
1162
1463
  {
1163
1464
  id: `${host}-claude-ext`,
@@ -1188,7 +1489,7 @@ async function extensionChecks(host, env, shippedBridge) {
1188
1489
  name: `Pharos bridge in ${flavour}`,
1189
1490
  tier: 2,
1190
1491
  unlocks: `\`pharos delegate --host ${host}\` at all: the bridge is what puts the prompt into a session inside ${flavour}. Nothing else can. From ${SESSION_BRIDGE_VERSION} it also honours \`--session\`, which is how a person picks between two sessions on one folder.`,
1191
- without: `Delegating to ${flavour} is refused by name (bridge-not-installed) and so is \`--session\` against a bridge older than ${SESSION_BRIDGE_VERSION}, which would otherwise report a prompt as sent while refusing the URI.`,
1492
+ without: `Delegating to ${flavour} installs the bridge this pharos ships first, and is refused by name (bridge-not-installed) only when that install fails or there is none to install. \`--session\` against a bridge older than ${SESSION_BRIDGE_VERSION} is refused ONCE even after it is replaced — a window already open keeps the old one, and the old one refuses the whole URI rather than ignoring the session, which would otherwise report a prompt as sent.`,
1192
1493
  ok: bridge !== undefined && !bridgeStale,
1193
1494
  version: bridge?.version ?? null,
1194
1495
  path: bridge?.path ?? null,