@floh-solutions/pharos-cli 0.31.1 → 0.33.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.
- package/README.md +223 -9
- package/dist/capabilities.d.ts +27 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +144 -41
- package/dist/capabilities.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +79 -16
- package/dist/cli.js.map +1 -1
- package/dist/commands/delegate.d.ts +211 -13
- package/dist/commands/delegate.d.ts.map +1 -1
- package/dist/commands/delegate.js +798 -52
- package/dist/commands/delegate.js.map +1 -1
- package/dist/commands/doctor.d.ts +30 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +382 -39
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/pr.d.ts +56 -0
- package/dist/commands/pr.d.ts.map +1 -0
- package/dist/commands/pr.js +202 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/setup.js +21 -3
- package/dist/commands/setup.js.map +1 -1
- package/dist/delegate/argus.d.ts +436 -0
- package/dist/delegate/argus.d.ts.map +1 -0
- package/dist/delegate/argus.js +709 -0
- package/dist/delegate/argus.js.map +1 -0
- package/dist/delegate/vsix.d.ts +26 -0
- package/dist/delegate/vsix.d.ts.map +1 -1
- package/dist/delegate/vsix.js +22 -5
- package/dist/delegate/vsix.js.map +1 -1
- package/package.json +3 -3
- package/skill/SKILL.md +44 -10
- package/vscode/pharos-bridge.json +1 -1
- package/vscode/pharos-bridge.vsix +0 -0
package/dist/commands/doctor.js
CHANGED
|
@@ -3,7 +3,9 @@ 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";
|
|
8
|
+
import { argusSocketPath, ArgusUnreachableError, askArgus, candidatesSupport, delegateCandidatesRequest, delegateStatusRequest, PROBE_REQUEST, readTimeoutMs, servesDelegate, systemArgusDeps, } from "../delegate/argus.js";
|
|
7
9
|
import { findApp, HOSTS } from "../delegate/hosts.js";
|
|
8
10
|
import { BRIDGE_EXTENSION_ID, bridgeAtLeast, bundledBridge, bundledVsixPath, CLAUDE_EXTENSION_ID, CODEX_EXTENSION_ID, extensionsDirectory, installedExtensions, SESSION_BRIDGE_VERSION, } from "../delegate/vsix.js";
|
|
9
11
|
import { emit, emitText } from "../output.js";
|
|
@@ -198,22 +200,24 @@ export async function runDoctor(io, env, options, pretty, version) {
|
|
|
198
200
|
+ "`pharos setup --convert` replaces it.";
|
|
199
201
|
}
|
|
200
202
|
const config = configCheck(env);
|
|
201
|
-
// **Run together, reported in order.** These
|
|
202
|
-
// other and every one of them is slow for a different reason —
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
const [
|
|
203
|
+
// **Run together, reported in order.** These four are independent of each
|
|
204
|
+
// other and every one of them is slow for a different reason — the skill rows
|
|
205
|
+
// shell out to another CLI, `--verify` makes two Azure DevOps calls, and `gh
|
|
206
|
+
// auth status` validates each token against github.com. Serially that is the
|
|
207
|
+
// sum; the report is assembled afterwards, so the order a reader sees is
|
|
208
|
+
// unchanged.
|
|
209
|
+
const [skills, scopes, github, delegation] = await Promise.all([
|
|
208
210
|
// The skill is what teaches a session this tool exists. A CLI installed
|
|
209
211
|
// without it is a CLI nobody reaches for, so its absence belongs in tier 0
|
|
210
|
-
// beside the things that make pharos work at all.
|
|
211
|
-
|
|
212
|
+
// beside the things that make pharos work at all. One row per agent that is
|
|
213
|
+
// actually here — the probes above have already answered that, so this asks
|
|
214
|
+
// nothing it can read.
|
|
215
|
+
skillChecks(env, checks.find((c) => c.id === "claude"), checks.find((c) => c.id === "codex")),
|
|
212
216
|
options.verify ? verifyScopes(env, config.ok) : Promise.resolve([]),
|
|
213
217
|
githubChecks(env, checks.find((c) => c.id === "gh"), options.verify),
|
|
214
218
|
delegationChecks(env),
|
|
215
219
|
]);
|
|
216
|
-
checks.push(
|
|
220
|
+
checks.push(...skills, config, ...scopes, ...github, ...delegation);
|
|
217
221
|
const result = {
|
|
218
222
|
ok: checks.filter((c) => c.tier === 0).every((c) => c.ok),
|
|
219
223
|
checks,
|
|
@@ -321,35 +325,90 @@ function installCommand(probe) {
|
|
|
321
325
|
return probe.install.apt ?? null;
|
|
322
326
|
}
|
|
323
327
|
/**
|
|
324
|
-
* Is the skill installed
|
|
328
|
+
* Is the skill installed for each agent on this machine, and is it CURRENT?
|
|
325
329
|
*
|
|
326
|
-
*
|
|
327
|
-
* new version to the marketplace does not update an installed plugin** — found
|
|
328
|
-
* on the author's own machine, running 1.17.0 at user scope and 1.12.0 at
|
|
329
|
-
* project scope while 1.19.0 had been published. Seven versions of corrections
|
|
330
|
-
* that shipped and were never read.
|
|
330
|
+
* ## One row per agent FOUND, and that is the whole shape (#1399)
|
|
331
331
|
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
332
|
+
* The skill used to be one row about Claude Code, because Claude Code was the
|
|
333
|
+
* only agent that could install it. **Codex reads the same Claude-format
|
|
334
|
+
* `.claude-plugin/marketplace.json`** — it generates its own
|
|
335
|
+
* `.codex-plugin/plugin.json` scoped to `./skills/pharos` and reports
|
|
336
|
+
* `pharos@floh-skills installed, enabled 1.28.0` — so a machine can now run two
|
|
337
|
+
* agents, and one of them can be several versions behind the other or missing
|
|
338
|
+
* the skill entirely. One row cannot hold two independent answers.
|
|
339
|
+
*
|
|
340
|
+
* So: `skill` keeps its id and now means *the skill in Claude Code*, and
|
|
341
|
+
* `skill-codex` is the Codex row. **Each is emitted only when its agent is
|
|
342
|
+
* actually on PATH**, which is the same rule the `<host>-bridge` rows follow —
|
|
343
|
+
* the `claude` and `codex` rows below in tier 2 are the fact for a missing
|
|
344
|
+
* agent, and a column of red about a tool you do not use is noise.
|
|
345
|
+
*
|
|
346
|
+
* That fixes something that was wrong before it: the old row reported `ok:
|
|
347
|
+
* false` whenever `claude` was absent, and it is TIER 0, so a perfectly healthy
|
|
348
|
+
* Codex-only machine was told pharos itself was not in working order.
|
|
349
|
+
*
|
|
350
|
+
* ## The half that hides
|
|
351
|
+
*
|
|
352
|
+
* **Publishing a new version to the marketplace does not update an installed
|
|
353
|
+
* plugin** — found on the author's own machine, running 1.17.0 at user scope
|
|
354
|
+
* and 1.12.0 at project scope while 1.19.0 had been published. Seven versions
|
|
355
|
+
* of corrections that shipped and were never read. Each agent has its own
|
|
356
|
+
* update command and they are not the same, so each row names its own.
|
|
357
|
+
*
|
|
358
|
+
* There is no comparison against "the version this CLI ships", deliberately:
|
|
359
|
+
* the skill's version is a field in the marketplace manifest over in
|
|
360
|
+
* `floh-skills`, bumped by hand at release time, and the npm package does not
|
|
361
|
+
* carry it. A constant copied in here would be the `CONVERT_RELEASE` hash
|
|
362
|
+
* written by hand — a number nobody checked, confidently wrong the first time
|
|
363
|
+
* somebody forgets it. What IS measurable is the two agents against each other,
|
|
364
|
+
* and {@link markSkillDrift} says so when they disagree.
|
|
335
365
|
*/
|
|
336
|
-
async function
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
366
|
+
async function skillChecks(env, claude, codex) {
|
|
367
|
+
const wanted = [];
|
|
368
|
+
if (claude?.ok === true)
|
|
369
|
+
wanted.push(claudeSkillCheck(env));
|
|
370
|
+
if (codex?.ok === true)
|
|
371
|
+
wanted.push(codexSkillCheck(env));
|
|
372
|
+
const rows = await Promise.all(wanted);
|
|
373
|
+
markSkillDrift(rows);
|
|
374
|
+
return rows;
|
|
375
|
+
}
|
|
376
|
+
/** What every skill row promises, whichever agent it is about. */
|
|
377
|
+
function skillBase(id, agent) {
|
|
378
|
+
return {
|
|
379
|
+
id,
|
|
380
|
+
name: `The pharos skill in ${agent}`,
|
|
340
381
|
tier: 0,
|
|
341
|
-
unlocks:
|
|
342
|
-
without:
|
|
382
|
+
unlocks: `${agent} knows to reach for pharos without being told.`,
|
|
383
|
+
without: `The CLI works, but no ${agent} session knows it is there.`,
|
|
343
384
|
install: "pharos setup --install skill",
|
|
344
385
|
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The Claude Code row. Only reached when `claude` is on PATH.
|
|
389
|
+
*
|
|
390
|
+
* A failure here is therefore NOT "no plugin system" — that was the old
|
|
391
|
+
* meaning, and it is now the absent row. It is a `claude` that could not answer,
|
|
392
|
+
* which is a third state and is worded as one: reporting "not installed" about
|
|
393
|
+
* a question nobody managed to ask is the overclaim `gh-auth` exists to avoid.
|
|
394
|
+
*/
|
|
395
|
+
async function claudeSkillCheck(env) {
|
|
396
|
+
const base = skillBase("skill", "Claude Code");
|
|
345
397
|
try {
|
|
346
398
|
const { stdout } = await run("claude", ["plugin", "list"], { timeout: 20_000, env });
|
|
347
399
|
// `pharos@floh-skills` then a Version line somewhere after it. Scoped to a
|
|
348
400
|
// window rather than the whole output, so another plugin's version cannot
|
|
349
401
|
// be read as this one's.
|
|
350
|
-
const at = stdout.indexOf(
|
|
402
|
+
const at = stdout.indexOf(PLUGIN);
|
|
351
403
|
if (at === -1) {
|
|
352
|
-
return {
|
|
404
|
+
return {
|
|
405
|
+
...base,
|
|
406
|
+
ok: false,
|
|
407
|
+
version: null,
|
|
408
|
+
path: null,
|
|
409
|
+
note: `Claude Code is here and \`${PLUGIN}\` is not among its plugins. `
|
|
410
|
+
+ "`pharos setup --install skill` adds the marketplace and installs it.",
|
|
411
|
+
};
|
|
353
412
|
}
|
|
354
413
|
const version = /Version:\s*(\S+)/.exec(stdout.slice(at, at + 200))?.[1] ?? null;
|
|
355
414
|
return {
|
|
@@ -358,22 +417,170 @@ async function skillCheck(env) {
|
|
|
358
417
|
version,
|
|
359
418
|
path: null,
|
|
360
419
|
note: "Publishing a new skill does NOT update an installed one — "
|
|
361
|
-
+
|
|
420
|
+
+ `\`claude plugin update ${PLUGIN}\` does, and a restart loads it.`,
|
|
362
421
|
};
|
|
363
422
|
}
|
|
364
|
-
catch {
|
|
365
|
-
// No `claude` on PATH: codex, antigravity and a plain shell all use this
|
|
366
|
-
// CLI and have no plugin system. Not a failure, just not applicable.
|
|
423
|
+
catch (error) {
|
|
367
424
|
return {
|
|
368
425
|
...base,
|
|
369
426
|
ok: false,
|
|
370
427
|
version: null,
|
|
371
428
|
path: null,
|
|
372
429
|
install: null,
|
|
373
|
-
note:
|
|
374
|
-
+ "
|
|
430
|
+
note: `\`claude plugin list\` could not be run: ${messageOf(error)}. So this is NOT known to `
|
|
431
|
+
+ "be missing — the question could not be asked. A Claude Code without a plugin "
|
|
432
|
+
+ "subcommand is the likely cause.",
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* The Codex row. Only reached when `codex` is on PATH.
|
|
438
|
+
*
|
|
439
|
+
* `codex plugin list --json --available --marketplace floh-skills` is the whole
|
|
440
|
+
* probe, and every part of that command line is load-bearing:
|
|
441
|
+
*
|
|
442
|
+
* - **`--marketplace`** narrows it to ours. Without it `--available`
|
|
443
|
+
* enumerates every remote marketplace Codex knows — 3712 entries on the
|
|
444
|
+
* machine this was measured on. Narrowed it is nine, offline, in 0.07s.
|
|
445
|
+
* - **`--available`** is what separates *the marketplace is configured and
|
|
446
|
+
* pharos is not installed from it* from *there is no floh-skills
|
|
447
|
+
* marketplace here at all*. Both are fixed by the same command, but they
|
|
448
|
+
* are different facts and a reader deserves the true one.
|
|
449
|
+
* - **`--json`** because the table is a table. An unknown marketplace name is
|
|
450
|
+
* two empty arrays and exit 0, so absence never arrives as an error.
|
|
451
|
+
*
|
|
452
|
+
* The table is still read as a fallback: a Codex old enough to have `codex
|
|
453
|
+
* plugin` and not `--json` would otherwise be reported as "not installed",
|
|
454
|
+
* which is the one answer that is worse than "could not tell".
|
|
455
|
+
*/
|
|
456
|
+
async function codexSkillCheck(env) {
|
|
457
|
+
const base = skillBase("skill-codex", "Codex");
|
|
458
|
+
const state = await codexPluginState(env);
|
|
459
|
+
if (state.kind === "installed") {
|
|
460
|
+
return {
|
|
461
|
+
...base,
|
|
462
|
+
ok: true,
|
|
463
|
+
version: state.version,
|
|
464
|
+
path: null,
|
|
465
|
+
note: "Publishing a new skill does NOT update an installed one, and for Codex it takes two "
|
|
466
|
+
+ `commands rather than one: \`codex plugin marketplace upgrade ${MARKETPLACE_NAME}\` `
|
|
467
|
+
+ `refreshes the snapshot, then \`codex plugin add ${PLUGIN}\` installs from it. `
|
|
468
|
+
+ "Codex reads the same Claude-format marketplace manifest we publish for Claude Code.",
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (state.kind === "not-installed") {
|
|
472
|
+
return {
|
|
473
|
+
...base,
|
|
474
|
+
ok: false,
|
|
475
|
+
version: null,
|
|
476
|
+
path: null,
|
|
477
|
+
note: `Codex has the ${MARKETPLACE_NAME} marketplace configured and has not installed `
|
|
478
|
+
+ `\`${PLUGIN}\` from it`
|
|
479
|
+
+ `${state.offered === null ? "" : ` — the snapshot offers ${state.offered}`}. `
|
|
480
|
+
+ "`pharos setup --install skill` installs it.",
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
if (state.kind === "no-marketplace") {
|
|
484
|
+
return {
|
|
485
|
+
...base,
|
|
486
|
+
ok: false,
|
|
487
|
+
version: null,
|
|
488
|
+
path: null,
|
|
489
|
+
note: `Codex has no ${MARKETPLACE_NAME} marketplace configured, so there is nothing to `
|
|
490
|
+
+ "install the skill from yet. `pharos setup --install skill` adds it and installs.",
|
|
375
491
|
};
|
|
376
492
|
}
|
|
493
|
+
return {
|
|
494
|
+
...base,
|
|
495
|
+
ok: false,
|
|
496
|
+
version: null,
|
|
497
|
+
path: null,
|
|
498
|
+
// No install command: we do not know that `pharos setup --install skill`
|
|
499
|
+
// can work here, and a button that fails is worse than no button. The
|
|
500
|
+
// likely fix is named in prose instead.
|
|
501
|
+
install: null,
|
|
502
|
+
note: `Codex is here but could not be asked: ${state.detail}. So this is NOT known to be `
|
|
503
|
+
+ "missing. A Codex predating `codex plugin` is the likely cause; `npm i -g @openai/codex` "
|
|
504
|
+
+ "updates it.",
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async function codexPluginState(env) {
|
|
508
|
+
const args = ["plugin", "list", "--json", "--available", "--marketplace", MARKETPLACE_NAME];
|
|
509
|
+
let jsonFailure;
|
|
510
|
+
try {
|
|
511
|
+
const { stdout } = await run("codex", args, {
|
|
512
|
+
timeout: 20_000,
|
|
513
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
514
|
+
env,
|
|
515
|
+
});
|
|
516
|
+
const parsed = JSON.parse(stdout);
|
|
517
|
+
const ours = (list) => (list ?? []).find((entry) => entry.pluginId === PLUGIN);
|
|
518
|
+
const installed = ours(parsed.installed);
|
|
519
|
+
if (installed !== undefined) {
|
|
520
|
+
return { kind: "installed", version: asVersion(installed.version) };
|
|
521
|
+
}
|
|
522
|
+
const available = ours(parsed.available);
|
|
523
|
+
if (available !== undefined) {
|
|
524
|
+
return { kind: "not-installed", offered: asVersion(available.version) };
|
|
525
|
+
}
|
|
526
|
+
return { kind: "no-marketplace" };
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
jsonFailure = messageOf(error);
|
|
530
|
+
}
|
|
531
|
+
// The table. Same question, worse grammar: one row per plugin, the status
|
|
532
|
+
// column reading `installed, enabled` or `not installed`.
|
|
533
|
+
try {
|
|
534
|
+
const { stdout } = await run("codex", ["plugin", "list", "--marketplace", MARKETPLACE_NAME], {
|
|
535
|
+
timeout: 20_000,
|
|
536
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
537
|
+
env,
|
|
538
|
+
});
|
|
539
|
+
const line = stdout.split("\n").find((row) => row.trimStart().startsWith(PLUGIN));
|
|
540
|
+
if (line === undefined)
|
|
541
|
+
return { kind: "no-marketplace" };
|
|
542
|
+
// Read only what follows the id, so a marketplace or path containing the
|
|
543
|
+
// word cannot be mistaken for the status.
|
|
544
|
+
const rest = line.slice(line.indexOf(PLUGIN) + PLUGIN.length);
|
|
545
|
+
if (/\bnot installed\b/.test(rest))
|
|
546
|
+
return { kind: "not-installed", offered: null };
|
|
547
|
+
if (/\binstalled\b/.test(rest)) {
|
|
548
|
+
return { kind: "installed", version: /(\d+\.\d+\.\d+[\w.+-]*)/.exec(rest)?.[1] ?? null };
|
|
549
|
+
}
|
|
550
|
+
return { kind: "unasked", detail: `\`codex plugin list\` printed a row this cannot read` };
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
return { kind: "unasked", detail: `${jsonFailure}; and without --json: ${messageOf(error)}` };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function asVersion(value) {
|
|
557
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Two agents, one skill, two different versions — say so on both rows.
|
|
561
|
+
*
|
|
562
|
+
* This is the comparison that is actually available here (see
|
|
563
|
+
* {@link skillChecks}), and it is the origin bug wearing the other hat: the
|
|
564
|
+
* operator's machine ran Claude Code on 1.27.0 while Codex, installed by hand
|
|
565
|
+
* minutes earlier, was on 1.28.0. Nothing anywhere reported it, and each row on
|
|
566
|
+
* its own looks entirely healthy.
|
|
567
|
+
*
|
|
568
|
+
* Exported because it is the assertable half: arranging two agents at two
|
|
569
|
+
* versions on a real machine is not something a test may do.
|
|
570
|
+
*/
|
|
571
|
+
export function markSkillDrift(rows) {
|
|
572
|
+
const installed = rows.filter((row) => row.ok && row.version !== null);
|
|
573
|
+
if (installed.length < 2)
|
|
574
|
+
return;
|
|
575
|
+
const versions = new Set(installed.map((row) => row.version));
|
|
576
|
+
if (versions.size < 2)
|
|
577
|
+
return;
|
|
578
|
+
const inventory = installed.map((row) => `${row.name} has ${row.version}`).join(", ");
|
|
579
|
+
for (const row of installed) {
|
|
580
|
+
row.note = `${row.note ?? ""} One skill, two versions on this machine: ${inventory}. `
|
|
581
|
+
+ "Update the older one — the two agents install from the same marketplace and there is "
|
|
582
|
+
+ "no reason for them to disagree.";
|
|
583
|
+
}
|
|
377
584
|
}
|
|
378
585
|
/** Configuration, read from the environment — no network unless asked. */
|
|
379
586
|
function configCheck(env) {
|
|
@@ -1001,6 +1208,9 @@ async function delegationChecks(env) {
|
|
|
1001
1208
|
app: await findApp(id, env),
|
|
1002
1209
|
})));
|
|
1003
1210
|
const checks = hosts.map(({ id, app }) => hostCheck(id, app));
|
|
1211
|
+
const navarch = hosts.find(({ id }) => id === "navarch");
|
|
1212
|
+
if (navarch?.app != null)
|
|
1213
|
+
checks.push(await argusDelegateCheck(env));
|
|
1004
1214
|
const bundled = await bundledBridge(env);
|
|
1005
1215
|
for (const { id, app } of hosts) {
|
|
1006
1216
|
if (app === null || (id !== "vscode" && id !== "vscode-insiders"))
|
|
@@ -1009,13 +1219,128 @@ async function delegationChecks(env) {
|
|
|
1009
1219
|
}
|
|
1010
1220
|
return checks;
|
|
1011
1221
|
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Does the argusd **running right now** serve `delegate`?
|
|
1224
|
+
*
|
|
1225
|
+
* The one fact about this route that nothing else on this machine reports, and
|
|
1226
|
+
* the one a person otherwise learns from their first failed hand-off. It is a
|
|
1227
|
+
* separate row rather than a footnote on `navarch` because the two answer
|
|
1228
|
+
* different questions: that row is "is the app installed", read off the disk,
|
|
1229
|
+
* and **argusd outlives the app** — that is the feature that keeps worker
|
|
1230
|
+
* sessions alive across a restart, and it is exactly why master, the installed
|
|
1231
|
+
* app and the running daemon drift apart. A machine can have Navarch 1.2 in
|
|
1232
|
+
* `/Applications` and a daemon from last week holding the socket.
|
|
1233
|
+
*
|
|
1234
|
+
* **Emitted only when Navarch is installed.** On a machine without it the
|
|
1235
|
+
* answer is always no and the `navarch` row already says why; a second
|
|
1236
|
+
* permanently-red row is the kind nobody reads.
|
|
1237
|
+
*
|
|
1238
|
+
* The probe is a `delegate_status` read about an all-zero id. That op is a pure
|
|
1239
|
+
* read of the daemon's own table — not gated, not rate-limited, and about an id
|
|
1240
|
+
* only it mints — so asking about one that cannot exist costs a round trip and
|
|
1241
|
+
* changes nothing. A current daemon answers `unknown-request`; one that
|
|
1242
|
+
* predates the verb answers `unknown op` with no `reason` key at all, which is
|
|
1243
|
+
* the same tell `delegate` itself uses.
|
|
1244
|
+
*
|
|
1245
|
+
* `install` is null on purpose: the fix is a reload, and a reload ends every
|
|
1246
|
+
* live worker session. That is a person's to time, never a button.
|
|
1247
|
+
*
|
|
1248
|
+
* **A daemon that serves the verb is then asked a second question**, because
|
|
1249
|
+
* there are two generations of "current" and they behave differently (#1412).
|
|
1250
|
+
* argus #1402 added a target worker, a GitHub account and the
|
|
1251
|
+
* `delegate_candidates` read in one change, so one probe answers for all
|
|
1252
|
+
* three — and the gap matters more than a version gap usually does: `delegate`
|
|
1253
|
+
* IGNORES a key it does not know, so on the older daemon `--session` would be
|
|
1254
|
+
* discarded and the prompt typed into whichever worker the find liked. The CLI
|
|
1255
|
+
* refuses rather than let that happen, and this row is where somebody finds
|
|
1256
|
+
* out before they hit it. It stays `ok: true`: the verb works, and a red row
|
|
1257
|
+
* for a feature the machine has never used is a red row nobody reads.
|
|
1258
|
+
*/
|
|
1259
|
+
async function argusDelegateCheck(env) {
|
|
1260
|
+
const socketPath = argusSocketPath(env);
|
|
1261
|
+
const base = {
|
|
1262
|
+
id: "argus-delegate",
|
|
1263
|
+
name: "Argus delegation (argusd)",
|
|
1264
|
+
tier: 2,
|
|
1265
|
+
unlocks: "`pharos delegate --host navarch` — handing a work item to a worker in Navarch, and `--list`"
|
|
1266
|
+
+ " of the workers on a folder.",
|
|
1267
|
+
without: "That host is refused by name; Terminal and the VS Code hosts still work.",
|
|
1268
|
+
version: null,
|
|
1269
|
+
path: socketPath,
|
|
1270
|
+
install: null,
|
|
1271
|
+
};
|
|
1272
|
+
let reply;
|
|
1273
|
+
try {
|
|
1274
|
+
reply = await askArgus(systemArgusDeps(env), delegateStatusRequest(PROBE_REQUEST, 0), readTimeoutMs(0));
|
|
1275
|
+
}
|
|
1276
|
+
catch (error) {
|
|
1277
|
+
return {
|
|
1278
|
+
...base,
|
|
1279
|
+
ok: false,
|
|
1280
|
+
note: error instanceof ArgusUnreachableError
|
|
1281
|
+
? `Nothing is listening on ${socketPath} (${error.code}). argusd holds a Navarch worker's`
|
|
1282
|
+
+ " terminal, and opening Navarch starts it."
|
|
1283
|
+
: `The socket at ${socketPath} did not answer: ${error instanceof Error ? error.message : String(error)}.`,
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
if (servesDelegate(reply)) {
|
|
1287
|
+
return {
|
|
1288
|
+
...base,
|
|
1289
|
+
ok: true,
|
|
1290
|
+
note: `argusd is running at ${socketPath} and serves the delegate verb.`
|
|
1291
|
+
+ ` ${await targetSupportNote(systemArgusDeps(env))}`,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
...base,
|
|
1296
|
+
ok: false,
|
|
1297
|
+
note: "argusd is running and PREDATES the delegate verb, so a delegation to Navarch is refused"
|
|
1298
|
+
+ " (argusd-outdated) however new the app is. Reload the daemon — deliberately, because a reload"
|
|
1299
|
+
+ " ends every live worker session, so it is yours to time. Note that its own refusal may instead"
|
|
1300
|
+
+ " say the request is missing a board todo; it is not, and that message is the old daemon"
|
|
1301
|
+
+ " forwarding a verb it does not itself know.",
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Does this daemon read a target worker and a GitHub account? — one sentence
|
|
1306
|
+
* for the `argus-delegate` row.
|
|
1307
|
+
*
|
|
1308
|
+
* The probe is `delegate_candidates` with **no `directory`**, which is the
|
|
1309
|
+
* cheapest form of the question: a daemon that serves the op refuses
|
|
1310
|
+
* `bad-directory` (it validates exactly as `delegate` does), and one that does
|
|
1311
|
+
* not answers `unknown-op`. Either way nothing is read, nothing is written,
|
|
1312
|
+
* and no folder has to exist for the question to be askable — which matters,
|
|
1313
|
+
* because `doctor` is asked on machines with no project linked at all.
|
|
1314
|
+
*
|
|
1315
|
+
* A socket that breaks between the two reads says so rather than guessing: the
|
|
1316
|
+
* row above it has already reported the daemon is up, so silence here would
|
|
1317
|
+
* read as an answer.
|
|
1318
|
+
*/
|
|
1319
|
+
export async function targetSupportNote(argus) {
|
|
1320
|
+
let reply;
|
|
1321
|
+
try {
|
|
1322
|
+
reply = await askArgus(argus, delegateCandidatesRequest("", "claude"), readTimeoutMs(0));
|
|
1323
|
+
}
|
|
1324
|
+
catch (error) {
|
|
1325
|
+
return "Whether it also serves `delegate_candidates` and `gh_account` could not be read: "
|
|
1326
|
+
+ `${error instanceof Error ? error.message : String(error)}.`;
|
|
1327
|
+
}
|
|
1328
|
+
return candidatesSupport(reply) === "no-op"
|
|
1329
|
+
? "It PREDATES `delegate_candidates` and `gh_account` (argus #1402), so `--list --host navarch`"
|
|
1330
|
+
+ " falls back to reading `fleet_status` itself, and `--session` and `--gh-account` are refused"
|
|
1331
|
+
+ " `argusd-outdated` there — this daemon would ignore both rather than refuse them, and type"
|
|
1332
|
+
+ " the prompt into whichever worker its own find picked. Reloading argusd lifts that, and"
|
|
1333
|
+
+ " ends every live worker session."
|
|
1334
|
+
: "It also serves `delegate_candidates` and `gh_account` (argus #1402), so `--list --host navarch`"
|
|
1335
|
+
+ " is the daemon's own answer to its own find, and `--session` and `--gh-account` are honoured.";
|
|
1336
|
+
}
|
|
1012
1337
|
function hostCheck(id, app) {
|
|
1013
1338
|
const spec = HOSTS[id];
|
|
1014
1339
|
const unlocks = {
|
|
1015
1340
|
terminal: "`pharos delegate --host terminal` — typed into the Terminal tab that already runs the agent, or a new window.",
|
|
1016
1341
|
vscode: "`pharos delegate --host vscode` — through the Pharos bridge, into the session in VS Code.",
|
|
1017
1342
|
"vscode-insiders": "`pharos delegate --host vscode-insiders` — the same, in VS Code Insiders.",
|
|
1018
|
-
navarch: "`pharos delegate --host navarch` — a worker in the Argus app,
|
|
1343
|
+
navarch: "`pharos delegate --host navarch` — a worker in the Argus app, over argusd's own socket.",
|
|
1019
1344
|
};
|
|
1020
1345
|
// Only the VS Code flavours have an install line. Terminal ships with macOS
|
|
1021
1346
|
// and Navarch is distributed by hand, so a command would be a guess.
|
|
@@ -1036,7 +1361,12 @@ function hostCheck(id, app) {
|
|
|
1036
1361
|
? `Not found by bundle id ${spec.bundleId} — asked Spotlight, then looked at ${spec.candidates.join(" and ")}.`
|
|
1037
1362
|
: `Found by ${app.via === "spotlight" ? "Spotlight" : "its well-known path"} (bundle id ${spec.bundleId}).`
|
|
1038
1363
|
+ (id === "navarch"
|
|
1039
|
-
? "
|
|
1364
|
+
? " Delegating there goes over argusd's board socket rather than through the app:"
|
|
1365
|
+
+ " the daemon finds a worker on the folder and Navarch types into it."
|
|
1366
|
+
+ " **argusd outlives Navarch**, so this row saying the app is installed does not mean"
|
|
1367
|
+
+ " the running daemon serves the verb — an argusd started before it answers"
|
|
1368
|
+
+ " `argusd-outdated`, and the fix is a reload, which ends every live worker session"
|
|
1369
|
+
+ " and is yours to time."
|
|
1040
1370
|
: ""),
|
|
1041
1371
|
};
|
|
1042
1372
|
}
|
|
@@ -1063,22 +1393,35 @@ async function extensionChecks(host, env, shippedBridge) {
|
|
|
1063
1393
|
* the one this pharos ships stays `ok:true` with a note, because `ok:false`
|
|
1064
1394
|
* should mean "something you asked for does not work" rather than "there is a
|
|
1065
1395
|
* newer one" — otherwise every future bump nags on every machine.
|
|
1396
|
+
*
|
|
1397
|
+
* **This row reports; it no longer has to be acted on to be fixed.** Since
|
|
1398
|
+
* #1394 `pharos delegate --host <flavour>` installs or upgrades the bridge on
|
|
1399
|
+
* its way to opening a URI, so an absent or older one is put right by the
|
|
1400
|
+
* first delegation. `pharos setup --install vscode-bridge` stays the way to
|
|
1401
|
+
* do it ahead of time — and the note says so, because a row that names no fix
|
|
1402
|
+
* reads as a row nobody can act on.
|
|
1066
1403
|
*/
|
|
1067
1404
|
const bridgeStale = bridge !== undefined && !bridgeAtLeast(bridge.version, SESSION_BRIDGE_VERSION);
|
|
1405
|
+
/** What now fixes an absent or outdated bridge, in both the words a person needs. */
|
|
1406
|
+
const onTheWay = `\`pharos delegate --host ${host}\` installs it on its way to the editor, and `
|
|
1407
|
+
+ "`pharos setup --install vscode-bridge` does it ahead of time.";
|
|
1068
1408
|
const bridgeNote = bridge === undefined
|
|
1069
1409
|
? shippedBridge === null
|
|
1070
1410
|
? `${where} This pharos ships no bridge either (${bundledVsixPath(env)} is absent) — the ship todo packs it; until then \`--install vscode-bridge\` refuses by name.`
|
|
1071
|
-
: `${where} This pharos ships ${shippedBridge}
|
|
1411
|
+
: `${where} This pharos ships ${shippedBridge}. ${onTheWay}`
|
|
1072
1412
|
: bridgeStale
|
|
1073
1413
|
? `${where} Installed ${bridge.version}, which predates \`pharos delegate --session\` (${SESSION_BRIDGE_VERSION}). `
|
|
1074
1414
|
+ "It does not ignore the session — it refuses the whole URI, because it rejects any query key it does "
|
|
1075
1415
|
+ `not know — so picking a session in ${flavour} sends nothing. Delegating without --session still works. `
|
|
1076
|
-
+
|
|
1416
|
+
+ (shippedBridge === null || !bridgeAtLeast(shippedBridge, SESSION_BRIDGE_VERSION)
|
|
1417
|
+
? `\`pharos setup --install vscode-bridge\` replaces it${shippedBridge === null ? "" : ` with ${shippedBridge}`}.`
|
|
1418
|
+
: `${onTheWay} A ${flavour} window that was already open keeps ${bridge.version} until it reloads, `
|
|
1419
|
+
+ "so the first --session delegation after the replacement is refused once and says so.")
|
|
1077
1420
|
: shippedBridge === null
|
|
1078
1421
|
? `${where} Installed ${bridge.version}; this pharos ships no bridge to compare it with.`
|
|
1079
1422
|
: bridge.version === shippedBridge
|
|
1080
1423
|
? `${where} Installed ${bridge.version}, which is the one this pharos ships.`
|
|
1081
|
-
: `${where} Installed ${bridge.version}, but this pharos ships ${shippedBridge}. It serves every verb this pharos has; the URI contract may still have moved.
|
|
1424
|
+
: `${where} Installed ${bridge.version}, but this pharos ships ${shippedBridge}. It serves every verb this pharos has; the URI contract may still have moved. ${onTheWay}`;
|
|
1082
1425
|
return [
|
|
1083
1426
|
{
|
|
1084
1427
|
id: `${host}-claude-ext`,
|
|
@@ -1109,7 +1452,7 @@ async function extensionChecks(host, env, shippedBridge) {
|
|
|
1109
1452
|
name: `Pharos bridge in ${flavour}`,
|
|
1110
1453
|
tier: 2,
|
|
1111
1454
|
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.`,
|
|
1112
|
-
without: `Delegating to ${flavour} is refused by name (bridge-not-installed)
|
|
1455
|
+
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.`,
|
|
1113
1456
|
ok: bridge !== undefined && !bridgeStale,
|
|
1114
1457
|
version: bridge?.version ?? null,
|
|
1115
1458
|
path: bridge?.path ?? null,
|