@cohortapp/agent-sdk 2.12.0 → 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/bin/maestro.mjs +6 -2
- package/docs/guides/front-door-session.md +49 -0
- package/lib/cli/design.mjs +185 -0
- package/lib/cli/design.test.mjs +270 -0
- package/lib/cli/global-setup-extras.mjs +44 -0
- package/lib/cli/global-setup-extras.test.mjs +95 -0
- package/lib/cli/session.mjs +11 -1
- package/lib/cli/session.test.mjs +17 -6
- package/lib/collective/global-config.mjs +5 -0
- package/lib/collective/global-config.test.mjs +5 -0
- package/lib/collective/vendor-skills.mjs +305 -0
- package/lib/collective/vendor-skills.test.mjs +306 -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/prompts/parallelism.mjs +79 -0
- package/lib/prompts/parallelism.test.mjs +177 -0
- package/package.json +1 -1
- package/plugins/maestro-skills/plugin.json +4 -0
- package/plugins/maestro-skills/skills/cohort-design.md +153 -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/scripts/ci/check-skill-packs.mjs +388 -0
- package/scripts/ci/check-skill-packs.test.mjs +495 -0
- package/scripts/ci/check.mjs +3 -0
- package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
- package/scripts/daemon/agent-daemon.mjs +108 -0
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +61 -2
- package/scripts/daemon/cadence-consumer.mjs +46 -22
- package/scripts/daemon/prompt-builder.mjs +19 -3
- package/scripts/local-triggers/autoupdate.test.mjs +33 -3
- 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/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
|
|
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
|
|
@@ -245,6 +245,53 @@ To the humans it talks to, the agent is one persona. Peer sessions and
|
|
|
245
245
|
sub-agents are "my team" / "a colleague"; the persona audit hook blocks any
|
|
246
246
|
outbound text that says otherwise.
|
|
247
247
|
|
|
248
|
+
## 6a. DESIGN.md and the parallelism directive
|
|
249
|
+
|
|
250
|
+
Two things reach every prompt a seat runs, and neither is left to a session to
|
|
251
|
+
remember.
|
|
252
|
+
|
|
253
|
+
**DESIGN.md and PRODUCT.md.** `maestro design sync` reads the workspace's SAVED
|
|
254
|
+
brand foundation (`branding.getFoundation` — never a draft) and renders it into
|
|
255
|
+
`DESIGN.md` (colour, type, radius, spacing and component tokens, in the
|
|
256
|
+
google-labs-code/design.md frontmatter shape, with `{colors.primary}`-style
|
|
257
|
+
refs) and `PRODUCT.md` (positioning, voice, tone, lexicon) in the agent dir,
|
|
258
|
+
plus the raw snapshot at `state/design/foundation.json`. The daemon re-runs the
|
|
259
|
+
same code path every 15 minutes and rewrites the files when the foundation's
|
|
260
|
+
version has moved, so a seat's design files follow the humans editing the
|
|
261
|
+
foundation without anyone re-running a command.
|
|
262
|
+
|
|
263
|
+
Both files are GENERATED. An edit to them is overwritten on the next sync;
|
|
264
|
+
foundation changes are human-gated and go through the `design_propose_change`
|
|
265
|
+
tool. The write is byte-idempotent — an unchanged foundation touches no mtime,
|
|
266
|
+
so the poll never churns the tree, and the render stamp is kept per output
|
|
267
|
+
directory so a `--out` run and the daemon's poll of the agent dir cannot
|
|
268
|
+
invalidate each other's files. Every posture here is fail-open: an un-enrolled
|
|
269
|
+
seat writes nothing, and a failed read keeps the last-good files rather than
|
|
270
|
+
blanking them. Design skills are told to read `$AGENT_ROOT/DESIGN.md` when the
|
|
271
|
+
working directory is not the agent dir.
|
|
272
|
+
|
|
273
|
+
A workspace that has never SAVED a foundation writes nothing at all, and the
|
|
274
|
+
CLI says why. hq answers such a read with its own product defaults and
|
|
275
|
+
`unversioned: true` — a complete, plausible palette that is nobody's brand — and
|
|
276
|
+
a DESIGN.md rendered from it would ground every design skill on the seat in the
|
|
277
|
+
wrong palette with a generated file's authority behind it.
|
|
278
|
+
|
|
279
|
+
maestro design sync # into the agent dir
|
|
280
|
+
maestro design sync --out docs/brand --json
|
|
281
|
+
|
|
282
|
+
**The parallelism directive.** `lib/prompts/parallelism.mjs` holds one constant,
|
|
283
|
+
prepended exactly once (it is idempotent, and the seams compose) to every
|
|
284
|
+
peer-session prompt by `maestro session spawn`, to every sub-session prompt
|
|
285
|
+
built by the daemon's prompt-builder, and to every cadence trigger prompt on the
|
|
286
|
+
escalate/guarded lane — both when a sub-session is spawned for it and when a
|
|
287
|
+
live main session is handed it (`renderCadencePromptBody`, one seam for both, so
|
|
288
|
+
the two lanes send the same bytes). It grants the standing permission to
|
|
289
|
+
dispatch independent tasks as one parallel batch, and carries the safety rule
|
|
290
|
+
that makes that safe on a shared checkout: check the planned file scope first,
|
|
291
|
+
and never assign two agents to edit the same file — sequence or re-scope
|
|
292
|
+
instead. Sessions ran plans strictly serially before it existed, because
|
|
293
|
+
nothing in the prompt had ever said they need not.
|
|
294
|
+
|
|
248
295
|
## 7. Files
|
|
249
296
|
|
|
250
297
|
| Path | What |
|
|
@@ -258,6 +305,8 @@ outbound text that says otherwise.
|
|
|
258
305
|
| `state/session/upgrade-notice.json` | written by autoupdate; the session restarts itself when idle |
|
|
259
306
|
| `state/session.lock` | the singleton lock |
|
|
260
307
|
| `state/inbox/cohort/*.yaml` | inbound items (`.dispatched` = claimed, `.processed` = done, `.deferred`) |
|
|
308
|
+
| `DESIGN.md`, `PRODUCT.md` | generated from the workspace brand foundation by `maestro design sync` and the daemon's 15-minute poll; edits are overwritten |
|
|
309
|
+
| `state/design/foundation.json` | `{fetchedAt, renderedAt, version, versionLabel, foundation}` — the last synced foundation; the refresh gate reads it back |
|
|
261
310
|
| `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
311
|
| `.maestro/upgrade-result.json` | `{from, to, at, steps:{plists, launchd, globalSetup, globalInstall, verify}}` — what the last `maestro upgrade` did to the seat |
|
|
263
312
|
| `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
|
|