ma-agents 3.5.6 → 3.6.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 (53) hide show
  1. package/.ma-agents.json +10 -0
  2. package/AGENTS.md +97 -0
  3. package/MANIFEST.yaml +3 -0
  4. package/README.md +17 -0
  5. package/_bmad-output/implementation-artifacts/21-10-profile-reconfigure.md +30 -6
  6. package/_bmad-output/implementation-artifacts/21-11-profile-uninstall.md +2 -1
  7. package/_bmad-output/implementation-artifacts/21-2-universal-instruction-block-expansion.md +217 -62
  8. package/_bmad-output/implementation-artifacts/21-3-roomodes-template-bmad-modes.md +196 -73
  9. package/_bmad-output/implementation-artifacts/21-4-agents-md-template-opencode.md +242 -53
  10. package/_bmad-output/implementation-artifacts/21-5-clinerules-template-extension.md +180 -41
  11. package/_bmad-output/implementation-artifacts/21-6-onprem-layered-guardrails.md +250 -75
  12. package/_bmad-output/implementation-artifacts/21-7-bmad-persona-phase-prefix.md +221 -89
  13. package/_bmad-output/implementation-artifacts/21-8-vllm-reference-doc-readme.md +121 -63
  14. package/_bmad-output/implementation-artifacts/21-9-tests-validation.md +332 -61
  15. package/_bmad-output/implementation-artifacts/bug-bmad-recompile-fails-on-airgapped-network.md +112 -0
  16. package/_bmad-output/implementation-artifacts/sprint-status.yaml +3 -2
  17. package/bin/cli.js +59 -0
  18. package/docs/deployment/vllm-nemotron.md +130 -0
  19. package/lib/agents.js +17 -2
  20. package/lib/bmad-customize/bmm-analyst.customize.yaml +8 -0
  21. package/lib/bmad-customize/bmm-architect.customize.yaml +2 -0
  22. package/lib/bmad-customize/bmm-dev.customize.yaml +2 -0
  23. package/lib/bmad-customize/bmm-pm.customize.yaml +2 -0
  24. package/lib/bmad-customize/bmm-qa.customize.yaml +2 -0
  25. package/lib/bmad-customize/bmm-quick-flow-solo-dev.customize.yaml +8 -0
  26. package/lib/bmad-customize/bmm-sm.customize.yaml +2 -0
  27. package/lib/bmad-customize/bmm-tech-writer.customize.yaml +2 -0
  28. package/lib/bmad-customize/bmm-ux-designer.customize.yaml +2 -0
  29. package/lib/bmad.js +293 -1
  30. package/lib/installer.js +617 -43
  31. package/lib/merge/roomodes.js +125 -0
  32. package/lib/profile.js +25 -2
  33. package/lib/reconfigure.js +334 -0
  34. package/lib/templates/agents-md.template.md +67 -0
  35. package/lib/templates/clinerules.template.md +13 -0
  36. package/lib/templates/instruction-block-onprem.template.md +86 -0
  37. package/lib/templates/instruction-block-universal.template.md +29 -0
  38. package/lib/templates/roomodes.template.yaml +96 -0
  39. package/lib/uninstall.js +314 -0
  40. package/package.json +4 -3
  41. package/test/agents-md.test.js +398 -0
  42. package/test/bmad-extension.test.js +2 -2
  43. package/test/bmad-persona-phase-prefix.test.js +271 -0
  44. package/test/clinerules.test.js +339 -0
  45. package/test/instruction-block.test.js +388 -0
  46. package/test/integration-verification.test.js +2 -2
  47. package/test/migration-validation.test.js +2 -2
  48. package/test/offline-recompile.test.js +237 -0
  49. package/test/onprem-injection.test.js +425 -32
  50. package/test/onprem-layer.test.js +419 -0
  51. package/test/reconfigure.test.js +436 -0
  52. package/test/roomodes.test.js +343 -0
  53. package/test/uninstall.test.js +402 -0
@@ -0,0 +1,419 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Story 21.6 — On-prem layered guardrails tests (composer + per-surface injection).
4
+ *
5
+ * Scope (story-local): verify the on-prem TEMPLATE contract and that on-prem
6
+ * content flows TRANSPARENTLY through every injection surface owned by
7
+ * Stories 21.2–21.5 when profile=on-prem, while remaining absent under
8
+ * profile=standard (NFR44).
9
+ *
10
+ * Cross-tool integration (every-agent-at-once, idempotency across all
11
+ * artifacts simultaneously) is Story 21.9's scope — not duplicated here.
12
+ *
13
+ * AC coverage:
14
+ * 7.1 Template exists + four AC #1 content categories present
15
+ * 7.2 Template contains NO {{...}} placeholders
16
+ * 7.3 NFR44 standard — composer output lacks the three literals
17
+ * 7.4 On-prem profile — composer output contains the three literals
18
+ * 7.5 Composer structure — universal + exactly one blank line + on-prem
19
+ * 7.6 Idempotency of composer for on-prem profile
20
+ * 7.7 Fresh standard-profile install: three literals absent from every
21
+ * rendered injection surface (excluding AGENTS.md's legitimate
22
+ * ~/.claude/ Critical Behavior Rules exception — AC #4)
23
+ * 7.8 Fresh on-prem-profile install: three literals present in every
24
+ * rendered injection surface (including .roomodes customInstructions
25
+ * for each of the four ma-agents-owned modes)
26
+ * 7.9 Two consecutive on-prem installs produce byte-identical content
27
+ * inside the marker region (NFR46)
28
+ * 7.10 NFR18 — opencode.json has exactly one [ma-agents] entry after
29
+ * on-prem install; other keys untouched; instructions[] length stable
30
+ * 7.11 NFR47 — .roomodes fileRegex patterns byte-identical between
31
+ * standard and on-prem renders (content diff only in customInstructions)
32
+ * 7.12 Shape A (AC #7 resolution) — AGENTS.md on-prem render carries
33
+ * /no_think inside the marker block
34
+ */
35
+ 'use strict';
36
+
37
+ const assert = require('assert');
38
+ const fs = require('fs');
39
+ const os = require('os');
40
+ const path = require('path');
41
+
42
+ let passed = 0;
43
+ let failed = 0;
44
+ const errors = [];
45
+
46
+ async function test(name, fn) {
47
+ try {
48
+ await fn();
49
+ console.log(` \u2713 ${name}`);
50
+ passed++;
51
+ } catch (err) {
52
+ console.error(` \u2717 ${name}: ${err.stack || err.message}`);
53
+ failed++;
54
+ errors.push({ name, error: err.message });
55
+ }
56
+ }
57
+
58
+ const installerModule = require('../lib/installer');
59
+ const {
60
+ composeInstructionBlock,
61
+ ONPREM_INSTRUCTION_TEMPLATE_PATH,
62
+ _testUpdateAgentInstructions: updateAgentInstructions,
63
+ stampExtraInstructionTemplates
64
+ } = installerModule;
65
+ const { setProfile } = require('../lib/profile');
66
+ const agents = require('../lib/agents');
67
+
68
+ function mktemp() {
69
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'ma-agents-onprem-layer-test-'));
70
+ }
71
+
72
+ function cleanup(dir) {
73
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
74
+ }
75
+
76
+ // Extract content between the MA-AGENTS markers (inclusive of markers).
77
+ function extractMarkerBlock(text) {
78
+ const start = text.indexOf('<!-- MA-AGENTS-START -->');
79
+ const end = text.indexOf('<!-- MA-AGENTS-END -->');
80
+ if (start < 0 || end < 0) return null;
81
+ return text.slice(start, end + '<!-- MA-AGENTS-END -->'.length);
82
+ }
83
+
84
+ async function installAllInjectionSurfaces(projectRoot, opts = {}) {
85
+ // Install every agent that receives an injection (markdown-marker + json-merge
86
+ // + extraInstructionTemplates). BMAD-category agents are skipped — their
87
+ // instruction files do not exist in a fresh install and 21.2 AC #9 preserves
88
+ // the "BMAD agent file not yet deployed" skip.
89
+ const ids = ['claude-code', 'gemini', 'copilot', 'kilocode', 'cline', 'roo-code', 'cursor', 'antigravity', 'opencode'];
90
+ const cwdOriginal = process.cwd();
91
+ process.chdir(projectRoot);
92
+ try {
93
+ for (const id of ids) {
94
+ const agent = agents.getAgent(id);
95
+ if (!agent) continue;
96
+ await updateAgentInstructions(agent, projectRoot, opts);
97
+ if (Array.isArray(agent.extraInstructionTemplates) && agent.extraInstructionTemplates.length > 0) {
98
+ await stampExtraInstructionTemplates(agent, projectRoot, opts);
99
+ }
100
+ }
101
+ } finally {
102
+ process.chdir(cwdOriginal);
103
+ }
104
+ }
105
+
106
+ console.log('\n onprem-layer unit + integration tests (Story 21.6)\n');
107
+
108
+ async function runAll() {
109
+ // 7.1 — Template exists + four AC #1 content categories
110
+ await test('7.1 on-prem template file exists and contains the four AC #1 categories', () => {
111
+ assert.ok(fs.existsSync(ONPREM_INSTRUCTION_TEMPLATE_PATH), 'template file must exist');
112
+ const t = fs.readFileSync(ONPREM_INSTRUCTION_TEMPLATE_PATH, 'utf-8');
113
+ assert.ok(t.includes('/no_think'), 'category 1: /no_think directive');
114
+ assert.ok(t.includes('~/.claude/'), 'category 2: no-home-dir-writes rule references ~/.claude/');
115
+ assert.ok(t.includes('str_replace_editor'), 'category 3: no-str_replace_editor rule');
116
+ // Category 4 — per-phase reasoning+sampling guidance
117
+ assert.ok(/planning/i.test(t) && /reasoning/i.test(t), 'category 4: per-phase reasoning guidance');
118
+ assert.ok(/temperature/i.test(t), 'category 4: sampling (temperature) guidance');
119
+ assert.ok(/implementation/i.test(t), 'category 4: implementation-phase guidance');
120
+ });
121
+
122
+ // 7.2 — No placeholders
123
+ await test('7.2 on-prem template contains no {{...}} placeholders', () => {
124
+ const t = fs.readFileSync(ONPREM_INSTRUCTION_TEMPLATE_PATH, 'utf-8');
125
+ assert.ok(!t.includes('{{'), 'no {{...}} placeholders allowed in on-prem template (AC #3)');
126
+ });
127
+
128
+ // 7.3 — NFR44: composer standard-profile output lacks the three literals
129
+ await test('7.3 NFR44 — composer standard-profile output lacks /no_think, str_replace_editor, ~/.claude/', () => {
130
+ const out = composeInstructionBlock({ profile: 'standard', projectRoot: mktemp() });
131
+ assert.ok(!out.includes('/no_think'));
132
+ assert.ok(!out.includes('str_replace_editor'));
133
+ assert.ok(!out.includes('~/.claude/'));
134
+ });
135
+
136
+ // 7.4 — On-prem: composer output contains the three literals
137
+ await test('7.4 on-prem composer output contains /no_think, str_replace_editor, ~/.claude/', () => {
138
+ const out = composeInstructionBlock({ profile: 'on-prem', projectRoot: mktemp() });
139
+ assert.ok(out.includes('/no_think'), 'missing /no_think');
140
+ assert.ok(out.includes('str_replace_editor'), 'missing str_replace_editor');
141
+ assert.ok(out.includes('~/.claude/'), 'missing ~/.claude/');
142
+ });
143
+
144
+ // 7.5 — Composer structure: universal + exactly one blank line + on-prem
145
+ await test('7.5 on-prem composer output = universal + one blank line + on-prem template', () => {
146
+ const onprem = composeInstructionBlock({ profile: 'on-prem', projectRoot: mktemp() });
147
+ const standard = composeInstructionBlock({ profile: 'standard', projectRoot: mktemp() });
148
+ // standard content is universal-only (normalized to end with exactly one \n).
149
+ // on-prem content must START with the same universal body, then "\n\n", then on-prem template.
150
+ const universalTrimmed = standard.replace(/\s+$/, '');
151
+ assert.ok(onprem.startsWith(universalTrimmed), 'on-prem output must begin with universal content');
152
+ const remainder = onprem.slice(universalTrimmed.length);
153
+ assert.ok(remainder.startsWith('\n\n'), 'exactly one blank line separator between universal and on-prem');
154
+ // Exactly one blank line means: \n\n followed by non-\n
155
+ assert.ok(!remainder.startsWith('\n\n\n'), 'more than one blank line between layers is forbidden');
156
+ });
157
+
158
+ // 7.6 — Idempotency
159
+ await test('7.6 composer is byte-identical across calls for on-prem profile', () => {
160
+ const a = composeInstructionBlock({ profile: 'on-prem', projectRoot: mktemp() });
161
+ const b = composeInstructionBlock({ profile: 'on-prem', projectRoot: mktemp() });
162
+ assert.strictEqual(a, b);
163
+ });
164
+
165
+ // 7.7 — Fresh standard-profile install: three literals absent from every injection surface
166
+ await test('7.7 NFR44 integration — standard profile install: three literals absent from every rendered file', async () => {
167
+ const dir = mktemp();
168
+ try {
169
+ setProfile(dir, 'standard');
170
+ await installAllInjectionSurfaces(dir);
171
+
172
+ const surfaces = [
173
+ '.claude/CLAUDE.md',
174
+ '.cline/clinerules.md',
175
+ '.clinerules',
176
+ '.roo/rules/00-ma-agents.md',
177
+ '.cursor/cursor.md',
178
+ '.kilocode/kilocode.md',
179
+ '.github/copilot/copilot.md',
180
+ '.gemini/gemini.md',
181
+ '.antigravity/antigravity.md',
182
+ '.roomodes'
183
+ ];
184
+ // Sanity guard: the standard-profile install MUST actually render at least
185
+ // the canonical Claude Code instruction file. Otherwise the loop below
186
+ // would pass vacuously on a regression that drops every injection surface.
187
+ assert.ok(fs.existsSync(path.join(dir, '.claude/CLAUDE.md')),
188
+ 'sanity: standard-profile install must render at least .claude/CLAUDE.md');
189
+
190
+ for (const rel of surfaces) {
191
+ const p = path.join(dir, rel);
192
+ if (!fs.existsSync(p)) continue;
193
+ const content = fs.readFileSync(p, 'utf-8');
194
+ assert.ok(!content.includes('/no_think'), `${rel} contains forbidden /no_think in standard profile`);
195
+ assert.ok(!content.includes('str_replace_editor'), `${rel} contains forbidden str_replace_editor in standard profile`);
196
+ assert.ok(!content.includes('~/.claude/'), `${rel} contains forbidden ~/.claude/ in standard profile`);
197
+ }
198
+
199
+ // AGENTS.md has one legitimate ~/.claude/ occurrence (Critical Behavior Rules — AC #4 exception).
200
+ // But /no_think and str_replace_editor must still be absent.
201
+ const agentsMd = path.join(dir, 'AGENTS.md');
202
+ if (fs.existsSync(agentsMd)) {
203
+ const content = fs.readFileSync(agentsMd, 'utf-8');
204
+ assert.ok(!content.includes('/no_think'), 'AGENTS.md contains forbidden /no_think in standard profile');
205
+ assert.ok(!content.includes('str_replace_editor'), 'AGENTS.md contains forbidden str_replace_editor in standard profile');
206
+ }
207
+
208
+ // OpenCode opencode.json::instructions[] ma-agents-prefixed entry
209
+ const opencodeJson = path.join(dir, 'opencode.json');
210
+ if (fs.existsSync(opencodeJson)) {
211
+ const data = JSON.parse(fs.readFileSync(opencodeJson, 'utf-8'));
212
+ const maEntry = (data.instructions || []).find(e => typeof e === 'string' && e.startsWith('[ma-agents]'));
213
+ if (maEntry) {
214
+ assert.ok(!maEntry.includes('/no_think'));
215
+ assert.ok(!maEntry.includes('str_replace_editor'));
216
+ assert.ok(!maEntry.includes('~/.claude/'));
217
+ }
218
+ }
219
+ } finally { cleanup(dir); }
220
+ });
221
+
222
+ // 7.8 — Fresh on-prem install: three literals present in every rendered injection surface
223
+ await test('7.8 on-prem integration — on-prem profile install: three literals present in every rendered file', async () => {
224
+ const dir = mktemp();
225
+ try {
226
+ setProfile(dir, 'on-prem');
227
+ await installAllInjectionSurfaces(dir);
228
+
229
+ const surfaces = [
230
+ '.claude/CLAUDE.md',
231
+ '.cline/clinerules.md',
232
+ '.clinerules',
233
+ '.roo/rules/00-ma-agents.md',
234
+ '.cursor/cursor.md',
235
+ '.kilocode/kilocode.md',
236
+ '.github/copilot/copilot.md',
237
+ '.gemini/gemini.md',
238
+ '.antigravity/antigravity.md',
239
+ 'AGENTS.md',
240
+ '.roomodes'
241
+ ];
242
+ // Sanity guard — the on-prem install MUST render the marker-bearing surfaces.
243
+ // Without this, a regression that drops every surface would pass the loop vacuously.
244
+ assert.ok(fs.existsSync(path.join(dir, '.claude/CLAUDE.md')),
245
+ 'sanity: on-prem install must render .claude/CLAUDE.md');
246
+ assert.ok(fs.existsSync(path.join(dir, '.roomodes')),
247
+ 'sanity: on-prem install must render .roomodes');
248
+ assert.ok(fs.existsSync(path.join(dir, 'AGENTS.md')),
249
+ 'sanity: on-prem install must render AGENTS.md');
250
+
251
+ for (const rel of surfaces) {
252
+ const p = path.join(dir, rel);
253
+ if (!fs.existsSync(p)) continue;
254
+ const content = fs.readFileSync(p, 'utf-8');
255
+ assert.ok(content.includes('/no_think'), `${rel} missing /no_think in on-prem profile`);
256
+ assert.ok(content.includes('str_replace_editor'), `${rel} missing str_replace_editor in on-prem profile`);
257
+ assert.ok(content.includes('~/.claude/'), `${rel} missing ~/.claude/ in on-prem profile`);
258
+ }
259
+
260
+ // OpenCode ma-agents entry must contain the three literals in its single ma-agents-prefixed string
261
+ const opencodeJson = path.join(dir, 'opencode.json');
262
+ if (fs.existsSync(opencodeJson)) {
263
+ const data = JSON.parse(fs.readFileSync(opencodeJson, 'utf-8'));
264
+ const maEntries = (data.instructions || []).filter(e => typeof e === 'string' && e.startsWith('[ma-agents]'));
265
+ assert.strictEqual(maEntries.length, 1, 'exactly one [ma-agents] entry');
266
+ assert.ok(maEntries[0].includes('/no_think'));
267
+ assert.ok(maEntries[0].includes('str_replace_editor'));
268
+ assert.ok(maEntries[0].includes('~/.claude/'));
269
+ }
270
+
271
+ // .roomodes AC #6: /no_think present inside EACH of the four ma-agents mode customInstructions
272
+ const roomodesPath = path.join(dir, '.roomodes');
273
+ if (fs.existsSync(roomodesPath)) {
274
+ const yaml = fs.readFileSync(roomodesPath, 'utf-8');
275
+ const modes = ['bmad-pm', 'bmad-architect', 'bmad-techlead', 'bmad-dev'];
276
+ for (const slug of modes) {
277
+ const idx = yaml.indexOf(`slug: ${slug}`);
278
+ assert.ok(idx >= 0, `mode ${slug} present`);
279
+ // Find the extent of this mode (until the next "- slug:" or EOF)
280
+ const nextIdx = yaml.indexOf('- slug:', idx + 1);
281
+ const modeText = nextIdx > 0 ? yaml.slice(idx, nextIdx) : yaml.slice(idx);
282
+ assert.ok(modeText.includes('/no_think'), `.roomodes mode ${slug} customInstructions missing /no_think in on-prem profile`);
283
+ }
284
+ }
285
+ } finally { cleanup(dir); }
286
+ });
287
+
288
+ // 7.9 — Two consecutive on-prem installs produce byte-identical marker block content
289
+ await test('7.9 NFR46 — two consecutive on-prem installs: byte-identical marker-block content', async () => {
290
+ const dir = mktemp();
291
+ try {
292
+ setProfile(dir, 'on-prem');
293
+ await installAllInjectionSurfaces(dir);
294
+ const snap1 = {};
295
+ const files = ['.claude/CLAUDE.md', '.clinerules', '.cline/clinerules.md', '.roo/rules/00-ma-agents.md', 'AGENTS.md', '.roomodes'];
296
+ for (const f of files) {
297
+ const p = path.join(dir, f);
298
+ if (fs.existsSync(p)) snap1[f] = fs.readFileSync(p, 'utf-8');
299
+ }
300
+ await installAllInjectionSurfaces(dir);
301
+ for (const f of Object.keys(snap1)) {
302
+ const p = path.join(dir, f);
303
+ const after = fs.readFileSync(p, 'utf-8');
304
+ // For marker-bearing files compare the marker block region for byte-identity.
305
+ // .roomodes (YAML) compares full file.
306
+ if (f === '.roomodes') {
307
+ assert.strictEqual(after, snap1[f], `${f}: re-install not byte-identical`);
308
+ } else {
309
+ const b1 = extractMarkerBlock(snap1[f]);
310
+ const b2 = extractMarkerBlock(after);
311
+ assert.ok(b1 !== null && b2 !== null, `${f}: marker block present`);
312
+ assert.strictEqual(b2, b1, `${f}: marker block not byte-identical across installs`);
313
+ }
314
+ }
315
+ } finally { cleanup(dir); }
316
+ });
317
+
318
+ // 7.10 — NFR18: opencode.json additive JSON-merge not regressed
319
+ await test('7.10 NFR18 — on-prem install leaves opencode.json additive-merged (single ma-agents entry, user entries preserved)', async () => {
320
+ const dir = mktemp();
321
+ try {
322
+ setProfile(dir, 'on-prem');
323
+ const opencode = agents.getAgent('opencode');
324
+ assert.ok(opencode);
325
+ const filePath = path.join(dir, opencode.instructionFiles[0]);
326
+ fs.mkdirSync(path.dirname(filePath) || dir, { recursive: true });
327
+ fs.writeFileSync(filePath, JSON.stringify({
328
+ instructions: ['user-kept-entry'],
329
+ otherKey: { keep: true, n: 42 }
330
+ }, null, 2), 'utf-8');
331
+
332
+ const cwdOriginal = process.cwd();
333
+ process.chdir(dir);
334
+ try {
335
+ await updateAgentInstructions(opencode, dir);
336
+ if (Array.isArray(opencode.extraInstructionTemplates)) {
337
+ await stampExtraInstructionTemplates(opencode, dir);
338
+ }
339
+ } finally { process.chdir(cwdOriginal); }
340
+
341
+ let data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
342
+ assert.deepStrictEqual(data.otherKey, { keep: true, n: 42 }, 'unrelated keys untouched');
343
+ assert.ok(data.instructions.includes('user-kept-entry'), 'user entry preserved');
344
+ const maEntries = data.instructions.filter(e => typeof e === 'string' && e.startsWith('[ma-agents]'));
345
+ assert.strictEqual(maEntries.length, 1, 'exactly one ma-agents entry');
346
+ const lenAfter1 = data.instructions.length;
347
+
348
+ // Second install — instructions[] length stable
349
+ process.chdir(dir);
350
+ try {
351
+ await updateAgentInstructions(opencode, dir);
352
+ if (Array.isArray(opencode.extraInstructionTemplates)) {
353
+ await stampExtraInstructionTemplates(opencode, dir);
354
+ }
355
+ } finally { process.chdir(cwdOriginal); }
356
+ data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
357
+ const maEntries2 = data.instructions.filter(e => typeof e === 'string' && e.startsWith('[ma-agents]'));
358
+ assert.strictEqual(maEntries2.length, 1, 'still exactly one ma-agents entry after re-install');
359
+ assert.strictEqual(data.instructions.length, lenAfter1, 'instructions[] length stable across re-install');
360
+ } finally { cleanup(dir); }
361
+ });
362
+
363
+ // 7.11 — NFR47: .roomodes fileRegex patterns byte-identical standard vs on-prem
364
+ await test('7.11 NFR47 — .roomodes fileRegex identical between standard and on-prem renders', async () => {
365
+ const dirS = mktemp();
366
+ const dirO = mktemp();
367
+ try {
368
+ setProfile(dirS, 'standard');
369
+ setProfile(dirO, 'on-prem');
370
+ const roo = agents.getAgent('roo-code');
371
+ assert.ok(roo);
372
+ for (const dir of [dirS, dirO]) {
373
+ const cwdOriginal = process.cwd();
374
+ process.chdir(dir);
375
+ try {
376
+ await updateAgentInstructions(roo, dir);
377
+ await stampExtraInstructionTemplates(roo, dir);
378
+ } finally { process.chdir(cwdOriginal); }
379
+ }
380
+ const yS = fs.readFileSync(path.join(dirS, '.roomodes'), 'utf-8');
381
+ const yO = fs.readFileSync(path.join(dirO, '.roomodes'), 'utf-8');
382
+ const re = /fileRegex:\s*([^\n]+)/g;
383
+ const mS = [...yS.matchAll(re)].map(m => m[1].trim());
384
+ const mO = [...yO.matchAll(re)].map(m => m[1].trim());
385
+ assert.ok(mS.length >= 3, 'at least 3 fileRegex patterns expected');
386
+ assert.deepStrictEqual(mO, mS, 'fileRegex sequence must be byte-identical across profiles');
387
+ } finally { cleanup(dirS); cleanup(dirO); }
388
+ });
389
+
390
+ // 7.12 — Shape A: AGENTS.md on-prem carries /no_think inside the marker block
391
+ await test('7.12 Shape A — AGENTS.md on-prem marker block contains /no_think (location via Universal Rules)', async () => {
392
+ const dir = mktemp();
393
+ try {
394
+ setProfile(dir, 'on-prem');
395
+ const opencode = agents.getAgent('opencode');
396
+ const cwdOriginal = process.cwd();
397
+ process.chdir(dir);
398
+ try {
399
+ await updateAgentInstructions(opencode, dir);
400
+ await stampExtraInstructionTemplates(opencode, dir);
401
+ } finally { process.chdir(cwdOriginal); }
402
+ const content = fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf-8');
403
+ const block = extractMarkerBlock(content);
404
+ assert.ok(block, 'marker block present');
405
+ assert.ok(block.includes('/no_think'), 'on-prem /no_think inside marker block (Shape A)');
406
+ } finally { cleanup(dir); }
407
+ });
408
+
409
+ console.log(`\n ${passed} passed, ${failed} failed\n`);
410
+ if (failed > 0) {
411
+ errors.forEach(e => console.error(` ${e.name}: ${e.error}`));
412
+ process.exit(1);
413
+ }
414
+ }
415
+
416
+ runAll().catch(err => {
417
+ console.error('Unhandled error:', err);
418
+ process.exit(1);
419
+ });