@dalzoubi/dev-agents-sync 2.0.16 → 2.0.17

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,645 @@
1
+ /**
2
+ * tests/steering-seed.test.mjs
3
+ *
4
+ * Behavior tests for S1 — Steering scaffold + CLI seed.
5
+ *
6
+ * What these tests prove (one named test per criterion — §7):
7
+ *
8
+ * B1. init writes three blank steering scaffold files when .claude/steering/ is absent.
9
+ * B2. update writes three blank steering scaffold files when .claude/steering/ is absent.
10
+ * B3. update leaves an already-edited steering/decisions.md byte-identical (seed-if-absent).
11
+ * B4. update leaves an already-edited steering/glossary.md byte-identical (seed-if-absent).
12
+ * B5. update leaves an already-edited steering/architecture.md byte-identical (seed-if-absent).
13
+ * B6. Seeded steering files do NOT contain the managed-by marker (consumer-owned).
14
+ * B7. After seeding, the lockfile contains no entry for any steering/* path.
15
+ * B8. build.mjs copies .agents/steering/*.md → .claude/steering/*.md WITHOUT prepending
16
+ * the marker and WITHOUT mangling frontmatter.
17
+ *
18
+ * All tests use isolated tmp dirs (mkdtempSync) and never touch the real repo.
19
+ * They are intentionally FAILING until the implement agent wires up the behavior.
20
+ */
21
+
22
+ import { describe, it, beforeEach, afterEach } from 'node:test';
23
+ import assert from 'node:assert/strict';
24
+ import {
25
+ mkdtempSync,
26
+ mkdirSync,
27
+ writeFileSync,
28
+ readFileSync,
29
+ existsSync,
30
+ rmSync,
31
+ readdirSync,
32
+ } from 'node:fs';
33
+ import { execFileSync } from 'node:child_process';
34
+ import { tmpdir } from 'node:os';
35
+ import path from 'node:path';
36
+ import { fileURLToPath } from 'node:url';
37
+
38
+ import { runInit } from '../src/commands/init.mjs';
39
+ import { runUpdate } from '../src/commands/update.mjs';
40
+ import { writeLockfile, readLockfile } from '../src/lockfile.mjs';
41
+ import { hasMarker } from '../src/marker.mjs';
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Paths
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
48
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
49
+ const REPO_ROOT = path.resolve(PACKAGE_ROOT, '..', '..');
50
+ const BUILD_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build.mjs');
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Fixture helpers (mirrors the pattern in init.test.mjs / update.test.mjs)
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'release-v1.0.0');
57
+
58
+ function collectFiles(baseDir, currentDir, map, prefix) {
59
+ const entries = readdirSync(currentDir, { withFileTypes: true });
60
+ for (const entry of entries) {
61
+ const full = path.join(currentDir, entry.name);
62
+ if (entry.isDirectory()) {
63
+ collectFiles(baseDir, full, map, prefix);
64
+ } else {
65
+ const rel = path.relative(baseDir, full).replace(/\\/g, '/');
66
+ map[`${prefix}/${rel}`] = readFileSync(full, 'utf8');
67
+ }
68
+ }
69
+ }
70
+
71
+ function buildFixtureFileMap(targets = ['claude', 'cursor']) {
72
+ const map = {};
73
+ for (const target of targets) {
74
+ const targetDir = path.join(FIXTURE_DIR, target);
75
+ if (!existsSync(targetDir)) continue;
76
+ collectFiles(targetDir, targetDir, map, target);
77
+ }
78
+ return map;
79
+ }
80
+
81
+ function makeFixtureFetcher() {
82
+ return async (_repo, _tag, _token) => buildFixtureFileMap(['claude', 'cursor']);
83
+ }
84
+
85
+ const AVAILABLE_TAGS = ['v1.0.0', 'v1.1.0', 'v1.2.0', 'v2.0.0'];
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Helpers
89
+ // ---------------------------------------------------------------------------
90
+
91
+ function makeTmpDir() {
92
+ return mkdtempSync(path.join(tmpdir(), 'das-steering-test-'));
93
+ }
94
+
95
+ /** Sets up a minimal already-initialized consumer repo (lockfile + one managed file). */
96
+ async function setupInitializedRepo(consumerDir, opts = {}) {
97
+ const {
98
+ targets = ['claude'],
99
+ resolvedVersion = '1.0.0',
100
+ range = '^1',
101
+ } = opts;
102
+
103
+ writeLockfile(consumerDir, {
104
+ source: 'github:dalzoubi/dev-agents',
105
+ range,
106
+ resolvedVersion,
107
+ targets,
108
+ lastUpdated: '2026-06-16T00:00:00Z',
109
+ });
110
+
111
+ // Write at least one managed file so update can find something to work with.
112
+ const agentsDir = path.join(consumerDir, '.claude', 'agents');
113
+ mkdirSync(agentsDir, { recursive: true });
114
+ const fixtureContent = readFileSync(
115
+ path.join(FIXTURE_DIR, 'claude', 'agents', 'define.md'),
116
+ 'utf8',
117
+ );
118
+ writeFileSync(path.join(agentsDir, 'define.md'), fixtureContent, 'utf8');
119
+ }
120
+
121
+ /** The three steering file names the spec requires. */
122
+ const STEERING_FILES = ['decisions.md', 'glossary.md', 'architecture.md'];
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // B1 — init seeds steering when absent
126
+ // ---------------------------------------------------------------------------
127
+
128
+ describe('S1 steering seed — init', () => {
129
+ let consumerDir;
130
+
131
+ beforeEach(() => {
132
+ consumerDir = makeTmpDir();
133
+ });
134
+
135
+ afterEach(() => {
136
+ rmSync(consumerDir, { recursive: true, force: true });
137
+ });
138
+
139
+ it('B1: init creates .claude/steering/ with three scaffold files when absent', async () => {
140
+ // No steering/ pre-exists. init must create all three.
141
+ await runInit(consumerDir, {
142
+ targets: ['claude'],
143
+ fetcher: makeFixtureFetcher(),
144
+ availableTags: AVAILABLE_TAGS,
145
+ });
146
+
147
+ const steeringDir = path.join(consumerDir, '.claude', 'steering');
148
+ assert.ok(
149
+ existsSync(steeringDir),
150
+ `.claude/steering/ must be created by init when absent`,
151
+ );
152
+
153
+ for (const file of STEERING_FILES) {
154
+ const absPath = path.join(steeringDir, file);
155
+ assert.ok(
156
+ existsSync(absPath),
157
+ `init must write .claude/steering/${file} when absent`,
158
+ );
159
+ }
160
+ });
161
+ });
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // B2 — update seeds steering when absent
165
+ // ---------------------------------------------------------------------------
166
+
167
+ describe('S1 steering seed — update', () => {
168
+ let consumerDir;
169
+
170
+ beforeEach(() => {
171
+ consumerDir = makeTmpDir();
172
+ });
173
+
174
+ afterEach(() => {
175
+ rmSync(consumerDir, { recursive: true, force: true });
176
+ });
177
+
178
+ it('B2: update creates .claude/steering/ with three scaffold files when absent', async () => {
179
+ await setupInitializedRepo(consumerDir);
180
+ // The lockfile resolvedVersion is 1.0.0, but we supply 1.2.0 as the latest
181
+ // tag so update will run (not short-circuit as up-to-date).
182
+ await runUpdate(consumerDir, {
183
+ fetcher: makeFixtureFetcher(),
184
+ availableTags: ['v1.0.0', 'v1.1.0', 'v1.2.0', 'v2.0.0'],
185
+ });
186
+
187
+ const steeringDir = path.join(consumerDir, '.claude', 'steering');
188
+ assert.ok(
189
+ existsSync(steeringDir),
190
+ `.claude/steering/ must be created by update when absent`,
191
+ );
192
+
193
+ for (const file of STEERING_FILES) {
194
+ const absPath = path.join(steeringDir, file);
195
+ assert.ok(
196
+ existsSync(absPath),
197
+ `update must write .claude/steering/${file} when absent`,
198
+ );
199
+ }
200
+ });
201
+ });
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // B3–B5 — update never clobbers existing steering content
205
+ // ---------------------------------------------------------------------------
206
+
207
+ describe('S1 steering seed — never-clobber (update)', () => {
208
+ let consumerDir;
209
+
210
+ beforeEach(() => {
211
+ consumerDir = makeTmpDir();
212
+ });
213
+
214
+ afterEach(() => {
215
+ rmSync(consumerDir, { recursive: true, force: true });
216
+ });
217
+
218
+ it('B3: update leaves edited steering/decisions.md byte-identical', async () => {
219
+ await setupInitializedRepo(consumerDir);
220
+
221
+ const steeringDir = path.join(consumerDir, '.claude', 'steering');
222
+ mkdirSync(steeringDir, { recursive: true });
223
+ const original = '# Decisions\n\n2026-06-16 — chose X: because of Y\n';
224
+ writeFileSync(path.join(steeringDir, 'decisions.md'), original, 'utf8');
225
+
226
+ await runUpdate(consumerDir, {
227
+ fetcher: makeFixtureFetcher(),
228
+ availableTags: ['v1.0.0', 'v1.1.0', 'v1.2.0', 'v2.0.0'],
229
+ });
230
+
231
+ const after = readFileSync(path.join(steeringDir, 'decisions.md'), 'utf8');
232
+ assert.equal(
233
+ after,
234
+ original,
235
+ 'update must leave edited steering/decisions.md byte-identical (seed-if-absent)',
236
+ );
237
+ });
238
+
239
+ it('B4: update leaves edited steering/glossary.md byte-identical', async () => {
240
+ await setupInitializedRepo(consumerDir);
241
+
242
+ const steeringDir = path.join(consumerDir, '.claude', 'steering');
243
+ mkdirSync(steeringDir, { recursive: true });
244
+ const original = '# Glossary\n\n**Managed file**: a file bearing the `managed-by` marker.\n';
245
+ writeFileSync(path.join(steeringDir, 'glossary.md'), original, 'utf8');
246
+
247
+ await runUpdate(consumerDir, {
248
+ fetcher: makeFixtureFetcher(),
249
+ availableTags: ['v1.0.0', 'v1.1.0', 'v1.2.0', 'v2.0.0'],
250
+ });
251
+
252
+ const after = readFileSync(path.join(steeringDir, 'glossary.md'), 'utf8');
253
+ assert.equal(
254
+ after,
255
+ original,
256
+ 'update must leave edited steering/glossary.md byte-identical (seed-if-absent)',
257
+ );
258
+ });
259
+
260
+ it('B5: update leaves edited steering/architecture.md byte-identical', async () => {
261
+ await setupInitializedRepo(consumerDir);
262
+
263
+ const steeringDir = path.join(consumerDir, '.claude', 'steering');
264
+ mkdirSync(steeringDir, { recursive: true });
265
+ const original = '# Architecture\n\nCore pattern: CLI → lockfile → fetcher → writer.\n';
266
+ writeFileSync(path.join(steeringDir, 'architecture.md'), original, 'utf8');
267
+
268
+ await runUpdate(consumerDir, {
269
+ fetcher: makeFixtureFetcher(),
270
+ availableTags: ['v1.0.0', 'v1.1.0', 'v1.2.0', 'v2.0.0'],
271
+ });
272
+
273
+ const after = readFileSync(path.join(steeringDir, 'architecture.md'), 'utf8');
274
+ assert.equal(
275
+ after,
276
+ original,
277
+ 'update must leave edited steering/architecture.md byte-identical (seed-if-absent)',
278
+ );
279
+ });
280
+
281
+ it('B3 variant: init also leaves edited steering/decisions.md byte-identical', async () => {
282
+ // .claude/steering/ already exists with content before init is called on a second run.
283
+ // Simulating a re-init (the consumer already ran init before and edited steering).
284
+ const steeringDir = path.join(consumerDir, '.claude', 'steering');
285
+ mkdirSync(steeringDir, { recursive: true });
286
+ const original = '# Decisions\n\nPre-existing consumer content.\n';
287
+ writeFileSync(path.join(steeringDir, 'decisions.md'), original, 'utf8');
288
+
289
+ await runInit(consumerDir, {
290
+ targets: ['claude'],
291
+ fetcher: makeFixtureFetcher(),
292
+ availableTags: AVAILABLE_TAGS,
293
+ });
294
+
295
+ const after = readFileSync(path.join(steeringDir, 'decisions.md'), 'utf8');
296
+ assert.equal(
297
+ after,
298
+ original,
299
+ 'init must leave existing steering/decisions.md byte-identical (seed-if-absent)',
300
+ );
301
+ });
302
+ });
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // B6 — seeded steering files carry no managed-by marker
306
+ // ---------------------------------------------------------------------------
307
+
308
+ describe('S1 steering seed — no marker on seeded files', () => {
309
+ let consumerDir;
310
+
311
+ beforeEach(() => {
312
+ consumerDir = makeTmpDir();
313
+ });
314
+
315
+ afterEach(() => {
316
+ rmSync(consumerDir, { recursive: true, force: true });
317
+ });
318
+
319
+ it('B6a: steering/decisions.md seeded by init has no managed-by marker', async () => {
320
+ await runInit(consumerDir, {
321
+ targets: ['claude'],
322
+ fetcher: makeFixtureFetcher(),
323
+ availableTags: AVAILABLE_TAGS,
324
+ });
325
+
326
+ const content = readFileSync(
327
+ path.join(consumerDir, '.claude', 'steering', 'decisions.md'),
328
+ 'utf8',
329
+ );
330
+ assert.ok(
331
+ !hasMarker(content),
332
+ 'seeded steering/decisions.md must NOT contain the managed-by marker (consumer-owned)',
333
+ );
334
+ });
335
+
336
+ it('B6b: steering/glossary.md seeded by init has no managed-by marker', async () => {
337
+ await runInit(consumerDir, {
338
+ targets: ['claude'],
339
+ fetcher: makeFixtureFetcher(),
340
+ availableTags: AVAILABLE_TAGS,
341
+ });
342
+
343
+ const content = readFileSync(
344
+ path.join(consumerDir, '.claude', 'steering', 'glossary.md'),
345
+ 'utf8',
346
+ );
347
+ assert.ok(
348
+ !hasMarker(content),
349
+ 'seeded steering/glossary.md must NOT contain the managed-by marker (consumer-owned)',
350
+ );
351
+ });
352
+
353
+ it('B6c: steering/architecture.md seeded by init has no managed-by marker', async () => {
354
+ await runInit(consumerDir, {
355
+ targets: ['claude'],
356
+ fetcher: makeFixtureFetcher(),
357
+ availableTags: AVAILABLE_TAGS,
358
+ });
359
+
360
+ const content = readFileSync(
361
+ path.join(consumerDir, '.claude', 'steering', 'architecture.md'),
362
+ 'utf8',
363
+ );
364
+ assert.ok(
365
+ !hasMarker(content),
366
+ 'seeded steering/architecture.md must NOT contain the managed-by marker (consumer-owned)',
367
+ );
368
+ });
369
+
370
+ it('B6d: steering/decisions.md seeded by update has no managed-by marker', async () => {
371
+ await setupInitializedRepo(consumerDir);
372
+
373
+ await runUpdate(consumerDir, {
374
+ fetcher: makeFixtureFetcher(),
375
+ availableTags: ['v1.0.0', 'v1.1.0', 'v1.2.0', 'v2.0.0'],
376
+ });
377
+
378
+ const content = readFileSync(
379
+ path.join(consumerDir, '.claude', 'steering', 'decisions.md'),
380
+ 'utf8',
381
+ );
382
+ assert.ok(
383
+ !hasMarker(content),
384
+ 'seeded steering/decisions.md must NOT contain the managed-by marker (consumer-owned)',
385
+ );
386
+ });
387
+ });
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // B7 — lockfile does not track steering/* paths
391
+ // ---------------------------------------------------------------------------
392
+
393
+ describe('S1 steering seed — lockfile does not track steering paths', () => {
394
+ let consumerDir;
395
+
396
+ beforeEach(() => {
397
+ consumerDir = makeTmpDir();
398
+ });
399
+
400
+ afterEach(() => {
401
+ rmSync(consumerDir, { recursive: true, force: true });
402
+ });
403
+
404
+ it('B7a: after init, lockfile contains no entry for steering/decisions.md', async () => {
405
+ await runInit(consumerDir, {
406
+ targets: ['claude'],
407
+ fetcher: makeFixtureFetcher(),
408
+ availableTags: AVAILABLE_TAGS,
409
+ });
410
+
411
+ // Read raw lockfile JSON so we catch any form of steering path tracking,
412
+ // regardless of the field name used (files, managedFiles, tracked, etc.).
413
+ const raw = readFileSync(
414
+ path.join(consumerDir, '.dev-agents-sync.json'),
415
+ 'utf8',
416
+ );
417
+ assert.ok(
418
+ !raw.includes('steering/'),
419
+ `lockfile must not reference any steering/ path after init, got:\n${raw}`,
420
+ );
421
+ });
422
+
423
+ it('B7b: after update, lockfile contains no entry for any steering/* path', async () => {
424
+ await setupInitializedRepo(consumerDir);
425
+
426
+ await runUpdate(consumerDir, {
427
+ fetcher: makeFixtureFetcher(),
428
+ availableTags: ['v1.0.0', 'v1.1.0', 'v1.2.0', 'v2.0.0'],
429
+ });
430
+
431
+ const raw = readFileSync(
432
+ path.join(consumerDir, '.dev-agents-sync.json'),
433
+ 'utf8',
434
+ );
435
+ assert.ok(
436
+ !raw.includes('steering/'),
437
+ `lockfile must not reference any steering/ path after update, got:\n${raw}`,
438
+ );
439
+ });
440
+ });
441
+
442
+ // ---------------------------------------------------------------------------
443
+ // B8 — build.mjs emits steering files to .claude/steering/ without marker
444
+ // ---------------------------------------------------------------------------
445
+
446
+ /**
447
+ * Helper: run build.mjs with a temporary .agents/ dir that contains both
448
+ * a minimal valid agent (so the build doesn't fail with "nothing to emit")
449
+ * and a steering/ subdirectory with test content.
450
+ *
451
+ * Returns { exitCode, stdout, stderr, mirrorRoot }.
452
+ */
453
+ function runBuildWithSteering(agentsDir, steeringContent, mirrorRoot) {
454
+ const promptsDir = path.dirname(agentsDir); // e.g. <tmp>/.agents
455
+ const distDir = path.join(path.dirname(promptsDir), 'dist');
456
+
457
+ // Write minimal valid agent so build doesn't bail out.
458
+ const agentsSubdir = path.join(agentsDir, 'agents');
459
+ mkdirSync(agentsSubdir, { recursive: true });
460
+ writeFileSync(
461
+ path.join(agentsSubdir, 'dummy.md'),
462
+ `---\nname: dummy\ndescription: "Dummy agent for build test."\n---\n\n# Dummy\n\nBody text.\n`,
463
+ 'utf8',
464
+ );
465
+
466
+ // Write steering source files.
467
+ const steeringDir = path.join(agentsDir, 'steering');
468
+ mkdirSync(steeringDir, { recursive: true });
469
+ for (const [filename, content] of Object.entries(steeringContent)) {
470
+ writeFileSync(path.join(steeringDir, filename), content, 'utf8');
471
+ }
472
+
473
+ try {
474
+ const stdout = execFileSync(
475
+ process.execPath,
476
+ [
477
+ BUILD_SCRIPT,
478
+ '--prompts-dir', agentsDir,
479
+ '--dist-dir', distDir,
480
+ '--no-mirror-root',
481
+ '--mirror-claude', mirrorRoot,
482
+ ],
483
+ { encoding: 'utf8', env: { ...process.env } },
484
+ );
485
+ return { exitCode: 0, stdout, stderr: '', distDir, mirrorRoot };
486
+ } catch (err) {
487
+ return {
488
+ exitCode: err.status ?? 1,
489
+ stdout: err.stdout ?? '',
490
+ stderr: err.stderr ?? '',
491
+ distDir,
492
+ mirrorRoot,
493
+ };
494
+ }
495
+ }
496
+
497
+ describe('S1 steering seed — build.mjs emits steering to mirror without marker', () => {
498
+ let tmpRoot;
499
+
500
+ beforeEach(() => {
501
+ tmpRoot = mkdtempSync(path.join(tmpdir(), 'das-steering-build-test-'));
502
+ });
503
+
504
+ afterEach(() => {
505
+ rmSync(tmpRoot, { recursive: true, force: true });
506
+ });
507
+
508
+ it('B8a: build --mirror-claude copies .agents/steering/decisions.md to .claude/steering/decisions.md', () => {
509
+ const agentsDir = path.join(tmpRoot, '.agents');
510
+ const mirrorRoot = path.join(tmpRoot, 'mirror');
511
+ mkdirSync(mirrorRoot, { recursive: true });
512
+
513
+ const { exitCode, stderr } = runBuildWithSteering(
514
+ agentsDir,
515
+ {
516
+ 'decisions.md': '# Decisions\n\nConsumer-written content.\n',
517
+ 'glossary.md': '# Glossary\n',
518
+ 'architecture.md': '# Architecture\n',
519
+ },
520
+ mirrorRoot,
521
+ );
522
+
523
+ assert.equal(exitCode, 0, `Build must succeed. stderr:\n${stderr}`);
524
+
525
+ const dest = path.join(mirrorRoot, '.claude', 'steering', 'decisions.md');
526
+ assert.ok(
527
+ existsSync(dest),
528
+ `.claude/steering/decisions.md must be written by build --mirror-claude`,
529
+ );
530
+ });
531
+
532
+ it('B8b: build emits all three steering files to .claude/steering/', () => {
533
+ const agentsDir = path.join(tmpRoot, '.agents');
534
+ const mirrorRoot = path.join(tmpRoot, 'mirror');
535
+ mkdirSync(mirrorRoot, { recursive: true });
536
+
537
+ const { exitCode, stderr } = runBuildWithSteering(
538
+ agentsDir,
539
+ {
540
+ 'decisions.md': '# Decisions\n',
541
+ 'glossary.md': '# Glossary\n',
542
+ 'architecture.md': '# Architecture\n',
543
+ },
544
+ mirrorRoot,
545
+ );
546
+
547
+ assert.equal(exitCode, 0, `Build must succeed. stderr:\n${stderr}`);
548
+
549
+ for (const file of STEERING_FILES) {
550
+ const dest = path.join(mirrorRoot, '.claude', 'steering', file);
551
+ assert.ok(
552
+ existsSync(dest),
553
+ `build must emit .claude/steering/${file} (all three steering files)`,
554
+ );
555
+ }
556
+ });
557
+
558
+ it('B8c: build-emitted steering file does NOT start with the managed-by marker', () => {
559
+ const agentsDir = path.join(tmpRoot, '.agents');
560
+ const mirrorRoot = path.join(tmpRoot, 'mirror');
561
+ mkdirSync(mirrorRoot, { recursive: true });
562
+
563
+ const sourceContent = '# Decisions\n\nThis is consumer steering content.\n';
564
+
565
+ const { exitCode, stderr } = runBuildWithSteering(
566
+ agentsDir,
567
+ {
568
+ 'decisions.md': sourceContent,
569
+ 'glossary.md': '# Glossary\n',
570
+ 'architecture.md': '# Architecture\n',
571
+ },
572
+ mirrorRoot,
573
+ );
574
+
575
+ assert.equal(exitCode, 0, `Build must succeed. stderr:\n${stderr}`);
576
+
577
+ const dest = path.join(mirrorRoot, '.claude', 'steering', 'decisions.md');
578
+ const emitted = readFileSync(dest, 'utf8');
579
+
580
+ assert.ok(
581
+ !hasMarker(emitted),
582
+ `build-emitted steering/decisions.md must NOT contain the managed-by marker. Content:\n${emitted}`,
583
+ );
584
+ });
585
+
586
+ it('B8d: build-emitted steering file preserves source content verbatim (no frontmatter mangling)', () => {
587
+ const agentsDir = path.join(tmpRoot, '.agents');
588
+ const mirrorRoot = path.join(tmpRoot, 'mirror');
589
+ mkdirSync(mirrorRoot, { recursive: true });
590
+
591
+ const sourceContent = '# Decisions\n\n2026-06-16 — chose ESM modules: tree-shaking benefits.\n';
592
+
593
+ const { exitCode, stderr } = runBuildWithSteering(
594
+ agentsDir,
595
+ {
596
+ 'decisions.md': sourceContent,
597
+ 'glossary.md': '# Glossary\n',
598
+ 'architecture.md': '# Architecture\n',
599
+ },
600
+ mirrorRoot,
601
+ );
602
+
603
+ assert.equal(exitCode, 0, `Build must succeed. stderr:\n${stderr}`);
604
+
605
+ const dest = path.join(mirrorRoot, '.claude', 'steering', 'decisions.md');
606
+ const emitted = readFileSync(dest, 'utf8');
607
+
608
+ // Normalize line endings in case the writer converts to LF.
609
+ const normalize = (s) => s.replace(/\r\n/g, '\n');
610
+ assert.equal(
611
+ normalize(emitted),
612
+ normalize(sourceContent),
613
+ 'build must copy steering file verbatim without prepending a marker or modifying content',
614
+ );
615
+ });
616
+
617
+ it('B8e: build-emitted steering file first line is NOT the managed-by marker', () => {
618
+ // Strictly verifies the physical first line — catches cases where the marker
619
+ // lands before rather than after the content.
620
+ const agentsDir = path.join(tmpRoot, '.agents');
621
+ const mirrorRoot = path.join(tmpRoot, 'mirror');
622
+ mkdirSync(mirrorRoot, { recursive: true });
623
+
624
+ const { exitCode, stderr } = runBuildWithSteering(
625
+ agentsDir,
626
+ {
627
+ 'decisions.md': '# Decisions\n',
628
+ 'glossary.md': '# Glossary\n',
629
+ 'architecture.md': '# Architecture\n',
630
+ },
631
+ mirrorRoot,
632
+ );
633
+
634
+ assert.equal(exitCode, 0, `Build must succeed. stderr:\n${stderr}`);
635
+
636
+ const dest = path.join(mirrorRoot, '.claude', 'steering', 'decisions.md');
637
+ const emitted = readFileSync(dest, 'utf8');
638
+ const firstLine = emitted.split('\n')[0];
639
+
640
+ assert.ok(
641
+ !/<!--\s*managed-by/.test(firstLine),
642
+ `First line of emitted steering file must NOT be the marker. Got: ${JSON.stringify(firstLine)}`,
643
+ );
644
+ });
645
+ });