@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,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor-checks.test.mjs — the pure front-door/auth/fleet checks behind
|
|
3
|
+
* `maestro doctor` (lib/cli/doctor-checks.mjs) and the injected runner.
|
|
4
|
+
*
|
|
5
|
+
* Run: node --test lib/cli/doctor-checks.test.mjs
|
|
6
|
+
*/
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
import { test } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
12
|
+
import { probeClaude } from "../setup/claude-probe.mjs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
parseDotEnv,
|
|
18
|
+
authMode,
|
|
19
|
+
orgIdShape,
|
|
20
|
+
effectiveSource,
|
|
21
|
+
sdkGlobalInstall,
|
|
22
|
+
tailscaleSsh,
|
|
23
|
+
sessionInstalled,
|
|
24
|
+
runDoctorChecks,
|
|
25
|
+
} from "./doctor-checks.mjs";
|
|
26
|
+
|
|
27
|
+
// ── parseDotEnv ──────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
test("parseDotEnv: KEY=value lines, quotes stripped, comments/blanks ignored, empty values dropped", () => {
|
|
30
|
+
const env = parseDotEnv('# c\nA=1\nB="two"\nC=\'three\'\nD=\n\nexport E=5\n');
|
|
31
|
+
assert.deepEqual(env, { A: "1", B: "two", C: "three", E: "5" });
|
|
32
|
+
assert.deepEqual(parseDotEnv(""), {});
|
|
33
|
+
assert.deepEqual(parseDotEnv(null), {});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── authMode ─────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
test("authMode: oauth-token wins, then keychain, then api-key, then none (fail)", () => {
|
|
39
|
+
const t = authMode({ CLAUDE_CODE_OAUTH_TOKEN: "x", MAESTRO_PREFER_SUBSCRIPTION_AUTH: "1", ANTHROPIC_API_KEY: "k" });
|
|
40
|
+
assert.equal(t.mode, "oauth-token");
|
|
41
|
+
assert.equal(t.level, "ok");
|
|
42
|
+
assert.match(t.msg, /OAuth token/);
|
|
43
|
+
|
|
44
|
+
const tokenOnly = authMode({ CLAUDE_CODE_OAUTH_TOKEN: "x" });
|
|
45
|
+
assert.equal(tokenOnly.mode, "oauth-token");
|
|
46
|
+
assert.equal(tokenOnly.level, "warn", "token without the prefer flag: spawns may still send ANTHROPIC_API_KEY");
|
|
47
|
+
assert.match(tokenOnly.msg, /MAESTRO_PREFER_SUBSCRIPTION_AUTH=1/);
|
|
48
|
+
|
|
49
|
+
const kc = authMode({ MAESTRO_PREFER_SUBSCRIPTION_AUTH: "1" });
|
|
50
|
+
assert.equal(kc.mode, "subscription");
|
|
51
|
+
assert.equal(kc.level, "warn", "keychain login expires headlessly — a seat machine should hold the token");
|
|
52
|
+
assert.match(kc.msg, /claude setup-token/);
|
|
53
|
+
|
|
54
|
+
const key = authMode({ ANTHROPIC_API_KEY: "sk" });
|
|
55
|
+
assert.equal(key.mode, "api-key");
|
|
56
|
+
assert.equal(key.level, "ok");
|
|
57
|
+
|
|
58
|
+
const none = authMode({});
|
|
59
|
+
assert.equal(none.mode, "none");
|
|
60
|
+
assert.equal(none.level, "fail");
|
|
61
|
+
assert.match(none.msg, /CLAUDE_CODE_OAUTH_TOKEN/);
|
|
62
|
+
assert.doesNotMatch(none.msg, /ANTHROPIC_API_KEY required/i);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── orgIdShape ───────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
test("orgIdShape: ID match ok; slug set → fail naming the right ID; unknown → shape heuristic", () => {
|
|
68
|
+
assert.equal(orgIdShape("org_default_adaptic", "org_default_adaptic", "adaptic").level, "ok");
|
|
69
|
+
|
|
70
|
+
const slug = orgIdShape("adaptic", "org_default_adaptic", "adaptic");
|
|
71
|
+
assert.equal(slug.level, "fail");
|
|
72
|
+
assert.match(slug.msg, /you set the slug/i);
|
|
73
|
+
assert.match(slug.msg, /org_default_adaptic/);
|
|
74
|
+
|
|
75
|
+
const other = orgIdShape("org_default_other", "org_default_adaptic", "adaptic");
|
|
76
|
+
assert.equal(other.level, "fail");
|
|
77
|
+
assert.match(other.msg, /does not match/i);
|
|
78
|
+
|
|
79
|
+
const unset = orgIdShape("", "org_default_adaptic", "adaptic");
|
|
80
|
+
assert.equal(unset.level, "warn");
|
|
81
|
+
assert.match(unset.msg, /not set/i);
|
|
82
|
+
|
|
83
|
+
// Server unreachable: fall back to the shape of the value alone.
|
|
84
|
+
assert.equal(orgIdShape("org_default_adaptic", "", "").level, "ok");
|
|
85
|
+
const slugShape = orgIdShape("adaptic", "", "");
|
|
86
|
+
assert.equal(slugShape.level, "warn");
|
|
87
|
+
assert.match(slugShape.msg, /looks like a slug/i);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ── effectiveSource ──────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
test("effectiveSource: org.yaml wins for orgId/token/base; env then .env; shadowed values are named", () => {
|
|
93
|
+
const rows = effectiveSource({
|
|
94
|
+
orgYaml: { org: { cohort: { enabled: true, base: "https://os.cohortapp.com", orgId: "org_default_acme", token: "nlk_yaml_secret" } } },
|
|
95
|
+
dotEnv: { COHORT_ORG_ID: "acme", COHORT_API_KEY: "nlk_dotenv_secret", COHORT_AGENT_ID: "A005" },
|
|
96
|
+
env: { COHORT_BASE: "https://staging.example" },
|
|
97
|
+
});
|
|
98
|
+
const by = Object.fromEntries(rows.map((r) => [r.key, r]));
|
|
99
|
+
assert.equal(by.orgId.source, "org.yaml");
|
|
100
|
+
assert.equal(by.orgId.value, "org_default_acme");
|
|
101
|
+
assert.deepEqual(by.orgId.shadowed, [".env COHORT_ORG_ID=acme"]);
|
|
102
|
+
assert.equal(by.token.source, "org.yaml");
|
|
103
|
+
assert.doesNotMatch(JSON.stringify(rows), /secret/, "token values are masked everywhere");
|
|
104
|
+
assert.deepEqual(by.token.shadowed, [".env COHORT_API_KEY=nlk_…cret"]);
|
|
105
|
+
assert.equal(by.base.source, "org.yaml");
|
|
106
|
+
assert.deepEqual(by.base.shadowed, ["env COHORT_BASE=https://staging.example"]);
|
|
107
|
+
assert.equal(by.agentId.source, ".env");
|
|
108
|
+
assert.equal(by.agentId.value, "A005");
|
|
109
|
+
|
|
110
|
+
// No org.yaml: env beats .env (dotenv never overrides an ambient var).
|
|
111
|
+
const rows2 = effectiveSource({ orgYaml: null, dotEnv: { COHORT_ORG_ID: "from-dotenv" }, env: { COHORT_ORG_ID: "from-env" } });
|
|
112
|
+
const o2 = rows2.find((r) => r.key === "orgId");
|
|
113
|
+
assert.equal(o2.source, "env");
|
|
114
|
+
assert.equal(o2.value, "from-env");
|
|
115
|
+
assert.deepEqual(o2.shadowed, [".env COHORT_ORG_ID=from-dotenv"]);
|
|
116
|
+
const none = rows2.find((r) => r.key === "token");
|
|
117
|
+
assert.equal(none.source, "none");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ── sdkGlobalInstall ─────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
test("sdkGlobalInstall: parses npm ls -g JSON and text, compares to npm latest", () => {
|
|
123
|
+
const json = JSON.stringify({ name: "lib", dependencies: { "@cohortapp/agent-sdk": { version: "2.11.17" } } });
|
|
124
|
+
const upToDate = sdkGlobalInstall(json, "2.11.17");
|
|
125
|
+
assert.equal(upToDate.installed, true);
|
|
126
|
+
assert.equal(upToDate.version, "2.11.17");
|
|
127
|
+
assert.equal(upToDate.level, "ok");
|
|
128
|
+
|
|
129
|
+
const behind = sdkGlobalInstall(json, "2.12.0\n");
|
|
130
|
+
assert.equal(behind.level, "warn");
|
|
131
|
+
assert.match(behind.msg, /2\.11\.17.*2\.12\.0/);
|
|
132
|
+
assert.match(behind.msg, /npm i -g @cohortapp\/agent-sdk@latest/);
|
|
133
|
+
|
|
134
|
+
const text = sdkGlobalInstall("/opt/homebrew/lib\n└── @cohortapp/agent-sdk@2.12.0\n", "2.12.0");
|
|
135
|
+
assert.equal(text.version, "2.12.0");
|
|
136
|
+
assert.equal(text.level, "ok");
|
|
137
|
+
|
|
138
|
+
const ahead = sdkGlobalInstall(json, "2.11.0");
|
|
139
|
+
assert.equal(ahead.level, "ok", "a local build ahead of npm is fine");
|
|
140
|
+
|
|
141
|
+
const missing = sdkGlobalInstall('{"name":"lib"}', "2.12.0");
|
|
142
|
+
assert.equal(missing.installed, false);
|
|
143
|
+
// WARN, not fail: every seat in the fleet today installs the SDK locally in
|
|
144
|
+
// the agent dir (autoupdate.sh `npm install … --save`); the global install
|
|
145
|
+
// arrives with upgrade() step 4 (WP-M6). A red doctor on a fully functional
|
|
146
|
+
// seat the hour 2.12.0 lands would read as an outage.
|
|
147
|
+
assert.equal(missing.level, "warn");
|
|
148
|
+
assert.match(missing.msg, /npm i -g/);
|
|
149
|
+
assert.match(missing.msg, /not installed globally/);
|
|
150
|
+
|
|
151
|
+
const offline = sdkGlobalInstall(json, "");
|
|
152
|
+
assert.equal(offline.level, "ok");
|
|
153
|
+
assert.match(offline.msg, /latest unknown/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ── tailscaleSsh ─────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
test("tailscaleSsh: prefs RunSSH is authoritative, else Self.SSH_HostKeys, else unknown", () => {
|
|
159
|
+
assert.equal(tailscaleSsh({ Self: { HostName: "h", TailscaleIPs: ["100.1.1.1"] } }, { RunSSH: true }).level, "ok");
|
|
160
|
+
const off = tailscaleSsh({ Self: { HostName: "h" } }, { RunSSH: false });
|
|
161
|
+
assert.equal(off.level, "warn");
|
|
162
|
+
assert.match(off.msg, /configure-macos\.sh --tailscale-ssh/);
|
|
163
|
+
assert.equal(tailscaleSsh({ Self: { SSH_HostKeys: ["ssh-ed25519 AAAA"] } }).level, "ok");
|
|
164
|
+
assert.equal(tailscaleSsh({ Self: { SSH_HostKeys: [] } }).level, "warn");
|
|
165
|
+
assert.equal(tailscaleSsh(JSON.stringify({ Self: { SSH_HostKeys: ["k"] } })).level, "ok");
|
|
166
|
+
const unknown = tailscaleSsh("");
|
|
167
|
+
assert.equal(unknown.level, "warn");
|
|
168
|
+
assert.match(unknown.msg, /not installed|not running/i);
|
|
169
|
+
assert.equal(tailscaleSsh("not json").level, "warn");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ── sessionInstalled ─────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
test("sessionInstalled: plist + loaded + fresh heartbeat → ok; each missing rung is named", () => {
|
|
175
|
+
const now = Date.parse("2026-09-08T12:00:00Z");
|
|
176
|
+
const fresh = { pid: 1, ts: "2026-09-08T11:59:45Z" };
|
|
177
|
+
const stale = { pid: 1, ts: "2026-09-08T11:00:00Z" };
|
|
178
|
+
const ok = sessionInstalled({ first: "ethan", plists: ["ai.maestro.ethan-daemon", "ai.maestro.ethan-session"], loaded: ["ai.maestro.ethan-session"], heartbeat: fresh, now });
|
|
179
|
+
assert.equal(ok.level, "ok");
|
|
180
|
+
assert.match(ok.msg, /live/);
|
|
181
|
+
|
|
182
|
+
const notInstalled = sessionInstalled({ first: "ethan", plists: ["ai.maestro.ethan-daemon"], loaded: [], heartbeat: null, now });
|
|
183
|
+
assert.equal(notInstalled.level, "warn");
|
|
184
|
+
assert.match(notInstalled.msg, /maestro session install/);
|
|
185
|
+
|
|
186
|
+
const notLoaded = sessionInstalled({ first: "ethan", plists: ["ai.maestro.ethan-session"], loaded: [], heartbeat: null, now });
|
|
187
|
+
// Plist present, not loaded, is exactly what `maestro session stop` leaves
|
|
188
|
+
// behind on purpose (launchctl bootout, plist kept) — a warn that asks, not
|
|
189
|
+
// a fail that tells the operator to undo their own stop.
|
|
190
|
+
assert.equal(notLoaded.level, "warn");
|
|
191
|
+
assert.match(notLoaded.msg, /not loaded/);
|
|
192
|
+
assert.match(notLoaded.msg, /session stop/);
|
|
193
|
+
assert.match(notLoaded.msg, /session start/);
|
|
194
|
+
assert.match(notLoaded.msg, /launchctl/);
|
|
195
|
+
|
|
196
|
+
const down = sessionInstalled({ first: "ethan", plists: ["ai.maestro.ethan-session"], loaded: ["ai.maestro.ethan-session"], heartbeat: stale, now });
|
|
197
|
+
assert.equal(down.level, "fail");
|
|
198
|
+
assert.match(down.msg, /stale/);
|
|
199
|
+
assert.match(down.msg, /daemon .*fallback/i);
|
|
200
|
+
|
|
201
|
+
const never = sessionInstalled({ first: "ethan", plists: ["ai.maestro.ethan-session"], loaded: ["ai.maestro.ethan-session"], heartbeat: null, now });
|
|
202
|
+
assert.equal(never.level, "warn");
|
|
203
|
+
assert.match(never.msg, /no heartbeat yet/);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── runDoctorChecks (injected edges) ─────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
function agentRoot({ env = "", orgYaml = "", agentJson = { firstName: "Ethan" } } = {}) {
|
|
209
|
+
const root = mkdtempSync(join(tmpdir(), "doctor-checks-"));
|
|
210
|
+
mkdirSync(join(root, "config"), { recursive: true });
|
|
211
|
+
writeFileSync(join(root, "config", "agent.json"), JSON.stringify(agentJson));
|
|
212
|
+
if (env) writeFileSync(join(root, ".env"), env);
|
|
213
|
+
if (orgYaml) writeFileSync(join(root, "config", "org.yaml"), orgYaml);
|
|
214
|
+
return root;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
test("runDoctorChecks: the happy seat — token auth, ID matches, session live, SDK current, SSH on", async () => {
|
|
218
|
+
const root = agentRoot({
|
|
219
|
+
env: "CLAUDE_CODE_OAUTH_TOKEN=tok\nMAESTRO_PREFER_SUBSCRIPTION_AUTH=1\n",
|
|
220
|
+
orgYaml: "org:\n cohort:\n enabled: true\n base: https://os.cohortapp.com\n orgId: org_default_acme\n token: nlk_x\n",
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
mkdirSync(join(root, "state", "session"), { recursive: true });
|
|
224
|
+
writeFileSync(join(root, "state", "session", "heartbeat.json"), JSON.stringify({ pid: 1, ts: "2026-09-08T11:59:50Z" }));
|
|
225
|
+
const calls = [];
|
|
226
|
+
const rows = await runDoctorChecks({
|
|
227
|
+
cwd: root,
|
|
228
|
+
env: { PATH: "/usr/bin" },
|
|
229
|
+
now: Date.parse("2026-09-08T12:00:00Z"),
|
|
230
|
+
probeClaude: () => ({ ok: true, mode: "subscription", auth: "ok", verified: false, name: "Claude auth (subscription — OAuth token)", detail: "CLI reports a login for the token; the token itself is only proven by a real spawn" }),
|
|
231
|
+
exec: (cmd, args) => {
|
|
232
|
+
calls.push([cmd, ...args].join(" "));
|
|
233
|
+
if (cmd === "npm" && args[0] === "ls") return JSON.stringify({ dependencies: { "@cohortapp/agent-sdk": { version: "2.12.0" } } });
|
|
234
|
+
if (cmd === "npm" && args[0] === "view") return "2.12.0\n";
|
|
235
|
+
if (/tailscale/i.test(cmd) && args[0] === "status") return JSON.stringify({ Self: { HostName: "h" } });
|
|
236
|
+
if (/tailscale/i.test(cmd) && args[0] === "debug") return JSON.stringify({ RunSSH: true });
|
|
237
|
+
if (cmd === "launchctl") return "-\t0\tai.maestro.ethan-session\n";
|
|
238
|
+
if (cmd === "which") return `/opt/homebrew/bin/${args[0]}\n`;
|
|
239
|
+
return "";
|
|
240
|
+
},
|
|
241
|
+
listPlists: () => ["ai.maestro.ethan-daemon.plist", "ai.maestro.ethan-session.plist"],
|
|
242
|
+
fetchImpl: async () => ({ ok: true, status: 200, headers: { get: () => undefined }, json: async () => ({ ok: true, result: { orgs: [{ id: "org_default_acme", slug: "acme", isActive: true }] } }) }),
|
|
243
|
+
});
|
|
244
|
+
const text = rows.map((r) => `${r.level}: ${r.msg}`).join("\n");
|
|
245
|
+
assert.ok(rows.every((r) => r.level !== "fail"), text);
|
|
246
|
+
assert.match(text, /OAuth token/);
|
|
247
|
+
// The probe cannot validate a token: the row says what the CLI REPORTS, never "verified".
|
|
248
|
+
assert.match(text, /ok: Claude auth \(subscription — OAuth token\): CLI reports a login/);
|
|
249
|
+
assert.doesNotMatch(text, /: verified/);
|
|
250
|
+
assert.match(text, /COHORT_ORG_ID.*org_default_acme/);
|
|
251
|
+
assert.match(text, /orgId .*org\.yaml/);
|
|
252
|
+
assert.match(text, /agent-sdk 2\.12\.0/);
|
|
253
|
+
assert.match(text, /Tailscale SSH/);
|
|
254
|
+
assert.match(text, /session.*live/i);
|
|
255
|
+
assert.ok(calls.some((c) => c.startsWith("npm ls -g @cohortapp/agent-sdk")), calls.join("\n"));
|
|
256
|
+
assert.ok(!text.includes("nlk_x"), "the token never appears in doctor output");
|
|
257
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("runDoctorChecks: the slug trap — org.list is asked WITHOUT the org pin, and the answer names the ID", async () => {
|
|
261
|
+
const root = agentRoot({
|
|
262
|
+
env: "ANTHROPIC_API_KEY=sk\nCOHORT_ORG_ID=acme\nCOHORT_API_KEY=nlk_y\nCOHORT_BASE=https://os.cohortapp.com\n",
|
|
263
|
+
});
|
|
264
|
+
try {
|
|
265
|
+
const seen = [];
|
|
266
|
+
const rows = await runDoctorChecks({
|
|
267
|
+
cwd: root,
|
|
268
|
+
env: { PATH: "/usr/bin" },
|
|
269
|
+
now: Date.now(),
|
|
270
|
+
probeClaude: () => ({ ok: true, mode: "api-key", auth: "ok", name: "Claude API reachable (api-key)" }),
|
|
271
|
+
exec: () => { throw Object.assign(new Error("ENOENT"), { code: "ENOENT" }); },
|
|
272
|
+
listPlists: () => [],
|
|
273
|
+
fetchImpl: async (url, init) => {
|
|
274
|
+
seen.push({ url: String(url), headers: init.headers });
|
|
275
|
+
return { ok: true, status: 200, headers: { get: () => undefined }, json: async () => ({ ok: true, result: { orgs: [{ id: "org_default_acme", slug: "acme", isActive: true }] } }) };
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
assert.equal(seen.length, 1);
|
|
279
|
+
assert.match(seen[0].url, /org\.list$/);
|
|
280
|
+
assert.equal(seen[0].headers["x-org-id"], undefined, "a slug pin would 401 the very probe that diagnoses it");
|
|
281
|
+
const text = rows.map((r) => `${r.level}: ${r.msg}`).join("\n");
|
|
282
|
+
assert.match(text, /fail: .*you set the slug/i);
|
|
283
|
+
assert.match(text, /org_default_acme/);
|
|
284
|
+
// Effective sources: no org.yaml → .env rows.
|
|
285
|
+
assert.match(text, /orgId .*\.env/);
|
|
286
|
+
// Edges that are absent degrade to warn, never throw.
|
|
287
|
+
assert.match(text, /warn: .*not installed globally|warn: .*npm ls/i);
|
|
288
|
+
assert.match(text, /warn: .*Tailscale/);
|
|
289
|
+
assert.match(text, /warn: .*maestro session install/);
|
|
290
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("runDoctorChecks: not enrolled → the org-id check is skipped with a note, never a network call", async () => {
|
|
294
|
+
const root = agentRoot({ env: "CLAUDE_CODE_OAUTH_TOKEN=t\nMAESTRO_PREFER_SUBSCRIPTION_AUTH=1\n" });
|
|
295
|
+
try {
|
|
296
|
+
let fetched = 0;
|
|
297
|
+
const rows = await runDoctorChecks({
|
|
298
|
+
cwd: root, env: {}, now: Date.now(),
|
|
299
|
+
probeClaude: () => ({ ok: true, auth: "unknown", mode: "subscription", name: "Claude auth", detail: "could not verify" }),
|
|
300
|
+
exec: () => "", listPlists: () => [],
|
|
301
|
+
fetchImpl: async () => { fetched += 1; return { ok: false, status: 500, headers: { get: () => undefined }, json: async () => ({}) }; },
|
|
302
|
+
});
|
|
303
|
+
assert.equal(fetched, 0);
|
|
304
|
+
const text = rows.map((r) => `${r.level}: ${r.msg}`).join("\n");
|
|
305
|
+
assert.match(text, /not enrolled/i);
|
|
306
|
+
assert.match(text, /could not verify/);
|
|
307
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("runDoctorChecks: a VERIFIED probe (api-key 200) says verified; a token that lives only in the ambient env yields two consistent rows", async () => {
|
|
311
|
+
const root = agentRoot({ env: "ANTHROPIC_API_KEY=sk\n" });
|
|
312
|
+
try {
|
|
313
|
+
const rows = await runDoctorChecks({
|
|
314
|
+
cwd: root, env: {}, now: Date.now(),
|
|
315
|
+
probeClaude: () => ({ ok: true, mode: "api-key", auth: "ok", verified: true, name: "Claude API reachable (api-key)" }),
|
|
316
|
+
exec: () => "", listPlists: () => [], fetchImpl: async () => ({ ok: false, status: 500, headers: { get: () => undefined }, json: async () => ({}) }),
|
|
317
|
+
});
|
|
318
|
+
const text = rows.map((r) => `${r.level}: ${r.msg}`).join("\n");
|
|
319
|
+
assert.match(text, /ok: Claude API reachable \(api-key\): verified/);
|
|
320
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
321
|
+
// No .env at all; the token arrives through launchd's environment (the headless runbook path).
|
|
322
|
+
const bare = agentRoot({});
|
|
323
|
+
try {
|
|
324
|
+
const seen = [];
|
|
325
|
+
const rows = await runDoctorChecks({
|
|
326
|
+
cwd: bare, env: { CLAUDE_CODE_OAUTH_TOKEN: "tok-from-launchd", MAESTRO_PREFER_SUBSCRIPTION_AUTH: "1" }, now: Date.now(),
|
|
327
|
+
probeClaude: (agentRoot, opts) => { seen.push(opts); return probeClaude(agentRoot, { ...opts, exec: () => '{"loggedIn":true,"authMethod":"oauth_token"}', claudeBin: "/fake/claude" }); },
|
|
328
|
+
exec: () => "", listPlists: () => [], fetchImpl: async () => ({ ok: false, status: 500, headers: { get: () => undefined }, json: async () => ({}) }),
|
|
329
|
+
});
|
|
330
|
+
const text = rows.map((r) => `${r.level}: ${r.msg}`).join("\n");
|
|
331
|
+
assert.match(text, /ok: Claude auth: subscription OAuth token/);
|
|
332
|
+
assert.match(text, /ok: Claude auth \(subscription — OAuth token\): CLI reports a login/);
|
|
333
|
+
assert.doesNotMatch(text, /fail:/, "the mode row and the probe row read the same env and cannot contradict each other");
|
|
334
|
+
assert.equal(seen[0].env.CLAUDE_CODE_OAUTH_TOKEN, "tok-from-launchd", "doctor hands the probe the same ambient env it judged the mode from");
|
|
335
|
+
} finally { rmSync(bare, { recursive: true, force: true }); }
|
|
336
|
+
});
|