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,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 (
|
|
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
|
-
|
|
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.
|