@cohortapp/agent-sdk 2.11.15 → 2.13.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/.env.example +37 -22
- package/README.md +2 -0
- package/bin/maestro.mjs +117 -39
- package/bin/maestro.test.mjs +175 -5
- package/docs/guides/front-door-session.md +313 -0
- package/docs/guides/mac-mini.md +100 -28
- package/docs/guides/org-onboarding.md +1 -1
- package/docs/guides/setup-wizard.md +9 -5
- package/docs/runbooks/cohort-cutover.md +11 -1
- package/docs/runbooks/mac-mini-bootstrap.md +38 -63
- package/lib/cadence-bus-requeue.test.mjs +83 -0
- package/lib/cadence-bus.mjs +43 -7
- package/lib/channels/inbox-item.mjs +59 -2
- package/lib/cli/board.mjs +285 -0
- package/lib/cli/board.test.mjs +227 -0
- package/lib/cli/design.mjs +185 -0
- package/lib/cli/design.test.mjs +270 -0
- package/lib/cli/doctor-checks.mjs +441 -0
- package/lib/cli/doctor-checks.test.mjs +336 -0
- package/lib/cli/global-setup-extras.mjs +454 -0
- package/lib/cli/global-setup-extras.test.mjs +462 -0
- package/lib/cli/inbox.mjs +304 -0
- package/lib/cli/inbox.test.mjs +230 -0
- package/lib/cli/session-ack.mjs +63 -0
- package/lib/cli/session-ack.test.mjs +63 -0
- package/lib/cli/session.mjs +760 -0
- package/lib/cli/session.test.mjs +613 -0
- package/lib/collective/global-config.mjs +209 -6
- package/lib/collective/global-config.test.mjs +145 -0
- package/lib/collective/global-skills.mjs +145 -0
- package/lib/collective/global-skills.test.mjs +126 -0
- package/lib/collective/presence.mjs +4 -3
- package/lib/collective/vendor-skills.mjs +305 -0
- package/lib/collective/vendor-skills.test.mjs +306 -0
- package/lib/comms/send-gate.mjs +115 -0
- package/lib/comms/send-gate.test.mjs +113 -0
- package/lib/design/design-md.mjs +793 -0
- package/lib/design/design-md.test.mjs +318 -0
- package/lib/design/fixtures/DESIGN.golden.md +238 -0
- package/lib/design/fixtures/PRODUCT.golden.md +67 -0
- package/lib/design/fixtures/foundation.json +133 -0
- package/lib/design/refresh-gate.mjs +154 -0
- package/lib/design/refresh-gate.test.mjs +144 -0
- package/lib/design/write.mjs +275 -0
- package/lib/design/write.test.mjs +241 -0
- package/lib/feature-init.mjs +2 -2
- package/lib/mcp/server.test.mjs +9 -4
- package/lib/model-router/spawn.test.mjs +21 -0
- package/lib/org/board-mine-cache.mjs +99 -0
- package/lib/org/board-mine-cache.test.mjs +53 -0
- package/lib/org/board.mjs +11 -0
- package/lib/org/board.test.mjs +11 -1
- package/lib/org/client.mjs +36 -0
- package/lib/org/client.test.mjs +46 -0
- package/lib/org/inbound/directedness.mjs +18 -2
- package/lib/org/inbound/directedness.test.mjs +58 -0
- package/lib/org/inbound/index.mjs +8 -1
- package/lib/org/inbound/index.test.mjs +22 -0
- package/lib/org/mesh-directives.test.mjs +110 -0
- package/lib/org/mesh.mjs +61 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +52 -0
- package/lib/org/protocol.test.mjs +12 -1
- package/lib/org/registry.mjs +3 -2
- package/lib/org/tool-surface.mjs +120 -0
- package/lib/org/tool-surface.test.mjs +118 -5
- package/lib/prompts/parallelism.mjs +79 -0
- package/lib/prompts/parallelism.test.mjs +177 -0
- package/lib/security/external-content.mjs +1 -1
- package/lib/security/external-content.test.mjs +17 -0
- package/lib/session/config.mjs +137 -0
- package/lib/session/config.test.mjs +92 -0
- package/lib/session/feed-core.mjs +229 -0
- package/lib/session/feed-core.test.mjs +198 -0
- package/lib/session/first-run.mjs +126 -0
- package/lib/session/first-run.test.mjs +121 -0
- package/lib/session/frontdoor.mjs +266 -0
- package/lib/session/frontdoor.test.mjs +205 -0
- package/lib/session/handoffs.mjs +295 -0
- package/lib/session/handoffs.test.mjs +183 -0
- package/lib/session/identity.mjs +220 -0
- package/lib/session/identity.test.mjs +180 -0
- package/lib/session/inbox-claims.mjs +434 -0
- package/lib/session/inbox-claims.test.mjs +286 -0
- package/lib/session/launch-args.mjs +161 -0
- package/lib/session/launch-args.test.mjs +157 -0
- package/lib/session/liveness.mjs +174 -0
- package/lib/session/liveness.test.mjs +100 -0
- package/lib/session/status-summary.mjs +172 -0
- package/lib/session/status-summary.test.mjs +118 -0
- package/lib/session-permissions.mjs +39 -3
- package/lib/session-permissions.test.mjs +20 -0
- package/lib/setup/claude-probe.mjs +161 -24
- package/lib/setup/claude-probe.test.mjs +187 -0
- package/lib/setup/sections/learning.mjs +2 -1
- package/lib/setup/sections/model.mjs +104 -24
- package/lib/setup/sections/model.test.mjs +240 -0
- package/lib/setup/sections/org.mjs +27 -2
- package/lib/setup/sections/org.test.mjs +35 -2
- package/lib/setup/sections/verify.mjs +5 -0
- package/lib/setup/state.mjs +30 -10
- package/lib/setup/state.test.mjs +24 -1
- package/lib/singleton.js +11 -3
- package/lib/singleton.test.mjs +16 -0
- package/lib/subagents/lock.mjs +1 -1
- package/lib/telemetry/collect.mjs +270 -6
- package/lib/telemetry/collect.test.mjs +196 -1
- package/lib/upgrade/global-refresh.mjs +108 -0
- package/lib/upgrade/global-refresh.test.mjs +65 -0
- package/lib/upgrade/launchd-reconcile.mjs +327 -0
- package/lib/upgrade/launchd-reconcile.test.mjs +272 -0
- package/lib/upgrade/post-steps.mjs +151 -0
- package/lib/upgrade/post-steps.test.mjs +200 -0
- package/lib/upgrade/verify.mjs +215 -0
- package/lib/upgrade/verify.test.mjs +164 -0
- package/lib/voice/outbound.mjs +3 -2
- package/lib/voice/post-call-brief.mjs +2 -1
- package/lib/voice/session-rotation.mjs +6 -1
- package/lib/voice/session-rotation.test.mjs +114 -0
- package/package.json +3 -3
- package/plugins/maestro-skills/plugin.json +25 -1
- package/plugins/maestro-skills/skills/board-work.md +63 -0
- package/plugins/maestro-skills/skills/cohort-design.md +153 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +80 -0
- package/plugins/maestro-skills/skills/main-session.md +102 -0
- package/plugins/maestro-skills/skills/peer-sessions.md +65 -0
- package/plugins/maestro-skills/skills/persona-discipline.md +75 -0
- package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
- package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
- package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
- package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
- package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
- package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
- package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
- package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
- package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
- package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
- package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
- package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
- package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
- package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
- package/scaffold/CLAUDE.md +24 -0
- package/scripts/ci/check-durable-write-seam.mjs +147 -0
- package/scripts/ci/check-durable-write-seam.test.mjs +90 -0
- package/scripts/ci/check-skill-packs.mjs +388 -0
- package/scripts/ci/check-skill-packs.test.mjs +495 -0
- package/scripts/ci/check.mjs +6 -0
- package/scripts/collective/hook-runner.mjs +39 -4
- package/scripts/collective/hook-runner.test.mjs +85 -2
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +96 -0
- package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +60 -0
- package/scripts/daemon/agent-daemon.mjs +249 -10
- package/scripts/daemon/agent-daemon.test.mjs +73 -0
- package/scripts/daemon/assurance-e2e.test.mjs +141 -6
- package/scripts/daemon/assurance.mjs +461 -37
- package/scripts/daemon/assurance.test.mjs +408 -43
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +393 -0
- package/scripts/daemon/cadence-consumer.mjs +289 -89
- package/scripts/daemon/cadence-handlers.mjs +53 -0
- package/scripts/daemon/classifier.mjs +1 -1
- package/scripts/daemon/dispatcher-resume.test.mjs +166 -0
- package/scripts/daemon/dispatcher.mjs +127 -19
- package/scripts/daemon/health.mjs +12 -1
- package/scripts/daemon/inbox-deferral-session.test.mjs +49 -0
- package/scripts/daemon/inbox-deferral.mjs +6 -0
- package/scripts/daemon/lib/self-echo.mjs +201 -0
- package/scripts/daemon/lib/self-echo.test.mjs +153 -0
- package/scripts/daemon/maestro-daemon.mjs +3 -0
- package/scripts/daemon/prompt-builder.mjs +19 -3
- package/scripts/daemon/responder.mjs +51 -40
- package/scripts/daemon/sdk-version.mjs +51 -0
- package/scripts/daemon/sdk-version.test.mjs +31 -0
- package/scripts/hooks/pre-send-audit.sh +97 -4
- package/scripts/hooks/pre-send-audit.test.mjs +140 -1
- package/scripts/local-triggers/autoupdate.sh +243 -19
- package/scripts/local-triggers/autoupdate.test.mjs +518 -0
- package/scripts/local-triggers/generate-plists.sh +24 -1
- package/scripts/local-triggers/generate-plists.test.mjs +49 -11
- package/scripts/org/send-orgmail.first-contact.test.mjs +102 -0
- package/scripts/org/send-orgmail.mjs +27 -3
- package/scripts/poller/inbox-privilege-injection.test.mjs +167 -0
- package/scripts/poller/slack-poller.mjs +13 -1
- package/scripts/poller/utils.mjs +46 -1
- package/scripts/poller-launchd/install.sh +19 -11
- package/scripts/poller-launchd/install.test.mjs +243 -0
- package/scripts/poller-launchd/launchd-poller-wrapper.sh +92 -0
- package/scripts/poller-launchd/migrate.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +4 -2
- package/scripts/session/feed.mjs +237 -0
- package/scripts/session/feed.test.mjs +196 -0
- package/scripts/session/supervisor-sh.test.mjs +218 -0
- package/scripts/session/supervisor.mjs +328 -0
- package/scripts/session/supervisor.sh +141 -0
- package/scripts/session/supervisor.test.mjs +482 -0
- package/scripts/setup/configure-macos.sh +250 -55
- package/scripts/setup/configure-macos.test.mjs +306 -0
- package/scripts/setup/init-agent.sh +112 -7
- package/scripts/setup/init-agent.test.mjs +220 -1
- package/scripts/vendor/skill-packs.mjs +354 -0
- package/scripts/vendor/sync-skill-packs.mjs +242 -0
- package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
- package/scripts/watchdog/memory-watchdog.sh +37 -1
- package/scripts/watchdog/memory-watchdog.test.mjs +64 -0
- package/scripts/setup/boot-claude-session.sh +0 -94
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/cli/doctor-checks.mjs — the auth / org-id / front-door / fleet checks
|
|
3
|
+
* behind `maestro doctor` (design 2026-09-08 §3.9; audit F1, F2, F3, F4).
|
|
4
|
+
*
|
|
5
|
+
* Every decision is a PURE function over values the caller already read —
|
|
6
|
+
* `authMode`, `orgIdShape`, `effectiveSource`, `sdkGlobalInstall`,
|
|
7
|
+
* `tailscaleSsh`, `sessionInstalled` — so each verdict is unit-testable with
|
|
8
|
+
* no machine, no network and no clock. `runDoctorChecks` is the one edge: it
|
|
9
|
+
* reads the files, forks the commands and makes the single org call through
|
|
10
|
+
* injectable `exec` / `fetchImpl` / `listPlists` / `probeClaude` seams, then
|
|
11
|
+
* hands the values to the pure checks. Fail-open at every edge: a command that
|
|
12
|
+
* is missing degrades that one line to `warn`, never a throw.
|
|
13
|
+
*
|
|
14
|
+
* Rows are `{level: "ok"|"warn"|"fail", msg}`; `fail` counts toward doctor's
|
|
15
|
+
* issue total, `warn` is advisory. Secrets never appear in a row — tokens are
|
|
16
|
+
* masked by `mask()` before they reach a message.
|
|
17
|
+
*
|
|
18
|
+
* What replaced what: the doctor block this feeds used to say
|
|
19
|
+
* "ANTHROPIC_API_KEY not set" and count it as an issue on every seat that
|
|
20
|
+
* runs the subscription token — the documented fleet posture. That block is
|
|
21
|
+
* gone; `authMode` is mode-aware.
|
|
22
|
+
*
|
|
23
|
+
* @module lib/cli/doctor-checks
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
"use strict";
|
|
27
|
+
|
|
28
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
29
|
+
import { join, resolve } from "node:path";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
import { spawnSync } from "node:child_process";
|
|
32
|
+
import { loadOrgConfig, call } from "../org/client.mjs";
|
|
33
|
+
import { probeClaude as defaultProbeClaude } from "../setup/claude-probe.mjs";
|
|
34
|
+
import { sessionLiveness, SESSION_STALE_MS } from "../telemetry/collect.mjs";
|
|
35
|
+
|
|
36
|
+
export const SDK_PACKAGE = "@cohortapp/agent-sdk";
|
|
37
|
+
|
|
38
|
+
/** Where the tailscale CLI lives, in the order we try it (mirrors telemetry). */
|
|
39
|
+
const TAILSCALE_BINS = Object.freeze([
|
|
40
|
+
"tailscale",
|
|
41
|
+
"/usr/local/bin/tailscale",
|
|
42
|
+
"/opt/homebrew/bin/tailscale",
|
|
43
|
+
"/Applications/Tailscale.app/Contents/MacOS/Tailscale",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Pure helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse a .env body into `{KEY: value}`. Quotes stripped, `export ` prefix
|
|
52
|
+
* tolerated, comments/blank lines ignored, EMPTY values dropped (an empty
|
|
53
|
+
* `KEY=` line is "unset" for every consumer we have).
|
|
54
|
+
* @param {string|null|undefined} body
|
|
55
|
+
* @returns {Record<string,string>}
|
|
56
|
+
*/
|
|
57
|
+
export function parseDotEnv(body) {
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const raw of String(body || "").split("\n")) {
|
|
60
|
+
const line = raw.trim();
|
|
61
|
+
if (!line || line.startsWith("#")) continue;
|
|
62
|
+
const m = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
63
|
+
if (!m) continue;
|
|
64
|
+
const v = m[2].trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
65
|
+
if (v) out[m[1]] = v;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `nlk_dotenv_secret` → `nlk_…cret`. Short values collapse to `…`. */
|
|
71
|
+
export function mask(v) {
|
|
72
|
+
const s = String(v || "");
|
|
73
|
+
if (!s) return "";
|
|
74
|
+
if (s.length <= 10) return "…";
|
|
75
|
+
return `${s.slice(0, 4)}…${s.slice(-4)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Which Claude auth path the seat is on, from its (parsed) env.
|
|
80
|
+
* @param {Record<string,string>} env the .env values (merged over the ambient env by the caller)
|
|
81
|
+
* @returns {{mode:"oauth-token"|"subscription"|"api-key"|"none", level:"ok"|"warn"|"fail", msg:string}}
|
|
82
|
+
*/
|
|
83
|
+
export function authMode(env = {}) {
|
|
84
|
+
const token = !!env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
85
|
+
const prefer = /^(1|true|yes)$/i.test(String(env.MAESTRO_PREFER_SUBSCRIPTION_AUTH || ""));
|
|
86
|
+
const key = !!env.ANTHROPIC_API_KEY;
|
|
87
|
+
if (token && prefer) return { mode: "oauth-token", level: "ok", msg: "Claude auth: subscription OAuth token (CLAUDE_CODE_OAUTH_TOKEN + MAESTRO_PREFER_SUBSCRIPTION_AUTH=1)" };
|
|
88
|
+
if (token) {
|
|
89
|
+
return {
|
|
90
|
+
mode: "oauth-token", level: "warn",
|
|
91
|
+
msg: "Claude auth: CLAUDE_CODE_OAUTH_TOKEN is set but MAESTRO_PREFER_SUBSCRIPTION_AUTH=1 is not — add it so spawns strip ANTHROPIC_API_KEY and ride the token",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (prefer) {
|
|
95
|
+
return {
|
|
96
|
+
mode: "subscription", level: "warn",
|
|
97
|
+
msg: "Claude auth: keychain login (MAESTRO_PREFER_SUBSCRIPTION_AUTH=1, no token) — it expires headlessly; run `claude setup-token` and put the result in .env as CLAUDE_CODE_OAUTH_TOKEN",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (key) return { mode: "api-key", level: "ok", msg: "Claude auth: ANTHROPIC_API_KEY (pay-per-token)" };
|
|
101
|
+
return {
|
|
102
|
+
mode: "none", level: "fail",
|
|
103
|
+
msg: "Claude auth: nothing configured — run `claude setup-token` on any logged-in machine and put the result in .env as CLAUDE_CODE_OAUTH_TOKEN (plus MAESTRO_PREFER_SUBSCRIPTION_AUTH=1), or set ANTHROPIC_API_KEY",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Does this look like an org ID (`org_default_<slug>`) rather than a bare slug?
|
|
109
|
+
* Shared with the setup wizard's org section so both judge the pin the same way.
|
|
110
|
+
*/
|
|
111
|
+
export function looksLikeOrgId(v) { return /^org_[a-z0-9]+_[a-z0-9-]+$/i.test(String(v || "")); }
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Is the configured COHORT_ORG_ID the org's ID (what hq compares with strict
|
|
115
|
+
* equality) or its slug (which 401s every call)? `whoamiOrgId`/`orgSlug` come
|
|
116
|
+
* from the server (`org.list` → the active org) when reachable, else "".
|
|
117
|
+
* @param {string} configured
|
|
118
|
+
* @param {string} whoamiOrgId
|
|
119
|
+
* @param {string} orgSlug
|
|
120
|
+
* @returns {{level:"ok"|"warn"|"fail", msg:string}}
|
|
121
|
+
*/
|
|
122
|
+
export function orgIdShape(configured, whoamiOrgId, orgSlug) {
|
|
123
|
+
const c = String(configured || "").trim();
|
|
124
|
+
const id = String(whoamiOrgId || "").trim();
|
|
125
|
+
const slug = String(orgSlug || "").trim();
|
|
126
|
+
if (!c) {
|
|
127
|
+
return { level: "warn", msg: `COHORT_ORG_ID is not set${id ? ` — this key belongs to org ${id}` : ""} (the x-org-id pin is optional, but pull-enrollment needs it)` };
|
|
128
|
+
}
|
|
129
|
+
if (id) {
|
|
130
|
+
if (c === id) return { level: "ok", msg: `COHORT_ORG_ID is the org ID (${id}) — matches the server` };
|
|
131
|
+
if (slug && c === slug) {
|
|
132
|
+
return { level: "fail", msg: `COHORT_ORG_ID=${c}: you set the slug — hq compares the pin with strict equality against the org ID, so every call 401s. Set COHORT_ORG_ID=${id}` };
|
|
133
|
+
}
|
|
134
|
+
return { level: "fail", msg: `COHORT_ORG_ID=${c} does not match the org this key belongs to (${id}${slug ? `, slug ${slug}` : ""})` };
|
|
135
|
+
}
|
|
136
|
+
// Server unreachable / not asked: judge the shape alone.
|
|
137
|
+
if (looksLikeOrgId(c)) return { level: "ok", msg: `COHORT_ORG_ID=${c} has the org-ID shape (server not consulted)` };
|
|
138
|
+
return { level: "warn", msg: `COHORT_ORG_ID=${c} looks like a slug — the org ID has the form org_default_<slug>; verify against Cohort → Settings` };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Where each Cohort value actually comes from, following the precedence the
|
|
143
|
+
* daemon lives by: `config/org.yaml` (the enrollment SoT `maestro setup`
|
|
144
|
+
* writes) beats the ambient env, which beats `.env` (dotenv never overrides
|
|
145
|
+
* an already-set variable). F3 was exactly that org.yaml quietly winning over
|
|
146
|
+
* a `.env` an operator had just edited — so every LOSING value is named too.
|
|
147
|
+
*
|
|
148
|
+
* @param {{orgYaml?:object|null, dotEnv?:Record<string,string>, env?:Record<string,string>}} src
|
|
149
|
+
* @returns {Array<{key:string, source:"org.yaml"|"env"|".env"|"none", value:string, shadowed:string[]}>}
|
|
150
|
+
*/
|
|
151
|
+
export function effectiveSource({ orgYaml, dotEnv, env } = {}) {
|
|
152
|
+
const n = (orgYaml && orgYaml.org && orgYaml.org.cohort) || {};
|
|
153
|
+
const de = dotEnv || {};
|
|
154
|
+
const ae = env || {};
|
|
155
|
+
const firstOf = (bag, names) => {
|
|
156
|
+
for (const name of names) if (bag[name]) return { name, value: String(bag[name]) };
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
const rows = [];
|
|
160
|
+
const push = (key, yamlValue, envNames, opts = {}) => {
|
|
161
|
+
const show = opts.secret ? mask : (v) => String(v);
|
|
162
|
+
const candidates = [];
|
|
163
|
+
if (yamlValue) candidates.push({ source: "org.yaml", label: `org.yaml ${key}`, value: String(yamlValue) });
|
|
164
|
+
const fromEnv = firstOf(ae, envNames);
|
|
165
|
+
if (fromEnv) candidates.push({ source: "env", label: `env ${fromEnv.name}`, value: fromEnv.value });
|
|
166
|
+
const fromDot = firstOf(de, envNames);
|
|
167
|
+
if (fromDot) candidates.push({ source: ".env", label: `.env ${fromDot.name}`, value: fromDot.value });
|
|
168
|
+
if (candidates.length === 0) { rows.push({ key, source: "none", value: "", shadowed: [] }); return; }
|
|
169
|
+
const [winner, ...losers] = candidates;
|
|
170
|
+
rows.push({
|
|
171
|
+
key,
|
|
172
|
+
source: winner.source,
|
|
173
|
+
value: show(winner.value),
|
|
174
|
+
shadowed: losers.filter((l) => l.value !== winner.value).map((l) => `${l.label}=${show(l.value)}`),
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
push("base", n.base || n.apiUrl, ["COHORT_BASE", "COHORT_API_URL"]);
|
|
178
|
+
push("orgId", n.orgId, ["COHORT_ORG_ID"]);
|
|
179
|
+
push("token", n.token, ["COHORT_API_TOKEN", "COHORT_TOKEN", "COHORT_API_KEY"], { secret: true });
|
|
180
|
+
push("agentId", n.agentId, ["COHORT_AGENT_ID"]);
|
|
181
|
+
return rows;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Compare two `x.y.z` strings: -1 / 0 / 1. Non-numeric parts compare as 0. */
|
|
185
|
+
function cmpSemver(a, b) {
|
|
186
|
+
const pa = String(a).split("-")[0].split(".").map((x) => parseInt(x, 10) || 0);
|
|
187
|
+
const pb = String(b).split("-")[0].split(".").map((x) => parseInt(x, 10) || 0);
|
|
188
|
+
for (let i = 0; i < 3; i += 1) {
|
|
189
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
190
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
191
|
+
}
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Version of SDK_PACKAGE in `npm ls -g` output (JSON or tree text), else "". */
|
|
196
|
+
function parseNpmLs(output) {
|
|
197
|
+
const s = String(output || "").trim();
|
|
198
|
+
if (!s) return "";
|
|
199
|
+
try {
|
|
200
|
+
const j = JSON.parse(s);
|
|
201
|
+
const dep = j && j.dependencies && j.dependencies[SDK_PACKAGE];
|
|
202
|
+
if (dep && typeof dep.version === "string") return dep.version;
|
|
203
|
+
return "";
|
|
204
|
+
} catch { /* text form */ }
|
|
205
|
+
const m = s.match(new RegExp(`${SDK_PACKAGE.replace("/", "\\/")}@(\\d+\\.\\d+\\.\\d+[^\\s]*)`));
|
|
206
|
+
return m ? m[1] : "";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Is the SDK installed globally, and is it current?
|
|
211
|
+
* @param {string} npmLsOutput `npm ls -g @cohortapp/agent-sdk --json --depth=0` stdout ("" when npm failed)
|
|
212
|
+
* @param {string} npmViewLatest `npm view @cohortapp/agent-sdk version` stdout ("" when offline)
|
|
213
|
+
* @returns {{installed:boolean, version:string, latest:string, level:"ok"|"warn"|"fail", msg:string}}
|
|
214
|
+
*/
|
|
215
|
+
export function sdkGlobalInstall(npmLsOutput, npmViewLatest) {
|
|
216
|
+
const version = parseNpmLs(npmLsOutput);
|
|
217
|
+
const latest = String(npmViewLatest || "").trim().split("\n").pop().trim();
|
|
218
|
+
if (!version) {
|
|
219
|
+
// WARN until upgrade() step 4 (WP-M6) refreshes the global install on every
|
|
220
|
+
// seat: today's fleet installs the SDK LOCALLY in the agent dir
|
|
221
|
+
// (autoupdate.sh `npm install … --save`) and is fully functional that way.
|
|
222
|
+
// A `fail` here would turn doctor red on every existing machine the hour
|
|
223
|
+
// 2.12.0 lands — and "doctor is the gate" would then read as an outage.
|
|
224
|
+
return { installed: false, version: "", latest, level: "warn", msg: `${SDK_PACKAGE} is not installed globally — a home-dir session cannot reach \`maestro\`/\`cohort-mcp\` on PATH until it is: npm i -g ${SDK_PACKAGE}@latest (the next \`maestro upgrade\` does this)` };
|
|
225
|
+
}
|
|
226
|
+
if (!latest) return { installed: true, version, latest, level: "ok", msg: `${SDK_PACKAGE} ${version} installed globally (npm latest unknown — offline?)` };
|
|
227
|
+
if (cmpSemver(version, latest) < 0) {
|
|
228
|
+
return { installed: true, version, latest, level: "warn", msg: `${SDK_PACKAGE} ${version} installed globally but npm latest is ${latest} — autoupdate should catch up within the hour, or: npm i -g ${SDK_PACKAGE}@latest` };
|
|
229
|
+
}
|
|
230
|
+
return { installed: true, version, latest, level: "ok", msg: `${SDK_PACKAGE} ${version} installed globally (npm latest ${latest})` };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Is Tailscale SSH advertised on this machine? `prefsJson` (`tailscale debug
|
|
235
|
+
* prefs`, `RunSSH`) is authoritative; without it, `Self.SSH_HostKeys` in
|
|
236
|
+
* `tailscale status --json` is the next-best evidence. Anything unparseable
|
|
237
|
+
* means Tailscale is absent or down — a warn, since fleet-wide operator access
|
|
238
|
+
* depends on it (F4).
|
|
239
|
+
* @param {object|string} statusJson
|
|
240
|
+
* @param {object|string} [prefsJson]
|
|
241
|
+
* @returns {{level:"ok"|"warn", msg:string, enabled:boolean|null}}
|
|
242
|
+
*/
|
|
243
|
+
export function tailscaleSsh(statusJson, prefsJson) {
|
|
244
|
+
const parse = (v) => {
|
|
245
|
+
if (v && typeof v === "object") return v;
|
|
246
|
+
try { return JSON.parse(String(v || "")); } catch { return null; }
|
|
247
|
+
};
|
|
248
|
+
const remedy = "enable with `scripts/setup/configure-macos.sh --tailscale-ssh` (and allow it in the tailnet ACL)";
|
|
249
|
+
const prefs = parse(prefsJson);
|
|
250
|
+
const status = parse(statusJson);
|
|
251
|
+
if (prefs && typeof prefs.RunSSH === "boolean") {
|
|
252
|
+
return prefs.RunSSH
|
|
253
|
+
? { level: "ok", msg: "Tailscale SSH is on (RunSSH=true)", enabled: true }
|
|
254
|
+
: { level: "warn", msg: `Tailscale SSH is off — operators cannot reach this seat remotely; ${remedy}`, enabled: false };
|
|
255
|
+
}
|
|
256
|
+
if (status && status.Self && typeof status.Self === "object") {
|
|
257
|
+
const keys = status.Self.SSH_HostKeys;
|
|
258
|
+
if (Array.isArray(keys) && keys.length > 0) return { level: "ok", msg: "Tailscale SSH is on (host keys advertised)", enabled: true };
|
|
259
|
+
return { level: "warn", msg: `Tailscale SSH is off — operators cannot reach this seat remotely; ${remedy}`, enabled: false };
|
|
260
|
+
}
|
|
261
|
+
return { level: "warn", msg: `Tailscale is not installed or not running — no remote operator access; install it, join the tailnet, then ${remedy}`, enabled: null };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Is the main-session launchd job installed, loaded and alive?
|
|
266
|
+
* @param {{first:string, plists:string[], loaded:string[], heartbeat:object|null, now:number, staleMs?:number}} o
|
|
267
|
+
* plists file names (or labels) in ~/Library/LaunchAgents
|
|
268
|
+
* loaded labels from `launchctl list`
|
|
269
|
+
* @returns {{level:"ok"|"warn"|"fail", msg:string, label:string}}
|
|
270
|
+
*/
|
|
271
|
+
export function sessionInstalled({ first, plists = [], loaded = [], heartbeat = null, now, staleMs = SESSION_STALE_MS } = {}) {
|
|
272
|
+
const label = `ai.maestro.${String(first || "").toLowerCase()}-session`;
|
|
273
|
+
const strip = (s) => String(s || "").replace(/\.plist$/, "");
|
|
274
|
+
const installed = plists.map(strip).includes(label);
|
|
275
|
+
if (!installed) {
|
|
276
|
+
return { level: "warn", label, msg: `Main session job ${label} is not installed — the daemon's --print lane is the front door; install with \`maestro session install\`` };
|
|
277
|
+
}
|
|
278
|
+
if (!loaded.map(strip).includes(label)) {
|
|
279
|
+
// Installed-but-unloaded is what a deliberate `maestro session stop`
|
|
280
|
+
// leaves behind (launchctl bootout, plist kept) — so this asks, it does
|
|
281
|
+
// not order the operator to undo their own stop.
|
|
282
|
+
return { level: "warn", label, msg: `Main session job ${label} is installed but not loaded — stopped on purpose (\`maestro session stop\`)? The daemon's --print lane is the front door meanwhile; \`maestro session start\` (launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/${label}.plist) brings it back` };
|
|
283
|
+
}
|
|
284
|
+
if (!heartbeat) {
|
|
285
|
+
return { level: "warn", label, msg: `Main session job ${label} is loaded but has written no heartbeat yet — give it a minute, then \`maestro session status\`` };
|
|
286
|
+
}
|
|
287
|
+
const live = sessionLiveness(heartbeat, now, { staleMs });
|
|
288
|
+
if (!live.sessionLive) {
|
|
289
|
+
const ts = typeof heartbeat.ts === "number" ? heartbeat.ts : Date.parse(heartbeat.ts);
|
|
290
|
+
const age = Number.isFinite(ts) ? Math.round((now - ts) / 1000) : NaN;
|
|
291
|
+
return { level: "fail", label, msg: `Main session job ${label} heartbeat is stale (${Number.isFinite(age) ? `${age} s` : "unreadable"} old) — the daemon is on its fallback lane; \`maestro session status\` / \`maestro session restart\`` };
|
|
292
|
+
}
|
|
293
|
+
const ts = typeof heartbeat.ts === "number" ? heartbeat.ts : Date.parse(heartbeat.ts);
|
|
294
|
+
return { level: "ok", label, msg: `Main session job ${label} loaded, heartbeat live (${Math.round((now - ts) / 1000)} s ago)` };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// The edge: read, fork, call — then hand off to the pure checks
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
function safeRead(p) { try { return readFileSync(p, "utf8"); } catch { return ""; } }
|
|
302
|
+
function safeJson(p) { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } }
|
|
303
|
+
|
|
304
|
+
/** Labels from `launchctl list` output (PID\tStatus\tLabel). */
|
|
305
|
+
function parseLaunchctlList(out) {
|
|
306
|
+
return String(out || "").split("\n").map((l) => l.trim().split(/\s+/).pop()).filter((l) => l && l !== "Label");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function agentFirstName(cwd) {
|
|
310
|
+
const aj = safeJson(join(cwd, "config", "agent.json"));
|
|
311
|
+
const fromCfg = aj && (aj.firstName || (aj.name && String(aj.name).split(/\s+/)[0]));
|
|
312
|
+
if (fromCfg) return String(fromCfg).toLowerCase();
|
|
313
|
+
return String(cwd).split("/").pop().replace(/-ai$/, "").toLowerCase();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Run every check for the agent repo at `cwd`. Each edge is injectable; each
|
|
318
|
+
* is fail-open.
|
|
319
|
+
* @param {object} o
|
|
320
|
+
* @param {string} o.cwd
|
|
321
|
+
* @param {Record<string,string>} [o.env] ambient env (default process.env)
|
|
322
|
+
* @param {number} [o.now]
|
|
323
|
+
* @param {(cmd:string,args:string[],opts:object)=>string} [o.exec] execFileSync-shaped
|
|
324
|
+
* @param {Function} [o.fetchImpl]
|
|
325
|
+
* @param {() => string[]} [o.listPlists] names in ~/Library/LaunchAgents
|
|
326
|
+
* @param {(agentRoot:string)=>object} [o.probeClaude]
|
|
327
|
+
* @returns {Promise<Array<{level:"ok"|"warn"|"fail", msg:string}>>}
|
|
328
|
+
*/
|
|
329
|
+
export async function runDoctorChecks(o = {}) {
|
|
330
|
+
const cwd = resolve(o.cwd || process.cwd());
|
|
331
|
+
const env = o.env || process.env;
|
|
332
|
+
const now = Number.isFinite(o.now) ? o.now : Date.now();
|
|
333
|
+
// Default edge: stdout regardless of exit status (`npm ls -g` exits 1 for a
|
|
334
|
+
// package that is NOT installed — and prints the JSON that says so, which is
|
|
335
|
+
// the answer we want); only a spawn error (missing binary, timeout) throws.
|
|
336
|
+
const exec = o.exec || ((cmd, args, opts) => {
|
|
337
|
+
const r = spawnSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 8000, ...opts });
|
|
338
|
+
if (r.error) throw r.error;
|
|
339
|
+
return r.stdout || "";
|
|
340
|
+
});
|
|
341
|
+
const tryExec = (cmd, args, opts) => { try { return String(exec(cmd, args, opts || {}) || ""); } catch { return ""; /* missing binary / non-zero exit → the check degrades to warn */ } };
|
|
342
|
+
const rows = [];
|
|
343
|
+
|
|
344
|
+
// ── Claude auth (F1) ──────────────────────────────────────────────────────
|
|
345
|
+
const dotEnv = parseDotEnv(safeRead(join(cwd, ".env")));
|
|
346
|
+
const merged = { ...pick(env, ["CLAUDE_CODE_OAUTH_TOKEN", "MAESTRO_PREFER_SUBSCRIPTION_AUTH", "ANTHROPIC_API_KEY"]), ...dotEnv };
|
|
347
|
+
const auth = authMode(merged);
|
|
348
|
+
rows.push({ level: auth.level, msg: auth.msg });
|
|
349
|
+
if (auth.mode !== "none") {
|
|
350
|
+
try {
|
|
351
|
+
// The probe judges the mode from the SAME env this row did (.env first,
|
|
352
|
+
// ambient second), so the two rows cannot contradict each other on a
|
|
353
|
+
// seat whose token arrives through launchd's environment.
|
|
354
|
+
const probe = (o.probeClaude || defaultProbeClaude)(cwd, { env });
|
|
355
|
+
if (probe.ok && probe.auth === "unknown") rows.push({ level: "warn", msg: `${probe.name}: ${probe.detail || "could not verify"}` });
|
|
356
|
+
// "verified" only when a real call proved the credential (api-key 200).
|
|
357
|
+
// `claude auth status` reports a login for ANY token value, so a
|
|
358
|
+
// subscription ok is worded as what it is: the CLI's report.
|
|
359
|
+
else if (probe.ok && probe.verified === true) rows.push({ level: "ok", msg: `${probe.name}: verified` });
|
|
360
|
+
else if (probe.ok) rows.push({ level: "ok", msg: `${probe.name}: ${probe.detail || "CLI reports a login (the token is only proven by a real spawn)"}` });
|
|
361
|
+
else rows.push({ level: "fail", msg: `${probe.name}: ${probe.remedy || "failed"}` });
|
|
362
|
+
} catch (err) {
|
|
363
|
+
rows.push({ level: "warn", msg: `Claude auth probe threw: ${err && err.message ? err.message : err}` });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ── Effective Cohort sources (F3) ─────────────────────────────────────────
|
|
368
|
+
const orgYaml = existsSync(join(cwd, "config", "org.yaml")) ? loadOrgConfig(cwd) : null;
|
|
369
|
+
const sources = effectiveSource({ orgYaml, dotEnv, env: pick(env, ["COHORT_BASE", "COHORT_API_URL", "COHORT_ORG_ID", "COHORT_API_TOKEN", "COHORT_TOKEN", "COHORT_API_KEY", "COHORT_AGENT_ID"]) });
|
|
370
|
+
for (const r of sources) {
|
|
371
|
+
if (r.source === "none") continue;
|
|
372
|
+
rows.push({ level: "ok", msg: `Cohort ${r.key} ← ${r.source} (${r.value})` });
|
|
373
|
+
if (r.shadowed.length) rows.push({ level: "warn", msg: ` Cohort ${r.key}: ${r.source} wins over ${r.shadowed.join(", ")} — the losing value is ignored` });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── COHORT_ORG_ID: ID vs slug (F2) ─────────────────────────────────────────
|
|
377
|
+
const val = (key) => { const r = sources.find((x) => x.key === key); return r && r.source !== "none" ? r.value : ""; };
|
|
378
|
+
const n = (orgYaml && orgYaml.org && orgYaml.org.cohort) || {};
|
|
379
|
+
const base = String(n.base || n.apiUrl || env.COHORT_BASE || env.COHORT_API_URL || dotEnv.COHORT_BASE || dotEnv.COHORT_API_URL || "").replace(/\/+$/, "");
|
|
380
|
+
const token = String(n.token || env.COHORT_API_TOKEN || env.COHORT_TOKEN || env.COHORT_API_KEY || dotEnv.COHORT_API_TOKEN || dotEnv.COHORT_TOKEN || dotEnv.COHORT_API_KEY || "");
|
|
381
|
+
const configuredOrgId = val("orgId");
|
|
382
|
+
if (!base || !token) {
|
|
383
|
+
rows.push({ level: "ok", msg: "Org plane not enrolled (no base/token) — COHORT_ORG_ID check skipped" });
|
|
384
|
+
} else {
|
|
385
|
+
// Deliberately UNPINNED: a slug-valued pin would 401 the very probe that
|
|
386
|
+
// diagnoses it. The key alone selects the tenant server-side.
|
|
387
|
+
let id = "";
|
|
388
|
+
let slug = "";
|
|
389
|
+
let note = "";
|
|
390
|
+
try {
|
|
391
|
+
const res = await call("org.list", {}, { base, token, orgId: "", fetchImpl: o.fetchImpl, logImpl: () => {} });
|
|
392
|
+
const orgs = res && res.ok && res.result && Array.isArray(res.result.orgs) ? res.result.orgs : [];
|
|
393
|
+
const active = orgs.find((x) => x && x.isActive) || orgs[0];
|
|
394
|
+
if (active) { id = String(active.id || ""); slug = String(active.slug || ""); }
|
|
395
|
+
else note = res && !res.ok && res.error ? ` (org.list: ${res.error.code || res.error.message || "error"})` : " (org.list returned no org)";
|
|
396
|
+
} catch (err) { note = ` (org.list failed: ${err && err.message ? err.message : err})`; }
|
|
397
|
+
const shape = orgIdShape(configuredOrgId, id, slug);
|
|
398
|
+
rows.push({ level: shape.level, msg: shape.msg + (id ? "" : note) });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Global SDK install + PATH (§3.7 item 6) ───────────────────────────────
|
|
402
|
+
const npmLs = tryExec("npm", ["ls", "-g", SDK_PACKAGE, "--json", "--depth=0"], { timeout: 8000 });
|
|
403
|
+
if (!npmLs) {
|
|
404
|
+
rows.push({ level: "warn", msg: `npm ls -g ${SDK_PACKAGE} failed — could not verify the global SDK install (is npm on PATH?)` });
|
|
405
|
+
} else {
|
|
406
|
+
const latest = tryExec("npm", ["view", SDK_PACKAGE, "version"], { timeout: 8000 });
|
|
407
|
+
const sdk = sdkGlobalInstall(npmLs, latest);
|
|
408
|
+
rows.push({ level: sdk.level, msg: sdk.msg });
|
|
409
|
+
}
|
|
410
|
+
for (const bin of ["maestro", "cohort-mcp"]) {
|
|
411
|
+
const where = tryExec("which", [bin], { timeout: 2000 }).trim();
|
|
412
|
+
if (!where) rows.push({ level: "warn", msg: `\`${bin}\` does not resolve on PATH — a home-dir session cannot reach the agent's tools; npm i -g ${SDK_PACKAGE}@latest` });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── Tailscale SSH (F4) ────────────────────────────────────────────────────
|
|
416
|
+
let status = "";
|
|
417
|
+
let prefs = "";
|
|
418
|
+
for (const bin of TAILSCALE_BINS) {
|
|
419
|
+
status = tryExec(bin, ["status", "--json"], { timeout: 4000 });
|
|
420
|
+
if (status) { prefs = tryExec(bin, ["debug", "prefs"], { timeout: 4000 }); break; }
|
|
421
|
+
}
|
|
422
|
+
const ts = tailscaleSsh(status, prefs);
|
|
423
|
+
rows.push({ level: ts.level, msg: ts.msg });
|
|
424
|
+
|
|
425
|
+
// ── Main session job (§3.1) ───────────────────────────────────────────────
|
|
426
|
+
const listPlists = o.listPlists || (() => { try { return readdirSync(join(homedir(), "Library", "LaunchAgents")); } catch { return []; } });
|
|
427
|
+
const loaded = parseLaunchctlList(tryExec("launchctl", ["list"], { timeout: 4000 }));
|
|
428
|
+
const heartbeat = safeJson(join(cwd, "state", "session", "heartbeat.json"));
|
|
429
|
+
const sess = sessionInstalled({ first: agentFirstName(cwd), plists: listPlists(), loaded, heartbeat, now });
|
|
430
|
+
rows.push({ level: sess.level, msg: sess.msg });
|
|
431
|
+
|
|
432
|
+
return rows;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function pick(obj, keys) {
|
|
436
|
+
const out = {};
|
|
437
|
+
for (const k of keys) if (obj && obj[k]) out[k] = String(obj[k]);
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export default { parseDotEnv, mask, authMode, looksLikeOrgId, orgIdShape, effectiveSource, sdkGlobalInstall, tailscaleSsh, sessionInstalled, runDoctorChecks };
|