@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.
Files changed (280) hide show
  1. package/.env.example +37 -22
  2. package/README.md +2 -0
  3. package/bin/maestro.mjs +117 -39
  4. package/bin/maestro.test.mjs +175 -5
  5. package/docs/guides/front-door-session.md +313 -0
  6. package/docs/guides/mac-mini.md +100 -28
  7. package/docs/guides/org-onboarding.md +1 -1
  8. package/docs/guides/setup-wizard.md +9 -5
  9. package/docs/runbooks/cohort-cutover.md +11 -1
  10. package/docs/runbooks/mac-mini-bootstrap.md +38 -63
  11. package/lib/cadence-bus-requeue.test.mjs +83 -0
  12. package/lib/cadence-bus.mjs +43 -7
  13. package/lib/channels/inbox-item.mjs +59 -2
  14. package/lib/cli/board.mjs +285 -0
  15. package/lib/cli/board.test.mjs +227 -0
  16. package/lib/cli/design.mjs +185 -0
  17. package/lib/cli/design.test.mjs +270 -0
  18. package/lib/cli/doctor-checks.mjs +441 -0
  19. package/lib/cli/doctor-checks.test.mjs +336 -0
  20. package/lib/cli/global-setup-extras.mjs +454 -0
  21. package/lib/cli/global-setup-extras.test.mjs +462 -0
  22. package/lib/cli/inbox.mjs +304 -0
  23. package/lib/cli/inbox.test.mjs +230 -0
  24. package/lib/cli/session-ack.mjs +63 -0
  25. package/lib/cli/session-ack.test.mjs +63 -0
  26. package/lib/cli/session.mjs +760 -0
  27. package/lib/cli/session.test.mjs +613 -0
  28. package/lib/collective/global-config.mjs +209 -6
  29. package/lib/collective/global-config.test.mjs +145 -0
  30. package/lib/collective/global-skills.mjs +145 -0
  31. package/lib/collective/global-skills.test.mjs +126 -0
  32. package/lib/collective/presence.mjs +4 -3
  33. package/lib/collective/vendor-skills.mjs +305 -0
  34. package/lib/collective/vendor-skills.test.mjs +306 -0
  35. package/lib/comms/send-gate.mjs +115 -0
  36. package/lib/comms/send-gate.test.mjs +113 -0
  37. package/lib/design/design-md.mjs +793 -0
  38. package/lib/design/design-md.test.mjs +318 -0
  39. package/lib/design/fixtures/DESIGN.golden.md +238 -0
  40. package/lib/design/fixtures/PRODUCT.golden.md +67 -0
  41. package/lib/design/fixtures/foundation.json +133 -0
  42. package/lib/design/refresh-gate.mjs +154 -0
  43. package/lib/design/refresh-gate.test.mjs +144 -0
  44. package/lib/design/write.mjs +275 -0
  45. package/lib/design/write.test.mjs +241 -0
  46. package/lib/feature-init.mjs +2 -2
  47. package/lib/mcp/server.test.mjs +9 -4
  48. package/lib/model-router/spawn.test.mjs +21 -0
  49. package/lib/org/board-mine-cache.mjs +99 -0
  50. package/lib/org/board-mine-cache.test.mjs +53 -0
  51. package/lib/org/board.mjs +11 -0
  52. package/lib/org/board.test.mjs +11 -1
  53. package/lib/org/client.mjs +36 -0
  54. package/lib/org/client.test.mjs +46 -0
  55. package/lib/org/inbound/directedness.mjs +18 -2
  56. package/lib/org/inbound/directedness.test.mjs +58 -0
  57. package/lib/org/inbound/index.mjs +8 -1
  58. package/lib/org/inbound/index.test.mjs +22 -0
  59. package/lib/org/mesh-directives.test.mjs +110 -0
  60. package/lib/org/mesh.mjs +61 -1
  61. package/lib/org/protocol.checksum +1 -1
  62. package/lib/org/protocol.mjs +52 -0
  63. package/lib/org/protocol.test.mjs +12 -1
  64. package/lib/org/registry.mjs +3 -2
  65. package/lib/org/tool-surface.mjs +120 -0
  66. package/lib/org/tool-surface.test.mjs +118 -5
  67. package/lib/prompts/parallelism.mjs +79 -0
  68. package/lib/prompts/parallelism.test.mjs +177 -0
  69. package/lib/security/external-content.mjs +1 -1
  70. package/lib/security/external-content.test.mjs +17 -0
  71. package/lib/session/config.mjs +137 -0
  72. package/lib/session/config.test.mjs +92 -0
  73. package/lib/session/feed-core.mjs +229 -0
  74. package/lib/session/feed-core.test.mjs +198 -0
  75. package/lib/session/first-run.mjs +126 -0
  76. package/lib/session/first-run.test.mjs +121 -0
  77. package/lib/session/frontdoor.mjs +266 -0
  78. package/lib/session/frontdoor.test.mjs +205 -0
  79. package/lib/session/handoffs.mjs +295 -0
  80. package/lib/session/handoffs.test.mjs +183 -0
  81. package/lib/session/identity.mjs +220 -0
  82. package/lib/session/identity.test.mjs +180 -0
  83. package/lib/session/inbox-claims.mjs +434 -0
  84. package/lib/session/inbox-claims.test.mjs +286 -0
  85. package/lib/session/launch-args.mjs +161 -0
  86. package/lib/session/launch-args.test.mjs +157 -0
  87. package/lib/session/liveness.mjs +174 -0
  88. package/lib/session/liveness.test.mjs +100 -0
  89. package/lib/session/status-summary.mjs +172 -0
  90. package/lib/session/status-summary.test.mjs +118 -0
  91. package/lib/session-permissions.mjs +39 -3
  92. package/lib/session-permissions.test.mjs +20 -0
  93. package/lib/setup/claude-probe.mjs +161 -24
  94. package/lib/setup/claude-probe.test.mjs +187 -0
  95. package/lib/setup/sections/learning.mjs +2 -1
  96. package/lib/setup/sections/model.mjs +104 -24
  97. package/lib/setup/sections/model.test.mjs +240 -0
  98. package/lib/setup/sections/org.mjs +27 -2
  99. package/lib/setup/sections/org.test.mjs +35 -2
  100. package/lib/setup/sections/verify.mjs +5 -0
  101. package/lib/setup/state.mjs +30 -10
  102. package/lib/setup/state.test.mjs +24 -1
  103. package/lib/singleton.js +11 -3
  104. package/lib/singleton.test.mjs +16 -0
  105. package/lib/subagents/lock.mjs +1 -1
  106. package/lib/telemetry/collect.mjs +270 -6
  107. package/lib/telemetry/collect.test.mjs +196 -1
  108. package/lib/upgrade/global-refresh.mjs +108 -0
  109. package/lib/upgrade/global-refresh.test.mjs +65 -0
  110. package/lib/upgrade/launchd-reconcile.mjs +327 -0
  111. package/lib/upgrade/launchd-reconcile.test.mjs +272 -0
  112. package/lib/upgrade/post-steps.mjs +151 -0
  113. package/lib/upgrade/post-steps.test.mjs +200 -0
  114. package/lib/upgrade/verify.mjs +215 -0
  115. package/lib/upgrade/verify.test.mjs +164 -0
  116. package/lib/voice/outbound.mjs +3 -2
  117. package/lib/voice/post-call-brief.mjs +2 -1
  118. package/lib/voice/session-rotation.mjs +6 -1
  119. package/lib/voice/session-rotation.test.mjs +114 -0
  120. package/package.json +3 -3
  121. package/plugins/maestro-skills/plugin.json +25 -1
  122. package/plugins/maestro-skills/skills/board-work.md +63 -0
  123. package/plugins/maestro-skills/skills/cohort-design.md +153 -0
  124. package/plugins/maestro-skills/skills/inbound-triage.md +80 -0
  125. package/plugins/maestro-skills/skills/main-session.md +102 -0
  126. package/plugins/maestro-skills/skills/peer-sessions.md +65 -0
  127. package/plugins/maestro-skills/skills/persona-discipline.md +75 -0
  128. package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
  129. package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
  130. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
  131. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
  132. package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
  133. package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
  134. package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
  135. package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
  136. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
  137. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
  138. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
  139. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
  140. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
  141. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
  142. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
  143. package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
  144. package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
  145. package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
  146. package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
  147. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
  148. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
  149. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
  150. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
  151. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
  152. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
  153. package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
  154. package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
  155. package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
  156. package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
  157. package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
  158. package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
  159. package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
  160. package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
  161. package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
  162. package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
  163. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
  164. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
  165. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
  166. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  167. package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
  168. package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
  169. package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
  170. package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
  171. package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
  172. package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
  173. package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
  174. package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
  175. package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
  176. package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
  177. package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
  178. package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
  179. package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
  180. package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
  181. package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
  182. package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
  183. package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
  184. package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
  185. package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
  186. package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
  187. package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
  188. package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
  189. package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
  190. package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
  191. package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
  192. package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
  193. package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
  194. package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
  195. package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
  196. package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
  197. package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
  198. package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
  199. package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
  200. package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
  201. package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
  202. package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
  203. package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
  204. package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
  205. package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
  206. package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
  207. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
  208. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
  209. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
  210. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
  211. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
  212. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
  213. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
  214. package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
  215. package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
  216. package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
  217. package/scaffold/CLAUDE.md +24 -0
  218. package/scripts/ci/check-durable-write-seam.mjs +147 -0
  219. package/scripts/ci/check-durable-write-seam.test.mjs +90 -0
  220. package/scripts/ci/check-skill-packs.mjs +388 -0
  221. package/scripts/ci/check-skill-packs.test.mjs +495 -0
  222. package/scripts/ci/check.mjs +6 -0
  223. package/scripts/collective/hook-runner.mjs +39 -4
  224. package/scripts/collective/hook-runner.test.mjs +85 -2
  225. package/scripts/daemon/agent-daemon-board-mine.test.mjs +96 -0
  226. package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
  227. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +60 -0
  228. package/scripts/daemon/agent-daemon.mjs +249 -10
  229. package/scripts/daemon/agent-daemon.test.mjs +73 -0
  230. package/scripts/daemon/assurance-e2e.test.mjs +141 -6
  231. package/scripts/daemon/assurance.mjs +461 -37
  232. package/scripts/daemon/assurance.test.mjs +408 -43
  233. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +393 -0
  234. package/scripts/daemon/cadence-consumer.mjs +289 -89
  235. package/scripts/daemon/cadence-handlers.mjs +53 -0
  236. package/scripts/daemon/classifier.mjs +1 -1
  237. package/scripts/daemon/dispatcher-resume.test.mjs +166 -0
  238. package/scripts/daemon/dispatcher.mjs +127 -19
  239. package/scripts/daemon/health.mjs +12 -1
  240. package/scripts/daemon/inbox-deferral-session.test.mjs +49 -0
  241. package/scripts/daemon/inbox-deferral.mjs +6 -0
  242. package/scripts/daemon/lib/self-echo.mjs +201 -0
  243. package/scripts/daemon/lib/self-echo.test.mjs +153 -0
  244. package/scripts/daemon/maestro-daemon.mjs +3 -0
  245. package/scripts/daemon/prompt-builder.mjs +19 -3
  246. package/scripts/daemon/responder.mjs +51 -40
  247. package/scripts/daemon/sdk-version.mjs +51 -0
  248. package/scripts/daemon/sdk-version.test.mjs +31 -0
  249. package/scripts/hooks/pre-send-audit.sh +97 -4
  250. package/scripts/hooks/pre-send-audit.test.mjs +140 -1
  251. package/scripts/local-triggers/autoupdate.sh +243 -19
  252. package/scripts/local-triggers/autoupdate.test.mjs +518 -0
  253. package/scripts/local-triggers/generate-plists.sh +24 -1
  254. package/scripts/local-triggers/generate-plists.test.mjs +49 -11
  255. package/scripts/org/send-orgmail.first-contact.test.mjs +102 -0
  256. package/scripts/org/send-orgmail.mjs +27 -3
  257. package/scripts/poller/inbox-privilege-injection.test.mjs +167 -0
  258. package/scripts/poller/slack-poller.mjs +13 -1
  259. package/scripts/poller/utils.mjs +46 -1
  260. package/scripts/poller-launchd/install.sh +19 -11
  261. package/scripts/poller-launchd/install.test.mjs +243 -0
  262. package/scripts/poller-launchd/launchd-poller-wrapper.sh +92 -0
  263. package/scripts/poller-launchd/migrate.sh +66 -0
  264. package/scripts/poller-launchd/poller.plist.template +4 -2
  265. package/scripts/session/feed.mjs +237 -0
  266. package/scripts/session/feed.test.mjs +196 -0
  267. package/scripts/session/supervisor-sh.test.mjs +218 -0
  268. package/scripts/session/supervisor.mjs +328 -0
  269. package/scripts/session/supervisor.sh +141 -0
  270. package/scripts/session/supervisor.test.mjs +482 -0
  271. package/scripts/setup/configure-macos.sh +250 -55
  272. package/scripts/setup/configure-macos.test.mjs +306 -0
  273. package/scripts/setup/init-agent.sh +112 -7
  274. package/scripts/setup/init-agent.test.mjs +220 -1
  275. package/scripts/vendor/skill-packs.mjs +354 -0
  276. package/scripts/vendor/sync-skill-packs.mjs +242 -0
  277. package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
  278. package/scripts/watchdog/memory-watchdog.sh +37 -1
  279. package/scripts/watchdog/memory-watchdog.test.mjs +64 -0
  280. package/scripts/setup/boot-claude-session.sh +0 -94
@@ -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
+ });