@brainervirus/workit-core 0.8.7 → 0.8.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "private": false,
5
5
  "description": "Workit — workflow rails for agentic coding: specs, plans, YouTrack, CI-gated commits (shared core)",
6
6
  "keywords": [
@@ -3,11 +3,27 @@
3
3
  // return nonzero when a required registration/config check fails. `installer:
4
4
  // true` limits the enforced checks to the ones the installer just guaranteed, so
5
5
  // a dev/stub checkout without built assets does not fail a post-install run.
6
+ // CA-02: `cursor --stale` is the installer's self-heal pre-check — it exits 2
7
+ // when the doctor's stale_install check fails and 0 otherwise. A
8
+ // registry-unreachable comparison warns as registry_unreachable and is NOT
9
+ // stale (CA-04): no false stale_install and no install failure. Other check
10
+ // failures are left to the post-install gate.
6
11
  import { runDoctor } from "../src/core/doctor";
7
12
 
8
13
  const host = process.argv[2] === "cursor" ? "cursor" : "opencode";
14
+ const staleOnly = process.argv.includes("--stale");
9
15
  const report = runDoctor({ host, installer: true });
10
16
 
17
+ if (staleOnly) {
18
+ const stale = report.checks.find((c) => c.id === "stale_install");
19
+ if (stale?.status === "fail") {
20
+ process.stderr.write(`STALE: ${stale.detail}\n`);
21
+ if (stale.fix) process.stderr.write(` fix: ${stale.fix}\n`);
22
+ process.exit(2);
23
+ }
24
+ process.exit(0);
25
+ }
26
+
11
27
  if (report.exitCode !== 0) {
12
28
  for (const check of report.checks) {
13
29
  if (check.status === "fail") {
@@ -43,6 +43,22 @@ else
43
43
  ROOT="$LOCAL_ROOT"
44
44
  fi
45
45
 
46
+ # CA-02: self-heal pre-check. The doctor's stale_install check runs against the
47
+ # INSTALLED plugin dir (version/selectors vs the current runtime). Exit 2 means
48
+ # stale: the refresh below syncs the plugin dir from ROOT and the registration
49
+ # pass rewrites the workit MCP/hook entries to the canonical current selector.
50
+ # The check reads only workit-owned files, so a healthy install is byte-untouched;
51
+ # a registry-unreachable comparison reports registry_unreachable, never stale
52
+ # (CA-04) — no false stale_install and no install failure.
53
+ if bun "$ROOT/packages/workit-core/scripts/doctor-check.ts" cursor --stale; then
54
+ :
55
+ elif [ $? -eq 2 ]; then
56
+ echo "workit: stale Cursor plugin install detected — self-healing (refresh + canonical re-registration)" >&2
57
+ else
58
+ echo "FATAL: pre-install doctor probe failed" >&2
59
+ exit 1
60
+ fi
61
+
46
62
  chmod +x "$ROOT/packages/workit-core/scripts/sync-runtime.sh" "$ROOT/packages/workit-core/scripts/"*.sh
47
63
  # Prefer syncing from this ROOT (dev or freshly cloned share)
48
64
  export WORKFLOW_TOOLKIT_DEV="$ROOT"
@@ -15,4 +15,6 @@ disable-model-invocation: true
15
15
  7. After any `workflow_handoff_session` result—success, partial, or failure—end the originating turn immediately after one status message. Never create todos, execute the plan inline, modify files, retry handoff, or call another tool.
16
16
  8. A destination run that executes the plan must still end with `workflow_plan_complete` (or the CLI `workit flow complete`) once the SDD ledger is complete and repository verification passes, and never finish the run while the plan is still `active`.
17
17
 
18
+ Handoff titles continuation sessions `Workit: <slug>` (never `Continue <slug>`). OpenCode's native `Continue opencode -s <session-id>` epilogue is the valid manual recovery command when host selection is unavailable (`stage: "select"`): `selected: false` with a `sessionID` is a partial success — use `opencode -s <session-id>` to resume, not a Workit bug.
19
+
18
20
  Never emit a continuation prompt, use the clipboard, or ask the user to copy text. If selected, report the session ID only if the current session remains visible. If staying, report the seeded session ID. On failure, report `stage` and `error`; preserve any returned session for the session picker and never recreate it automatically. `todowrite` and `task` are unnecessary here.
@@ -31,13 +31,15 @@ For every plan task whose ID is absent from `completed_task_ids`:
31
31
  3. Use `task` with the built-in `explore` agent for read-only discovery when needed, then a fresh built-in `general` agent to implement from the brief. The parent remains coordinator-only.
32
32
  4. Require product changes to follow TDD: failing check first, minimal implementation, passing focused check.
33
33
  5. Create the review package with `workflow_sdd_review_package` using `confirmed: true`.
34
- 6. Dispatch separate `general` agents for spec-compliance review and code-quality review. **Blocking findings** (Critical, Important, or spec-compliance) may trigger at most **two** fix+re-review rounds per task. **Advisory** findings (Minor, style, YAGNI, taste) never pause the loop — append them to `<SDD_DIR>/advisories.md` with the task id.
34
+ 6. Dispatch separate `general` agents for spec-compliance review and code-quality review. **Blocking findings** (Critical, Important, or spec-compliance) may trigger at most **two** fix+re-review rounds per task. **Advisory** findings (Minor, style, YAGNI, taste) never pause the loop — append them with `workflow_sdd_append_advisory` (`--task <id> --text <text>`) using `confirmed: true`.
35
35
  7. Append the validated ledger line with `workflow_sdd_append_progress` using `confirmed: true`, then mark the task completed with `todowrite`.
36
36
 
37
37
  Each task lands exactly one contiguous non-empty commit range (`base..head`): fix rounds append commits to that range and never rewrite/amend an active review range; each progress line records the task's real base..head shas.
38
38
 
39
39
  Never redispatch completed task IDs. Pass task briefs and review diffs to agents; do not make them reparse the plan. Keep commits on the in-place feature/bugfix branch.
40
40
 
41
+ Delegated authority is direct-child-only: a worker is the session whose host `parentID` exactly equals the activating coordinator's recorded `coordinator_session_id`; missing, mismatched, or multi-owner lineage fails closed with `delegation_lineage_denied`, and nested `opencode` launches are denied during active delegated work. An authorized child receives only the compact worker contract — execute the supplied brief, follow TDD, land one contiguous non-empty commit range, report results — never coordinator guidance, `wk-implement`, or ledger management. Coordinator bookkeeping (briefs, review packages, progress, advisories via `workflow_sdd_*`) stays with the coordinator session.
42
+
41
43
  ## Final gate
42
44
 
43
45
  After all remaining tasks, dispatch a final full-branch code review, run `workflow_verify`, and report exact per-check results. Present the full `<SDD_DIR>/advisories.md` roll-up once, then use native `question` so the user can choose which advisory items to fix, discuss, or discard. Only then may advisory fixes run. Use `workflow_git_context` for the final commit preview and the `wk-commit` skill for any approved commit. If a tracked stash reference exists, preview reapplication with `question`, then call `workflow_branch_setup` with `confirmed: true` only after approval.
@@ -1,9 +1,10 @@
1
1
  // Shared offline doctor (DG-07/DG-08, CA-09). One host-neutral engine checks the
2
2
  // installed Workit surfaces — pins, versions, assets, launchers, runtimes,
3
3
  // utilities, registrations, config, workspace match, credential metadata, and
4
- // log writability — with no network access. Never reads credential values: only
5
- // existence, mode, and a placeholder flag are evaluated; token bytes never enter
6
- // the report or any log event.
4
+ // log writability — with no network access except the optional registry probe
5
+ // behind the stale-install comparison (CA-04), which fails open. Never reads
6
+ // credential values: only existence, mode, and a placeholder flag are evaluated;
7
+ // token bytes never enter the report or any log event.
7
8
  import { spawnSync } from "node:child_process";
8
9
  import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
9
10
  import os from "node:os";
@@ -11,7 +12,13 @@ import path from "node:path";
11
12
  import { SUPPORT_MATRIX } from "./support-matrix";
12
13
  import { EVENT } from "./boundary";
13
14
  import { getDiagnosticLogger, isConfigObject } from "./config";
14
- import { cursorHooksEntry, cursorMcpServerEntry, isWorkitPlugin } from "./registration";
15
+ import { packageRoot } from "./package-root";
16
+ import {
17
+ CURSOR_RUNTIME_PACKAGE,
18
+ cursorHooksEntry,
19
+ cursorMcpServerEntry,
20
+ isWorkitPlugin,
21
+ } from "./registration";
15
22
  import { resolveWorkspaceFrom } from "./workspaces";
16
23
  import { validateCursorSkills } from "./skill-manifests";
17
24
 
@@ -28,6 +35,8 @@ export type DoctorCheckId =
28
35
  | "launcher"
29
36
  | "utility"
30
37
  | "stale_pin"
38
+ | "stale_install"
39
+ | "registry_unreachable"
31
40
  | "duplicate_registration"
32
41
  | "malformed_config"
33
42
  | "workspace_mismatch"
@@ -46,12 +55,18 @@ export type DoctorCheck = {
46
55
 
47
56
  export type DoctorFix = { id: DoctorCheckId; fix: string };
48
57
 
49
- export type DoctorSummary = { passed: number; warned: number; failed: number; total: number };
58
+ export type DoctorSummary = {
59
+ passed: number;
60
+ warned: number;
61
+ failed: number;
62
+ total: number;
63
+ };
50
64
 
51
65
  export type DoctorReport = {
52
66
  ok: boolean;
53
67
  exitCode: number;
54
- offline: true;
68
+ /** False when the doctor consulted the npm registry (local-dist version probe). */
69
+ offline: boolean;
55
70
  host: DoctorHost;
56
71
  checked_at: string;
57
72
  summary: DoctorSummary;
@@ -534,7 +549,11 @@ const checkLauncher = (res: Resolved): DoctorCheck => {
534
549
  detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping non-Cursor launcher checks",
535
550
  };
536
551
  }
537
- return { id: "launcher", status: "pass", detail: `${res.host} launcher/hook entries present` };
552
+ return {
553
+ id: "launcher",
554
+ status: "pass",
555
+ detail: `${res.host} launcher/hook entries present`,
556
+ };
538
557
  };
539
558
 
540
559
  const checkUtility = (res: Resolved): DoctorCheck => {
@@ -555,7 +574,11 @@ const checkUtility = (res: Resolved): DoctorCheck => {
555
574
  fix: "Install util-linux (flock)",
556
575
  };
557
576
  }
558
- return { id: "utility", status: "pass", detail: "git (and flock where required) on PATH" };
577
+ return {
578
+ id: "utility",
579
+ status: "pass",
580
+ detail: "git (and flock where required) on PATH",
581
+ };
559
582
  };
560
583
 
561
584
  const staleEntry = (entry: string): "ok" | "stale" | "missing-file" => {
@@ -579,7 +602,11 @@ const checkStalePin = (res: Resolved): DoctorCheck => {
579
602
  };
580
603
  }
581
604
  if (!existsSync(res.opencodeConfig)) {
582
- return { id: "stale_pin", status: "pass", detail: "no opencode config — not registered" };
605
+ return {
606
+ id: "stale_pin",
607
+ status: "pass",
608
+ detail: "no opencode config — not registered",
609
+ };
583
610
  }
584
611
  const cfg = readJson(res.opencodeConfig);
585
612
  const entries = pluginEntries(cfg);
@@ -611,6 +638,169 @@ const checkStalePin = (res: Resolved): DoctorCheck => {
611
638
  return { id: "stale_pin", status: "pass", detail: "opencode pin resolves" };
612
639
  };
613
640
 
641
+ // Cursor plugin stale-install detection (CA-01/CA-04). Compares the installed
642
+ // `~/.cursor/plugins/local/workit` against the current runtime source of truth
643
+ // (`CURSOR_RUNTIME_PACKAGE` + the canonical entry builders from registration.ts,
644
+ // D-02) and surfaces a structured `stale_install` finding with the exact repair
645
+ // step. A stale install means the plugin's sessionStart hook and MCP entry run
646
+ // an ancient runtime that no longer auto-registers the workflow features.
647
+ //
648
+ // Three independent staleness signals are read from the installed plugin dir:
649
+ // 1. Legacy selectors (offline evidence, checked first): the plugin's own
650
+ // mcp.json (`--package=...workit-cursor@<exact>` pins other than the
651
+ // canonical `@latest`) and hooks-cursor.json sessionStart command.
652
+ // 2. Installed version behind the current source release (offline): the dev
653
+ // checkout's workit-cursor version (or the doctor's own package version)
654
+ // is the release train the doctor ships with.
655
+ // 3. Local-dist installs behind the published runtime (needs the registry):
656
+ // the probe is the sole network read in the doctor. A SUCCEEDED probe with
657
+ // the installed version genuinely behind the published runtime is a hard
658
+ // `stale_install` fail (the installer self-heals on it); an unreachable
659
+ // registry fails open (CA-04) as a `registry_unreachable` warning, never a
660
+ // false `stale_install` and never a hard doctor failure. Canonical
661
+ // `@latest` installs skip the probe entirely: the selector resolves fresh
662
+ // at launch, so the installed package.json version is metadata, not a
663
+ // freshness signal.
664
+ // Test seams (no spawns on the canonical path): WORKIT_DOCTOR_STALE_REGISTRY_VERSION
665
+ // short-circuits the probe with the resolved latest version; the optional
666
+ // WORKIT_DOCTOR_STALE_REGISTRY_CMD replaces the `npm view` binary (a
667
+ // nonexistent path deterministically exercises the fail-open path).
668
+ const pluginMcpSelectors = (res: Resolved): string[] | null => {
669
+ const p = path.join(res.cursorPluginDir, "mcp.json");
670
+ if (!existsSync(p)) return null;
671
+ const cfg = readJson(p);
672
+ const server = cfg?.mcpServers?.workit;
673
+ if (!server || typeof server !== "object" || Array.isArray(server)) return null;
674
+ const args = server.args;
675
+ if (!Array.isArray(args)) return null;
676
+ return args.map(String).filter((a) => a.startsWith("--package="));
677
+ };
678
+
679
+ const registryLatestVersion = (res: Resolved): string | null => {
680
+ const seam = res.env.WORKIT_DOCTOR_STALE_REGISTRY_VERSION;
681
+ if (typeof seam === "string" && seam) {
682
+ return /^\d+(?:\.\d+){1,2}$/.test(seam) ? seam : null;
683
+ }
684
+ const cmd =
685
+ res.env.WORKIT_DOCTOR_STALE_REGISTRY_CMD ?? (commandOnPath("npm", res.env) ? "npm" : null);
686
+ if (!cmd) return null;
687
+ try {
688
+ const r = spawnSync(cmd, ["view", CURSOR_RUNTIME_PACKAGE, "version"], {
689
+ encoding: "utf8",
690
+ timeout: 20_000,
691
+ env: res.env,
692
+ });
693
+ const v = (r.stdout ?? "").trim().split(/\s+/)[0];
694
+ return r.status === 0 && /^\d+(?:\.\d+){1,2}$/.test(v) ? v : null;
695
+ } catch {
696
+ return null;
697
+ }
698
+ };
699
+
700
+ const installedPluginVersion = (res: Resolved): string | null => {
701
+ const pkg = readJson(path.join(res.cursorPluginDir, "package.json"));
702
+ return typeof pkg?.version === "string" && pkg.version ? pkg.version : null;
703
+ };
704
+
705
+ const checkStaleInstall = (res: Resolved): DoctorCheck & { registryProbed?: boolean } => {
706
+ if (res.host !== "cursor" && res.host !== "cli") {
707
+ return {
708
+ id: "stale_install",
709
+ status: "pass",
710
+ detail: "cursor plugin not inspected on the opencode host",
711
+ };
712
+ }
713
+ const canonicalMcp = cursorMcpServerEntry("").args.find((a) => a.startsWith("--package="));
714
+ const mcpSelectors = pluginMcpSelectors(res);
715
+ const legacyPin = (mcpSelectors ?? []).find(
716
+ (s) => s !== canonicalMcp && s.includes("@brainervirus/workit-cursor@"),
717
+ );
718
+ if (legacyPin) {
719
+ return {
720
+ id: "stale_install",
721
+ status: "fail",
722
+ detail: `stale_install: plugin mcp.json pins a legacy selector ${legacyPin} (canonical: ${canonicalMcp})`,
723
+ fix: "Re-run install-cursor-plugin.sh — it rewrites the workit MCP entry to the canonical @latest selector",
724
+ };
725
+ }
726
+ const hooksFile = path.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
727
+ const hookCmd =
728
+ (readJson(hooksFile)?.hooks?.sessionStart?.[0]?.command as string | undefined) ?? null;
729
+ const canonicalHook = canonicalCursorHook;
730
+ const staleHook =
731
+ hookCmd !== null &&
732
+ hookCmd !== canonicalHook &&
733
+ !hookCmd.startsWith("node ") &&
734
+ hookCmd.includes("@brainervirus/workit-cursor@");
735
+ if (staleHook) {
736
+ return {
737
+ id: "stale_install",
738
+ status: "fail",
739
+ detail: `stale_install: sessionStart hook runs a legacy selector (canonical: ${canonicalHook})`,
740
+ fix: "Re-run install-cursor-plugin.sh — it rewrites the sessionStart hook to the canonical @latest selector",
741
+ };
742
+ }
743
+ const installed = installedPluginVersion(res);
744
+ const source = res.dev
745
+ ? ((readJson(path.join(res.dev, "packages/workit-cursor/package.json"))?.version as
746
+ | string
747
+ | undefined) ?? null)
748
+ : ((readJson(path.join(packageRoot(), "package.json"))?.version as string | undefined) ?? null);
749
+ // A local-dist install (node entry) runs the installed dir's own dist, so its
750
+ // version is comparable against the current runtime (and the published one
751
+ // below). Canonical @latest installs resolve fresh at launch — the installed
752
+ // package.json version is metadata, not a freshness signal (CA-04), so they
753
+ // skip both version comparisons and never fail stale_install on metadata.
754
+ const localDist = mcpSelectors === null && hookCmd !== null && hookCmd.startsWith("node ");
755
+ if (localDist && installed !== null && source !== null && !semverAtLeast(installed, source)) {
756
+ return {
757
+ id: "stale_install",
758
+ status: "fail",
759
+ detail: `stale_install: installed workit-cursor ${installed} is behind the current runtime ${source}`,
760
+ fix: "Re-run install-cursor-plugin.sh — it refreshes the plugin directory and rewrites the workit MCP/hook entries",
761
+ };
762
+ }
763
+ if (installed !== null && localDist) {
764
+ const expected = registryLatestVersion(res);
765
+ if (expected === null) {
766
+ return {
767
+ id: "registry_unreachable",
768
+ status: "warn",
769
+ registryProbed: true,
770
+ detail:
771
+ "registry_unreachable: cannot compare installed workit-cursor against the published runtime",
772
+ fix: "Retry when the npm registry is reachable, or re-run install-cursor-plugin.sh to refresh the install",
773
+ };
774
+ }
775
+ if (!semverAtLeast(installed, expected)) {
776
+ // The probe SUCCEEDED and the installed version is genuinely behind the
777
+ // published runtime: report a hard fail so `doctor-check.ts cursor --stale`
778
+ // exits 2 and the installer self-heals the local-dist install.
779
+ return {
780
+ id: "stale_install",
781
+ status: "fail",
782
+ registryProbed: true,
783
+ detail: `stale_install: local-dist workit-cursor ${installed} is behind the published runtime ${expected}`,
784
+ fix: "Re-run install-cursor-plugin.sh — it refreshes the plugin directory with the current build",
785
+ };
786
+ }
787
+ return {
788
+ id: "stale_install",
789
+ status: "pass",
790
+ registryProbed: true,
791
+ detail: `installed local-dist workit-cursor ${installed} matches the published runtime ${expected}`,
792
+ };
793
+ }
794
+ return {
795
+ id: "stale_install",
796
+ status: "pass",
797
+ detail:
798
+ installed === null
799
+ ? "installed workit-cursor selectors are canonical"
800
+ : `installed workit-cursor ${installed} is metadata on the canonical @latest install (fresh at launch)`,
801
+ };
802
+ };
803
+
614
804
  const checkDuplicateRegistration = (res: Resolved): DoctorCheck => {
615
805
  const problems: string[] = [];
616
806
  const opencodeHost = res.host !== "cursor";
@@ -659,7 +849,11 @@ const checkDuplicateRegistration = (res: Resolved): DoctorCheck => {
659
849
  }
660
850
  }
661
851
  if (problems.length === 0) {
662
- return { id: "duplicate_registration", status: "pass", detail: "no duplicate registrations" };
852
+ return {
853
+ id: "duplicate_registration",
854
+ status: "pass",
855
+ detail: "no duplicate registrations",
856
+ };
663
857
  }
664
858
  return {
665
859
  id: "duplicate_registration",
@@ -682,7 +876,11 @@ const checkMalformedConfig = (res: Resolved): DoctorCheck => {
682
876
  if (cursorHost && existsSync(res.cursorMcp)) files.push(res.cursorMcp);
683
877
  const bad = files.filter((p) => !parsesAsConfigObject(p));
684
878
  if (bad.length === 0)
685
- return { id: "malformed_config", status: "pass", detail: "config files parse" };
879
+ return {
880
+ id: "malformed_config",
881
+ status: "pass",
882
+ detail: "config files parse",
883
+ };
686
884
  return {
687
885
  id: "malformed_config",
688
886
  status: "fail",
@@ -694,10 +892,18 @@ const checkMalformedConfig = (res: Resolved): DoctorCheck => {
694
892
  const checkWorkspaceMismatch = (res: Resolved): DoctorCheck => {
695
893
  const file = path.join(res.configDir, "workspaces.json");
696
894
  if (!existsSync(file))
697
- return { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
895
+ return {
896
+ id: "workspace_mismatch",
897
+ status: "pass",
898
+ detail: "no workspaces configured",
899
+ };
698
900
  const ws = readJson(file);
699
901
  if (!Array.isArray(ws?.workspaces)) {
700
- return { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
902
+ return {
903
+ id: "workspace_mismatch",
904
+ status: "pass",
905
+ detail: "no workspaces configured",
906
+ };
701
907
  }
702
908
  const match = resolveWorkspaceFrom(res.cwd, res.configDir);
703
909
  if (match)
@@ -748,7 +954,11 @@ const checkCredentialMetadata = (res: Resolved): DoctorCheck => {
748
954
  }
749
955
 
750
956
  if (tokenPaths.length === 0) {
751
- return { id: "credential_metadata", status: "pass", detail: "no credentials configured" };
957
+ return {
958
+ id: "credential_metadata",
959
+ status: "pass",
960
+ detail: "no credentials configured",
961
+ };
752
962
  }
753
963
  const problems: string[] = [];
754
964
  for (const raw of tokenPaths) {
@@ -786,7 +996,11 @@ const checkLogWritable = (res: Resolved): DoctorCheck => {
786
996
  try {
787
997
  mkdirSync(logsDir, { recursive: true, mode: 0o700 });
788
998
  writeFileSync(probe, '{"probe":true}\n', { mode: 0o600 });
789
- return { id: "log_writable", status: "pass", detail: "log directory writable" };
999
+ return {
1000
+ id: "log_writable",
1001
+ status: "pass",
1002
+ detail: "log directory writable",
1003
+ };
790
1004
  } catch (err) {
791
1005
  return {
792
1006
  id: "log_writable",
@@ -810,6 +1024,7 @@ const RUN_CHECKS: Array<(res: Resolved) => DoctorCheck> = [
810
1024
  checkLauncher,
811
1025
  checkUtility,
812
1026
  checkStalePin,
1027
+ checkStaleInstall,
813
1028
  checkDuplicateRegistration,
814
1029
  checkMalformedConfig,
815
1030
  checkWorkspaceMismatch,
@@ -827,6 +1042,7 @@ const INSTALLER_REQUIRED = new Set<DoctorCheckId>([
827
1042
  "launcher",
828
1043
  "utility",
829
1044
  "stale_pin",
1045
+ "stale_install",
830
1046
  "duplicate_registration",
831
1047
  "malformed_config",
832
1048
  ]);
@@ -837,7 +1053,11 @@ export const runDoctor = (options: DoctorOptions = {}): DoctorReport => {
837
1053
  const checks = res.installer
838
1054
  ? raw.map((c) =>
839
1055
  c.status === "fail" && !INSTALLER_REQUIRED.has(c.id)
840
- ? { ...c, status: "warn" as const, detail: `${c.detail} (not enforced by installer)` }
1056
+ ? {
1057
+ ...c,
1058
+ status: "warn" as const,
1059
+ detail: `${c.detail} (not enforced by installer)`,
1060
+ }
841
1061
  : c,
842
1062
  )
843
1063
  : raw;
@@ -846,10 +1066,11 @@ export const runDoctor = (options: DoctorOptions = {}): DoctorReport => {
846
1066
  const warned = checks.filter((c) => c.status === "warn").length;
847
1067
  const passed = checks.filter((c) => c.status === "pass").length;
848
1068
  const exitCode = failed > 0 ? 1 : 0;
1069
+ const offline = !raw.some((c) => "registryProbed" in c && c.registryProbed);
849
1070
  const report: DoctorReport = {
850
1071
  ok: failed === 0,
851
1072
  exitCode,
852
- offline: true,
1073
+ offline,
853
1074
  host: res.host,
854
1075
  checked_at: new Date().toISOString(),
855
1076
  summary: { passed, warned, failed, total: checks.length },
@@ -862,7 +1083,7 @@ export const runDoctor = (options: DoctorOptions = {}): DoctorReport => {
862
1083
  getDiagnosticLogger()?.info(EVENT.doctor, {
863
1084
  host: res.host,
864
1085
  exit_code: exitCode,
865
- offline: true,
1086
+ offline,
866
1087
  failed: checks.filter((c) => c.status === "fail").map((c) => c.id),
867
1088
  total: checks.length,
868
1089
  });