baldart 4.82.0 → 4.83.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.
@@ -0,0 +1,418 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+ const {
5
+ mergeOverlay,
6
+ shortSha,
7
+ MARKER_PREFIX,
8
+ isGeneratedFile,
9
+ } = require('./overlay-merger');
10
+
11
+ /**
12
+ * Root-file primitives writer (AGENTS.md + CLAUDE.md).
13
+ *
14
+ * BALDART ships versioned skeletons under
15
+ * `.framework/framework/templates/primitives/<NAME>.md`. This module resolves
16
+ * their `{{ slot }}` / `{{> partial }}` / `{{#flag}}` placeholders from
17
+ * `baldart.config.yml` (+ package.json / README probes) at install/update time,
18
+ * merges `.baldart/overlays/<NAME>.md` (OVERRIDE/APPEND/PREPEND via
19
+ * overlay-merger), stamps a `baldart-generated` marker, and writes the concrete
20
+ * file to the consumer repo root.
21
+ *
22
+ * The marker reuses overlay-merger's MARKER_PREFIX and keeps `kind`, `name`,
23
+ * `base_sha`, `overlay_sha` in the positions overlay-merger's `readMarker`
24
+ * expects — so `isGeneratedFile()` and the framework-edit-gate recognise these
25
+ * root files as generated. Two extra trailing fields (`config_sha`,
26
+ * `primitive_version`) are appended AFTER `overlay_sha`, which readMarker
27
+ * tolerates (it matches a prefix). `base_sha` carries the raw-template sha.
28
+ *
29
+ * States handled per file (checked in this order): generated (regenerate) →
30
+ * symlink (legacy AGENTS.md → replace) → handwritten (adopt) → absent (create).
31
+ */
32
+
33
+ const FRAMEWORK_PAYLOAD = path.join('.framework', 'framework');
34
+ const PRIMITIVES = ['AGENTS', 'CLAUDE'];
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // small fs helpers (mirror configure.js detect() ergonomics)
38
+ // ---------------------------------------------------------------------------
39
+ function readSafe(p) { try { return fs.readFileSync(p, 'utf8'); } catch (_) { return ''; } }
40
+ function readJsonSafe(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; } }
41
+ function existsAt(p) { try { fs.accessSync(p); return true; } catch (_) { return false; } }
42
+
43
+ function loadConfig(cwd) {
44
+ const p = path.join(cwd, 'baldart.config.yml');
45
+ try { return yaml.load(fs.readFileSync(p, 'utf8')) || {}; }
46
+ catch (_) { return {}; }
47
+ }
48
+
49
+ function readPrimitiveVersion(templateContent) {
50
+ const m = templateContent.match(/baldart-primitive:[^\n]*primitive_version=([0-9]+\.[0-9]+\.[0-9]+)/);
51
+ return m ? m[1] : '0.0.0';
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // slot resolution — deterministic, no LLM, no invention
56
+ // ---------------------------------------------------------------------------
57
+ const PRETTY = {
58
+ nextjs: 'Next.js', remix: 'Remix', sveltekit: 'SvelteKit', astro: 'Astro',
59
+ nuxt: 'Nuxt', rails: 'Rails', django: 'Django', fastapi: 'FastAPI', express: 'Express',
60
+ supabase: 'Supabase', firestore: 'Firestore', postgres: 'Postgres', mysql: 'MySQL',
61
+ mongodb: 'MongoDB', dynamodb: 'DynamoDB', sqlite: 'SQLite',
62
+ 'firebase-auth': 'Firebase Auth', 'supabase-auth': 'Supabase Auth', clerk: 'Clerk',
63
+ auth0: 'Auth0', cognito: 'Cognito', nextauth: 'NextAuth', lucia: 'Lucia',
64
+ playwright: 'Playwright', cypress: 'Cypress', biome: 'Biome', vitest: 'Vitest',
65
+ eslint: 'ESLint', prettier: 'Prettier', tsc: 'TypeScript', jest: 'Jest',
66
+ };
67
+ function pretty(v) { return PRETTY[v] || v; }
68
+
69
+ function detectPackageManager(cwd) {
70
+ if (existsAt(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
71
+ if (existsAt(path.join(cwd, 'yarn.lock'))) return 'yarn';
72
+ if (existsAt(path.join(cwd, 'bun.lockb'))) return 'bun';
73
+ return 'npm';
74
+ }
75
+
76
+ function deriveSourceRoot(cwd) {
77
+ for (const d of ['src', 'app', 'lib']) {
78
+ if (existsAt(path.join(cwd, d))) return `${d}/`;
79
+ }
80
+ return '';
81
+ }
82
+
83
+ function firstParagraph(readme) {
84
+ if (!readme) return '';
85
+ const lines = readme.split('\n');
86
+ let i = 0;
87
+ // skip leading blanks + a single H1
88
+ while (i < lines.length && lines[i].trim() === '') i++;
89
+ if (i < lines.length && /^#\s+/.test(lines[i])) i++;
90
+ while (i < lines.length && lines[i].trim() === '') i++;
91
+ const para = [];
92
+ while (i < lines.length && lines[i].trim() !== '' && !/^#{1,6}\s/.test(lines[i]) && !/^[-*]\s/.test(lines[i])) {
93
+ para.push(lines[i].trim());
94
+ i++;
95
+ }
96
+ const text = para.join(' ').trim();
97
+ return text.length > 200 ? text.slice(0, 197).trimEnd() + '…' : text;
98
+ }
99
+
100
+ /**
101
+ * Resolve every slot + partial + flag from config and repo probes.
102
+ * Returns { scalars, partials, flags, meta }.
103
+ */
104
+ function resolveSlots(cwd, cfg) {
105
+ const pkg = readJsonSafe(path.join(cwd, 'package.json')) || {};
106
+ const readme = readSafe(path.join(cwd, 'README.md'));
107
+ const identity = cfg.identity || {};
108
+ const stack = cfg.stack || {};
109
+ const paths = cfg.paths || {};
110
+ const features = cfg.features || {};
111
+ const git = cfg.git || {};
112
+ const toolchain = cfg.toolchain || {};
113
+ const pm = detectPackageManager(cwd);
114
+
115
+ // identity.name
116
+ let name = identity.brand_name || pkg.name || path.basename(cwd);
117
+ if (typeof name === 'string' && name.startsWith('@') && name.includes('/')) name = name.split('/')[1];
118
+
119
+ // identity.description
120
+ const description = firstParagraph(readme)
121
+ || (pkg.description ? String(pkg.description).trim() : '')
122
+ || '<!-- TODO: one-line description of what this project is -->';
123
+
124
+ // stack.summary (compose present, pretty)
125
+ const stackParts = [];
126
+ if (stack.framework && stack.framework !== 'none') stackParts.push(pretty(stack.framework));
127
+ if (stack.database && stack.database !== 'none') stackParts.push(pretty(stack.database));
128
+ if (stack.auth_provider && stack.auth_provider !== 'none') stackParts.push(pretty(stack.auth_provider));
129
+ const e2e = stack.testing && stack.testing.e2e;
130
+ if (e2e) stackParts.push(pretty(e2e));
131
+ if (Array.isArray(toolchain.installed_tools)) {
132
+ for (const t of toolchain.installed_tools) { const p = pretty(t); if (!stackParts.includes(p)) stackParts.push(p); }
133
+ }
134
+ const stackSummary = stackParts.length ? stackParts.join(' · ') : 'the project stack';
135
+
136
+ // stack.node
137
+ let node = (pkg.engines && pkg.engines.node) ? String(pkg.engines.node).trim() : '';
138
+ if (!node) { const nvmrc = readSafe(path.join(cwd, '.nvmrc')).trim(); if (nvmrc) node = nvmrc.replace(/^v/, ''); }
139
+ const nodeSuffix = node ? ` Node ${node}.` : '';
140
+
141
+ // output style (Claude-only, personal settings)
142
+ const settingsLocal = readJsonSafe(path.join(cwd, '.claude', 'settings.local.json')) || {};
143
+ const outputStyle = typeof settingsLocal.outputStyle === 'string' ? settingsLocal.outputStyle : '';
144
+
145
+ // flags
146
+ const flags = {
147
+ has_backlog: features.has_backlog === true,
148
+ has_adrs: features.has_adrs === true,
149
+ has_i18n: features.has_i18n === true,
150
+ has_design_system: features.has_design_system === true,
151
+ has_toolchain: features.has_toolchain === true,
152
+ has_project_status: !!(paths.references_dir && existsAt(path.join(cwd, paths.references_dir, 'project-status.md'))),
153
+ output_style: !!outputStyle,
154
+ };
155
+
156
+ const scalars = {
157
+ 'identity.name': name,
158
+ 'identity.description': description,
159
+ 'stack.summary': stackSummary,
160
+ 'stack.node_suffix': nodeSuffix,
161
+ 'git.trunk_branch': git.trunk_branch || 'main',
162
+ 'git.feature_prefix': 'feat', // STATIC BALDART convention
163
+ 'git.commit_format': '[FEAT-XXXX] description', // STATIC default (override via overlay)
164
+ 'i18n.registry': paths.i18n_registry || 'the i18n registry',
165
+ 'output_style': outputStyle,
166
+ };
167
+
168
+ const partials = {
169
+ commands_section: buildCommandsSection(pkg, toolchain, pm),
170
+ repo_map: buildRepoMap(cwd, paths, flags),
171
+ };
172
+
173
+ return { scalars, partials, flags };
174
+ }
175
+
176
+ function buildCommandsSection(pkg, toolchain, pm) {
177
+ const scripts = (pkg && pkg.scripts) || {};
178
+ const tc = (toolchain && toolchain.commands) || {};
179
+ const run = (s) => `${pm} run ${s}`;
180
+ const rows = [];
181
+ const push = (label, cmd) => { if (cmd) rows.push(`| \`${cmd}\` | ${label} |`); };
182
+ push('Run the app (dev)', scripts.dev ? run('dev') : (scripts.start ? run('start') : ''));
183
+ push('Production build', tc.build || (scripts.build ? run('build') : ''));
184
+ push('Lint', tc.lint || (scripts.lint ? run('lint') : ''));
185
+ push('Type check', tc.typecheck || (scripts.typecheck ? run('typecheck') : ''));
186
+ push('Unit tests', tc.test || (scripts.test ? run('test') : ''));
187
+ push('E2E tests', scripts['test:e2e'] ? run('test:e2e') : '');
188
+ push('Full QA gate', scripts.qa ? run('qa') : '');
189
+ if (!rows.length) return '';
190
+ return '## Commands\n\n| Command | Purpose |\n|---|---|\n' + rows.join('\n') + '\n';
191
+ }
192
+
193
+ function buildRepoMap(cwd, paths, flags) {
194
+ const lines = ['- `AGENTS.md` (this file) + `CLAUDE.md` (Claude Code entry).'];
195
+ const sourceRoot = deriveSourceRoot(cwd);
196
+ if (sourceRoot) lines.push(`- \`${sourceRoot}\` — application code.`);
197
+ if (flags.has_backlog && paths.backlog_dir) lines.push(`- \`${paths.backlog_dir}\` — work cards.`);
198
+ if (paths.references_dir) lines.push(`- \`${paths.references_dir}\` — data model, API, status, history.`);
199
+ if (flags.has_adrs && paths.adrs_dir) lines.push(`- \`${paths.adrs_dir}\` — architectural decision records.`);
200
+ if (flags.has_design_system && paths.design_system) lines.push(`- \`${paths.design_system}\` — design-system SSOT.`);
201
+ return lines.join('\n');
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // minimal template engine: {{#flag}}…{{/flag}}, {{> partial}}, {{ scalar }}
206
+ // ---------------------------------------------------------------------------
207
+ function fillTemplate(template, { scalars, partials, flags }) {
208
+ // 1. strip leading authoring banner (<!-- baldart-primitive: … -->)
209
+ let out = template.replace(/^?\s*<!--\s*baldart-primitive:[\s\S]*?-->\s*\n/, '');
210
+
211
+ // 2. conditionals (non-nested; each flag block resolved independently)
212
+ out = out.replace(/\{\{#([\w.]+)\}\}\n?([\s\S]*?)\{\{\/\1\}\}\n?/g, (_, key, body) =>
213
+ (flags[key] ? body : ''));
214
+
215
+ // 3. partials (whole-line replacement)
216
+ out = out.replace(/^[ \t]*\{\{>\s*([\w.]+)\s*\}\}[ \t]*$/gm, (_, key) =>
217
+ (partials[key] != null ? partials[key] : ''));
218
+
219
+ // 4. scalars
220
+ out = out.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) =>
221
+ (scalars[key] != null ? String(scalars[key]) : ''));
222
+
223
+ // 5. cleanup — drop empty bullets/rows, collapse blank runs, single trailing NL
224
+ out = out
225
+ .split('\n')
226
+ .filter((l) => !/^\s*[-|]\s*$/.test(l))
227
+ .join('\n')
228
+ .replace(/\n{3,}/g, '\n\n')
229
+ .replace(/\s+$/, '') + '\n';
230
+ return out;
231
+ }
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // marker
235
+ // ---------------------------------------------------------------------------
236
+ function buildRootMarker({ name, templateSha, overlaySha, configSha, primitiveVersion }) {
237
+ return `${MARKER_PREFIX} kind=root name=${name} base_sha=${templateSha} overlay_sha=${overlaySha || 'none'} config_sha=${configSha} primitive_version=${primitiveVersion}
238
+ DO NOT EDIT MANUALLY. Edit .baldart/overlays/${name}.md instead;
239
+ this file is regenerated by \`npx baldart update\`. -->`;
240
+ }
241
+
242
+ function readRootMarker(content) {
243
+ const idx = content.indexOf(MARKER_PREFIX);
244
+ if (idx === -1) return null;
245
+ const block = content.slice(idx, idx + 4000);
246
+ const end = block.indexOf('-->');
247
+ if (end === -1) return null;
248
+ const seg = block.slice(0, end);
249
+ const kind = (seg.match(/kind=(\S+)/) || [])[1];
250
+ if (kind !== 'root') return null;
251
+ return {
252
+ name: (seg.match(/name=(\S+)/) || [])[1] || null,
253
+ templateSha: (seg.match(/base_sha=(\S+)/) || [])[1] || null,
254
+ overlaySha: (seg.match(/overlay_sha=(\S+)/) || [])[1] || null,
255
+ configSha: (seg.match(/config_sha=(\S+)/) || [])[1] || null,
256
+ primitiveVersion: (seg.match(/primitive_version=(\S+)/) || [])[1] || null,
257
+ };
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // build the generated content for one primitive (no fs side effects)
262
+ // ---------------------------------------------------------------------------
263
+ function renderPrimitive({ cwd, name, cfg, frameworkVersion, overlayContent }) {
264
+ const templatePath = path.join(cwd, FRAMEWORK_PAYLOAD, 'templates', 'primitives', `${name}.md`);
265
+ const template = readSafe(templatePath);
266
+ if (!template) return null;
267
+ const primitiveVersion = readPrimitiveVersion(template);
268
+ const resolved = resolveSlots(cwd, cfg);
269
+ const filled = fillTemplate(template, resolved);
270
+
271
+ // section-merge overlay via the proven engine, then swap in our own marker.
272
+ const { generated } = mergeOverlay({
273
+ kind: 'root', name, baseContent: filled,
274
+ overlayContent: overlayContent || '', frameworkVersion,
275
+ });
276
+ // mergeOverlay puts its marker first (no frontmatter on root files); strip it.
277
+ let body = generated;
278
+ const mstart = body.indexOf(MARKER_PREFIX);
279
+ if (mstart !== -1) {
280
+ const mend = body.indexOf('-->', mstart);
281
+ if (mend !== -1) body = body.slice(mend + 3).replace(/^\n/, '');
282
+ }
283
+
284
+ const templateSha = shortSha(template);
285
+ const overlaySha = overlayContent ? shortSha(overlayContent) : 'none';
286
+ const configSha = shortSha(JSON.stringify(resolved) + '|' + templateSha);
287
+ const marker = buildRootMarker({ name, templateSha, overlaySha, configSha, primitiveVersion });
288
+ return { content: `${marker}\n${body}`.replace(/\s+$/, '') + '\n', templateSha, overlaySha, configSha, primitiveVersion };
289
+ }
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // state detection
293
+ // ---------------------------------------------------------------------------
294
+ function detectState(cwd, name) {
295
+ const rootPath = path.join(cwd, `${name}.md`);
296
+ let lstat = null;
297
+ try { lstat = fs.lstatSync(rootPath); } catch (_) { return { state: 'absent', content: '' }; }
298
+ if (lstat.isSymbolicLink()) return { state: 'symlink', content: '' };
299
+ const content = readSafe(rootPath);
300
+ if (isGeneratedFile(content) && readRootMarker(content)) return { state: 'generated', content };
301
+ return { state: 'handwritten', content };
302
+ }
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // public: status (for doctor) — does NOT write
306
+ // ---------------------------------------------------------------------------
307
+ function rootPrimitiveStatus({ cwd, name, cfg }) {
308
+ cfg = cfg || loadConfig(cwd);
309
+ const { state, content } = detectState(cwd, name);
310
+ if (state === 'absent') return { state, stale: true, reason: 'missing' };
311
+ if (state === 'symlink') return { state, stale: true, reason: 'legacy-symlink' };
312
+ if (state === 'handwritten') return { state, stale: false, reason: 'handwritten (needs adoption — run interactively)' };
313
+ // generated → compare marker vs freshly rendered
314
+ const rendered = renderPrimitive({ cwd, name, cfg, frameworkVersion: readFrameworkVersion(cwd), overlayContent: readOverlay(cwd, name) });
315
+ if (!rendered) return { state, stale: false, reason: 'no-template' };
316
+ const stale = content.replace(/\s+$/, '') !== rendered.content.replace(/\s+$/, '');
317
+ return { state, stale, reason: stale ? 'template / overlay / config drift' : 'up-to-date' };
318
+ }
319
+
320
+ function readOverlay(cwd, name) {
321
+ return readSafe(path.join(cwd, '.baldart', 'overlays', `${name}.md`));
322
+ }
323
+ function readFrameworkVersion(cwd) {
324
+ // The subtree pulls the whole repo into `.framework/`, so the VERSION file
325
+ // (repo root) lands at `.framework/VERSION` — NOT under the `framework/`
326
+ // payload subdir. (Informational only: feeds mergeOverlay's baseVersionTargeted.)
327
+ return readSafe(path.join(cwd, '.framework', 'VERSION')).trim() || 'unknown';
328
+ }
329
+
330
+ function isAutonomous(opts) {
331
+ if (opts && typeof opts.autonomous === 'boolean') return opts.autonomous;
332
+ return !!(process.env.CI || process.env.BALDART_AUTONOMOUS) || !process.stdout.isTTY;
333
+ }
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // public: write all root primitives (add / update / doctor / migrate)
337
+ // ---------------------------------------------------------------------------
338
+ /**
339
+ * @param {Object} opts
340
+ * @param {string} opts.cwd
341
+ * @param {string[]} [opts.tools] enabled tools; CLAUDE.md needs 'claude'
342
+ * @param {boolean} [opts.autonomous] CI/unattended → never mutate a handwritten file
343
+ * @returns {{ written:[], adopted:[], skipped:[], blockers:[] }}
344
+ */
345
+ function writeRootPrimitives(opts = {}) {
346
+ const cwd = opts.cwd || process.cwd();
347
+ const tools = (opts.tools && opts.tools.length) ? opts.tools : ['claude'];
348
+ const UI = safeUI();
349
+ const cfg = loadConfig(cwd);
350
+ const frameworkVersion = readFrameworkVersion(cwd);
351
+ const autonomous = isAutonomous(opts);
352
+ const result = { written: [], adopted: [], skipped: [], blockers: [] };
353
+
354
+ for (const name of PRIMITIVES) {
355
+ // CLAUDE.md is Claude-only.
356
+ if (name === 'CLAUDE' && !tools.includes('claude')) { result.skipped.push({ name, reason: 'claude-disabled' }); continue; }
357
+
358
+ const templatePath = path.join(cwd, FRAMEWORK_PAYLOAD, 'templates', 'primitives', `${name}.md`);
359
+ if (!existsAt(templatePath)) { result.skipped.push({ name, reason: 'no-template' }); continue; }
360
+
361
+ const rootPath = path.join(cwd, `${name}.md`);
362
+ const overlayPath = path.join(cwd, '.baldart', 'overlays', `${name}.md`);
363
+ const { state, content } = detectState(cwd, name);
364
+
365
+ // Adoption: a handwritten file with no overlay yet.
366
+ if (state === 'handwritten' && !existsAt(overlayPath)) {
367
+ if (autonomous) {
368
+ result.blockers.push({ name, reason: `${name}.md è scritto a mano — esegui \`npx baldart\` in modalità interattiva per adottarlo in un overlay (nessuna modifica applicata ora)` });
369
+ result.skipped.push({ name, reason: 'handwritten-autonomous' });
370
+ continue;
371
+ }
372
+ // Preserve the handwritten file verbatim into the overlay (zero-loss on disk).
373
+ try {
374
+ fs.mkdirSync(path.dirname(overlayPath), { recursive: true });
375
+ fs.writeFileSync(overlayPath, content, 'utf8');
376
+ } catch (e) {
377
+ result.blockers.push({ name, reason: `impossibile scrivere l'overlay di adozione: ${e.message}` });
378
+ continue;
379
+ }
380
+ result.adopted.push({ name, overlay: path.relative(cwd, overlayPath) });
381
+ UI && UI.info(`Adottato ${name}.md → ${path.relative(cwd, overlayPath)} (verbatim, zero perdita). Rivedi e ri-smista: molte sezioni potrebbero appartenere all'overlay di AGENTS.md.`);
382
+ }
383
+
384
+ // Render + write (generated / symlink / absent / just-adopted).
385
+ const overlayContent = readOverlay(cwd, name);
386
+ const rendered = renderPrimitive({ cwd, name, cfg, frameworkVersion, overlayContent });
387
+ if (!rendered) { result.skipped.push({ name, reason: 'render-failed' }); continue; }
388
+
389
+ // Byte-stable: skip write if unchanged (avoids spurious diffs on update).
390
+ if (state === 'generated' && content.replace(/\s+$/, '') === rendered.content.replace(/\s+$/, '')) {
391
+ result.skipped.push({ name, reason: 'up-to-date' });
392
+ continue;
393
+ }
394
+ try {
395
+ if (state === 'symlink') { try { fs.unlinkSync(rootPath); } catch (_) {} }
396
+ fs.writeFileSync(rootPath, rendered.content, 'utf8');
397
+ result.written.push({ name, from: state });
398
+ UI && UI.success(`Root primitive scritto: ${name}.md (${state})`);
399
+ } catch (e) {
400
+ result.blockers.push({ name, reason: `impossibile scrivere ${name}.md: ${e.message}` });
401
+ }
402
+ }
403
+ return result;
404
+ }
405
+
406
+ function safeUI() { try { return require('./ui'); } catch (_) { return null; } }
407
+
408
+ module.exports = {
409
+ writeRootPrimitives,
410
+ rootPrimitiveStatus,
411
+ renderPrimitive,
412
+ resolveSlots,
413
+ fillTemplate,
414
+ readRootMarker,
415
+ buildRootMarker,
416
+ detectState,
417
+ PRIMITIVES,
418
+ };
@@ -150,15 +150,12 @@ class SymlinkUtils {
150
150
  async createBulkSymlinks(opts = {}) {
151
151
  const mode = opts.mode || 'prompt';
152
152
 
153
- // AGENTS.md and the cross-tool `agents/` reference dir remain BULK
154
- // symlinks they're single canonical artifacts of the framework that
155
- // do not need per-item user-overlay behaviour.
156
- await this.createSymlink(
157
- path.join(FRAMEWORK_PAYLOAD, 'AGENTS.md'),
158
- 'AGENTS.md',
159
- { mode }
160
- );
161
-
153
+ // NB: `AGENTS.md` is NO LONGER a bulk symlink since v4.83.0 — it is a
154
+ // generated real file (skeleton + slots + overlay) written by
155
+ // writeRootPrimitives(), which createAllSymlinks() invokes after the
156
+ // per-item merges. A pre-v4.83.0 install has AGENTS.md as a symlink; the
157
+ // writer detects that (state 'symlink') and replaces it with the generated
158
+ // file. The cross-tool `agents/` reference dir stays a bulk symlink.
162
159
  await this.createSymlink(
163
160
  path.join(FRAMEWORK_PAYLOAD, 'agents'),
164
161
  'agents',
@@ -358,6 +355,22 @@ class SymlinkUtils {
358
355
  this.mergeOutputStyles({ tools: opts.tools });
359
356
 
360
357
  UI.newline();
358
+ UI.section('Writing Root Primitives');
359
+ this.writeRootPrimitives({ tools: opts.tools, autonomous: opts.autonomous });
360
+
361
+ UI.newline();
362
+ }
363
+
364
+ /**
365
+ * Generate the root-file primitives (AGENTS.md + CLAUDE.md) from the
366
+ * versioned skeletons under `.framework/framework/templates/primitives/`,
367
+ * filling slots from baldart.config.yml and merging `.baldart/overlays/`.
368
+ * Delegates to the standalone writer module (see src/utils/root-primitives.js).
369
+ */
370
+ writeRootPrimitives(opts = {}) {
371
+ return require('./root-primitives').writeRootPrimitives({
372
+ cwd: this.cwd, tools: opts.tools, autonomous: opts.autonomous,
373
+ });
361
374
  }
362
375
 
363
376
  // -----------------------------------------------------------------------
@@ -695,8 +708,9 @@ class SymlinkUtils {
695
708
  }
696
709
 
697
710
  verifySymlinks() {
711
+ // NB: `AGENTS.md` is NOT here since v4.83.0 — it is a generated real file
712
+ // (writeRootPrimitives), no longer a bulk symlink. Only `agents/` remains.
698
713
  const bulkSymlinks = [
699
- 'AGENTS.md',
700
714
  'agents'
701
715
  ];
702
716