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.
- package/.ma-agents.json +10 -0
- package/AGENTS.md +97 -0
- package/MANIFEST.yaml +3 -0
- package/README.md +17 -0
- package/_bmad-output/implementation-artifacts/21-10-profile-reconfigure.md +30 -6
- package/_bmad-output/implementation-artifacts/21-11-profile-uninstall.md +2 -1
- package/_bmad-output/implementation-artifacts/21-2-universal-instruction-block-expansion.md +217 -62
- package/_bmad-output/implementation-artifacts/21-3-roomodes-template-bmad-modes.md +196 -73
- package/_bmad-output/implementation-artifacts/21-4-agents-md-template-opencode.md +242 -53
- package/_bmad-output/implementation-artifacts/21-5-clinerules-template-extension.md +180 -41
- package/_bmad-output/implementation-artifacts/21-6-onprem-layered-guardrails.md +250 -75
- package/_bmad-output/implementation-artifacts/21-7-bmad-persona-phase-prefix.md +221 -89
- package/_bmad-output/implementation-artifacts/21-8-vllm-reference-doc-readme.md +121 -63
- package/_bmad-output/implementation-artifacts/21-9-tests-validation.md +332 -61
- package/_bmad-output/implementation-artifacts/bug-bmad-recompile-fails-on-airgapped-network.md +112 -0
- package/_bmad-output/implementation-artifacts/sprint-status.yaml +3 -2
- package/bin/cli.js +59 -0
- package/docs/deployment/vllm-nemotron.md +130 -0
- package/lib/agents.js +17 -2
- package/lib/bmad-customize/bmm-analyst.customize.yaml +8 -0
- package/lib/bmad-customize/bmm-architect.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-dev.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-pm.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-qa.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-quick-flow-solo-dev.customize.yaml +8 -0
- package/lib/bmad-customize/bmm-sm.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-tech-writer.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-ux-designer.customize.yaml +2 -0
- package/lib/bmad.js +293 -1
- package/lib/installer.js +617 -43
- package/lib/merge/roomodes.js +125 -0
- package/lib/profile.js +25 -2
- package/lib/reconfigure.js +334 -0
- package/lib/templates/agents-md.template.md +67 -0
- package/lib/templates/clinerules.template.md +13 -0
- package/lib/templates/instruction-block-onprem.template.md +86 -0
- package/lib/templates/instruction-block-universal.template.md +29 -0
- package/lib/templates/roomodes.template.yaml +96 -0
- package/lib/uninstall.js +314 -0
- package/package.json +4 -3
- package/test/agents-md.test.js +398 -0
- package/test/bmad-extension.test.js +2 -2
- package/test/bmad-persona-phase-prefix.test.js +271 -0
- package/test/clinerules.test.js +339 -0
- package/test/instruction-block.test.js +388 -0
- package/test/integration-verification.test.js +2 -2
- package/test/migration-validation.test.js +2 -2
- package/test/offline-recompile.test.js +237 -0
- package/test/onprem-injection.test.js +425 -32
- package/test/onprem-layer.test.js +419 -0
- package/test/reconfigure.test.js +436 -0
- package/test/roomodes.test.js +343 -0
- 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
|
+
});
|