@tpsdev-ai/flair 0.49.0 → 0.51.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 (39) hide show
  1. package/README.md +15 -4
  2. package/dist/bridges/runtime/roundtrip.js +91 -2
  3. package/dist/build-info.json +3 -3
  4. package/dist/cli.js +565 -171
  5. package/dist/deploy.js +20 -3
  6. package/dist/doctor-client.js +62 -34
  7. package/dist/federation/scheduler.js +24 -3
  8. package/dist/hook-install.js +45 -13
  9. package/dist/install/clients.js +28 -17
  10. package/dist/lib/doctor-run.js +481 -0
  11. package/dist/lib/launchd-management.js +7 -26
  12. package/dist/lib/scheduler-platform.js +132 -10
  13. package/dist/lib/scratch-owner.js +49 -0
  14. package/dist/rem/scheduler.js +23 -5
  15. package/dist/resources/Federation.js +42 -20
  16. package/dist/resources/MemoryBootstrap.js +8 -4
  17. package/dist/resources/RecordUsage.js +13 -6
  18. package/dist/resources/SemanticSearch.js +8 -1
  19. package/dist/resources/federation-classify.js +90 -0
  20. package/dist/resources/health.js +51 -7
  21. package/dist/resources/mcp-tools.js +10 -6
  22. package/dist/resources/search-readiness.js +123 -0
  23. package/dist/resources/semantic-retrieval-core.js +48 -21
  24. package/dist/resources/sort-comparators.js +45 -0
  25. package/dist/resources/usage-ids.js +63 -0
  26. package/dist/src/lib/scheduler-platform.js +132 -10
  27. package/dist/src/rem/scheduler.js +23 -5
  28. package/docs/auth.md +5 -0
  29. package/docs/deepseek-harness.md +1 -1
  30. package/docs/federation.md +11 -0
  31. package/docs/hosted-on-fabric.md +2 -0
  32. package/docs/integrations.md +53 -1
  33. package/docs/mcp-clients.md +67 -15
  34. package/docs/quickstart-fabric.md +1 -1
  35. package/docs/supply-chain-policy.md +1 -1
  36. package/docs/troubleshooting.md +25 -0
  37. package/docs/upgrade.md +17 -1
  38. package/package.json +3 -3
  39. package/schemas/federation.graphql +1 -1
@@ -0,0 +1,481 @@
1
+ /**
2
+ * doctor-run.ts — the enumerable install-health runner (flair#1439).
3
+ *
4
+ * `flair doctor` and `flair upgrade` used to keep two definitions of
5
+ * "healthy". Upgrade's success line (`renderVerifiedSummary`) could only
6
+ * qualify a ✅ by the facts in its signature — version plus launchd — so
7
+ * every other doctor check was invisible and rendered green. The previous
8
+ * fix special-cased one known-unhealthy form (launchd detach). That is a
9
+ * blacklist: the next unmeasured fact (a Codex SessionStart hook 0.49.0
10
+ * never wrote) printed the same unqualified success marker.
11
+ *
12
+ * This module is the single entry point that runs the named install-health
13
+ * checks. Adding a check to `DOCTOR_CHECK_IDS` automatically widens what
14
+ * upgrade claims. Success is asserted POSITIVELY: every catalog id must
15
+ * have been executed and none may have failed. An `unrun` member is never
16
+ * treated as a pass — if a reachable state cannot produce "unrun ⇒ no ✅",
17
+ * the enumeration is decorative and we have rebuilt the same guard.
18
+ *
19
+ * Skip (N/A) is not unrun. Linux has no launchd; a machine without Codex
20
+ * does not owe a Codex hook. Those checks execute and return `skip`.
21
+ */
22
+ import { existsSync, readdirSync } from "node:fs";
23
+ import { checkClaudeMdBootstrap, checkSessionStartHook, effectiveFlairUrl, hookCommandIsSilenced, isFlairHookCommand, partitionKeyIds, planAgentIterations, readClientMcpBlock, } from "../doctor-client.js";
24
+ import { hookInstallHint, hookSettingsPath, installHook, SUPPORTED_HARNESSES, } from "../hook-install.js";
25
+ import { isDetached, renderDetachedWarning, } from "./launchd-management.js";
26
+ /**
27
+ * The catalog. This is the contract upgrade asserts against.
28
+ *
29
+ * A new doctor install-health check is added HERE (id + runner). An id
30
+ * without a runner is emitted as `unrun` and blocks the success marker —
31
+ * that is the point, not a fallback.
32
+ *
33
+ * Flint's six named client-integration checks plus launchd (the previous
34
+ * special-case, now a catalog member rather than a side channel).
35
+ */
36
+ export const DOCTOR_CHECK_IDS = [
37
+ "mcp-block",
38
+ "flair-url",
39
+ "claude-md",
40
+ "session-start-hook",
41
+ "verified-read",
42
+ "keys-prune",
43
+ "launchd-management",
44
+ ];
45
+ const MCP_CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "antigravity"];
46
+ function result(id, label, status, extra = {}) {
47
+ return { id, label, status, ...extra };
48
+ }
49
+ function runMcpBlock(ctx) {
50
+ const id = "mcp-block";
51
+ const label = "MCP server block";
52
+ const mcp = ctx.detectedClientIds.filter((c) => MCP_CLIENT_IDS.includes(c));
53
+ if (mcp.length === 0) {
54
+ return result(id, label, "skip", { detail: "no MCP client detected" });
55
+ }
56
+ const missing = [];
57
+ for (const clientId of mcp) {
58
+ const block = readClientMcpBlock(clientId, ctx.homeDir);
59
+ if (!block.present)
60
+ missing.push(`${clientId} (${block.configPath})`);
61
+ }
62
+ if (missing.length > 0) {
63
+ return result(id, label, "fail", {
64
+ detail: `no Flair MCP server configured: ${missing.join(", ")}`,
65
+ remedy: "flair doctor --fix",
66
+ });
67
+ }
68
+ return result(id, label, "pass", { detail: `configured for ${mcp.join(", ")}` });
69
+ }
70
+ function runFlairUrl(ctx) {
71
+ const id = "flair-url";
72
+ const label = "FLAIR_URL";
73
+ const mcp = ctx.detectedClientIds.filter((c) => MCP_CLIENT_IDS.includes(c));
74
+ if (mcp.length === 0) {
75
+ return result(id, label, "skip", { detail: "no MCP client detected" });
76
+ }
77
+ // Presence of a working URL (explicit or client-defaulted) is the check.
78
+ // Unreachable is a doctor warn, not an install-health failure — the
79
+ // instance probe already covered liveness on the upgrade path.
80
+ const present = mcp.filter((clientId) => readClientMcpBlock(clientId, ctx.homeDir).present);
81
+ if (present.length === 0) {
82
+ return result(id, label, "skip", { detail: "no MCP block present to take a URL from" });
83
+ }
84
+ const urls = present.map((clientId) => {
85
+ const block = readClientMcpBlock(clientId, ctx.homeDir);
86
+ const eff = effectiveFlairUrl(block);
87
+ return `${clientId}=${eff.url}${eff.defaulted ? " (client default)" : ""}`;
88
+ });
89
+ return result(id, label, "pass", { detail: urls.join(", ") });
90
+ }
91
+ function runClaudeMd(ctx) {
92
+ const id = "claude-md";
93
+ const label = "CLAUDE.md bootstrap";
94
+ if (!ctx.detectedClientIds.includes("claude-code")) {
95
+ return result(id, label, "skip", { detail: "Claude Code not detected" });
96
+ }
97
+ const check = checkClaudeMdBootstrap(ctx.cwd, ctx.homeDir);
98
+ if (!check.present) {
99
+ return result(id, label, "fail", {
100
+ detail: "bootstrap instruction not found",
101
+ remedy: "flair doctor --fix",
102
+ });
103
+ }
104
+ return result(id, label, "pass", { detail: check.path ?? undefined });
105
+ }
106
+ function runSessionStartHook(ctx) {
107
+ const id = "session-start-hook";
108
+ const label = "SessionStart hook";
109
+ const harnesses = SUPPORTED_HARNESSES.filter((h) => ctx.detectedClientIds.includes(h));
110
+ if (harnesses.length === 0) {
111
+ return result(id, label, "skip", { detail: "no hook-capable client detected" });
112
+ }
113
+ const missing = [];
114
+ const loud = [];
115
+ for (const harness of harnesses) {
116
+ const path = hookSettingsPath(ctx.homeDir, harness);
117
+ const hook = checkSessionStartHook(ctx.homeDir, path);
118
+ if (!hook.present) {
119
+ missing.push(harness);
120
+ continue;
121
+ }
122
+ if (hook.command && isFlairHookCommand(hook.command) && !hookCommandIsSilenced(hook.command)) {
123
+ loud.push(harness);
124
+ }
125
+ }
126
+ if (missing.length > 0) {
127
+ const harness = missing[0];
128
+ const path = hookSettingsPath(ctx.homeDir, harness);
129
+ const detail = missing.length === 1
130
+ ? `SessionStart hook (${harness}): not found in ${path}`
131
+ : `SessionStart hook: not found for ${missing.join(", ")} (${missing.map((h) => hookSettingsPath(ctx.homeDir, h)).join("; ")})`;
132
+ return result(id, label, "fail", {
133
+ detail,
134
+ remedy: missing.map((h) => hookInstallHint(h)).join(" ; "),
135
+ missingHarnesses: missing,
136
+ });
137
+ }
138
+ if (loud.length > 0) {
139
+ const harness = loud[0];
140
+ return result(id, label, "fail", {
141
+ detail: `SessionStart hook (${harness}): a failure would print an error on every session`,
142
+ remedy: hookInstallHint(harness),
143
+ });
144
+ }
145
+ return result(id, label, "pass", {
146
+ detail: `wired for ${harnesses.join(", ")}`,
147
+ });
148
+ }
149
+ function runVerifiedRead(ctx) {
150
+ const id = "verified-read";
151
+ const label = "per-agent verified-read";
152
+ if (ctx.keyAgentIds === undefined) {
153
+ return result(id, label, "skip", { detail: "no keys enumeration in context" });
154
+ }
155
+ const plan = planAgentIterations(ctx.keyAgentIds, ctx.agentFlag);
156
+ if (plan.length === 0) {
157
+ return result(id, label, "skip", { detail: "no agents to iterate" });
158
+ }
159
+ return result(id, label, "pass", { detail: `iterate ${plan.join(", ")}` });
160
+ }
161
+ function runKeysPrune(ctx) {
162
+ const id = "keys-prune";
163
+ const label = "keys prune classification";
164
+ if (!ctx.keysDir) {
165
+ return result(id, label, "skip", { detail: "no keys dir in context" });
166
+ }
167
+ if (!existsSync(ctx.keysDir)) {
168
+ return result(id, label, "skip", { detail: "keys dir absent" });
169
+ }
170
+ const ids = readdirSync(ctx.keysDir)
171
+ .filter((f) => f.endsWith(".key"))
172
+ .map((f) => f.replace(/\.key$/, ""));
173
+ const { agentKeyIds, nodeKeyIds } = partitionKeyIds(ids, ctx.keysDir);
174
+ return result(id, label, "pass", {
175
+ detail: `classified ${agentKeyIds.length} agent / ${nodeKeyIds.length} node key(s)`,
176
+ });
177
+ }
178
+ function runLaunchdManagement(ctx) {
179
+ const id = "launchd-management";
180
+ const label = "launchd management";
181
+ if (!ctx.launchd) {
182
+ return result(id, label, "skip", { detail: "launchd not observed" });
183
+ }
184
+ const m = ctx.launchd;
185
+ if (isDetached(m)) {
186
+ return result(id, label, "fail", {
187
+ detail: m.detail,
188
+ remedy: m.remedy?.join(" && "),
189
+ launchd: m,
190
+ });
191
+ }
192
+ if (m.state === "not-applicable" || m.state === "no-service") {
193
+ return result(id, label, "skip", { detail: m.detail, launchd: m });
194
+ }
195
+ return result(id, label, "pass", { detail: m.detail, launchd: m });
196
+ }
197
+ /** Default implementations. An id in DOCTOR_CHECK_IDS missing from this
198
+ * list is emitted as `unrun` — that is a reachable incomplete state. */
199
+ export const DOCTOR_CHECKS = [
200
+ { id: "mcp-block", label: "MCP server block", run: runMcpBlock },
201
+ { id: "flair-url", label: "FLAIR_URL", run: runFlairUrl },
202
+ { id: "claude-md", label: "CLAUDE.md bootstrap", run: runClaudeMd },
203
+ { id: "session-start-hook", label: "SessionStart hook", run: runSessionStartHook },
204
+ { id: "verified-read", label: "per-agent verified-read", run: runVerifiedRead },
205
+ { id: "keys-prune", label: "keys prune classification", run: runKeysPrune },
206
+ { id: "launchd-management", label: "launchd management", run: runLaunchdManagement },
207
+ ];
208
+ /**
209
+ * Run every catalog id. Missing implementations and thrown runners become
210
+ * `unrun`, never `pass`.
211
+ */
212
+ export function runDoctorChecks(ctx, opts = {}) {
213
+ const catalogIds = opts.catalogIds ?? DOCTOR_CHECK_IDS;
214
+ const defs = new Map((opts.checks ?? DOCTOR_CHECKS).map((c) => [c.id, c]));
215
+ const results = [];
216
+ for (const id of catalogIds) {
217
+ const stub = opts.stubs?.[id];
218
+ const def = defs.get(id);
219
+ if (stub) {
220
+ try {
221
+ results.push(normalizeResult(id, def?.label ?? id, stub(ctx)));
222
+ }
223
+ catch {
224
+ results.push(unrunResult(id, def?.label ?? id, "check threw before returning"));
225
+ }
226
+ continue;
227
+ }
228
+ if (!def) {
229
+ results.push(unrunResult(id, id, "check was not executed — no runner registered"));
230
+ continue;
231
+ }
232
+ try {
233
+ results.push(normalizeResult(id, def.label, def.run(ctx)));
234
+ }
235
+ catch {
236
+ results.push(unrunResult(id, def.label, "check threw before returning"));
237
+ }
238
+ }
239
+ return summarizeDoctorRunResults(results, catalogIds);
240
+ }
241
+ function normalizeResult(id, label, r) {
242
+ return { ...r, id, label: r.label || label };
243
+ }
244
+ function unrunResult(id, label, detail) {
245
+ return { id, label, status: "unrun", detail };
246
+ }
247
+ /**
248
+ * Positive assertion over an already-collected result list. Any catalog id
249
+ * absent from `results` is filled in as `unrun` so a partial list cannot
250
+ * look complete.
251
+ */
252
+ export function summarizeDoctorRunResults(results, catalogIds = DOCTOR_CHECK_IDS) {
253
+ const byId = new Map(results.map((r) => [r.id, r]));
254
+ const complete = catalogIds.map((id) => byId.get(id) ?? unrunResult(id, id, "check was not executed"));
255
+ const incomplete = complete.some((r) => r.status === "unrun");
256
+ const failed = complete.some((r) => r.status === "fail");
257
+ return {
258
+ results: complete,
259
+ healthy: catalogIds.length > 0 && !incomplete && !failed,
260
+ incomplete,
261
+ };
262
+ }
263
+ /**
264
+ * The final status line of a successful `flair upgrade`.
265
+ *
266
+ * Success is asserted from the enumerated doctor run — never from "facts I
267
+ * gathered, minus known-bad cases." The unqualified `✅ verified: healthy`
268
+ * marker is emitted only when `run.healthy` is true. An unrun member, a
269
+ * failed member, or an empty catalog all withhold it.
270
+ */
271
+ export function renderVerifiedSummary(version, run, opts) {
272
+ // `authenticated` is a probe fact, not a doctor-check special case. The
273
+ // healthy-unverified upgrade branch could not read /HealthDetail; claiming
274
+ // "authenticated" there contradicts the next line. Default stays true so
275
+ // the ok path is unchanged.
276
+ const authed = opts?.authenticated !== false;
277
+ const facts = `healthy${authed ? ", authenticated" : ""}${version ? `, running ${version}` : ""}`;
278
+ if (run.healthy) {
279
+ return { degraded: false, lines: [`✅ verified: ${facts}`] };
280
+ }
281
+ const lines = [];
282
+ const launchdFail = run.results.find((r) => r.id === "launchd-management" && r.status === "fail");
283
+ if (launchdFail?.launchd && isDetached(launchdFail.launchd)) {
284
+ lines.push(...renderDetachedWarning(launchdFail.launchd, `upgrade landed (${facts}) but the instance is NOT running under launchd.`));
285
+ }
286
+ else if (run.incomplete) {
287
+ const unrun = run.results.filter((r) => r.status === "unrun");
288
+ lines.push(`⚠️ upgrade landed (${facts}) but install checks are incomplete — ${unrun.length} check(s) were not run.`);
289
+ }
290
+ else {
291
+ const failed = run.results.filter((r) => r.status === "fail");
292
+ lines.push(`⚠️ upgrade landed (${facts}) but doctor found ${failed.length} issue${failed.length === 1 ? "" : "s"}.`);
293
+ }
294
+ for (const r of run.results) {
295
+ if (r.status === "unrun") {
296
+ lines.push(` UNRUN: ${r.label} — not yet checked (cannot treat as passing)`);
297
+ }
298
+ else if (r.status === "fail" && r.id !== "launchd-management") {
299
+ lines.push(` ✗ ${r.detail ?? r.label}`);
300
+ if (r.remedy)
301
+ lines.push(` Fix: ${r.remedy}`);
302
+ }
303
+ }
304
+ return { degraded: true, lines };
305
+ }
306
+ /**
307
+ * Whether upgrade may write a SessionStart hook. Installing something that
308
+ * executes at every session start is consent-bearing — the flag is the
309
+ * documented non-interactive consent; a TTY prompt is the interactive one.
310
+ * Silence (no flag, no TTY) never writes.
311
+ */
312
+ export function resolveHookInstallConsent(opts) {
313
+ if (opts.installHooksFlag)
314
+ return "install";
315
+ if (opts.interactive && opts.promptAccepted === undefined)
316
+ return "prompt";
317
+ if (opts.interactive && opts.promptAccepted)
318
+ return "install";
319
+ if (opts.interactive)
320
+ return "skip-declined";
321
+ return "skip-noninteractive";
322
+ }
323
+ /**
324
+ * Interactive upgrade prompt for missing SessionStart hooks.
325
+ *
326
+ * Names EVERY missing harness (and its path) before asking. A yes after
327
+ * this prompt is consent to write each named hook — not an unnamed extra.
328
+ * Codex-only rationale is attached only when Codex is in the set.
329
+ */
330
+ export function missingHookPromptLines(missing, homeDir) {
331
+ const named = missing.length > 0 ? [...missing] : ["codex"];
332
+ const preamble = [];
333
+ for (const h of named) {
334
+ preamble.push(`SessionStart hook (${h}) is not installed at ${hookSettingsPath(homeDir, h)}.`);
335
+ }
336
+ if (named.includes("codex")) {
337
+ preamble.push("Without the Codex hook, memory never bootstraps (no CLAUDE.md alternative on Codex).");
338
+ }
339
+ if (named.length > 1) {
340
+ preamble.push(`A yes installs the hook for ${named.join(" and ")} — each executes at session start.`);
341
+ }
342
+ const question = named.length === 1
343
+ ? " Install the flair-session-start hook now? [y/N] "
344
+ : ` Install the flair-session-start hook for ${named.join(" and ")} now? [y/N] `;
345
+ return { preamble, question };
346
+ }
347
+ /** Plain-language lines when upgrade will not write the hook. */
348
+ export function missingHookWithoutConsentLines(harness, path) {
349
+ return [
350
+ `SessionStart hook (${harness}) is not installed at ${path}.`,
351
+ "This hook runs at every session start — a consent-bearing write, so upgrade will not install it unprompted.",
352
+ `Install with: flair upgrade --install-hooks`,
353
+ ` or: ${hookInstallHint(harness)}`,
354
+ ];
355
+ }
356
+ /** True when the SessionStart-hook check failed because a hook file is missing. */
357
+ export function sessionStartHookMissing(run) {
358
+ const hook = run.results.find((r) => r.id === "session-start-hook");
359
+ return hook?.status === "fail" && (hook.missingHarnesses?.length ?? 0) > 0;
360
+ }
361
+ /**
362
+ * Inputs for a consented `installHook` after upgrade. Prefers the missing
363
+ * harness's own MCP block, then env, then the provided fallbacks.
364
+ */
365
+ export function resolveUpgradeHookInstall(homeDir, harness, fallbacks) {
366
+ const clientId = harness === "codex" ? "codex" : "claude-code";
367
+ const block = readClientMcpBlock(clientId, homeDir);
368
+ const agentId = (typeof process.env.FLAIR_AGENT_ID === "string" && process.env.FLAIR_AGENT_ID) ||
369
+ block.agentId ||
370
+ fallbacks.agentId;
371
+ const flairUrl = (typeof process.env.FLAIR_URL === "string" && process.env.FLAIR_URL) ||
372
+ block.flairUrl ||
373
+ fallbacks.flairUrl ||
374
+ (fallbacks.port ? `http://127.0.0.1:${fallbacks.port}` : "http://127.0.0.1:9926");
375
+ if (!agentId) {
376
+ return { error: "no agent id known — pass --agent or set FLAIR_AGENT_ID so the hook can be wired" };
377
+ }
378
+ return { agentId, flairUrl };
379
+ }
380
+ /**
381
+ * How many catalog members block doctor / withhold upgrade's ✅.
382
+ * `fail` and `unrun` both count — an unrun member is never a pass.
383
+ */
384
+ export function catalogBlockingCount(run) {
385
+ return run.results.filter((r) => r.status === "fail" || r.status === "unrun").length;
386
+ }
387
+ /**
388
+ * Found/fixed delta for `flair doctor --fix`. `found` is the pre-fix
389
+ * catalog blocking count; `fixed` is how many of those cleared after.
390
+ */
391
+ export function catalogIssueDelta(before, after) {
392
+ const found = catalogBlockingCount(before);
393
+ const remaining = catalogBlockingCount(after);
394
+ return { found, fixed: Math.max(0, found - remaining) };
395
+ }
396
+ /** Compact catalog lines for doctor's Install health section. */
397
+ export function renderCatalogDoctorLines(run) {
398
+ return run.results.map((r) => {
399
+ const icon = r.status === "pass" || r.status === "skip" ? "ok" : r.status === "unrun" ? "warn" : "error";
400
+ const word = r.status === "skip" ? "n/a" : r.status;
401
+ const detail = r.detail ? ` — ${r.detail}` : "";
402
+ return { icon, line: `${r.label}: ${word}${detail}` };
403
+ });
404
+ }
405
+ /**
406
+ * Consent → write composition for missing SessionStart hooks after a
407
+ * catalog run. Silence (no flag, not interactive) never creates a hook
408
+ * file. `--install-hooks` or an accepted prompt writes every named
409
+ * missing harness via `installHook` (or `writeHook`).
410
+ *
411
+ * This is the path `flair upgrade` actually uses. Testing
412
+ * `resolveHookInstallConsent` alone would stay green if the caller
413
+ * always wrote anyway.
414
+ */
415
+ export function applyUpgradeHookConsent(opts) {
416
+ const homeDir = opts.homeDir;
417
+ const empty = {
418
+ run: opts.run,
419
+ consent: "skip-declined",
420
+ missing: [],
421
+ written: [],
422
+ writes: [],
423
+ messages: [],
424
+ };
425
+ if (!sessionStartHookMissing(opts.run)) {
426
+ return { ...empty, consent: "install" };
427
+ }
428
+ const missing = opts.run.results.find((r) => r.id === "session-start-hook")?.missingHarnesses ?? [];
429
+ const consent = resolveHookInstallConsent({
430
+ installHooksFlag: opts.installHooksFlag,
431
+ interactive: opts.interactive,
432
+ promptAccepted: opts.promptAccepted,
433
+ });
434
+ const prompt = missingHookPromptLines(missing, homeDir);
435
+ if (consent === "prompt") {
436
+ return { ...empty, consent, missing, prompt };
437
+ }
438
+ if (consent === "install") {
439
+ const write = opts.writeHook ?? installHook;
440
+ const writes = [];
441
+ const written = [];
442
+ const messages = [];
443
+ for (const harness of missing) {
444
+ const inputs = resolveUpgradeHookInstall(homeDir, harness, {
445
+ port: opts.port,
446
+ agentId: opts.agentId,
447
+ flairUrl: opts.flairUrl,
448
+ });
449
+ if ("error" in inputs) {
450
+ messages.push(inputs.error);
451
+ writes.push({ harness, ok: false, message: inputs.error });
452
+ continue;
453
+ }
454
+ const installed = write({
455
+ homeDir,
456
+ harness,
457
+ agentId: inputs.agentId,
458
+ flairUrl: inputs.flairUrl,
459
+ });
460
+ writes.push({ harness, ok: installed.ok, message: installed.message });
461
+ messages.push(installed.message);
462
+ if (installed.ok)
463
+ written.push(harness);
464
+ }
465
+ return {
466
+ run: runDoctorChecks(opts.ctx),
467
+ consent,
468
+ missing,
469
+ written,
470
+ writes,
471
+ messages,
472
+ };
473
+ }
474
+ const messages = [];
475
+ if (consent === "skip-noninteractive") {
476
+ for (const harness of missing) {
477
+ messages.push(...missingHookWithoutConsentLines(harness, hookSettingsPath(homeDir, harness)));
478
+ }
479
+ }
480
+ return { ...empty, consent, missing, messages };
481
+ }
@@ -300,29 +300,10 @@ export function renderDetachedWarning(m, headline) {
300
300
  lines.push(` Fix: ${m.remedy.join(" && ")}`);
301
301
  return lines;
302
302
  }
303
- /**
304
- * The final status line of a successful `flair upgrade`, given what the probe
305
- * found AND what launchd is doing.
306
- *
307
- * This is the reported defect reduced to a function, on purpose. The bug was
308
- * never in the probe — `healthy, authenticated, running <version>` was true
309
- * it was that those three facts were rendered as an unqualified ✅ while a
310
- * fourth, unmeasured fact (the instance had been dropped out of its process
311
- * manager) was the one that mattered. Deciding it here rather than inline in
312
- * the command means the decision is testable without performing an upgrade,
313
- * which on the darwin path nothing else can do: no CI lane runs it.
314
- *
315
- * The verified facts are still reported in the degraded case. The upgrade DID
316
- * land, and hiding that would swap one misleading summary for another; what
317
- * changes is the marker and the sentence around it.
318
- */
319
- export function renderVerifiedSummary(version, m) {
320
- const facts = `healthy, authenticated${version ? `, running ${version}` : ""}`;
321
- if (!isDetached(m)) {
322
- return { degraded: false, lines: [`✅ verified: ${facts}`] };
323
- }
324
- return {
325
- degraded: true,
326
- lines: renderDetachedWarning(m, `upgrade landed (${facts}) but the instance is NOT running under launchd.`),
327
- };
328
- }
303
+ // renderVerifiedSummary used to live here and qualify ✅ from version +
304
+ // LaunchdManagement alone (flair#1022). That signature was the ceiling on
305
+ // what it could notice a blacklist of one known-unhealthy form. The
306
+ // enumerated doctor runner in doctor-run.ts is now the source of the
307
+ // success marker (flair#1439). renderDetachedWarning stays: it is the
308
+ // wording for a detached launchd check, used by the runner and by
309
+ // `flair restart`.