@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.
Files changed (127) hide show
  1. package/bin/maestro.mjs +6 -2
  2. package/docs/guides/front-door-session.md +49 -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/package.json +1 -1
  25. package/plugins/maestro-skills/plugin.json +4 -0
  26. package/plugins/maestro-skills/skills/cohort-design.md +153 -0
  27. package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
  28. package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
  29. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
  30. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
  31. package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
  32. package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
  33. package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
  34. package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
  35. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
  36. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
  37. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
  38. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
  39. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
  40. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
  41. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
  42. package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
  43. package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
  44. package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
  45. package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
  46. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
  47. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
  48. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
  49. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
  50. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
  51. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
  52. package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
  53. package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
  54. package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
  55. package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
  56. package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
  57. package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
  58. package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
  59. package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
  60. package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
  61. package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
  62. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
  63. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
  64. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
  65. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  66. package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
  67. package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
  68. package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
  69. package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
  70. package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
  71. package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
  72. package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
  73. package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
  74. package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
  75. package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
  76. package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
  77. package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
  78. package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
  79. package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
  80. package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
  81. package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
  82. package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
  83. package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
  84. package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
  85. package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
  86. package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
  87. package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
  88. package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
  89. package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
  90. package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
  91. package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
  92. package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
  93. package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
  94. package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
  95. package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
  96. package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
  97. package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
  98. package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
  99. package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
  100. package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
  101. package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
  102. package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
  103. package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
  104. package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
  105. package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
  106. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
  107. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
  108. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
  109. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
  110. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
  111. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
  112. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
  113. package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
  114. package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
  115. package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
  116. package/scripts/ci/check-skill-packs.mjs +388 -0
  117. package/scripts/ci/check-skill-packs.test.mjs +495 -0
  118. package/scripts/ci/check.mjs +3 -0
  119. package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
  120. package/scripts/daemon/agent-daemon.mjs +108 -0
  121. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +61 -2
  122. package/scripts/daemon/cadence-consumer.mjs +46 -22
  123. package/scripts/daemon/prompt-builder.mjs +19 -3
  124. package/scripts/local-triggers/autoupdate.test.mjs +33 -3
  125. package/scripts/vendor/skill-packs.mjs +354 -0
  126. package/scripts/vendor/sync-skill-packs.mjs +242 -0
  127. package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
@@ -0,0 +1,275 @@
1
+ /**
2
+ * lib/design/write.mjs — the effect edge of the design sync (WP-M7 mechanic 4).
3
+ *
4
+ * `design-md.mjs` decides WHAT the files say and `refresh-gate.mjs` decides
5
+ * WHEN; this module is the only place that touches the disk, so the CLI
6
+ * (`maestro design sync`) and the daemon's poller write the same bytes with the
7
+ * same modes and the same idempotence rule.
8
+ *
9
+ * IDEMPOTENCE IS BY BYTES, not by timestamp. A re-sync of an unchanged
10
+ * foundation must not touch DESIGN.md — the file is read by editors and skills,
11
+ * and a pointless mtime bump is how a watcher storms and how a git tree looks
12
+ * dirty for no reason. So each file is read back and compared before writing.
13
+ * That also means `generatedAt` must NOT be part of the rendered bytes when
14
+ * idempotence matters: the caller passes the SAME `generatedAt` it recorded on
15
+ * the previous sync, or none. (`sync.mjs` callers pass the stored value.)
16
+ *
17
+ * EVERY write here goes through `lib/fs-atomic.mjs` — the JSON snapshot and
18
+ * both markdown files. There are TWO writers of the same two absolute paths (a
19
+ * hand-run `maestro design sync` and the daemon's 15-minute poll), and a design
20
+ * skill reads `$AGENT_ROOT/DESIGN.md` at arbitrary moments; an in-place
21
+ * `writeFileSync` would let it read a half-written frontmatter block. The mode
22
+ * is applied with an explicit `chmod` after the rename as well as on the temp
23
+ * file, because Node honours a `mode` option only when it CREATES a file — a
24
+ * DESIGN.md some other tool left at 0600 would otherwise stay 0600 forever.
25
+ *
26
+ * Node builtins only. ESM.
27
+ *
28
+ * @module lib/design/write
29
+ */
30
+
31
+ "use strict";
32
+
33
+ import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
34
+ import { dirname, join, resolve } from "node:path";
35
+ import { writeFileAtomic, writeJsonAtomic } from "../fs-atomic.mjs";
36
+ import { renderDesignBundle } from "./design-md.mjs";
37
+ import { DESIGN_STATE_REL, foundationVersion, foundationVersionLabel } from "./refresh-gate.mjs";
38
+
39
+ /** The mode the generated docs are written with (readable by any tool on the seat). */
40
+ export const DESIGN_FILE_MODE = 0o644;
41
+
42
+ /**
43
+ * The workspace/brand name for the rendered heading, from the loaded org config.
44
+ *
45
+ * SHARED ON PURPOSE. `maestro design sync` and the daemon's poller both write
46
+ * DESIGN.md, and the heading is part of the rendered bytes — two definitions of
47
+ * "what is this workspace called" would make a hand-run sync and an automatic
48
+ * one disagree, and the idempotence rule above would then rewrite the file on
49
+ * every alternation. One function, both callers.
50
+ *
51
+ * The foundation's own `positioning` is prose, not a name, so it is never used
52
+ * here. Nothing is invented: an org config without a name reads "Workspace".
53
+ *
54
+ * @param {object} cfg the loaded org config (`lib/org/client.mjs#loadOrgConfig`)
55
+ * @returns {string}
56
+ */
57
+ export function workspaceName(cfg) {
58
+ const org = cfg && typeof cfg === "object" ? cfg.org : null;
59
+ const cohort = org && typeof org === "object" ? org.cohort : null;
60
+ const name = cohort && typeof cohort === "object" ? cohort.orgName || cohort.workspace : null;
61
+ return typeof name === "string" && name.trim() ? name.trim() : "Workspace";
62
+ }
63
+
64
+ /**
65
+ * Write the rendered bundle, skipping any file whose bytes are already correct.
66
+ *
67
+ * @param {object} o
68
+ * @param {Array<{rel:string, content:string}>} o.files rendered by `renderDesignBundle`
69
+ * @param {string} o.outDir where the two markdown files go
70
+ * @param {object} [io] injectable I/O for tests: {readFileImpl, writeFileImpl, existsImpl, mkdirImpl}
71
+ * @returns {{written:string[], unchanged:string[]}}
72
+ */
73
+ export function writeDesignFiles({ files, outDir }, io = {}) {
74
+ const readImpl = io.readFileImpl || ((p) => readFileSync(p, "utf8"));
75
+ const writeImpl = io.writeFileImpl || ((p, data, mode) => {
76
+ writeFileAtomic(p, data, { mode });
77
+ // The rename published a file created with `mode`, but only because it was
78
+ // new. chmod the published name too, so a pre-existing file that some other
79
+ // tool left at a tighter mode is corrected rather than inherited.
80
+ try { chmodSync(p, mode); } catch { /* best effort: the bytes matter more than the bits */ }
81
+ });
82
+ const existsImpl = io.existsImpl || existsSync;
83
+ const mkdirImpl = io.mkdirImpl || ((p) => mkdirSync(p, { recursive: true }));
84
+ const written = [];
85
+ const unchanged = [];
86
+ if (!existsImpl(outDir)) mkdirImpl(outDir);
87
+ for (const file of Array.isArray(files) ? files : []) {
88
+ if (!file || typeof file.rel !== "string" || typeof file.content !== "string") continue;
89
+ const target = join(outDir, file.rel);
90
+ let current = null;
91
+ try {
92
+ current = existsImpl(target) ? readImpl(target) : null;
93
+ } catch {
94
+ current = null; // unreadable is the same as absent: rewrite it
95
+ }
96
+ if (current === file.content) {
97
+ unchanged.push(file.rel);
98
+ continue;
99
+ }
100
+ writeImpl(target, file.content, DESIGN_FILE_MODE);
101
+ written.push(file.rel);
102
+ }
103
+ return { written, unchanged };
104
+ }
105
+
106
+ /**
107
+ * Persist the foundation snapshot the gate reads back next tick.
108
+ * `{fetchedAt, version, versionLabel, foundation}` — durable JSON, atomic.
109
+ *
110
+ * @param {string} agentRoot
111
+ * @param {{fetchedAt:string, version:string|null, versionLabel?:string, foundation:object}} doc
112
+ * @param {object} [io] {writeJsonImpl, mkdirImpl}
113
+ * @returns {string} the path written
114
+ */
115
+ export function writeFoundationState(agentRoot, doc, io = {}) {
116
+ const path = join(String(agentRoot || ""), DESIGN_STATE_REL);
117
+ const mkdirImpl = io.mkdirImpl || ((p) => mkdirSync(p, { recursive: true }));
118
+ mkdirImpl(dirname(path));
119
+ (io.writeJsonImpl || writeJsonAtomic)(path, doc);
120
+ return path;
121
+ }
122
+
123
+ /**
124
+ * Read back the last synced snapshot. Returns null when absent or corrupt — a
125
+ * corrupt snapshot is treated as "never synced", which re-syncs rather than
126
+ * wedging.
127
+ * @param {string} agentRoot
128
+ * @param {object} [io] {readFileImpl}
129
+ * @returns {{fetchedAt?:string, version?:string|null, versionLabel?:string, foundation?:object}|null}
130
+ */
131
+ export function readFoundationState(agentRoot, io = {}) {
132
+ const path = join(String(agentRoot || ""), DESIGN_STATE_REL);
133
+ try {
134
+ const raw = (io.readFileImpl || ((p) => readFileSync(p, "utf8")))(path);
135
+ const doc = JSON.parse(raw);
136
+ return doc && typeof doc === "object" && !Array.isArray(doc) ? doc : null;
137
+ } catch {
138
+ return null; // absent or corrupt → re-sync
139
+ }
140
+ }
141
+
142
+ /**
143
+ * The per-output-directory render stamps from a state snapshot, as a mutable
144
+ * map of resolved path → ISO string.
145
+ *
146
+ * MIGRATION. Snapshots written before the map existed carry one bare
147
+ * `renderedAt` string, which described the agent dir (the only directory the
148
+ * daemon ever writes). Reading it as that entry means an upgraded seat finds
149
+ * its DESIGN.md already matching and rewrites nothing.
150
+ *
151
+ * @param {object|null} prior
152
+ * @param {string} agentRoot
153
+ * @returns {Record<string,string>}
154
+ */
155
+ export function priorRenderStamps(prior, agentRoot) {
156
+ const out = {};
157
+ const p = prior && typeof prior === "object" ? prior : {};
158
+ if (p.rendered && typeof p.rendered === "object" && !Array.isArray(p.rendered)) {
159
+ for (const [k, v] of Object.entries(p.rendered)) {
160
+ if (typeof k === "string" && typeof v === "string" && v) out[k] = v;
161
+ }
162
+ }
163
+ const legacy = typeof p.renderedAt === "string" && p.renderedAt ? p.renderedAt : "";
164
+ const rootKey = resolve(String(agentRoot || ""));
165
+ if (legacy && !out[rootKey]) out[rootKey] = legacy;
166
+ return out;
167
+ }
168
+
169
+ /**
170
+ * Do all rendered files already match what is on disk? The idempotence probe.
171
+ * @param {Array<{rel:string, content:string}>} files
172
+ * @param {string} outDir
173
+ * @param {object} [io]
174
+ * @returns {boolean}
175
+ */
176
+ export function filesMatch(files, outDir, io = {}) {
177
+ const readImpl = io.readFileImpl || ((p) => readFileSync(p, "utf8"));
178
+ const existsImpl = io.existsImpl || existsSync;
179
+ for (const file of Array.isArray(files) ? files : []) {
180
+ const target = join(outDir, file.rel);
181
+ let current = null;
182
+ try {
183
+ current = existsImpl(target) ? readImpl(target) : null;
184
+ } catch {
185
+ return false; // unreadable → rewrite
186
+ }
187
+ if (current !== file.content) return false;
188
+ }
189
+ return true;
190
+ }
191
+
192
+ /**
193
+ * Render a fetched foundation and land it on the seat: DESIGN.md + PRODUCT.md in
194
+ * `outDir`, the snapshot in `state/design/foundation.json`. The ONE code path
195
+ * both `maestro design sync` and the daemon's poller use, so a hand-run sync and
196
+ * an automatic one produce identical bytes.
197
+ *
198
+ * THE `generatedAt` DANCE. A timestamp inside the rendered bytes would make
199
+ * every sync a rewrite, defeating the idempotence the whole design turns on. So
200
+ * the files are first rendered carrying the PREVIOUS sync's stamp: if they then
201
+ * match the disk byte-for-byte, nothing changed and nothing is written. Only
202
+ * when something genuinely differs are they re-rendered with `now` — so the
203
+ * stamp means "when this content was minted", which is the honest reading, and
204
+ * an unchanged foundation touches no file.
205
+ *
206
+ * THE STAMP IS PER OUTPUT DIRECTORY (`rendered`, keyed by resolved path). One
207
+ * shared stamp is a permanent rewrite loop the moment a second directory
208
+ * exists: `maestro design sync --out docs/brand` stamps the state with its own
209
+ * `now`, the daemon's next poll of the agent dir renders with that stamp, does
210
+ * not match ITS files, rewrites them and re-stamps — and each lane then
211
+ * invalidates the other's files forever, with a foundation that never changed.
212
+ * A legacy state file carrying a bare `renderedAt` string is read as the agent
213
+ * dir's entry, so an existing seat migrates without one spurious rewrite.
214
+ *
215
+ * @param {object} o
216
+ * @param {string} o.agentRoot the agent dir (where state/ lives)
217
+ * @param {string} [o.outDir] where the docs go (default: agentRoot)
218
+ * @param {object} o.result the `branding.getFoundation` result frame's `result`
219
+ * @param {number} o.now epoch ms (injected)
220
+ * @param {string} [o.name] workspace/brand name for the heading
221
+ * @param {string} [o.source] provenance line
222
+ * @param {object} [io] injectable I/O (see writeDesignFiles / writeFoundationState)
223
+ * @returns {{written:string[], unchanged:string[], version:string|null, statePath:string, outDir:string}}
224
+ */
225
+ export function syncDesign(o, io = {}) {
226
+ const agentRoot = String(o.agentRoot || "");
227
+ const outDir = String(o.outDir || agentRoot);
228
+ const now = Number.isFinite(o.now) ? o.now : 0;
229
+ const nowIso = new Date(now).toISOString();
230
+ const result = o.result && typeof o.result === "object" ? o.result : {};
231
+ const foundation = result.foundation && typeof result.foundation === "object" ? result.foundation : {};
232
+ const version = foundationVersion(result);
233
+ const versionLabel = foundationVersionLabel(result);
234
+ const prior = readFoundationState(agentRoot, io);
235
+ const rendered = priorRenderStamps(prior, agentRoot);
236
+ const key = resolve(outDir);
237
+ const priorStamp = rendered[key] || nowIso;
238
+
239
+ const base = { name: o.name, source: o.source, versionId: version || "", versionLabel };
240
+ let renderedAt = priorStamp;
241
+ let files = renderDesignBundle(foundation, { ...base, generatedAt: renderedAt });
242
+ let out = { written: [], unchanged: files.map((f) => f.rel) };
243
+ if (!filesMatch(files, outDir, io)) {
244
+ renderedAt = nowIso;
245
+ files = renderDesignBundle(foundation, { ...base, generatedAt: renderedAt });
246
+ out = writeDesignFiles({ files, outDir }, io);
247
+ }
248
+ rendered[key] = renderedAt;
249
+ const statePath = writeFoundationState(
250
+ agentRoot,
251
+ {
252
+ fetchedAt: nowIso,
253
+ // The agent dir's own stamp stays at the top level: it is what a reader
254
+ // (and an older SDK) means by "when was DESIGN.md minted".
255
+ renderedAt: rendered[resolve(agentRoot)] || renderedAt,
256
+ rendered,
257
+ version: version || null,
258
+ versionLabel,
259
+ foundation,
260
+ },
261
+ io,
262
+ );
263
+ return { ...out, version: version || null, statePath, outDir };
264
+ }
265
+
266
+ export default {
267
+ workspaceName,
268
+ priorRenderStamps,
269
+ writeDesignFiles,
270
+ writeFoundationState,
271
+ readFoundationState,
272
+ filesMatch,
273
+ syncDesign,
274
+ DESIGN_FILE_MODE,
275
+ };
@@ -0,0 +1,241 @@
1
+ /**
2
+ * write.test.mjs — the effect edge of the design sync (WP-M7 mechanic 4).
3
+ *
4
+ * Real files in a temp dir, not stubs: the claims worth holding here are about
5
+ * the FILESYSTEM — that an unchanged foundation touches no mtime, and that the
6
+ * two markdown files land at 0644 so any tool on the seat can read them.
7
+ */
8
+
9
+ import { test } from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { chmodSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, writeFileSync, statSync, utimesSync, existsSync, rmSync } from "node:fs";
12
+ import { join, resolve } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+
15
+ import { syncDesign, writeDesignFiles, writeFoundationState, readFoundationState, filesMatch, workspaceName, DESIGN_FILE_MODE } from "./write.mjs";
16
+ import { renderDesignBundle } from "./design-md.mjs";
17
+ import { DESIGN_STATE_REL } from "./refresh-gate.mjs";
18
+
19
+ const NOW = Date.parse("2026-09-08T12:00:00Z");
20
+ const FIXTURE = JSON.parse(readFileSync(new URL("./fixtures/foundation.json", import.meta.url), "utf8"));
21
+
22
+ /** A `branding.getFoundation` result frame's `result`. */
23
+ const result = (over = {}) => ({
24
+ foundation: FIXTURE,
25
+ version: { id: "bv_01HQZ", seq: 7, label: "V2.7" },
26
+ headVersionId: "bv_01HQZ",
27
+ isHead: true,
28
+ unversioned: false,
29
+ history: [],
30
+ ...over,
31
+ });
32
+
33
+ function tmpRoot(t) {
34
+ const dir = mkdtempSync(join(tmpdir(), "maestro-design-write-"));
35
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
36
+ return dir;
37
+ }
38
+
39
+ test("a first sync writes both files plus the state snapshot", (t) => {
40
+ const root = tmpRoot(t);
41
+ const out = syncDesign({ agentRoot: root, result: result(), now: NOW, name: "Northwind" });
42
+
43
+ assert.deepEqual(out.written, ["DESIGN.md", "PRODUCT.md"]);
44
+ assert.deepEqual(out.unchanged, []);
45
+ assert.equal(out.version, "bv_01HQZ");
46
+ assert.equal(out.outDir, root);
47
+
48
+ const design = readFileSync(join(root, "DESIGN.md"), "utf8");
49
+ assert.match(design, /^---\nname: "Northwind"\n/);
50
+ assert.match(design, /\{colors\.primary\}/);
51
+ assert.match(readFileSync(join(root, "PRODUCT.md"), "utf8"), /# Northwind — product voice/);
52
+
53
+ const state = JSON.parse(readFileSync(join(root, DESIGN_STATE_REL), "utf8"));
54
+ assert.equal(state.version, "bv_01HQZ");
55
+ assert.equal(state.versionLabel, "V2.7");
56
+ assert.equal(state.fetchedAt, "2026-09-08T12:00:00.000Z");
57
+ assert.deepEqual(state.foundation, FIXTURE, "the raw foundation is kept verbatim for the next gate read");
58
+ });
59
+
60
+ test("both markdown files are 0644", (t) => {
61
+ const root = tmpRoot(t);
62
+ syncDesign({ agentRoot: root, result: result(), now: NOW, name: "Northwind" });
63
+ for (const rel of ["DESIGN.md", "PRODUCT.md"]) {
64
+ assert.equal(statSync(join(root, rel)).mode & 0o777, DESIGN_FILE_MODE, rel);
65
+ }
66
+ });
67
+
68
+ test("re-syncing an unchanged foundation rewrites NOTHING — mtimes are untouched", (t) => {
69
+ const root = tmpRoot(t);
70
+ syncDesign({ agentRoot: root, result: result(), now: NOW, name: "Northwind" });
71
+
72
+ // Backdate both files so any rewrite is unmistakable.
73
+ const old = new Date(NOW - 86_400_000);
74
+ for (const rel of ["DESIGN.md", "PRODUCT.md"]) utimesSync(join(root, rel), old, old);
75
+ const before = ["DESIGN.md", "PRODUCT.md"].map((rel) => statSync(join(root, rel)).mtimeMs);
76
+
77
+ // A LATER clock, deliberately: the timestamp inside the file must not be what
78
+ // makes a sync a rewrite, or the interval poll would churn the tree forever.
79
+ const again = syncDesign({ agentRoot: root, result: result(), now: NOW + 20 * 60_000, name: "Northwind" });
80
+ assert.deepEqual(again.written, [], "nothing written");
81
+ assert.deepEqual(again.unchanged, ["DESIGN.md", "PRODUCT.md"]);
82
+
83
+ const after = ["DESIGN.md", "PRODUCT.md"].map((rel) => statSync(join(root, rel)).mtimeMs);
84
+ assert.deepEqual(after, before, "the files were not touched at all");
85
+
86
+ const design = readFileSync(join(root, "DESIGN.md"), "utf8");
87
+ assert.match(design, /\*\*Synced:\*\* 2026-09-08T12:00:00\.000Z/, "the stamp still says when the CONTENT was minted");
88
+
89
+ // The snapshot is still refreshed, so the gate knows the seat did check.
90
+ const state = JSON.parse(readFileSync(join(root, DESIGN_STATE_REL), "utf8"));
91
+ assert.equal(state.fetchedAt, "2026-09-08T12:20:00.000Z");
92
+ assert.equal(state.renderedAt, "2026-09-08T12:00:00.000Z");
93
+ });
94
+
95
+ test("a changed foundation rewrites the affected file and re-stamps it", (t) => {
96
+ const root = tmpRoot(t);
97
+ syncDesign({ agentRoot: root, result: result(), now: NOW, name: "Northwind" });
98
+
99
+ const moved = JSON.parse(JSON.stringify(FIXTURE));
100
+ moved.colors[2].value = "#111111";
101
+ const out = syncDesign({
102
+ agentRoot: root,
103
+ result: result({ foundation: moved, headVersionId: "bv_02", version: { id: "bv_02", seq: 8, label: "V2.8" } }),
104
+ now: NOW + 20 * 60_000,
105
+ name: "Northwind",
106
+ });
107
+
108
+ assert.ok(out.written.includes("DESIGN.md"));
109
+ assert.equal(out.version, "bv_02");
110
+ const design = readFileSync(join(root, "DESIGN.md"), "utf8");
111
+ assert.match(design, /primary-moss: "#111111"/);
112
+ assert.match(design, /\*\*Synced:\*\* 2026-09-08T12:20:00\.000Z/);
113
+ assert.match(design, /\*\*Version:\*\* V2\.8/);
114
+ });
115
+
116
+ test("--out sends the docs elsewhere; the snapshot always stays under the agent root", (t) => {
117
+ const root = tmpRoot(t);
118
+ const outDir = join(root, "docs", "brand");
119
+ const out = syncDesign({ agentRoot: root, outDir, result: result(), now: NOW, name: "Northwind" });
120
+
121
+ assert.equal(out.outDir, outDir);
122
+ assert.ok(existsSync(join(outDir, "DESIGN.md")));
123
+ assert.ok(!existsSync(join(root, "DESIGN.md")));
124
+ assert.ok(existsSync(join(root, DESIGN_STATE_REL)));
125
+ });
126
+
127
+ test("writeDesignFiles skips a file whose bytes already match and rewrites an unreadable one", (t) => {
128
+ const root = tmpRoot(t);
129
+ const files = renderDesignBundle(FIXTURE, { name: "Northwind" });
130
+ assert.deepEqual(writeDesignFiles({ files, outDir: root }).written, ["DESIGN.md", "PRODUCT.md"]);
131
+ assert.deepEqual(writeDesignFiles({ files, outDir: root }), { written: [], unchanged: ["DESIGN.md", "PRODUCT.md"] });
132
+
133
+ writeFileSync(join(root, "DESIGN.md"), "someone edited this by hand\n");
134
+ assert.deepEqual(writeDesignFiles({ files, outDir: root }).written, ["DESIGN.md"], "a hand edit is overwritten — the file is generated");
135
+
136
+ const boom = writeDesignFiles({ files, outDir: root }, { readFileImpl: () => { throw new Error("EIO"); } });
137
+ assert.deepEqual(boom.written, ["DESIGN.md", "PRODUCT.md"], "unreadable is treated as absent");
138
+ });
139
+
140
+ test("writeDesignFiles ignores malformed entries rather than throwing", (t) => {
141
+ const root = tmpRoot(t);
142
+ const out = writeDesignFiles({ files: [null, { rel: "A.md" }, { content: "x" }, { rel: "ok.md", content: "y" }], outDir: root });
143
+ assert.deepEqual(out.written, ["ok.md"]);
144
+ assert.deepEqual(writeDesignFiles({ files: "not a list", outDir: root }), { written: [], unchanged: [] });
145
+ });
146
+
147
+ test("readFoundationState reads back what writeFoundationState wrote; corrupt reads as never-synced", (t) => {
148
+ const root = tmpRoot(t);
149
+ assert.equal(readFoundationState(root), null, "absent → null");
150
+
151
+ const path = writeFoundationState(root, { fetchedAt: "2026-09-08T12:00:00.000Z", version: "bv_1", foundation: { colors: [] } });
152
+ assert.equal(path, join(root, DESIGN_STATE_REL));
153
+ assert.equal(readFoundationState(root).version, "bv_1");
154
+
155
+ writeFileSync(path, "{ not json");
156
+ assert.equal(readFoundationState(root), null, "corrupt → null → re-sync, never a wedge");
157
+
158
+ writeFileSync(path, JSON.stringify([1, 2]));
159
+ assert.equal(readFoundationState(root), null, "an array is not a snapshot");
160
+ });
161
+
162
+ test("filesMatch is the idempotence probe the CLI's claim rests on", (t) => {
163
+ const root = tmpRoot(t);
164
+ const files = renderDesignBundle(FIXTURE, { name: "Northwind" });
165
+ assert.equal(filesMatch(files, root), false, "nothing on disk yet");
166
+ writeDesignFiles({ files, outDir: root });
167
+ assert.equal(filesMatch(files, root), true);
168
+ writeFileSync(join(root, "PRODUCT.md"), "drift\n");
169
+ assert.equal(filesMatch(files, root), false);
170
+ });
171
+
172
+ test("workspaceName reads the org config and invents nothing", () => {
173
+ assert.equal(workspaceName({ org: { cohort: { orgName: "Northwind" } } }), "Northwind");
174
+ assert.equal(workspaceName({ org: { cohort: { workspace: "adaptic-hq" } } }), "adaptic-hq");
175
+ assert.equal(workspaceName({ org: { cohort: { orgName: " " } } }), "Workspace");
176
+ assert.equal(workspaceName({}), "Workspace");
177
+ assert.equal(workspaceName(null), "Workspace");
178
+ });
179
+
180
+ test("a state directory that does not exist yet is created", (t) => {
181
+ const root = join(tmpRoot(t), "not", "made", "yet");
182
+ mkdirSync(root, { recursive: true });
183
+ const out = syncDesign({ agentRoot: root, result: result(), now: NOW, name: "Northwind" });
184
+ assert.ok(existsSync(out.statePath));
185
+ });
186
+
187
+ // ── two output directories, one state file ──────────────────────────────────
188
+
189
+ test("the daemon's agent dir and a --out directory do not invalidate each other, ever", (t) => {
190
+ // The scenario: an operator runs `maestro design sync --out docs/brand`
191
+ // (advertised in the guide) on a seat whose daemon polls the agent dir every
192
+ // 15 minutes. With one shared render stamp, each lane re-stamped the state
193
+ // and the other lane then found its own files "stale" — both files rewritten
194
+ // on every tick, forever, with a foundation that never changed.
195
+ const root = tmpRoot(t);
196
+ const brand = join(root, "docs", "brand");
197
+ let now = NOW;
198
+ const daemon = () => syncDesign({ agentRoot: root, result: result(), now: (now += 900_000), name: "Northwind" });
199
+ const cli = () => syncDesign({ agentRoot: root, outDir: brand, result: result(), now: (now += 60_000), name: "Northwind" });
200
+
201
+ assert.deepEqual(daemon().written, ["DESIGN.md", "PRODUCT.md"], "first write of the agent dir");
202
+ assert.deepEqual(cli().written, ["DESIGN.md", "PRODUCT.md"], "first write of docs/brand");
203
+
204
+ for (let i = 0; i < 6; i++) {
205
+ assert.deepEqual(daemon().written, [], `daemon poll ${i + 1} rewrote nothing`);
206
+ assert.deepEqual(cli().written, [], `--out run ${i + 1} rewrote nothing`);
207
+ }
208
+
209
+ const state = readFoundationState(root);
210
+ assert.equal(typeof state.rendered, "object");
211
+ assert.equal(Object.keys(state.rendered).length, 2, "one stamp per output directory");
212
+ assert.equal(state.renderedAt, state.rendered[resolve(root)], "the top-level stamp still describes the agent dir");
213
+ });
214
+
215
+ test("a legacy state file with a bare renderedAt migrates without a spurious rewrite", (t) => {
216
+ const root = tmpRoot(t);
217
+ syncDesign({ agentRoot: root, result: result(), now: NOW, name: "Northwind" });
218
+ // Rewrite the snapshot in the pre-map shape, as an older SDK left it.
219
+ const doc = readFoundationState(root);
220
+ const legacy = { ...doc };
221
+ delete legacy.rendered;
222
+ writeFoundationState(root, legacy);
223
+ assert.equal(readFoundationState(root).rendered, undefined);
224
+
225
+ const out = syncDesign({ agentRoot: root, result: result(), now: NOW + 3_600_000, name: "Northwind" });
226
+ assert.deepEqual(out.written, [], "the old stamp is read as the agent dir's, so the bytes still match");
227
+ assert.equal(readFoundationState(root).rendered[resolve(root)], legacy.renderedAt);
228
+ });
229
+
230
+ test("both markdown files are published atomically and forced to 0644 even over a tighter file", (t) => {
231
+ const root = tmpRoot(t);
232
+ syncDesign({ agentRoot: root, result: result(), now: NOW, name: "Northwind" });
233
+ // Some other tool tightens the mode and truncates the content.
234
+ chmodSync(join(root, "DESIGN.md"), 0o600);
235
+ writeFileSync(join(root, "DESIGN.md"), "half a file");
236
+ syncDesign({ agentRoot: root, result: result(), now: NOW + 1000, name: "Northwind" });
237
+ assert.equal(statSync(join(root, "DESIGN.md")).mode & 0o777, DESIGN_FILE_MODE, "chmod runs on a rewrite, not only on create");
238
+
239
+ // No temp file is left behind by the atomic publish.
240
+ assert.deepEqual(readdirSync(root).filter((f) => f.includes(".tmp.")), []);
241
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * lib/prompts/parallelism.mjs — the fleet-wide parallelism directive (WP-M7).
3
+ *
4
+ * THE GAP this closes (owner directive 2026-09-09): a session handed a plan with
5
+ * six independent tasks ran them one at a time, because nothing in its prompt
6
+ * ever said it was allowed not to. The directive below is the standing
7
+ * permission — plus the ONE safety rule that makes parallel dispatch safe on a
8
+ * shared checkout (no two agents editing the same file).
9
+ *
10
+ * It is a CONSTANT, not a template: the same bytes reach `maestro session spawn`
11
+ * (peer sessions) and `scripts/daemon/prompt-builder.mjs` (escalate/guarded
12
+ * sub-sessions), so the wording can be audited in one place and a test can pin
13
+ * it byte-for-byte.
14
+ *
15
+ * {@link withParallelism} is IDEMPOTENT. Two prompt seams compose (a daemon
16
+ * prompt handed to `session spawn`), and a directive repeated twice reads as
17
+ * emphasis to a model, which is exactly how "maximum safe parallelism" turns
18
+ * into "ignore the file-scope rule" — so a prompt that already carries the text
19
+ * is returned unchanged.
20
+ *
21
+ * Pure: no clock, no env, no I/O. Node builtins only. ESM.
22
+ *
23
+ * @module lib/prompts/parallelism
24
+ */
25
+
26
+ "use strict";
27
+
28
+ /**
29
+ * The directive, verbatim (design spec 2026-09-08 §WP-M7 mechanic 5). Do not
30
+ * reword without changing the spec — `parallelism.test.mjs` pins these bytes.
31
+ * @type {string}
32
+ */
33
+ export const PARALLELISM_DIRECTIVE = `You are allowed to run multiple sub-agents concurrently.
34
+
35
+ When the plan contains independent tasks, dispatch them together in a single parallel batch rather than waiting for each task to finish before starting the next.
36
+
37
+ Only run tasks sequentially when one task genuinely depends on the output of another.
38
+
39
+ Before dispatching any batch, check the planned file scope for each task and ensure that no two agents are assigned to edit the same file. If tasks would overlap on a file, sequence those tasks or redefine their scope to eliminate the conflict.
40
+
41
+ Default to maximum safe parallelism.`;
42
+
43
+ /**
44
+ * Prepend {@link PARALLELISM_DIRECTIVE} to a prompt, exactly once.
45
+ *
46
+ * A prompt that already contains the directive anywhere in its body is returned
47
+ * unchanged (idempotent — see the module docstring). A non-string or empty
48
+ * prompt yields the directive alone, so a caller that lost its prompt still
49
+ * sends something coherent rather than `undefined`.
50
+ *
51
+ * @param {string} prompt the prompt to lead
52
+ * @returns {string}
53
+ */
54
+ export function withParallelism(prompt) {
55
+ const body = typeof prompt === "string" ? prompt : "";
56
+ if (body.includes(PARALLELISM_DIRECTIVE)) return body;
57
+ if (!body.trim()) return PARALLELISM_DIRECTIVE;
58
+ return `${PARALLELISM_DIRECTIVE}\n\n${body}`;
59
+ }
60
+
61
+ /**
62
+ * How many times the directive appears in a string. The seam a test (or a
63
+ * caller composing two prompts) uses to assert "exactly once".
64
+ * @param {string} text
65
+ * @returns {number}
66
+ */
67
+ export function countParallelism(text) {
68
+ const body = typeof text === "string" ? text : "";
69
+ if (!body) return 0;
70
+ let n = 0;
71
+ let i = body.indexOf(PARALLELISM_DIRECTIVE);
72
+ while (i !== -1) {
73
+ n += 1;
74
+ i = body.indexOf(PARALLELISM_DIRECTIVE, i + PARALLELISM_DIRECTIVE.length);
75
+ }
76
+ return n;
77
+ }
78
+
79
+ export default { PARALLELISM_DIRECTIVE, withParallelism, countParallelism };