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,125 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Story 21.3 — .roomodes YAML merger (pure splicer).
5
+ *
6
+ * Contract (per AC #5, decision A):
7
+ * - Input: two strings — existingYaml (may be empty/undefined when the
8
+ * target file does not exist) and templateYaml (the already-composed
9
+ * template content; the caller — lib/installer.js — substituted
10
+ * {{UNIVERSAL_BLOCK}} via composeInstructionBlock BEFORE calling us).
11
+ * - This function MUST NOT call composeInstructionBlock. It treats
12
+ * templateYaml as opaque pre-composed input.
13
+ * - Parse both as YAML via js-yaml.
14
+ * - Output: serialized YAML string whose customModes array is
15
+ * [ ...user entries whose slug ∉ MA_AGENTS_OWNED_SLUGS,
16
+ * ...all entries from templateYaml.customModes ]
17
+ * (preserved-user-first ordering — NFR46 idempotency).
18
+ * - Any other top-level YAML keys from existingYaml are preserved (FR176).
19
+ * - For each user entry whose slug collides with an ma-agents-owned slug,
20
+ * emit exactly one console.warn line naming the slug verbatim (AC #6).
21
+ * - Pure function — no file I/O.
22
+ *
23
+ * Deterministic serialization (NFR46 — AC #10):
24
+ * js-yaml dump options are pinned so consecutive runs with identical
25
+ * inputs produce byte-identical output.
26
+ */
27
+
28
+ const yaml = require('js-yaml');
29
+
30
+ /**
31
+ * Canonical set of slugs owned by ma-agents. Exported for downstream
32
+ * consumers (Stories 21.10 profile-reconfigure, 21.11 profile-uninstall).
33
+ * Order is canonical — do not sort or reshuffle.
34
+ */
35
+ const MA_AGENTS_OWNED_SLUGS = Object.freeze([
36
+ 'bmad-pm',
37
+ 'bmad-architect',
38
+ 'bmad-techlead',
39
+ 'bmad-dev'
40
+ ]);
41
+
42
+ const DUMP_OPTIONS = Object.freeze({
43
+ indent: 2,
44
+ lineWidth: -1,
45
+ noRefs: true,
46
+ quotingType: '"',
47
+ sortKeys: false
48
+ });
49
+
50
+ /**
51
+ * Parse a YAML string into an object, returning {} for empty/undefined input.
52
+ * Throws with a descriptive message when the YAML is malformed.
53
+ *
54
+ * @param {string|undefined|null} text
55
+ * @param {string} label - 'existing' or 'template' (for error context)
56
+ * @returns {object}
57
+ */
58
+ function parseYaml(text, label) {
59
+ if (text == null || String(text).trim() === '') return {};
60
+ let parsed;
61
+ try {
62
+ parsed = yaml.load(text);
63
+ } catch (err) {
64
+ throw new Error(`mergeRoomodes: failed to parse ${label} YAML — ${err.message}`);
65
+ }
66
+ if (parsed == null) return {};
67
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) {
68
+ throw new Error(`mergeRoomodes: ${label} YAML must be a mapping, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);
69
+ }
70
+ return parsed;
71
+ }
72
+
73
+ /**
74
+ * Merge the ma-agents roomodes template into an existing .roomodes YAML string.
75
+ * See file header for the full contract.
76
+ *
77
+ * @param {string|undefined} existingYaml - existing file content (or empty string / undefined)
78
+ * @param {string} templateYaml - already-composed template content (NO placeholders remain)
79
+ * @param {{warn?: (msg: string) => void}} [options] - injection point for warn emitter (tests)
80
+ * @returns {string} serialized merged YAML
81
+ */
82
+ function mergeRoomodes(existingYaml, templateYaml, options = {}) {
83
+ const warn = typeof options.warn === 'function' ? options.warn : (msg) => console.warn(msg);
84
+
85
+ const existing = parseYaml(existingYaml, 'existing');
86
+ const template = parseYaml(templateYaml, 'template');
87
+
88
+ const templateModes = Array.isArray(template.customModes) ? template.customModes : [];
89
+ if (templateModes.length === 0) {
90
+ throw new Error('mergeRoomodes: template YAML has no customModes entries — refusing to produce an empty file');
91
+ }
92
+
93
+ const ownedSet = new Set(MA_AGENTS_OWNED_SLUGS);
94
+ const existingModes = Array.isArray(existing.customModes) ? existing.customModes : [];
95
+
96
+ const preservedUserModes = [];
97
+ for (const entry of existingModes) {
98
+ // Defensive: tolerate malformed entries (null / non-object / missing slug).
99
+ // Non-object entries are preserved as-is; entries without a valid slug
100
+ // cannot collide by definition and are kept in place.
101
+ const slug = entry && typeof entry === 'object' ? entry.slug : undefined;
102
+ if (typeof slug === 'string' && ownedSet.has(slug)) {
103
+ // AC #6 — one warning per colliding slug, slug cited verbatim.
104
+ warn(`WARNING: .roomodes slug "${slug}" overwritten by ma-agents template`);
105
+ continue;
106
+ }
107
+ preservedUserModes.push(entry);
108
+ }
109
+
110
+ // Build the merged document: non-customModes keys from existing + merged modes.
111
+ const merged = {};
112
+ for (const key of Object.keys(existing)) {
113
+ if (key === 'customModes') continue;
114
+ merged[key] = existing[key];
115
+ }
116
+ merged.customModes = [...preservedUserModes, ...templateModes];
117
+
118
+ return yaml.dump(merged, DUMP_OPTIONS);
119
+ }
120
+
121
+ module.exports = {
122
+ mergeRoomodes,
123
+ MA_AGENTS_OWNED_SLUGS,
124
+ _DUMP_OPTIONS: DUMP_OPTIONS
125
+ };
package/lib/profile.js CHANGED
@@ -3,12 +3,14 @@
3
3
  /**
4
4
  * Story 21.1 — Install-time profile persistence and resolution.
5
5
  *
6
- * Public API (exactly three functions):
6
+ * Public API (four functions):
7
7
  * getProfile(projectRoot) -> "on-prem" | "standard" | undefined
8
8
  * setProfile(projectRoot, value) -> void (persists to .ma-agents.json)
9
9
  * resolveProfile({ persisted, yesMode }) -> resolved value | null (pure, no I/O)
10
+ * clearProfile(projectRoot) -> void (deletes the 'profile' key from .ma-agents.json)
10
11
  *
11
12
  * NFR44: setProfile is the ONLY mutator of the "profile" field.
13
+ * Story 21.11: clearProfile is the ONLY remover of the "profile" field.
12
14
  * Back-compat: manifests at 1.1.0 without "profile" are read without error;
13
15
  * getProfile returns undefined for missing field.
14
16
  * setProfile migrates 1.1.0 → 1.2.0 on write (profile field is
@@ -104,4 +106,25 @@ function resolveProfile({ persisted, yesMode } = {}) {
104
106
  return null;
105
107
  }
106
108
 
107
- module.exports = { getProfile, setProfile, resolveProfile };
109
+ /**
110
+ * Story 21.11 — Removes the 'profile' key from .ma-agents.json entirely.
111
+ * Does NOT set to null or empty string — the key is deleted so getProfile
112
+ * returns undefined after this call.
113
+ * If .ma-agents.json does not exist, this is a no-op.
114
+ * Preserves all other fields (profileHistory, roomodesOverwriteLog, etc.).
115
+ */
116
+ function clearProfile(projectRoot) {
117
+ const manifestPath = path.join(projectRoot, MANIFEST_FILE);
118
+ if (!fs.existsSync(manifestPath)) return;
119
+ let manifest;
120
+ try {
121
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
122
+ } catch {
123
+ return;
124
+ }
125
+ if (!manifest || typeof manifest !== 'object') return;
126
+ delete manifest.profile;
127
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
128
+ }
129
+
130
+ module.exports = { getProfile, setProfile, resolveProfile, clearProfile };
@@ -0,0 +1,334 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Story 21.10 — Profile Reconfigure orchestrator.
5
+ *
6
+ * Public API:
7
+ * async reconfigure({ projectRoot, argv, promptsLib }) -> { status, from, to }
8
+ *
9
+ * Orchestrates:
10
+ * 1. Pre-flight: refuse --yes, require .ma-agents.json.
11
+ * 2. Re-run the profile-selection prompt (same shape as Story 21.1 wizard)
12
+ * with the currently-persisted value as the default.
13
+ * 3. Same-value shortcut.
14
+ * 4. Per-file drift guards:
15
+ * - Story 21.3 slug-stomp protection (RoomodesSlugDivergenceError)
16
+ * unless --force-roomodes-overwrite.
17
+ * - Story 21.5 .clinerules dual-file drift (ClinerulesDualFileDriftError,
18
+ * no override).
19
+ * 5. Two-step confirmation listing files to be modified (AC #7).
20
+ * 6. Persist new profile via setProfile (source="wizard" log line).
21
+ * 7. Re-stamp all profile-dependent artifacts by calling the canonical
22
+ * updateAgentInstructions helper from lib/installer.js for every agent in
23
+ * the manifest. Backups are written by the installer's drift handler via
24
+ * the canonical buildBackupFilename helper (Story 21.2 AC #8).
25
+ * 8. Append a { date, from, to, source: "reconfigure" } entry to
26
+ * .ma-agents.json::profileHistory, capped at PROFILE_HISTORY_CAP entries
27
+ * with oldest-first eviction.
28
+ *
29
+ * Does NOT modify lib/profile.js's public contract — it is a new consumer.
30
+ */
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+ const chalk = require('chalk');
35
+ const defaultPrompts = require('prompts');
36
+
37
+ const { getProfile, setProfile } = require('./profile');
38
+ const installer = require('./installer');
39
+ const { getAgent } = require('./agents');
40
+
41
+ const MANIFEST_FILE = '.ma-agents.json';
42
+ const PROFILE_HISTORY_CAP = 20;
43
+ const YES_REJECT_MESSAGE =
44
+ '--yes is not valid for reconfigure — this command is interactive by design to prevent accidental CI-triggered profile changes.';
45
+
46
+ /**
47
+ * Story 21.10 AC #9 — thrown when the user has edited content of an
48
+ * ma-agents-owned `.roomodes` slug in place. Reconfigure would otherwise
49
+ * silently overwrite those edits. Bypass with --force-roomodes-overwrite.
50
+ */
51
+ class RoomodesSlugDivergenceError extends Error {
52
+ constructor({ divergentSlugs, path: roomodesPath }) {
53
+ const slugs = divergentSlugs.map(s => `"${s}"`).join(', ');
54
+ const header = `ma-agents-owned .roomodes slug(s) ${slugs} diverge from the shipped template in ${roomodesPath}.`;
55
+ const guidance = 'Either rename the slug(s) so they are user-owned, or pass --force-roomodes-overwrite to accept the overwrite.';
56
+ super(`${header}\n${guidance}`);
57
+ this.name = 'RoomodesSlugDivergenceError';
58
+ this.divergentSlugs = divergentSlugs;
59
+ this.path = roomodesPath;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Story 21.10 AC #1 — named error when .ma-agents.json is absent.
65
+ * Distinct name so CLI + tests can dispatch on it without message-matching.
66
+ */
67
+ class ManifestNotFoundError extends Error {
68
+ constructor(projectRoot) {
69
+ super(`.ma-agents.json not found at ${projectRoot} — run \`ma-agents install\` first. reconfigure presumes a prior install.`);
70
+ this.name = 'ManifestNotFoundError';
71
+ this.projectRoot = projectRoot;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Story 21.10 AC #6 — thrown when --yes is supplied.
77
+ * Exposed so callers can dispatch on .name rather than matching the message.
78
+ */
79
+ class ReconfigureYesRejectedError extends Error {
80
+ constructor() {
81
+ super(YES_REJECT_MESSAGE);
82
+ this.name = 'ReconfigureYesRejectedError';
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Returns the list of project-relative files reconfigure would touch for the
88
+ * given installed-agent list. Used for the AC #7 confirmation preview and by
89
+ * tests. Order is stable.
90
+ */
91
+ function listTouchedFiles(projectRoot, agentEntries) {
92
+ const files = new Set();
93
+ for (const agent of agentEntries) {
94
+ if (!agent) continue;
95
+ // Primary instruction files.
96
+ if (Array.isArray(agent.instructionFiles)) {
97
+ for (const rel of agent.instructionFiles) files.add(rel);
98
+ }
99
+ // Extra templates (AGENTS.md, .roomodes, etc.).
100
+ if (Array.isArray(agent.extraInstructionTemplates)) {
101
+ for (const entry of agent.extraInstructionTemplates) {
102
+ if (entry && typeof entry.target === 'string') files.add(entry.target);
103
+ }
104
+ }
105
+ }
106
+ return Array.from(files).sort();
107
+ }
108
+
109
+ /**
110
+ * Story 21.10 AC #9 — slug-stomp protection.
111
+ *
112
+ * Parses the project-root `.roomodes` (YAML). For each ma-agents-owned slug
113
+ * present in the existing file whose customInstructions (or any non-slug field)
114
+ * differs from the shipped template, record the slug as divergent. Throws
115
+ * RoomodesSlugDivergenceError when any are divergent and `force` is false.
116
+ *
117
+ * If `.roomodes` or the roomodes template is absent, the check is a no-op —
118
+ * nothing to diverge from.
119
+ */
120
+ function checkRoomodesSlugDivergence(projectRoot, { force } = {}) {
121
+ const roomodesPath = path.join(projectRoot, '.roomodes');
122
+ if (!fs.existsSync(roomodesPath)) return;
123
+ const templatePath = path.join(installer.EXTRA_TEMPLATE_DIR, 'roomodes.template.yaml');
124
+ if (!fs.existsSync(templatePath)) return;
125
+
126
+ const yaml = require('js-yaml');
127
+ const { MA_AGENTS_OWNED_SLUGS } = require('./merge/roomodes');
128
+
129
+ let existing;
130
+ let template;
131
+ try {
132
+ existing = yaml.load(fs.readFileSync(roomodesPath, 'utf-8')) || {};
133
+ template = yaml.load(fs.readFileSync(templatePath, 'utf-8')) || {};
134
+ } catch {
135
+ // Malformed YAML — not our problem at this layer; let the merger surface it.
136
+ return;
137
+ }
138
+
139
+ const existingModes = Array.isArray(existing.customModes) ? existing.customModes : [];
140
+ const templateModes = Array.isArray(template.customModes) ? template.customModes : [];
141
+ const templateBySlug = new Map();
142
+ for (const m of templateModes) {
143
+ if (m && typeof m === 'object' && typeof m.slug === 'string') {
144
+ templateBySlug.set(m.slug, m);
145
+ }
146
+ }
147
+
148
+ const divergent = [];
149
+ for (const entry of existingModes) {
150
+ if (!entry || typeof entry !== 'object') continue;
151
+ const slug = entry.slug;
152
+ if (typeof slug !== 'string') continue;
153
+ if (!MA_AGENTS_OWNED_SLUGS.includes(slug)) continue;
154
+ const tpl = templateBySlug.get(slug);
155
+ if (!tpl) {
156
+ // Owned slug present but not in template — treat as user-modified.
157
+ divergent.push(slug);
158
+ continue;
159
+ }
160
+ // Compare YAML dumps of the two entries minus placeholder expansion. The
161
+ // template still contains `{{UNIVERSAL_BLOCK}}` sentinels inside
162
+ // customInstructions; compare all other fields strictly. For
163
+ // customInstructions, ignore the {{UNIVERSAL_BLOCK}}-bearing line and
164
+ // compare the surrounding static text.
165
+ const normalize = (mode) => {
166
+ const copy = { ...mode };
167
+ // Strip customInstructions — it contains per-profile composed text after
168
+ // installer stamping; its content legitimately changes per profile.
169
+ delete copy.customInstructions;
170
+ return yaml.dump(copy, { sortKeys: true });
171
+ };
172
+ if (normalize(entry) !== normalize(tpl)) {
173
+ divergent.push(slug);
174
+ }
175
+ }
176
+
177
+ if (divergent.length > 0 && !force) {
178
+ throw new RoomodesSlugDivergenceError({
179
+ divergentSlugs: divergent,
180
+ path: path.relative(projectRoot, roomodesPath).replace(/\\/g, '/') || '.roomodes'
181
+ });
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Story 21.10 AC #11 — append { date, from, to, source } to
187
+ * .ma-agents.json::profileHistory, capped at PROFILE_HISTORY_CAP entries
188
+ * (oldest-first eviction when the cap is exceeded).
189
+ *
190
+ * Performed as a read-modify-write through the same JSON-IO path used by
191
+ * setProfile so we avoid parallel I/O semantics (AC spec: "Ensure the write
192
+ * goes through setProfile or an equivalent atomic write path").
193
+ */
194
+ function appendProfileHistory(projectRoot, entry) {
195
+ const manifestPath = path.join(projectRoot, MANIFEST_FILE);
196
+ // Read current manifest (setProfile has just written to it).
197
+ const raw = fs.readFileSync(manifestPath, 'utf-8');
198
+ const manifest = JSON.parse(raw);
199
+ const history = Array.isArray(manifest.profileHistory) ? manifest.profileHistory.slice() : [];
200
+ history.push(entry);
201
+ while (history.length > PROFILE_HISTORY_CAP) {
202
+ history.shift(); // oldest-first eviction
203
+ }
204
+ manifest.profileHistory = history;
205
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
206
+ }
207
+
208
+ /**
209
+ * Main orchestrator.
210
+ *
211
+ * @param {object} opts
212
+ * @param {string} opts.projectRoot
213
+ * @param {string[]} [opts.argv] raw argv (excluding the 'reconfigure' verb)
214
+ * @param {object} [opts.promptsLib] prompts implementation (for tests)
215
+ * @param {Date} [opts.now] date injection (for tests)
216
+ * @returns {Promise<{status: 'unchanged'|'aborted'|'reconfigured', from: string|undefined, to: string|undefined}>}
217
+ */
218
+ async function reconfigure({ projectRoot, argv = [], promptsLib, now } = {}) {
219
+ const prompts = promptsLib || defaultPrompts;
220
+ const nowDate = now || new Date();
221
+
222
+ // --- AC #6: --yes rejection ---
223
+ if (argv.includes('--yes')) {
224
+ throw new ReconfigureYesRejectedError();
225
+ }
226
+
227
+ const forceRoomodes = argv.includes('--force-roomodes-overwrite');
228
+
229
+ // --- AC #1: require .ma-agents.json ---
230
+ const manifestPath = path.join(projectRoot, MANIFEST_FILE);
231
+ if (!fs.existsSync(manifestPath)) {
232
+ throw new ManifestNotFoundError(projectRoot);
233
+ }
234
+
235
+ const previousProfile = getProfile(projectRoot);
236
+
237
+ // --- AC #2: re-prompt with persisted value as default ---
238
+ const choices = [
239
+ { title: 'Yes — apply local-LLM guardrails (recommended for non-Claude models)', value: 'on-prem' },
240
+ { title: 'No — standard install (Claude on web, Anthropic API, etc.)', value: 'standard' }
241
+ ];
242
+ const initialIdx = previousProfile === 'on-prem' ? 0 : 1;
243
+ const promptResponse = await prompts({
244
+ type: 'select',
245
+ name: 'chosenProfile',
246
+ message: `Current profile: ${previousProfile || '(unset)'}. Change to?`,
247
+ choices,
248
+ initial: initialIdx
249
+ });
250
+ const chosenProfile = promptResponse && promptResponse.chosenProfile;
251
+ if (!chosenProfile) {
252
+ // User hit Ctrl-C at the prompt — abort silently, nothing modified.
253
+ return { status: 'aborted', from: previousProfile, to: undefined };
254
+ }
255
+
256
+ // --- AC #4: same-value short-circuit ---
257
+ if (chosenProfile === previousProfile) {
258
+ console.log(`Profile unchanged: ${chosenProfile}. No re-stamp needed.`);
259
+ return { status: 'unchanged', from: previousProfile, to: chosenProfile };
260
+ }
261
+
262
+ // --- Enumerate installed agents from manifest BEFORE any mutation ---
263
+ const rawManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
264
+ const agentIds = installer.getManifestAgents(rawManifest);
265
+ const agentEntries = agentIds.map(id => getAgent(id)).filter(Boolean);
266
+
267
+ // --- AC #9: slug-stomp pre-check (before any write) ---
268
+ checkRoomodesSlugDivergence(projectRoot, { force: forceRoomodes });
269
+
270
+ // --- AC #10: .clinerules dual-file drift pre-check (no override) ---
271
+ // Only check when Cline is installed.
272
+ const clineInstalled = agentEntries.some(a => a && a.id === 'cline');
273
+ if (clineInstalled) {
274
+ installer.checkClinerulesDualFileDrift(projectRoot);
275
+ }
276
+
277
+ // --- AC #7: two-step confirmation — list files and ask to continue ---
278
+ const touched = listTouchedFiles(projectRoot, agentEntries);
279
+ const list = touched.length ? touched.map(f => ` - ${f}`).join('\n') : ' (no files)';
280
+ console.log(`The following files will be updated to match profile=${chosenProfile}:\n${list}`);
281
+ const continueResp = await prompts({
282
+ type: 'confirm',
283
+ name: 'proceed',
284
+ message: 'Continue?',
285
+ initial: false
286
+ });
287
+ if (!continueResp || continueResp.proceed !== true) {
288
+ console.log(chalk.gray('Aborted — no files modified.'));
289
+ return { status: 'aborted', from: previousProfile, to: chosenProfile };
290
+ }
291
+
292
+ // --- AC #3: persist new profile, log canonical format ---
293
+ setProfile(projectRoot, chosenProfile);
294
+ console.log(chalk.cyan(`Using profile: ${chosenProfile} (from wizard)`));
295
+
296
+ // --- AC #5 + #8: re-stamp all profile-dependent artifacts.
297
+ // updateAgentInstructions handles drift detection + backup
298
+ // via the canonical buildBackupFilename helper; yesMode=true
299
+ // suppresses per-file interactive prompts since we've already
300
+ // obtained a single global confirmation in the step above.
301
+ for (const agent of agentEntries) {
302
+ try {
303
+ await installer._testUpdateAgentInstructions(agent, projectRoot, { yesMode: true });
304
+ } catch (err) {
305
+ // Surface the error to the caller but make sure the history is NOT
306
+ // appended — incomplete re-stamp should not record a success event.
307
+ throw err;
308
+ }
309
+ }
310
+
311
+ // --- AC #11: append profileHistory entry (capped) ---
312
+ appendProfileHistory(projectRoot, {
313
+ date: nowDate.toISOString(),
314
+ from: previousProfile,
315
+ to: chosenProfile,
316
+ source: 'reconfigure'
317
+ });
318
+
319
+ console.log(chalk.green(`Reconfigure complete: ${previousProfile} → ${chosenProfile}`));
320
+ return { status: 'reconfigured', from: previousProfile, to: chosenProfile };
321
+ }
322
+
323
+ module.exports = {
324
+ reconfigure,
325
+ RoomodesSlugDivergenceError,
326
+ ManifestNotFoundError,
327
+ ReconfigureYesRejectedError,
328
+ PROFILE_HISTORY_CAP,
329
+ YES_REJECT_MESSAGE,
330
+ // exposed for tests
331
+ listTouchedFiles,
332
+ checkRoomodesSlugDivergence,
333
+ appendProfileHistory
334
+ };
@@ -0,0 +1,67 @@
1
+ # Project Agent Instructions
2
+
3
+ This file is auto-discovered by OpenCode and any agent that respects `AGENTS.md`.
4
+ It establishes universal safety rules, text-vs-file discipline, and BMAD phase
5
+ discipline for every agent operating in this project.
6
+
7
+ ## Universal Rules
8
+
9
+ The universal rules block below is stamped and maintained by ma-agents. Edit
10
+ outside the HTML-comment `MA-AGENTS` start and end markers to preserve your
11
+ own additions — content inside the markers is regenerated on every install.
12
+
13
+ <!-- MA-AGENTS-START -->
14
+ <!-- MA-AGENTS-END -->
15
+
16
+ ## Critical Behavior Rules
17
+
18
+ These rules are non-negotiable across every profile and every agent.
19
+
20
+ - **Never create files in `~/.claude/` or any user home directory.** All
21
+ project artifacts must land inside the current working directory. Home-
22
+ directory writes cross-contaminate between projects and are a common source
23
+ of secret/config leakage.
24
+ - **Never write outside the project root without an explicit user request
25
+ naming the absolute path.** "Write to disk" means the project, not the
26
+ operator's machine.
27
+ - **Do not modify files you did not read first.** Read the current content
28
+ before proposing or performing an edit — blind writes silently destroy user
29
+ work.
30
+
31
+ ## BMAD Phase Declaration
32
+
33
+ BMAD-METHOD organizes work into four phases. Respect the currently declared
34
+ phase; do not skip ahead to the next phase without a phase transition signal
35
+ from the user, the active skill, or the story status.
36
+
37
+ - **Discovery / PM (analysis, planning).** Deliverables: product briefs,
38
+ PRDs, market and domain research, epics and stories lists. Do NOT produce
39
+ code, architecture diagrams, or implementation artifacts in this phase.
40
+ When asked "what do you think", respond in text.
41
+ - **Architecture.** Deliverables: solution design, component boundaries,
42
+ data-flow, interface contracts. Do NOT write application code or skill
43
+ implementations. Narrate decisions and capture them as documents.
44
+ - **Tech Lead / Stories.** Deliverables: individual story files with full
45
+ acceptance criteria, task breakdowns, and dev notes. Do NOT begin
46
+ implementation — stories are contracts the implementer consumes later.
47
+ - **Implementation.** Deliverables: code, tests, and the Dev Agent Record on
48
+ the story file. At this phase, write files. Do NOT retroactively fabricate
49
+ planning documents for code that already exists — flag the gap instead.
50
+
51
+ When no phase is declared (no active skill, no story in progress, no explicit
52
+ user statement), ask before assuming.
53
+
54
+ ## Project BMAD Output Structure
55
+
56
+ BMAD artifacts live under `_bmad-output/` (or the paths configured in
57
+ `_bmad/bmm/config.yaml` when present). The install-time resolver logs the
58
+ resolved paths on each run; consult that log output if in doubt.
59
+
60
+ - **Planning artifacts** — PRDs, product briefs, market and domain research.
61
+ - **Architecture artifacts** — solution design, component boundaries. May be
62
+ co-located with planning artifacts when no separate directory is configured.
63
+ - **Implementation artifacts (stories)** — individual story files and their
64
+ Dev Agent Records.
65
+
66
+ Always consult the `MANIFEST.yaml` referenced inside the universal block above
67
+ for the full list of installed skills and their locations.
@@ -0,0 +1,13 @@
1
+ # Cline Project Rules
2
+
3
+ These rules apply to every Cline session in this project. Cline auto-loads
4
+ `.clinerules` (and `.cline/clinerules.md`) on session start. The universal rule
5
+ body below is stamped and maintained by ma-agents — edit outside the HTML
6
+ comment `MA-AGENTS` start and end markers to preserve your own additions;
7
+ content inside the markers is regenerated on every install.
8
+
9
+ Use Cline's Architect mode for BMAD planning phases (PM, Architect, Tech Lead).
10
+ Switch to Code mode only for the implementation phase.
11
+
12
+ <!-- MA-AGENTS-START -->
13
+ <!-- MA-AGENTS-END -->
@@ -0,0 +1,86 @@
1
+ ## On-Prem / Local-LLM Guardrails
2
+
3
+ These rules apply ONLY when this project is installed with `profile: on-prem`.
4
+ They are appended to the universal block by the composer in `lib/installer.js`.
5
+ Local LLMs (Nemotron, Qwen, DeepSeek, Llama-3, etc. served via vLLM, Ollama, or
6
+ TGI) fail in patterns cloud LLMs rarely exhibit — the rules below pin those
7
+ failure modes down explicitly. Keep these rules verbatim in every response
8
+ context where tool use is possible.
9
+
10
+ ### Reasoning mode: `/no_think` on planning-phase prompts
11
+
12
+ Local reasoning-capable models (Nemotron-variants and similar) default to
13
+ chain-of-thought reasoning that bloats planning-phase prompts with internal
14
+ deliberation the operator does not need to read. Prepend the literal token
15
+ `/no_think` as the first line of any planning-phase system prompt or user turn
16
+ you compose. The token is consumed by the serving layer and suppresses the
17
+ model's reasoning trace on that turn.
18
+
19
+ - Planning-phase turns (PM, Architect, Tech Lead): begin the turn with
20
+ `/no_think` on its own line. Reasoning-mode OFF.
21
+ - Implementation-phase turns (Dev): omit `/no_think`. Reasoning-mode ON is
22
+ desirable for stepwise code synthesis and debugging.
23
+ - Review / QA turns: omit `/no_think` when the review benefits from explicit
24
+ reasoning (root-cause analysis). Include `/no_think` for mechanical checks
25
+ (lint, style, formatting).
26
+
27
+ If the downstream serving layer does not recognize `/no_think`, it is a no-op
28
+ text token — safe to include unconditionally on planning turns.
29
+
30
+ ### No writes to `~/.claude/` or any user home directory
31
+
32
+ Local LLMs frequently hallucinate paths under `~/.claude/`, `~/.cache/`,
33
+ `~/Library/`, or `%APPDATA%` — imitating patterns learned from Claude Code and
34
+ Cursor training data. These paths are OUTSIDE the project and cross-contaminate
35
+ other projects on the same machine.
36
+
37
+ - NEVER create, write, or modify files under `~/.claude/`, `~/.cache/`,
38
+ `~/Library/`, `~/AppData/`, `%APPDATA%`, or any path that resolves outside
39
+ the current project directory.
40
+ - All project artifacts — code, configuration, logs, scratch notes, and agent
41
+ state — MUST land under the current working directory (the project root) or
42
+ an explicitly-named subdirectory thereof.
43
+ - When a tool call appears to target a home-directory path, refuse the write
44
+ and respond in text explaining the violation. Ask the user for an explicit
45
+ in-project path before proceeding.
46
+
47
+ ### No `str_replace_editor` or Claude Code-specific tools
48
+
49
+ Local LLMs hallucinate Anthropic-proprietary tools — most commonly
50
+ `str_replace_editor`, `text_editor_20241022`, and `computer_use_preview` — that
51
+ do NOT exist outside the Anthropic API. Calling them against a local-LLM
52
+ serving layer produces a tool-not-found error or, worse, a silent no-op.
53
+
54
+ - Do NOT emit tool calls named `str_replace_editor`, `text_editor_*`,
55
+ `computer_use_*`, or any other tool whose name includes `str_replace_editor`
56
+ or matches Anthropic-specific tool schemas.
57
+ - Use only tools enumerated in the active tool manifest (`MANIFEST.yaml`) or
58
+ the IDE's native tool surface (Roo Code, Cline, OpenCode native tools).
59
+ - When you want to edit a file, use the native file-write tool of the active
60
+ agent — not `str_replace_editor`. If unsure what tool is available, list
61
+ available tools or ask the user before emitting a tool call.
62
+
63
+ ### Per-phase reasoning and sampling guidance
64
+
65
+ Local LLMs require tighter sampling control than cloud LLMs. Use these defaults
66
+ unless the serving layer overrides them.
67
+
68
+ - **Planning phase (PM, Architect, Tech Lead):**
69
+ - Reasoning: OFF (`/no_think` prepended).
70
+ - Temperature: low (0.0 – 0.3). Planning artifacts should be deterministic
71
+ and reproducible.
72
+ - Top-p: 0.9 or unset. Top-k: unset.
73
+ - Max tokens: generous (8k+) — planning documents are long.
74
+ - **Implementation phase (Dev):**
75
+ - Reasoning: ON (omit `/no_think`).
76
+ - Temperature: moderate (0.3 – 0.6). Code synthesis benefits from controlled
77
+ exploration but not creative rewriting.
78
+ - Top-p: 0.95 or unset. Top-k: unset.
79
+ - Max tokens: generous (8k+) — full-file rewrites are common.
80
+ - **Review / QA phase:**
81
+ - Reasoning: ON for root-cause analysis; OFF for mechanical checks.
82
+ - Temperature: low (0.0 – 0.2). Reviews should be deterministic.
83
+
84
+ If the serving layer applies its own sampler defaults, the per-phase guidance
85
+ above is advisory — but the phase boundary and `/no_think` placement are
86
+ load-bearing and MUST be honored on every turn.