@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,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* global-setup-extras.test.mjs — the front-door half of `maestro global-setup`.
|
|
3
|
+
* Run: node --test lib/cli/global-setup-extras.test.mjs
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync, readdirSync, lstatSync, statSync, chmodSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import { applyFrontDoorSetup, mergeJsonFileAtomic, findOnPath, pickClaudeBin, checkPersonaHookScript } from "./global-setup-extras.mjs";
|
|
14
|
+
import { IDENTITY_BEGIN, CLAUDE_MD_BEGIN, PRE_SEND_HOOK_MATCHER } from "../collective/global-config.mjs";
|
|
15
|
+
import { SDK_ROOT } from "./global-setup-extras.mjs";
|
|
16
|
+
|
|
17
|
+
function fixture() {
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), "gs-extras-"));
|
|
19
|
+
const agentRoot = join(root, "alex-ai");
|
|
20
|
+
mkdirSync(join(agentRoot, "config"), { recursive: true });
|
|
21
|
+
writeFileSync(join(agentRoot, "config", "agent.json"), JSON.stringify({ firstName: "Alex", fullName: "Alex Rivera", title: "Chief of Staff", company: "Acme", repoSlug: "alex-ai" }));
|
|
22
|
+
const sdkRoot = join(root, "sdk");
|
|
23
|
+
mkdirSync(join(sdkRoot, "plugins", "maestro-skills", "skills"), { recursive: true });
|
|
24
|
+
writeFileSync(join(sdkRoot, "plugins", "maestro-skills", "skills", "main-session.md"), "---\nname: main-session\n---\nbody\n");
|
|
25
|
+
const claudeDir = join(root, "home", ".claude");
|
|
26
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
27
|
+
const claudeJsonPath = join(root, "home", ".claude.json");
|
|
28
|
+
const logs = [];
|
|
29
|
+
const io = { log: (m) => logs.push(`log:${m}`), ok: (m) => logs.push(`ok:${m}`), warn: (m) => logs.push(`warn:${m}`) };
|
|
30
|
+
return { root, agentRoot, sdkRoot, claudeDir, claudeJsonPath, logs, io };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test("applyFrontDoorSetup: identity block, JSON-merged MCP entry (no claude CLI), settings, skills copy — then a no-op re-run", async () => {
|
|
34
|
+
const f = fixture();
|
|
35
|
+
try {
|
|
36
|
+
writeFileSync(join(f.claudeDir, "CLAUDE.md"), `# mine\n\n${CLAUDE_MD_BEGIN}\nold collective\n<!-- END maestro:collective -->\n`);
|
|
37
|
+
writeFileSync(join(f.claudeDir, "settings.json"), JSON.stringify({ permissions: { allow: ["Bash(ls)"] }, hooks: { Stop: [] } }));
|
|
38
|
+
writeFileSync(f.claudeJsonPath, JSON.stringify({ mcpServers: { other: { command: "x" } }, oauthAccount: { id: "keep" } }));
|
|
39
|
+
const execCalls = [];
|
|
40
|
+
const r = await applyFrontDoorSetup({
|
|
41
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
42
|
+
execImpl: (bin, args) => { execCalls.push({ bin, args }); return { status: 1, stderr: "no such command" }; },
|
|
43
|
+
claudeBin: null, globalRoot: "/opt/homebrew/lib/node_modules", now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
44
|
+
});
|
|
45
|
+
assert.equal(r.ok, true);
|
|
46
|
+
// 1) identity block written, collective block preserved.
|
|
47
|
+
const md = readFileSync(join(f.claudeDir, "CLAUDE.md"), "utf8");
|
|
48
|
+
assert.match(md, /# mine/);
|
|
49
|
+
assert.match(md, /old collective/);
|
|
50
|
+
assert.match(md, new RegExp(IDENTITY_BEGIN));
|
|
51
|
+
assert.match(md, /Alex Rivera/);
|
|
52
|
+
assert.equal(r.identity.changed, true);
|
|
53
|
+
// 2) MCP entry merged via JSON (no CLI available → not even attempted).
|
|
54
|
+
assert.equal(execCalls.length, 0, "claudeBin:null → no CLI attempt");
|
|
55
|
+
const cj = JSON.parse(readFileSync(f.claudeJsonPath, "utf8"));
|
|
56
|
+
assert.deepEqual(cj.mcpServers.other, { command: "x" });
|
|
57
|
+
assert.equal(cj.mcpServers.cohort.command, "cohort-mcp");
|
|
58
|
+
assert.equal(cj.mcpServers.cohort.env.COHORT_AGENT_ROOT, f.agentRoot);
|
|
59
|
+
assert.deepEqual(cj.oauthAccount, { id: "keep" });
|
|
60
|
+
assert.equal(r.mcp.via, "json");
|
|
61
|
+
assert.ok(readdirSync(join(f.root, "home")).some((n) => n.startsWith(".claude.json.backup.")), "timestamped backup of ~/.claude.json");
|
|
62
|
+
// 3) settings merged with backup.
|
|
63
|
+
const st = JSON.parse(readFileSync(join(f.claudeDir, "settings.json"), "utf8"));
|
|
64
|
+
assert.equal(st.crossSessionInbound, "accept");
|
|
65
|
+
assert.ok(st.permissions.allow.includes("Bash(ls)"));
|
|
66
|
+
assert.ok(st.permissions.allow.includes("mcp__cohort__*"));
|
|
67
|
+
assert.ok(st.hooks.PreToolUse.some((g) => g.matcher === PRE_SEND_HOOK_MATCHER && /messaging_send\|/.test(g.matcher)));
|
|
68
|
+
assert.deepEqual(st.hooks.Stop, []);
|
|
69
|
+
assert.ok(readdirSync(f.claudeDir).some((n) => n.startsWith("settings.json.backup.")), "timestamped backup of settings.json");
|
|
70
|
+
assert.ok(r.settings.changes.length > 0);
|
|
71
|
+
// 4) skills copied (sdk is not under the global root).
|
|
72
|
+
assert.equal(r.skills.mode, "copy");
|
|
73
|
+
const skill = join(f.claudeDir, "skills", "maestro-main-session", "SKILL.md");
|
|
74
|
+
assert.ok(existsSync(skill));
|
|
75
|
+
assert.equal(lstatSync(skill).isSymbolicLink(), false);
|
|
76
|
+
assert.deepEqual(r.skills.installed, ["main-session"]);
|
|
77
|
+
|
|
78
|
+
// Re-run: nothing changes, no new backups.
|
|
79
|
+
const backupsBefore = readdirSync(f.claudeDir).filter((n) => n.includes(".backup.")).length;
|
|
80
|
+
const r2 = await applyFrontDoorSetup({
|
|
81
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
82
|
+
execImpl: () => ({ status: 1 }), claudeBin: null, globalRoot: "/opt/homebrew/lib/node_modules", now: Date.parse("2026-09-08T12:05:00Z"), ...f.io,
|
|
83
|
+
});
|
|
84
|
+
assert.equal(r2.ok, true);
|
|
85
|
+
assert.equal(r2.identity.changed, false);
|
|
86
|
+
assert.equal(r2.mcp.changed, false);
|
|
87
|
+
assert.deepEqual(r2.settings.changes, []);
|
|
88
|
+
assert.deepEqual(r2.skills.installed, []);
|
|
89
|
+
assert.equal(readdirSync(f.claudeDir).filter((n) => n.includes(".backup.")).length, backupsBefore, "idempotent: no new backups");
|
|
90
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("applyFrontDoorSetup: uses `claude mcp add-json --scope user` when the CLI is available and it succeeds; symlinks skills for a global install", async () => {
|
|
94
|
+
const f = fixture();
|
|
95
|
+
try {
|
|
96
|
+
const globalRoot = join(f.root, "global", "node_modules");
|
|
97
|
+
const sdkRoot = join(globalRoot, "@cohortapp", "agent-sdk");
|
|
98
|
+
mkdirSync(join(sdkRoot, "plugins", "maestro-skills", "skills"), { recursive: true });
|
|
99
|
+
writeFileSync(join(sdkRoot, "plugins", "maestro-skills", "skills", "board-work.md"), "---\nname: board-work\n---\nbw\n");
|
|
100
|
+
const execCalls = [];
|
|
101
|
+
const r = await applyFrontDoorSetup({
|
|
102
|
+
agentRoot: f.agentRoot, sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
103
|
+
claudeBin: "/fake/claude",
|
|
104
|
+
execImpl: (bin, args) => {
|
|
105
|
+
execCalls.push({ bin, args });
|
|
106
|
+
// Emulate the CLI writing the user-scope entry.
|
|
107
|
+
const cur = existsSync(f.claudeJsonPath) ? JSON.parse(readFileSync(f.claudeJsonPath, "utf8")) : {};
|
|
108
|
+
cur.mcpServers = { ...(cur.mcpServers || {}), [args[2]]: JSON.parse(args[3]) };
|
|
109
|
+
writeFileSync(f.claudeJsonPath, JSON.stringify(cur));
|
|
110
|
+
return { status: 0, stdout: "Added stdio MCP server cohort" };
|
|
111
|
+
},
|
|
112
|
+
globalRoot, now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
113
|
+
});
|
|
114
|
+
assert.equal(r.ok, true);
|
|
115
|
+
assert.equal(execCalls.length, 1);
|
|
116
|
+
assert.equal(execCalls[0].bin, "/fake/claude");
|
|
117
|
+
assert.deepEqual(execCalls[0].args.slice(0, 3), ["mcp", "add-json", "cohort"]);
|
|
118
|
+
assert.ok(execCalls[0].args.includes("--scope") && execCalls[0].args.includes("user"));
|
|
119
|
+
assert.equal(JSON.parse(execCalls[0].args[3]).env.COHORT_AGENT_ROOT, f.agentRoot);
|
|
120
|
+
assert.equal(r.mcp.via, "cli");
|
|
121
|
+
assert.equal(r.mcp.changed, true);
|
|
122
|
+
const cj = JSON.parse(readFileSync(f.claudeJsonPath, "utf8"));
|
|
123
|
+
assert.equal(cj.mcpServers.cohort.command, "cohort-mcp");
|
|
124
|
+
// Skills: symlink for a global install.
|
|
125
|
+
assert.equal(r.skills.mode, "symlink");
|
|
126
|
+
assert.ok(lstatSync(join(f.claudeDir, "skills", "maestro-board-work", "SKILL.md")).isSymbolicLink());
|
|
127
|
+
// Settings created from scratch when absent.
|
|
128
|
+
const st = JSON.parse(readFileSync(join(f.claudeDir, "settings.json"), "utf8"));
|
|
129
|
+
assert.equal(st.crossSessionInbound, "accept");
|
|
130
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("applyFrontDoorSetup: CLI failure falls back to the JSON merge; dry-run writes nothing", async () => {
|
|
134
|
+
const f = fixture();
|
|
135
|
+
try {
|
|
136
|
+
const r = await applyFrontDoorSetup({
|
|
137
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
138
|
+
claudeBin: "/fake/claude", execImpl: () => { throw new Error("spawn ENOENT"); },
|
|
139
|
+
globalRoot: "", now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
140
|
+
});
|
|
141
|
+
assert.equal(r.ok, true);
|
|
142
|
+
assert.equal(r.mcp.via, "json", "a throwing CLI is survivable");
|
|
143
|
+
assert.equal(JSON.parse(readFileSync(f.claudeJsonPath, "utf8")).mcpServers.cohort.command, "cohort-mcp");
|
|
144
|
+
|
|
145
|
+
const g = fixture();
|
|
146
|
+
try {
|
|
147
|
+
const d = await applyFrontDoorSetup({
|
|
148
|
+
agentRoot: g.agentRoot, sdkRoot: g.sdkRoot, claudeDir: g.claudeDir, claudeJsonPath: g.claudeJsonPath,
|
|
149
|
+
claudeBin: null, execImpl: () => ({ status: 1 }), globalRoot: "", dryRun: true, now: Date.parse("2026-09-08T12:00:00Z"), ...g.io,
|
|
150
|
+
});
|
|
151
|
+
assert.equal(d.ok, true);
|
|
152
|
+
assert.equal(d.identity.changed, true, "reports what it WOULD do");
|
|
153
|
+
assert.equal(existsSync(join(g.claudeDir, "CLAUDE.md")), false);
|
|
154
|
+
assert.equal(existsSync(join(g.claudeDir, "settings.json")), false);
|
|
155
|
+
assert.equal(existsSync(g.claudeJsonPath), false);
|
|
156
|
+
assert.equal(existsSync(join(g.claudeDir, "skills")), false);
|
|
157
|
+
} finally { rmSync(g.root, { recursive: true, force: true }); }
|
|
158
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("applyFrontDoorSetup: a repo with no agent.json still wires MCP/settings/skills and skips the identity block with a warning", async () => {
|
|
162
|
+
const f = fixture();
|
|
163
|
+
try {
|
|
164
|
+
rmSync(join(f.agentRoot, "config", "agent.json"));
|
|
165
|
+
const r = await applyFrontDoorSetup({
|
|
166
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
167
|
+
claudeBin: null, execImpl: () => ({ status: 1 }), globalRoot: "", now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
168
|
+
});
|
|
169
|
+
assert.equal(r.ok, true);
|
|
170
|
+
assert.equal(r.identity.changed, false);
|
|
171
|
+
assert.equal(r.identity.skipped, true);
|
|
172
|
+
assert.ok(f.logs.some((l) => l.startsWith("warn:") && /identity/i.test(l)));
|
|
173
|
+
assert.equal(JSON.parse(readFileSync(f.claudeJsonPath, "utf8")).mcpServers.cohort.command, "cohort-mcp");
|
|
174
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ── Review fixes (2026-09-08): never clobber a state file we could not parse ─
|
|
178
|
+
|
|
179
|
+
test("applyFrontDoorSetup: an unparseable ~/.claude.json or settings.json is left byte-for-byte alone (warned, skipped) — never replaced with a fresh {}", async () => {
|
|
180
|
+
const f = fixture();
|
|
181
|
+
try {
|
|
182
|
+
const garbage = "{\"oauthAccount\":{\"id\":\"keep\"},\"projects\":{} trailing-garbage";
|
|
183
|
+
writeFileSync(f.claudeJsonPath, garbage);
|
|
184
|
+
const badSettings = "{ hooks: [ not json";
|
|
185
|
+
writeFileSync(join(f.claudeDir, "settings.json"), badSettings);
|
|
186
|
+
const execCalls = [];
|
|
187
|
+
const r = await applyFrontDoorSetup({
|
|
188
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
189
|
+
claudeBin: "/fake/claude", execImpl: (bin, args) => { execCalls.push(args); return { status: 0 }; },
|
|
190
|
+
globalRoot: "", now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
191
|
+
});
|
|
192
|
+
assert.equal(readFileSync(f.claudeJsonPath, "utf8"), garbage, "~/.claude.json untouched");
|
|
193
|
+
assert.equal(readFileSync(join(f.claudeDir, "settings.json"), "utf8"), badSettings, "settings.json untouched");
|
|
194
|
+
assert.equal(execCalls.length, 0, "the CLI is not asked to rewrite a file we cannot read back");
|
|
195
|
+
assert.equal(r.mcp.skipped, "unparseable");
|
|
196
|
+
assert.equal(r.settings.skipped, "unparseable");
|
|
197
|
+
assert.equal(r.ok, false, "a skipped step is reported, not hidden");
|
|
198
|
+
assert.ok(f.logs.some((l) => l.startsWith("warn:") && /\.claude\.json/.test(l) && /parse/i.test(l)));
|
|
199
|
+
assert.ok(f.logs.some((l) => l.startsWith("warn:") && /settings\.json/.test(l) && /parse/i.test(l)));
|
|
200
|
+
assert.equal(readdirSync(join(f.root, "home")).filter((n) => n.startsWith(".claude.json.backup.")).length, 0, "no backup of a file we did not write");
|
|
201
|
+
// The other steps still ran.
|
|
202
|
+
assert.equal(r.identity.changed, true);
|
|
203
|
+
assert.deepEqual(r.skills.installed, ["main-session"]);
|
|
204
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("applyFrontDoorSetup: the JSON merge keeps the file's mode (0600 stays 0600); a fresh ~/.claude.json is created 0600", async () => {
|
|
208
|
+
const f = fixture();
|
|
209
|
+
try {
|
|
210
|
+
writeFileSync(f.claudeJsonPath, JSON.stringify({ oauthAccount: { id: "x" } }), { mode: 0o600 });
|
|
211
|
+
chmodSync(f.claudeJsonPath, 0o600);
|
|
212
|
+
writeFileSync(join(f.claudeDir, "settings.json"), "{}", { mode: 0o640 });
|
|
213
|
+
chmodSync(join(f.claudeDir, "settings.json"), 0o640);
|
|
214
|
+
await applyFrontDoorSetup({
|
|
215
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
216
|
+
claudeBin: null, globalRoot: "", now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
217
|
+
});
|
|
218
|
+
assert.equal(statSync(f.claudeJsonPath).mode & 0o777, 0o600);
|
|
219
|
+
assert.equal(statSync(join(f.claudeDir, "settings.json")).mode & 0o777, 0o640);
|
|
220
|
+
assert.equal(JSON.parse(readFileSync(f.claudeJsonPath, "utf8")).mcpServers.cohort.command, "cohort-mcp");
|
|
221
|
+
|
|
222
|
+
const g = fixture();
|
|
223
|
+
try {
|
|
224
|
+
await applyFrontDoorSetup({
|
|
225
|
+
agentRoot: g.agentRoot, sdkRoot: g.sdkRoot, claudeDir: g.claudeDir, claudeJsonPath: g.claudeJsonPath,
|
|
226
|
+
claudeBin: null, globalRoot: "", now: Date.parse("2026-09-08T12:00:00Z"), ...g.io,
|
|
227
|
+
});
|
|
228
|
+
assert.equal(statSync(g.claudeJsonPath).mode & 0o777, 0o600, "a new ~/.claude.json is private: it will carry oauthAccount");
|
|
229
|
+
} finally { rmSync(g.root, { recursive: true, force: true }); }
|
|
230
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("mergeJsonFileAtomic: a concurrent rewrite between read and publish is re-merged, never lost", () => {
|
|
234
|
+
const root = mkdtempSync(join(tmpdir(), "gs-merge-"));
|
|
235
|
+
try {
|
|
236
|
+
const p = join(root, "state.json");
|
|
237
|
+
writeFileSync(p, JSON.stringify({ numStartups: 1, projects: {} }));
|
|
238
|
+
let calls = 0;
|
|
239
|
+
const r = mergeJsonFileAtomic(p, (cur) => {
|
|
240
|
+
calls += 1;
|
|
241
|
+
if (calls === 1) {
|
|
242
|
+
// Another process (an interactive claude) rewrites the file while we merge.
|
|
243
|
+
writeFileSync(p, JSON.stringify({ numStartups: 2, projects: { "/x": { trust: true } } }));
|
|
244
|
+
}
|
|
245
|
+
return { config: { ...cur, mcpServers: { cohort: { command: "cohort-mcp" } } }, changed: true };
|
|
246
|
+
});
|
|
247
|
+
assert.equal(r.ok, true);
|
|
248
|
+
assert.equal(r.attempts, 2, "first attempt detected the concurrent write and retried");
|
|
249
|
+
const final = JSON.parse(readFileSync(p, "utf8"));
|
|
250
|
+
assert.equal(final.numStartups, 2, "the other writer's update survived");
|
|
251
|
+
assert.deepEqual(final.projects, { "/x": { trust: true } });
|
|
252
|
+
assert.equal(final.mcpServers.cohort.command, "cohort-mcp", "ours landed too");
|
|
253
|
+
// Unparseable → refuses, untouched.
|
|
254
|
+
writeFileSync(p, "nope{");
|
|
255
|
+
const bad = mergeJsonFileAtomic(p, () => ({ config: {}, changed: true }));
|
|
256
|
+
assert.equal(bad.ok, false);
|
|
257
|
+
assert.equal(bad.reason, "unparseable");
|
|
258
|
+
assert.equal(readFileSync(p, "utf8"), "nope{");
|
|
259
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("pickClaudeBin / findOnPath: a bare `claude` from the resolver is looked up on PATH instead of being discarded", () => {
|
|
263
|
+
const root = mkdtempSync(join(tmpdir(), "gs-path-"));
|
|
264
|
+
try {
|
|
265
|
+
const bin = join(root, "nvm", "bin");
|
|
266
|
+
mkdirSync(bin, { recursive: true });
|
|
267
|
+
writeFileSync(join(bin, "claude"), "#!/bin/sh\n", { mode: 0o755 });
|
|
268
|
+
const pathEnv = `${join(root, "empty")}:${bin}`;
|
|
269
|
+
assert.equal(findOnPath("claude", pathEnv), join(bin, "claude"));
|
|
270
|
+
assert.equal(findOnPath("claude", join(root, "empty")), null);
|
|
271
|
+
assert.equal(findOnPath("claude", ""), null);
|
|
272
|
+
assert.equal(pickClaudeBin("claude", { pathEnv }), join(bin, "claude"), "bare name → PATH lookup");
|
|
273
|
+
assert.equal(pickClaudeBin("claude", { pathEnv: "" }), null, "not on PATH → null (JSON merge path)");
|
|
274
|
+
assert.equal(pickClaudeBin(join(bin, "claude"), { pathEnv: "" }), join(bin, "claude"), "absolute + exists → itself");
|
|
275
|
+
assert.equal(pickClaudeBin(join(root, "missing", "claude"), { pathEnv }), null, "absolute + missing → null");
|
|
276
|
+
assert.equal(pickClaudeBin("", { pathEnv }), null);
|
|
277
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("applyFrontDoorSetup: when `cohort-mcp` is not on PATH the MCP entry runs the SDK's own bin under the current node (a 2.11 seat without a global install still gets a working server)", async () => {
|
|
281
|
+
const f = fixture();
|
|
282
|
+
try {
|
|
283
|
+
mkdirSync(join(f.sdkRoot, "bin"), { recursive: true });
|
|
284
|
+
writeFileSync(join(f.sdkRoot, "bin", "cohort-mcp.mjs"), "// stub\n");
|
|
285
|
+
const r = await applyFrontDoorSetup({
|
|
286
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
287
|
+
claudeBin: null, globalRoot: "", pathEnv: join(f.root, "nothing-here"), now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
288
|
+
});
|
|
289
|
+
assert.equal(r.ok, true);
|
|
290
|
+
const cj = JSON.parse(readFileSync(f.claudeJsonPath, "utf8"));
|
|
291
|
+
assert.equal(cj.mcpServers.cohort.command, process.execPath);
|
|
292
|
+
assert.deepEqual(cj.mcpServers.cohort.args, [join(f.sdkRoot, "bin", "cohort-mcp.mjs")]);
|
|
293
|
+
assert.equal(cj.mcpServers.cohort.env.COHORT_AGENT_ROOT, f.agentRoot);
|
|
294
|
+
assert.ok(f.logs.some((l) => /not on PATH/.test(l)), "says why the entry is not the bare command");
|
|
295
|
+
|
|
296
|
+
// Once the global install lands (cohort-mcp on PATH), a re-run rewrites to the bare command.
|
|
297
|
+
const bin = join(f.root, "gbin");
|
|
298
|
+
mkdirSync(bin, { recursive: true });
|
|
299
|
+
writeFileSync(join(bin, "cohort-mcp"), "#!/bin/sh\n", { mode: 0o755 });
|
|
300
|
+
const r2 = await applyFrontDoorSetup({
|
|
301
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
302
|
+
claudeBin: null, globalRoot: "", pathEnv: bin, now: Date.parse("2026-09-08T12:10:00Z"), ...f.io,
|
|
303
|
+
});
|
|
304
|
+
assert.equal(r2.mcp.changed, true);
|
|
305
|
+
const cj2 = JSON.parse(readFileSync(f.claudeJsonPath, "utf8"));
|
|
306
|
+
assert.equal(cj2.mcpServers.cohort.command, "cohort-mcp");
|
|
307
|
+
assert.deepEqual(cj2.mcpServers.cohort.args, []);
|
|
308
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("applyFrontDoorSetup: warns when the agent's pre-send-audit.sh predates the persona gate (upgrade keeps locally modified copies), and when it is missing", async () => {
|
|
312
|
+
const f = fixture();
|
|
313
|
+
try {
|
|
314
|
+
const hooks = join(f.agentRoot, "scripts", "hooks");
|
|
315
|
+
mkdirSync(hooks, { recursive: true });
|
|
316
|
+
writeFileSync(join(hooks, "pre-send-audit.sh"), "#!/bin/bash\nBANNED_SUBSTRINGS=(\"As an AI\")\nexit 0\n");
|
|
317
|
+
const r = await applyFrontDoorSetup({
|
|
318
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
319
|
+
claudeBin: null, globalRoot: "", now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
320
|
+
});
|
|
321
|
+
assert.equal(r.settings.hookScript.present, true);
|
|
322
|
+
assert.equal(r.settings.hookScript.personaGate, false);
|
|
323
|
+
assert.ok(f.logs.some((l) => l.startsWith("warn:") && /no persona gate/.test(l) && /pre-send-audit\.sh/.test(l)));
|
|
324
|
+
assert.equal(checkPersonaHookScript(f.agentRoot).personaGate, false);
|
|
325
|
+
|
|
326
|
+
writeFileSync(join(hooks, "pre-send-audit.sh"), "#!/bin/bash\nPERSONA_PATTERNS=('sub-sessions?')\nexit 0\n");
|
|
327
|
+
assert.equal(checkPersonaHookScript(f.agentRoot).personaGate, true);
|
|
328
|
+
f.logs.length = 0;
|
|
329
|
+
await applyFrontDoorSetup({
|
|
330
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
331
|
+
claudeBin: null, globalRoot: "", now: Date.parse("2026-09-08T12:01:00Z"), ...f.io,
|
|
332
|
+
});
|
|
333
|
+
assert.ok(!f.logs.some((l) => /persona gate/.test(l) || /pre-send-audit\.sh is missing/.test(l)), "no warning once the gate is present");
|
|
334
|
+
|
|
335
|
+
rmSync(join(hooks, "pre-send-audit.sh"));
|
|
336
|
+
assert.deepEqual(checkPersonaHookScript(f.agentRoot).present, false);
|
|
337
|
+
f.logs.length = 0;
|
|
338
|
+
await applyFrontDoorSetup({
|
|
339
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
340
|
+
claudeBin: null, globalRoot: "", now: Date.parse("2026-09-08T12:02:00Z"), ...f.io,
|
|
341
|
+
});
|
|
342
|
+
assert.ok(f.logs.some((l) => l.startsWith("warn:") && /is missing/.test(l)));
|
|
343
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("applyFrontDoorSetup: skills are rendered to the agent's identity (config/agent.json + config/company.json) — a global install still symlinks token-free skills", async () => {
|
|
347
|
+
const f = fixture();
|
|
348
|
+
try {
|
|
349
|
+
const globalRoot = join(f.root, "global", "node_modules");
|
|
350
|
+
const sdkRoot = join(globalRoot, "@cohortapp", "agent-sdk");
|
|
351
|
+
const src = join(sdkRoot, "plugins", "maestro-skills", "skills");
|
|
352
|
+
mkdirSync(src, { recursive: true });
|
|
353
|
+
writeFileSync(join(src, "main-session.md"), "---\nname: main-session\n---\nYou are `{{agent.firstName}}`'s main session; {{agent.fullName}}, {{agent.title}} at {{company.name}}.\n");
|
|
354
|
+
writeFileSync(join(src, "board-work.md"), "---\nname: board-work\n---\nplain\n");
|
|
355
|
+
writeFileSync(join(f.agentRoot, "config", "company.json"), JSON.stringify({ name: "Acme Corp" }));
|
|
356
|
+
const r = await applyFrontDoorSetup({
|
|
357
|
+
agentRoot: f.agentRoot, sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
358
|
+
claudeBin: null, globalRoot, now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
359
|
+
});
|
|
360
|
+
assert.equal(r.skills.mode, "symlink");
|
|
361
|
+
assert.deepEqual(r.skills.rendered, ["main-session"]);
|
|
362
|
+
const main = join(f.claudeDir, "skills", "maestro-main-session", "SKILL.md");
|
|
363
|
+
assert.equal(lstatSync(main).isSymbolicLink(), false);
|
|
364
|
+
assert.match(readFileSync(main, "utf8"), /You are `Alex`'s main session; Alex Rivera, Chief of Staff at Acme Corp\./);
|
|
365
|
+
assert.ok(lstatSync(join(f.claudeDir, "skills", "maestro-board-work", "SKILL.md")).isSymbolicLink());
|
|
366
|
+
assert.ok(f.logs.some((l) => /rendered/.test(l)), "the summary line says which skills were rendered");
|
|
367
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Step 5 (WP-M7): the vendored design skill packs. These run against the REAL
|
|
372
|
+
// vendored trees in this checkout, into a throwaway HOME — the seam that
|
|
373
|
+
// matters is "global-setup put impeccable/unlazy/... on the machine", and a
|
|
374
|
+
// synthetic pack would not prove it.
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
test("applyFrontDoorSetup: installs the vendored design packs into a throwaway HOME under their own names, idempotently", async () => {
|
|
378
|
+
const f = fixture();
|
|
379
|
+
try {
|
|
380
|
+
const opts = {
|
|
381
|
+
agentRoot: f.agentRoot, sdkRoot: SDK_ROOT, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
382
|
+
claudeBin: null, globalRoot: "", now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
383
|
+
};
|
|
384
|
+
const r = await applyFrontDoorSetup(opts);
|
|
385
|
+
assert.deepEqual(r.vendorSkills.errors, []);
|
|
386
|
+
assert.deepEqual(r.vendorSkills.skipped, []);
|
|
387
|
+
for (const name of ["impeccable", "unlazy", "animate", "design-taste-frontend"]) {
|
|
388
|
+
assert.ok(r.vendorSkills.installed.includes(name), `${name} installed`);
|
|
389
|
+
assert.ok(existsSync(join(f.claudeDir, "skills", name, "SKILL.md")), `${name}/SKILL.md on disk`);
|
|
390
|
+
}
|
|
391
|
+
// Under its own name, NOT the maestro- prefix the shipped skills use.
|
|
392
|
+
assert.equal(existsSync(join(f.claudeDir, "skills", "maestro-impeccable")), false);
|
|
393
|
+
// Licences travel with the content.
|
|
394
|
+
assert.ok(existsSync(join(f.claudeDir, "skills", "impeccable", "NOTICE.md")));
|
|
395
|
+
assert.ok(existsSync(join(f.claudeDir, "skills", "animate", "LICENSE")));
|
|
396
|
+
// No hook manifest is ever written next to a vendored skill.
|
|
397
|
+
assert.equal(existsSync(join(f.claudeDir, "skills", "unlazy", "hooks.json")), false);
|
|
398
|
+
assert.equal(existsSync(join(f.claudeDir, "skills", "unlazy", "settings.json")), false);
|
|
399
|
+
|
|
400
|
+
const again = await applyFrontDoorSetup({ ...opts, now: Date.parse("2026-09-08T12:05:00Z") });
|
|
401
|
+
assert.deepEqual(again.vendorSkills.installed, [], "second run writes nothing");
|
|
402
|
+
assert.ok(again.vendorSkills.unchanged.length >= 14);
|
|
403
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("applyFrontDoorSetup: a design-skill directory that is not ours is left alone and reported", async () => {
|
|
407
|
+
const f = fixture();
|
|
408
|
+
try {
|
|
409
|
+
const foreign = join(f.claudeDir, "skills", "impeccable");
|
|
410
|
+
mkdirSync(foreign, { recursive: true });
|
|
411
|
+
const body = "---\nname: impeccable\n---\nthe human's own install\n";
|
|
412
|
+
writeFileSync(join(foreign, "SKILL.md"), body);
|
|
413
|
+
const r = await applyFrontDoorSetup({
|
|
414
|
+
agentRoot: f.agentRoot, sdkRoot: SDK_ROOT, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
415
|
+
claudeBin: null, globalRoot: "", now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
416
|
+
});
|
|
417
|
+
assert.equal(readFileSync(join(foreign, "SKILL.md"), "utf8"), body, "untouched");
|
|
418
|
+
assert.deepEqual(r.vendorSkills.skipped.map((s) => s.name), ["impeccable"]);
|
|
419
|
+
assert.ok(f.logs.some((l) => l.startsWith("warn:") && /impeccable/.test(l) && /left alone/.test(l)));
|
|
420
|
+
assert.ok(r.vendorSkills.installed.includes("unlazy"), "the other packs still install");
|
|
421
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("applyFrontDoorSetup: MAESTRO_SKIP_VENDOR_SKILLS keeps the packs off the machine and every other step on", async () => {
|
|
425
|
+
// ~/.claude/skills is global: these packs load for work in the product repo,
|
|
426
|
+
// in customer repos and in scratch directories that have nothing to do with
|
|
427
|
+
// Cohort design. A seat that does not want them needs a way to say so that
|
|
428
|
+
// does not cost it its identity block or its tools.
|
|
429
|
+
const f = fixture();
|
|
430
|
+
const prev = process.env.MAESTRO_SKIP_VENDOR_SKILLS;
|
|
431
|
+
process.env.MAESTRO_SKIP_VENDOR_SKILLS = "1";
|
|
432
|
+
try {
|
|
433
|
+
const r = await applyFrontDoorSetup({
|
|
434
|
+
agentRoot: f.agentRoot, sdkRoot: SDK_ROOT, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
435
|
+
claudeBin: null, globalRoot: "", now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
436
|
+
});
|
|
437
|
+
assert.equal(r.vendorSkills.skippedByEnv, true);
|
|
438
|
+
assert.equal(existsSync(join(f.claudeDir, "skills", "impeccable")), false);
|
|
439
|
+
assert.equal(existsSync(join(f.claudeDir, "skills", "design-taste-frontend")), false);
|
|
440
|
+
assert.equal(r.ok, true);
|
|
441
|
+
assert.equal(r.identity.changed, true, "identity still wired");
|
|
442
|
+
assert.ok(existsSync(join(f.claudeDir, "skills", "maestro-cohort-design")), "maestro's own skills still install");
|
|
443
|
+
} finally {
|
|
444
|
+
if (prev === undefined) delete process.env.MAESTRO_SKIP_VENDOR_SKILLS;
|
|
445
|
+
else process.env.MAESTRO_SKIP_VENDOR_SKILLS = prev;
|
|
446
|
+
rmSync(f.root, { recursive: true, force: true });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("applyFrontDoorSetup: an SDK copy with no vendored trees warns and still wires everything else", async () => {
|
|
451
|
+
const f = fixture();
|
|
452
|
+
try {
|
|
453
|
+
const r = await applyFrontDoorSetup({
|
|
454
|
+
agentRoot: f.agentRoot, sdkRoot: f.sdkRoot, claudeDir: f.claudeDir, claudeJsonPath: f.claudeJsonPath,
|
|
455
|
+
claudeBin: null, globalRoot: "", now: Date.parse("2026-09-08T12:00:00Z"), ...f.io,
|
|
456
|
+
});
|
|
457
|
+
assert.equal(r.ok, true, "a missing vendor tree is fail-open, not a failed setup");
|
|
458
|
+
assert.ok(r.vendorSkills.errors.length > 0);
|
|
459
|
+
assert.ok(f.logs.some((l) => l.startsWith("warn:") && /design pack/.test(l)));
|
|
460
|
+
assert.equal(r.identity.changed, true, "identity still wired");
|
|
461
|
+
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
462
|
+
});
|