@cohortapp/agent-sdk 2.12.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/bin/maestro.mjs +6 -2
  2. package/docs/guides/front-door-session.md +86 -0
  3. package/lib/cli/design.mjs +185 -0
  4. package/lib/cli/design.test.mjs +270 -0
  5. package/lib/cli/global-setup-extras.mjs +44 -0
  6. package/lib/cli/global-setup-extras.test.mjs +95 -0
  7. package/lib/cli/session.mjs +11 -1
  8. package/lib/cli/session.test.mjs +17 -6
  9. package/lib/collective/global-config.mjs +5 -0
  10. package/lib/collective/global-config.test.mjs +5 -0
  11. package/lib/collective/vendor-skills.mjs +305 -0
  12. package/lib/collective/vendor-skills.test.mjs +306 -0
  13. package/lib/design/design-md.mjs +793 -0
  14. package/lib/design/design-md.test.mjs +318 -0
  15. package/lib/design/fixtures/DESIGN.golden.md +238 -0
  16. package/lib/design/fixtures/PRODUCT.golden.md +67 -0
  17. package/lib/design/fixtures/foundation.json +133 -0
  18. package/lib/design/refresh-gate.mjs +154 -0
  19. package/lib/design/refresh-gate.test.mjs +144 -0
  20. package/lib/design/write.mjs +275 -0
  21. package/lib/design/write.test.mjs +241 -0
  22. package/lib/prompts/parallelism.mjs +79 -0
  23. package/lib/prompts/parallelism.test.mjs +177 -0
  24. package/lib/telemetry/collect.mjs +357 -5
  25. package/lib/telemetry/collect.test.mjs +285 -0
  26. package/package.json +1 -1
  27. package/plugins/maestro-skills/plugin.json +4 -0
  28. package/plugins/maestro-skills/skills/cohort-design.md +153 -0
  29. package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
  30. package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
  31. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
  32. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
  33. package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
  34. package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
  35. package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
  36. package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
  37. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
  38. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
  39. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
  40. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
  41. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
  42. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
  43. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
  44. package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
  45. package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
  46. package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
  47. package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
  48. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
  49. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
  50. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
  51. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
  52. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
  53. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
  54. package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
  55. package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
  56. package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
  57. package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
  58. package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
  59. package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
  60. package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
  61. package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
  62. package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
  63. package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
  64. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
  65. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
  66. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
  67. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  68. package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
  69. package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
  70. package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
  71. package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
  72. package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
  73. package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
  74. package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
  75. package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
  76. package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
  77. package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
  78. package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
  79. package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
  80. package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
  81. package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
  82. package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
  83. package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
  84. package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
  85. package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
  86. package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
  87. package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
  88. package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
  89. package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
  90. package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
  91. package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
  92. package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
  93. package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
  94. package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
  95. package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
  96. package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
  97. package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
  98. package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
  99. package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
  100. package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
  101. package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
  102. package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
  103. package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
  104. package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
  105. package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
  106. package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
  107. package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
  108. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
  109. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
  110. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
  111. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
  112. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
  113. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
  114. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
  115. package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
  116. package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
  117. package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
  118. package/scripts/ci/check-skill-packs.mjs +388 -0
  119. package/scripts/ci/check-skill-packs.test.mjs +495 -0
  120. package/scripts/ci/check.mjs +3 -0
  121. package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
  122. package/scripts/daemon/agent-daemon.mjs +108 -0
  123. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +61 -2
  124. package/scripts/daemon/cadence-consumer.mjs +46 -22
  125. package/scripts/daemon/prompt-builder.mjs +19 -3
  126. package/scripts/local-triggers/autoupdate.test.mjs +33 -3
  127. package/scripts/vendor/skill-packs.mjs +354 -0
  128. package/scripts/vendor/sync-skill-packs.mjs +242 -0
  129. package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
package/bin/maestro.mjs CHANGED
@@ -1181,7 +1181,8 @@ and in .maestro/upgrade-result.json {from,to,at,steps}):
1181
1181
  launchd install / replace / load every generated plist under
1182
1182
  ~/Library/LaunchAgents (mode 600, launchctl bootstrap);
1183
1183
  -daemon and -session are rewritten, never restarted
1184
- globalSetup maestro global-setup (identity block, cohort MCP, settings, skills)
1184
+ globalSetup maestro global-setup (identity block, cohort MCP, settings, skills,
1185
+ vendored design skill packs)
1185
1186
  globalInstall npm i -g @cohortapp/agent-sdk@<this version>
1186
1187
  (MAESTRO_SKIP_GLOBAL_INSTALL=1 skips)
1187
1188
  verify the --verify report
@@ -3356,7 +3357,8 @@ async function globalSetup(args = []) {
3356
3357
  ok(`${dryRun ? "[dry-run] " : ""}machine pointer → ${pointerFile}`);
3357
3358
 
3358
3359
  // 4) Front-door session (2026-09 §3.7): identity block, cohort MCP server,
3359
- // settings additions, global skills — lib/cli/global-setup-extras.mjs.
3360
+ // settings additions, global skills and the vendored design skill packs
3361
+ // (WP-M7) — lib/cli/global-setup-extras.mjs.
3360
3362
  try {
3361
3363
  const extras = await import("../lib/cli/global-setup-extras.mjs");
3362
3364
  await extras.applyFrontDoorSetup({ agentRoot: cwd, claudeDir, dryRun, log, ok, warn });
@@ -3806,6 +3808,7 @@ switch (command) {
3806
3808
  case "session-ack": process.exitCode = await (await import("../lib/cli/session-ack.mjs")).runSessionAck(args); break;
3807
3809
  case "session": { const r = await (await import("../lib/cli/session.mjs")).run(args); process.exitCode = r.code; break; }
3808
3810
  case "board": await (await import("../lib/cli/board.mjs")).boardCmd(args); break;
3811
+ case "design": await (await import("../lib/cli/design.mjs")).designCmd(args); break;
3809
3812
  case "init":
3810
3813
  case "update-init":
3811
3814
  case "init-update":
@@ -3835,6 +3838,7 @@ Usage:
3835
3838
  npx @cohortapp/agent-sdk session-ack <tickId> Ack a cadence tick handed to the main session
3836
3839
  npx @cohortapp/agent-sdk session <cmd> Front-door session: status|attach|start|stop|restart|spawn|peers|handoffs|ack
3837
3840
  npx @cohortapp/agent-sdk board mine|track|claim|complete Your work across every board (maestro board --help)
3841
+ npx @cohortapp/agent-sdk design sync [--out <dir>] Pull the brand foundation → DESIGN.md + PRODUCT.md
3838
3842
 
3839
3843
  Upgrade flags:
3840
3844
  --dry-run, -n Preview changes without writing
@@ -47,6 +47,43 @@ direction. The beat reports which lane is active: `machine.frontDoor`
47
47
  open record — rather than `session`, which hq validates strictly and which
48
48
  stays `null` while no work session runs.
49
49
 
50
+ **And WHY it is not live.** `sessionLive: false` on its own is a symptom with
51
+ several cures, and no seat in the fleet accepts SSH, so the beat has to carry
52
+ the cause too. When (and only when) the front door is down, the beat adds
53
+ `machine.sessionNote` — `{reason, since?, detail?}`:
54
+
55
+ | `reason` | what happened | what to do |
56
+ | --- | --- | --- |
57
+ | `job-absent` | no `ai.maestro.<first>-session` plist in `~/Library/LaunchAgents`. A statement of configuration, not necessarily a fault — a seat whose front door is deliberately the daemon lane reports this on every beat | nothing, if that is the intent; else `maestro session install` |
58
+ | `awaiting-input` | `state/session/attention.json` exists and no later heartbeat has superseded it: the session is up but has never beaten, so it is sitting on a first-run trust/permission dialog | attach with the command in `detail` and answer it |
59
+ | `heartbeat-stale` | a heartbeat exists but has aged out — supervisor died, or the mux session ended; `since` is the last beat | `maestro session restart` |
60
+ | `heartbeat-unreadable` | `heartbeat.json` is there and will not parse. The job may well have beaten — this is NOT "never beaten" | delete the file and let the feed rewrite it |
61
+ | `never-beaten` | no heartbeat file at all and no watchdog record yet (the supervisor's watchdog fires 120 s in) | wait a minute, then `maestro session status` |
62
+
63
+ `detail` is capped at 200 characters and scrubbed of home paths, session ids
64
+ and token-shaped words before it leaves the machine
65
+ (`lib/telemetry/collect.mjs#sanitizeNoteDetail`); the attach command survives,
66
+ because that is the actionable part. `since` is never passed through from the
67
+ file either — it is parsed and re-emitted as canonical ISO 8601, so nothing can
68
+ ride it that the `detail` scrubber would have caught.
69
+
70
+ Two rules the reasons obey, because a note that misleads is worse than no note:
71
+
72
+ - **An unknown is reported as neither an absence nor a presence.** `job-absent`
73
+ comes only from a directory that was read and genuinely did not hold the
74
+ label; an unreadable `~/Library/LaunchAgents` (or a non-Mac seat) yields
75
+ `never-beaten` with a `detail` that says the job's presence could not be
76
+ checked, rather than claiming an installation nobody verified.
77
+ - **Every reason is PRESENT-TENSE and expires with the snapshot**, like
78
+ `frontDoor` and `sessionLive` and unlike `machine.upgrade`. `job-absent` and
79
+ `never-beaten` carry no `since`, so a consumer dates them from the snapshot's
80
+ own `ts` — printing "attach and answer the dialog" off a week-old snapshot is
81
+ the mistake this note exists to prevent, not to cause.
82
+
83
+ The decision itself is the pure `sessionNote(...)` over injected inputs — the
84
+ reads that feed it are fail-open, so a throw anywhere in them drops the field
85
+ rather than the beat.
86
+
50
87
  **Exactly one.** The supervisor takes an O_EXCL lock (`lib/singleton.js`,
51
88
  name `session`); a second supervisor exits 0 so launchd does not thrash. An
52
89
  orphaned mux session with no lock holder is adopted, not duplicated.
@@ -245,6 +282,53 @@ To the humans it talks to, the agent is one persona. Peer sessions and
245
282
  sub-agents are "my team" / "a colleague"; the persona audit hook blocks any
246
283
  outbound text that says otherwise.
247
284
 
285
+ ## 6a. DESIGN.md and the parallelism directive
286
+
287
+ Two things reach every prompt a seat runs, and neither is left to a session to
288
+ remember.
289
+
290
+ **DESIGN.md and PRODUCT.md.** `maestro design sync` reads the workspace's SAVED
291
+ brand foundation (`branding.getFoundation` — never a draft) and renders it into
292
+ `DESIGN.md` (colour, type, radius, spacing and component tokens, in the
293
+ google-labs-code/design.md frontmatter shape, with `{colors.primary}`-style
294
+ refs) and `PRODUCT.md` (positioning, voice, tone, lexicon) in the agent dir,
295
+ plus the raw snapshot at `state/design/foundation.json`. The daemon re-runs the
296
+ same code path every 15 minutes and rewrites the files when the foundation's
297
+ version has moved, so a seat's design files follow the humans editing the
298
+ foundation without anyone re-running a command.
299
+
300
+ Both files are GENERATED. An edit to them is overwritten on the next sync;
301
+ foundation changes are human-gated and go through the `design_propose_change`
302
+ tool. The write is byte-idempotent — an unchanged foundation touches no mtime,
303
+ so the poll never churns the tree, and the render stamp is kept per output
304
+ directory so a `--out` run and the daemon's poll of the agent dir cannot
305
+ invalidate each other's files. Every posture here is fail-open: an un-enrolled
306
+ seat writes nothing, and a failed read keeps the last-good files rather than
307
+ blanking them. Design skills are told to read `$AGENT_ROOT/DESIGN.md` when the
308
+ working directory is not the agent dir.
309
+
310
+ A workspace that has never SAVED a foundation writes nothing at all, and the
311
+ CLI says why. hq answers such a read with its own product defaults and
312
+ `unversioned: true` — a complete, plausible palette that is nobody's brand — and
313
+ a DESIGN.md rendered from it would ground every design skill on the seat in the
314
+ wrong palette with a generated file's authority behind it.
315
+
316
+ maestro design sync # into the agent dir
317
+ maestro design sync --out docs/brand --json
318
+
319
+ **The parallelism directive.** `lib/prompts/parallelism.mjs` holds one constant,
320
+ prepended exactly once (it is idempotent, and the seams compose) to every
321
+ peer-session prompt by `maestro session spawn`, to every sub-session prompt
322
+ built by the daemon's prompt-builder, and to every cadence trigger prompt on the
323
+ escalate/guarded lane — both when a sub-session is spawned for it and when a
324
+ live main session is handed it (`renderCadencePromptBody`, one seam for both, so
325
+ the two lanes send the same bytes). It grants the standing permission to
326
+ dispatch independent tasks as one parallel batch, and carries the safety rule
327
+ that makes that safe on a shared checkout: check the planned file scope first,
328
+ and never assign two agents to edit the same file — sequence or re-scope
329
+ instead. Sessions ran plans strictly serially before it existed, because
330
+ nothing in the prompt had ever said they need not.
331
+
248
332
  ## 7. Files
249
333
 
250
334
  | Path | What |
@@ -258,6 +342,8 @@ outbound text that says otherwise.
258
342
  | `state/session/upgrade-notice.json` | written by autoupdate; the session restarts itself when idle |
259
343
  | `state/session.lock` | the singleton lock |
260
344
  | `state/inbox/cohort/*.yaml` | inbound items (`.dispatched` = claimed, `.processed` = done, `.deferred`) |
345
+ | `DESIGN.md`, `PRODUCT.md` | generated from the workspace brand foundation by `maestro design sync` and the daemon's 15-minute poll; edits are overwritten |
346
+ | `state/design/foundation.json` | `{fetchedAt, renderedAt, version, versionLabel, foundation}` — the last synced foundation; the refresh gate reads it back |
261
347
  | `state/org/board-mine.json` | `{ts, items}` — the daemon's 5-minute cache of hq `board.mine` (this agent's items across every board); read by the SessionStart primer, the `session_status` MCP tool and `maestro board mine` |
262
348
  | `.maestro/upgrade-result.json` | `{from, to, at, steps:{plists, launchd, globalSetup, globalInstall, verify}}` — what the last `maestro upgrade` did to the seat |
263
349
  | `state/autoupdate/last.json` | `{from, to, at, ok, healthy, reason}` — the last autoupdate attempt; the beat's `machine.upgrade` |
@@ -0,0 +1,185 @@
1
+ /**
2
+ * lib/cli/design.mjs — `maestro design sync`.
3
+ *
4
+ * maestro design sync [--out <dir>] [--json]
5
+ *
6
+ * Pulls the workspace's SAVED brand foundation from hq (`branding.getFoundation`
7
+ * — the same read the `design_foundation` tool makes; drafts are never visible
8
+ * to a seat) and lands it as `DESIGN.md` + `PRODUCT.md` in the agent dir, plus
9
+ * the raw snapshot at `state/design/foundation.json`. Design skills read those
10
+ * files; this command is how they stop being fiction.
11
+ *
12
+ * FAIL POSTURE. A fetch that fails writes NOTHING and exits non-zero with the
13
+ * server's own message. That is deliberate and is the opposite of the daemon's
14
+ * posture: a human who typed `design sync` is owed the error, whereas a
15
+ * background poll must never overwrite a good DESIGN.md with a half-answer from
16
+ * an hq that was briefly down. Nothing here throws — expected failures are
17
+ * printed and become exit 1.
18
+ *
19
+ * IDEMPOTENT. Re-running against an unchanged foundation rewrites no file (the
20
+ * comparison is on rendered bytes — `lib/design/write.mjs#syncDesign`), so a
21
+ * cron-y re-sync leaves mtimes and the git tree alone.
22
+ *
23
+ * `runDesign(argv, deps)` is the testable core (every I/O injectable);
24
+ * `designCmd(argv)` is the bin entry.
25
+ *
26
+ * @module lib/cli/design
27
+ */
28
+
29
+ "use strict";
30
+
31
+ import { loadOrgConfig, configFromAgent, isEnabled, brandingGetFoundation } from "../org/client.mjs";
32
+ import { syncDesign, workspaceName } from "../design/write.mjs";
33
+ import { DESIGN_FILES, DESIGN_STATE_REL, isUnversionedFoundation } from "../design/refresh-gate.mjs";
34
+
35
+ export function usage() {
36
+ return [
37
+ "Usage:",
38
+ " maestro design sync [--out <dir>] [--json] Pull the brand foundation and write DESIGN.md + PRODUCT.md",
39
+ "",
40
+ `Writes ${DESIGN_FILES.join(" + ")} into the agent dir (or --out) and the raw`,
41
+ `snapshot to ${DESIGN_STATE_REL}. Unchanged files are left untouched.`,
42
+ "Foundation changes are human-gated: propose one with the design_propose_change tool.",
43
+ ].join("\n");
44
+ }
45
+
46
+ /**
47
+ * Parse argv → {sub, out, json, help, error}. Pure.
48
+ * @param {string[]} argv
49
+ */
50
+ export function parseDesignArgs(argv) {
51
+ const args = Array.isArray(argv) ? argv.map(String) : [];
52
+ const out = { sub: null, out: null, json: false, help: false, error: null };
53
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) { out.help = true; return out; }
54
+ out.sub = args[0];
55
+ const rest = args.slice(1);
56
+ for (let i = 0; i < rest.length; i++) {
57
+ const a = rest[i];
58
+ const eq = a.indexOf("=");
59
+ const key = a.startsWith("--") && eq > 0 ? a.slice(0, eq) : a;
60
+ const inline = a.startsWith("--") && eq > 0 ? a.slice(eq + 1) : undefined;
61
+ const take = () => (inline !== undefined ? inline : rest[++i]);
62
+ if (key === "--json") out.json = true;
63
+ else if (key === "--out") {
64
+ // A MISSING value is an error, never a silent fallback. `--out` with the
65
+ // path forgotten (or eaten by the shell) used to leave `out` null and
66
+ // write DESIGN.md into the agent dir — exactly where the operator was
67
+ // trying not to write — and `--out --json` used to create a directory
68
+ // called "--json". Both exit 1 now and write nothing.
69
+ const v = take();
70
+ if (v === undefined) { out.error = "--out needs a directory"; return out; }
71
+ if (typeof v === "string" && v.startsWith("--")) { out.error = `--out needs a directory, not ${v}`; return out; }
72
+ out.out = v;
73
+ }
74
+ else { out.error = `unknown argument ${a}`; return out; }
75
+ }
76
+ if (out.sub !== "sync") out.error = `unknown design command: ${out.sub}`;
77
+ else if (out.out !== null && !String(out.out).trim()) out.error = "--out needs a directory";
78
+ return out;
79
+ }
80
+
81
+ /** A protocol error frame → one readable line. */
82
+ export function describeFrame(frame) {
83
+ const e = frame && frame.error ? frame.error : null;
84
+ if (e) return `${e.code || "error"}: ${e.message || ""}`.trim();
85
+ return "no response";
86
+ }
87
+
88
+ /**
89
+ * Re-exported so `maestro design sync` and the daemon's poller name the
90
+ * workspace identically — the heading is part of the rendered bytes, so two
91
+ * definitions would make the two lanes rewrite each other's files forever.
92
+ * The definition lives in `lib/design/write.mjs`.
93
+ */
94
+ export { workspaceName };
95
+
96
+ /**
97
+ * The testable core. deps: {agentRoot, cfg, now, write, fetchImpl,
98
+ * getFoundationImpl, syncImpl, io}.
99
+ * @param {string[]} argv
100
+ * @param {object} [deps]
101
+ * @returns {Promise<{code:number, data?:object}>}
102
+ */
103
+ export async function runDesign(argv, deps = {}) {
104
+ const write = typeof deps.write === "function" ? deps.write : (s) => process.stdout.write(`${s}\n`);
105
+ const p = parseDesignArgs(argv);
106
+ if (p.help) { write(usage()); return { code: 0 }; }
107
+ if (p.error) { write(`error: ${p.error}\n\n${usage()}`); return { code: 1 }; }
108
+
109
+ const agentRoot = deps.agentRoot || process.cwd();
110
+ const cfg = deps.cfg || loadOrgConfig(agentRoot);
111
+ if (!isEnabled(cfg)) {
112
+ write("error: the org (Cohort) is not enabled for this agent — run `maestro setup --only org`.");
113
+ return { code: 1 };
114
+ }
115
+ const conn = configFromAgent(cfg) || {};
116
+ const now = Number.isFinite(deps.now) ? deps.now : Date.now();
117
+ const outDir = p.out ? String(p.out) : agentRoot;
118
+
119
+ let frame;
120
+ try {
121
+ frame = await (deps.getFoundationImpl || brandingGetFoundation)(
122
+ { historyLimit: 0 },
123
+ { base: conn.base, token: conn.token, orgId: conn.orgId, fetchImpl: deps.fetchImpl },
124
+ );
125
+ } catch (err) {
126
+ // A transport throw is the same class of failure as an error frame: say so
127
+ // and write nothing. The last-good DESIGN.md on disk stays authoritative.
128
+ write(`error: could not read the brand foundation — ${err && err.message ? err.message : String(err)}. Nothing was written.`);
129
+ return { code: 1 };
130
+ }
131
+ if (!frame || !frame.ok || !frame.result || typeof frame.result !== "object") {
132
+ write(`error: could not read the brand foundation — ${describeFrame(frame)}. Nothing was written.`);
133
+ return { code: 1 };
134
+ }
135
+ if (!frame.result.foundation || typeof frame.result.foundation !== "object") {
136
+ write("error: the workspace has no brand foundation yet — set one up in Design first. Nothing was written.");
137
+ return { code: 1 };
138
+ }
139
+ // AN UNVERSIONED WORKSPACE IS NOT A BRAND. hq answers a workspace that has
140
+ // never saved a foundation with its own product defaults and `unversioned:
141
+ // true` — a complete, plausible palette that is nobody's brand. Writing it
142
+ // would hand every design skill on the seat a wrong palette wearing the
143
+ // authority of a generated file. See refresh-gate.mjs#isUnversionedFoundation.
144
+ if (isUnversionedFoundation(frame.result)) {
145
+ write("error: this workspace has never saved a brand foundation — hq answered with stock defaults, which are not your brand. Set one up in Design (or propose one with design_propose_change) and re-run. Nothing was written.");
146
+ return { code: 1 };
147
+ }
148
+
149
+ let result;
150
+ try {
151
+ result = (deps.syncImpl || syncDesign)(
152
+ { agentRoot, outDir, result: frame.result, now, name: deps.name || workspaceName(cfg), source: "cohort · branding.getFoundation" },
153
+ deps.io || {},
154
+ );
155
+ } catch (err) {
156
+ write(`error: could not write the design files — ${err && err.message ? err.message : String(err)}`);
157
+ return { code: 1 };
158
+ }
159
+
160
+ if (p.json) {
161
+ write(JSON.stringify(result, null, 2));
162
+ return { code: 0, data: result };
163
+ }
164
+ const wrote = result.written.length
165
+ ? `wrote ${result.written.join(", ")}`
166
+ : `unchanged (${result.unchanged.join(", ")})`;
167
+ write(`design sync: ${wrote} in ${result.outDir}${result.version ? ` — foundation ${result.version}` : ""}`);
168
+ return { code: 0, data: result };
169
+ }
170
+
171
+ /** bin entry: resolve the agent root (env → walk → machine pointer) and run. */
172
+ export async function designCmd(argv) {
173
+ let agentRoot = null;
174
+ try {
175
+ const { resolveAgentRoot } = await import("../collective/config.mjs");
176
+ agentRoot = resolveAgentRoot(process.cwd());
177
+ } catch {
178
+ agentRoot = null; // fall through to cwd
179
+ }
180
+ const r = await runDesign(argv, { agentRoot: agentRoot || process.cwd() });
181
+ process.exitCode = r.code;
182
+ return r;
183
+ }
184
+
185
+ export default { runDesign, designCmd, parseDesignArgs, workspaceName, describeFrame, usage };
@@ -0,0 +1,270 @@
1
+ /**
2
+ * design.test.mjs — `maestro design sync` (WP-M7 mechanic 4).
3
+ *
4
+ * Drives the REAL `runDesign` against a temp agent dir with an injected
5
+ * `branding.getFoundation`. Two claims carry the command and both are load
6
+ * bearing:
7
+ *
8
+ * · FAIL-OPEN MEANS "WRITE NOTHING". A seat's DESIGN.md is what every design
9
+ * skill grounds on. A failed read must leave the last-good file exactly
10
+ * where it is and exit non-zero — the opposite of half-writing a file that
11
+ * then looks authoritative.
12
+ * · IDEMPOTENT BY BYTES. A second run against an unchanged foundation must
13
+ * not touch a single mtime, so the 15-minute daemon poll never churns the
14
+ * tree or wakes an editor's file watcher.
15
+ */
16
+
17
+ import { test } from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { mkdtempSync, readFileSync, writeFileSync, statSync, utimesSync, existsSync, rmSync, readdirSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { tmpdir } from "node:os";
22
+ import { execFileSync } from "node:child_process";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ import { runDesign, parseDesignArgs, describeFrame, workspaceName, usage } from "./design.mjs";
26
+
27
+ const NOW = Date.parse("2026-09-08T12:00:00Z");
28
+ const FIXTURE = JSON.parse(readFileSync(new URL("../design/fixtures/foundation.json", import.meta.url), "utf8"));
29
+ const CFG = { org: { cohort: { enabled: true, base: "https://os.example.test", token: "nlk_t", orgId: "org_1", orgName: "Northwind" } } };
30
+
31
+ const okFrame = (over = {}) => ({
32
+ ok: true,
33
+ result: {
34
+ foundation: FIXTURE,
35
+ version: { id: "bv_01HQZ", seq: 7, label: "V2.7" },
36
+ headVersionId: "bv_01HQZ",
37
+ isHead: true,
38
+ unversioned: false,
39
+ history: [],
40
+ ...over,
41
+ },
42
+ });
43
+
44
+ function harness(t, opts = {}) {
45
+ const root = mkdtempSync(join(tmpdir(), "maestro-design-cli-"));
46
+ t.after(() => rmSync(root, { recursive: true, force: true }));
47
+ const lines = [];
48
+ const calls = [];
49
+ const deps = {
50
+ agentRoot: root,
51
+ cfg: opts.cfg === undefined ? CFG : opts.cfg,
52
+ now: opts.now === undefined ? NOW : opts.now,
53
+ write: (s) => lines.push(s),
54
+ getFoundationImpl: opts.getFoundationImpl || (async (params, o) => { calls.push({ params, o }); return okFrame(); }),
55
+ };
56
+ return { root, lines, calls, deps, out: () => lines.join("\n") };
57
+ }
58
+
59
+ // ── argument parsing ────────────────────────────────────────────────────────
60
+
61
+ test("parseDesignArgs handles the flags, both spellings, and rejects the rest", () => {
62
+ assert.deepEqual(parseDesignArgs(["sync"]), { sub: "sync", out: null, json: false, help: false, error: null });
63
+ assert.equal(parseDesignArgs(["sync", "--json"]).json, true);
64
+ assert.equal(parseDesignArgs(["sync", "--out", "docs"]).out, "docs");
65
+ assert.equal(parseDesignArgs(["sync", "--out=docs"]).out, "docs");
66
+ assert.equal(parseDesignArgs([]).help, true);
67
+ assert.equal(parseDesignArgs(["--help"]).help, true);
68
+ assert.equal(parseDesignArgs(["-h"]).help, true);
69
+ assert.match(parseDesignArgs(["shove"]).error, /unknown design command: shove/);
70
+ assert.match(parseDesignArgs(["sync", "--wat"]).error, /unknown argument --wat/);
71
+ assert.match(parseDesignArgs(["sync", "--out", ""]).error, /--out needs a directory/);
72
+
73
+ // A MISSING value is an error, not a silent fallback to the agent dir —
74
+ // which is precisely the directory the operator was trying not to write to.
75
+ assert.match(parseDesignArgs(["sync", "--out"]).error, /--out needs a directory/);
76
+ assert.equal(parseDesignArgs(["sync", "--out"]).out, null);
77
+ assert.match(parseDesignArgs(["sync", "--out", "--json"]).error, /--out needs a directory, not --json/);
78
+ assert.equal(parseDesignArgs(["sync", "--out", "--json"]).json, false, "and --json was not eaten as a path");
79
+ });
80
+
81
+ test("`--out` with no path exits 1 and writes nothing at all", async (t) => {
82
+ const h = harness(t);
83
+ const r = await runDesign(["sync", "--out"], h.deps);
84
+ assert.equal(r.code, 1);
85
+ assert.match(h.out(), /--out needs a directory/);
86
+ assert.equal(h.calls.length, 0, "not even a read is spent");
87
+ assert.deepEqual(readdirSync(h.root), [], "and no DESIGN.md lands in the agent dir by accident");
88
+ });
89
+
90
+ test("--help and a bad argument both print the usage; only the bad argument is an error", async (t) => {
91
+ const h = harness(t);
92
+ assert.deepEqual(await runDesign(["--help"], h.deps), { code: 0 });
93
+ assert.match(h.out(), /maestro design sync/);
94
+
95
+ const h2 = harness(t);
96
+ const r = await runDesign(["sync", "--nope"], h2.deps);
97
+ assert.equal(r.code, 1);
98
+ assert.match(h2.out(), /error: unknown argument --nope/);
99
+ assert.match(h2.out(), /maestro design sync/, "the usage follows the error");
100
+ assert.ok(usage().includes("design_propose_change"), "the usage says how a change is actually made");
101
+ });
102
+
103
+ // ── the happy path ──────────────────────────────────────────────────────────
104
+
105
+ test("sync writes DESIGN.md, PRODUCT.md and the snapshot, and reads the SAVED foundation", async (t) => {
106
+ const h = harness(t);
107
+ const r = await runDesign(["sync"], h.deps);
108
+
109
+ assert.equal(r.code, 0);
110
+ assert.deepEqual(r.data.written, ["DESIGN.md", "PRODUCT.md"]);
111
+ assert.ok(existsSync(join(h.root, "DESIGN.md")));
112
+ assert.ok(existsSync(join(h.root, "PRODUCT.md")));
113
+ assert.ok(existsSync(join(h.root, "state", "design", "foundation.json")));
114
+ assert.match(h.out(), /design sync: wrote DESIGN\.md, PRODUCT\.md/);
115
+ assert.match(h.out(), /foundation bv_01HQZ/);
116
+
117
+ assert.equal(h.calls.length, 1);
118
+ assert.deepEqual(h.calls[0].params, { historyLimit: 0 }, "no history is fetched — the seat only needs the head");
119
+ assert.equal(h.calls[0].o.base, "https://os.example.test", "the agent's own connection is used");
120
+ assert.equal(h.calls[0].o.orgId, "org_1");
121
+
122
+ const design = readFileSync(join(h.root, "DESIGN.md"), "utf8");
123
+ assert.match(design, /^---\nname: "Northwind"\n/, "the heading comes from the org config, not the foundation prose");
124
+ assert.match(design, /\{colors\.primary\}/);
125
+ assert.equal(statSync(join(h.root, "DESIGN.md")).mode & 0o777, 0o644);
126
+ assert.equal(statSync(join(h.root, "PRODUCT.md")).mode & 0o777, 0o644);
127
+ });
128
+
129
+ test("--json prints the machine result and still writes the files", async (t) => {
130
+ const h = harness(t);
131
+ const r = await runDesign(["sync", "--json"], h.deps);
132
+ assert.equal(r.code, 0);
133
+ const doc = JSON.parse(h.out());
134
+ assert.deepEqual(doc.written, ["DESIGN.md", "PRODUCT.md"]);
135
+ assert.equal(doc.version, "bv_01HQZ");
136
+ assert.equal(doc.outDir, h.root);
137
+ });
138
+
139
+ test("--out puts the docs elsewhere and keeps the snapshot under the agent root", async (t) => {
140
+ const h = harness(t);
141
+ const outDir = join(h.root, "brand");
142
+ const r = await runDesign(["sync", "--out", outDir], h.deps);
143
+ assert.equal(r.code, 0);
144
+ assert.ok(existsSync(join(outDir, "DESIGN.md")));
145
+ assert.ok(!existsSync(join(h.root, "DESIGN.md")));
146
+ assert.ok(existsSync(join(h.root, "state", "design", "foundation.json")));
147
+ });
148
+
149
+ test("re-running against an unchanged foundation rewrites nothing", async (t) => {
150
+ const h = harness(t);
151
+ await runDesign(["sync"], h.deps);
152
+ const old = new Date(NOW - 86_400_000);
153
+ for (const rel of ["DESIGN.md", "PRODUCT.md"]) utimesSync(join(h.root, rel), old, old);
154
+ const before = ["DESIGN.md", "PRODUCT.md"].map((rel) => statSync(join(h.root, rel)).mtimeMs);
155
+
156
+ const r = await runDesign(["sync"], { ...h.deps, now: NOW + 3_600_000 });
157
+ assert.equal(r.code, 0);
158
+ assert.deepEqual(r.data.written, []);
159
+ assert.deepEqual(r.data.unchanged, ["DESIGN.md", "PRODUCT.md"]);
160
+ assert.match(h.out(), /design sync: unchanged \(DESIGN\.md, PRODUCT\.md\)/);
161
+ assert.deepEqual(["DESIGN.md", "PRODUCT.md"].map((rel) => statSync(join(h.root, rel)).mtimeMs), before);
162
+ });
163
+
164
+ // ── the failure postures ────────────────────────────────────────────────────
165
+
166
+ test("an error frame exits 1 and writes NOTHING", async (t) => {
167
+ const h = harness(t, { getFoundationImpl: async () => ({ ok: false, error: { code: "FORBIDDEN", message: "design.read is not granted to this seat." } }) });
168
+ const r = await runDesign(["sync"], h.deps);
169
+ assert.equal(r.code, 1);
170
+ assert.match(h.out(), /FORBIDDEN: design\.read is not granted to this seat\./);
171
+ assert.match(h.out(), /Nothing was written\./);
172
+ assert.deepEqual(readdirSync(h.root), [], "not one file, not even the state dir");
173
+ });
174
+
175
+ test("a transport throw exits 1, says so, and leaves the last-good files exactly as they were", async (t) => {
176
+ const h = harness(t);
177
+ await runDesign(["sync"], h.deps); // establish a good DESIGN.md first
178
+ const good = readFileSync(join(h.root, "DESIGN.md"), "utf8");
179
+ const old = new Date(NOW - 86_400_000);
180
+ utimesSync(join(h.root, "DESIGN.md"), old, old);
181
+ const mtime = statSync(join(h.root, "DESIGN.md")).mtimeMs;
182
+
183
+ const r = await runDesign(["sync"], { ...h.deps, getFoundationImpl: async () => { throw new Error("fetch failed: ECONNREFUSED"); } });
184
+ assert.equal(r.code, 1);
185
+ assert.match(h.out(), /could not read the brand foundation — fetch failed: ECONNREFUSED/);
186
+ assert.match(h.out(), /Nothing was written\./);
187
+ assert.equal(readFileSync(join(h.root, "DESIGN.md"), "utf8"), good, "the last-good file is untouched");
188
+ assert.equal(statSync(join(h.root, "DESIGN.md")).mtimeMs, mtime);
189
+ });
190
+
191
+ test("a workspace with no foundation yet is named as such, not rendered as an empty brand", async (t) => {
192
+ const h = harness(t, { getFoundationImpl: async () => ({ ok: true, result: { foundation: null, headVersionId: null, unversioned: true, history: [] } }) });
193
+ const r = await runDesign(["sync"], h.deps);
194
+ assert.equal(r.code, 1);
195
+ assert.match(h.out(), /no brand foundation yet — set one up in Design first/);
196
+ assert.deepEqual(readdirSync(h.root), []);
197
+ });
198
+
199
+ test("hq's stock defaults are refused: an unversioned workspace writes nothing and is told why", async (t) => {
200
+ // The FRAME HQ ACTUALLY SENDS for a workspace that never saved a foundation:
201
+ // a complete `dsDefaults()` palette, `unversioned: true`, `headVersionId:
202
+ // null`. The `foundation: null` case above cannot occur in production, so
203
+ // this is the guard that has to hold.
204
+ const h = harness(t, {
205
+ getFoundationImpl: async () => ({
206
+ ok: true,
207
+ result: {
208
+ foundation: { ...FIXTURE, colors: [{ name: "Ink", value: "#0d0d0d" }] },
209
+ version: null,
210
+ headVersionId: null,
211
+ isHead: false,
212
+ unversioned: true,
213
+ history: [],
214
+ },
215
+ }),
216
+ });
217
+ const r = await runDesign(["sync"], h.deps);
218
+ assert.equal(r.code, 1);
219
+ assert.match(h.out(), /never saved a brand foundation/);
220
+ assert.match(h.out(), /stock defaults, which are not your brand/);
221
+ assert.match(h.out(), /Nothing was written\./);
222
+ assert.deepEqual(readdirSync(h.root), [], "no DESIGN.md wearing another product's palette");
223
+ });
224
+
225
+ test("an older hq that omits `unversioned` is judged by the version identity instead", async (t) => {
226
+ const h = harness(t, {
227
+ getFoundationImpl: async () => ({ ok: true, result: { foundation: FIXTURE, version: null, headVersionId: null, history: [] } }),
228
+ });
229
+ const r = await runDesign(["sync"], h.deps);
230
+ assert.equal(r.code, 1);
231
+ assert.match(h.out(), /never saved a brand foundation/);
232
+ assert.deepEqual(readdirSync(h.root), []);
233
+ });
234
+
235
+ test("a seat that is not enrolled in the org exits 1 and points at setup", async (t) => {
236
+ const h = harness(t, { cfg: { org: { cohort: { enabled: false } } } });
237
+ const r = await runDesign(["sync"], h.deps);
238
+ assert.equal(r.code, 1);
239
+ assert.match(h.out(), /not enabled for this agent — run `maestro setup --only org`/);
240
+ assert.equal(h.calls.length, 0, "no hq call is attempted");
241
+ assert.deepEqual(readdirSync(h.root), []);
242
+ });
243
+
244
+ test("a write failure is reported, not thrown", async (t) => {
245
+ const h = harness(t);
246
+ const r = await runDesign(["sync"], { ...h.deps, syncImpl: () => { throw new Error("EROFS: read-only file system"); } });
247
+ assert.equal(r.code, 1);
248
+ assert.match(h.out(), /could not write the design files — EROFS/);
249
+ });
250
+
251
+ test("describeFrame turns any frame into one readable line", () => {
252
+ assert.equal(describeFrame({ ok: false, error: { code: "RATE_LIMITED", message: "Slow down." } }), "RATE_LIMITED: Slow down.");
253
+ assert.equal(describeFrame({ ok: false, error: { message: "no code" } }), "error: no code");
254
+ assert.equal(describeFrame(null), "no response");
255
+ assert.equal(describeFrame({ ok: true }), "no response");
256
+ });
257
+
258
+ test("workspaceName is the one shared with the daemon", async () => {
259
+ const { workspaceName: fromWriter } = await import("../design/write.mjs");
260
+ assert.equal(workspaceName, fromWriter, "one definition, so the CLI and the daemon cannot render different headings");
261
+ });
262
+
263
+ // ── the bin seam ────────────────────────────────────────────────────────────
264
+
265
+ test("`maestro design` is wired into bin/maestro.mjs", () => {
266
+ const bin = fileURLToPath(new URL("../../bin/maestro.mjs", import.meta.url));
267
+ const out = execFileSync(process.execPath, [bin, "design", "--help"], { encoding: "utf8" });
268
+ assert.match(out, /maestro design sync \[--out <dir>\] \[--json\]/);
269
+ assert.match(execFileSync(process.execPath, [bin, "--help"], { encoding: "utf8" }), /design sync/, "and listed in the top-level help");
270
+ });
@@ -15,6 +15,12 @@
15
15
  * persona/audit hook
16
16
  * 4. ~/.claude/skills/maestro-<name>/ every shipped skill (symlink for a global
17
17
  * install, copy otherwise)
18
+ * 5. ~/.claude/skills/<pack skill>/ the vendored design skill packs (WP-M7),
19
+ * copied under their own frontmatter names;
20
+ * a directory that is not ours is never touched,
21
+ * local edits inside one that IS ours are
22
+ * copied aside first, and the whole step is
23
+ * opt-out via MAESTRO_SKIP_VENDOR_SKILLS=1
18
24
  *
19
25
  * Additive, idempotent, backed up: JSON files are copied to
20
26
  * `<file>.backup.<stamp>` before the first write of a run, and a re-run that
@@ -232,6 +238,7 @@ export async function applyFrontDoorSetup(o = {}) {
232
238
  mcp: { changed: false, via: null, skipped: null },
233
239
  settings: { changes: [], skipped: null, hookScript: null },
234
240
  skills: { mode: null, installed: [], unchanged: [] },
241
+ vendorSkills: { installed: [], unchanged: [], skipped: [], errors: [] },
235
242
  };
236
243
  let agentJson = null; // config/agent.json, shared by the identity block and the skill renderer
237
244
 
@@ -404,6 +411,43 @@ export async function applyFrontDoorSetup(o = {}) {
404
411
  warn(`skills install failed: ${err && err.message}`);
405
412
  }
406
413
 
414
+ // 5) Vendored design skill packs (WP-M7) — impeccable, emilkowalski, taste-skill,
415
+ // unlazy, installed under their OWN frontmatter names because that is what
416
+ // their prose and the cohort-design harmoniser refer to. Copies, never
417
+ // links: the point of vendoring is that the seat holds reviewed bytes. A
418
+ // directory that is not ours (the human installed the upstream pack
419
+ // themselves, launcher and hooks and all) is left exactly as it is.
420
+ try {
421
+ // An opt-out, because this step writes into the GLOBAL ~/.claude/skills and
422
+ // so reaches every piece of work done on the machine, including repos that
423
+ // have nothing to do with Cohort design. A seat that does not want the
424
+ // packs sets MAESTRO_SKIP_VENDOR_SKILLS=1 and keeps every other step.
425
+ const skip = String(process.env.MAESTRO_SKIP_VENDOR_SKILLS || "").trim();
426
+ if (skip && skip !== "0" && skip.toLowerCase() !== "false") {
427
+ summary.vendorSkills = { skippedByEnv: true };
428
+ ok("vendored design skills skipped (MAESTRO_SKIP_VENDOR_SKILLS)");
429
+ return summary;
430
+ }
431
+ const [{ PACKS }, vendor] = await Promise.all([
432
+ import("../../scripts/vendor/skill-packs.mjs"),
433
+ import("../collective/vendor-skills.mjs"),
434
+ ]);
435
+ const r = vendor.installVendorSkills({ sdkRoot, skillsDir: join(claudeDir, "skills"), packs: PACKS, dryRun });
436
+ summary.vendorSkills = { installed: r.installed, unchanged: r.unchanged, skipped: r.skipped, backups: r.backups || [], errors: r.errors };
437
+ if (r.installed.length === 0 && r.skipped.length === 0 && r.errors.length === 0) {
438
+ ok(`vendored design skills up to date (${r.unchanged.length} pack skill(s))`);
439
+ } else if (r.installed.length) {
440
+ ok(`${pre}vendored design skills copied: ${r.installed.join(", ")}${r.unchanged.length ? ` (+${r.unchanged.length} unchanged)` : ""}`);
441
+ }
442
+ for (const b of r.backups || []) warn(`design skill ${b.name}: local edits found (${b.files.join(", ")}) — copied to ${b.path} before rewriting`);
443
+ for (const sk of r.skipped) warn(`design skill ${sk.name} left alone: ${sk.reason}`);
444
+ for (const e of r.errors || []) warn(`design pack ${e.pack}: ${e.error}`);
445
+ } catch (err) {
446
+ // Fail-open: an SDK copy that predates WP-M7 has no vendor tree, and a seat
447
+ // that cannot install design skills must still get its identity and tools.
448
+ warn(`vendored design skills not installed: ${err && err.message}`);
449
+ }
450
+
407
451
  return summary;
408
452
  }
409
453