ma-agents 3.13.0 → 3.13.1
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/bin/cli.js +67 -13
- package/lib/bmad-extension-plugin/.claude-plugin/marketplace.json +1 -1
- package/lib/bmad.js +270 -11
- package/lib/installer.js +269 -21
- package/lib/profile.js +96 -5
- package/lib/templates/instruction-block-git.template.md +25 -25
- package/lib/templates/instruction-block-onprem.template.md +86 -86
- package/lib/templates/instruction-block-universal.template.md +29 -29
- package/package.json +1 -1
- package/skills/git-workflow-skill/skill.json +21 -21
package/bin/cli.js
CHANGED
|
@@ -33,8 +33,8 @@ const chalk = require('chalk');
|
|
|
33
33
|
const path = require('path');
|
|
34
34
|
const fs = require('fs');
|
|
35
35
|
const { execFileSync } = require('child_process');
|
|
36
|
-
const { installSkill, uninstallSkill, getStatus, listSkills, listAgents, updateProjectContextRepoLayout, migrateRetiredSkills } = require('../lib/installer');
|
|
37
|
-
const { getProfile, setProfile, resolveProfile } = require('../lib/profile');
|
|
36
|
+
const { installSkill, uninstallSkill, getStatus, listSkills, listAgents, updateProjectContextRepoLayout, migrateRetiredSkills, reconcileAgentsSharedSkills } = require('../lib/installer');
|
|
37
|
+
const { getProfile, setProfile, resolveProfile, getBmadModules, setBmadModules } = require('../lib/profile');
|
|
38
38
|
const { reconfigure: runReconfigure, ReconfigureYesRejectedError, ManifestNotFoundError, RoomodesSlugDivergenceError } = require('../lib/reconfigure');
|
|
39
39
|
const { uninstallProfileArtifacts } = require('../lib/uninstall');
|
|
40
40
|
const bmad = require('../lib/bmad');
|
|
@@ -495,6 +495,19 @@ function writeConfigField(content, fieldName, value) {
|
|
|
495
495
|
return content.trimEnd() + '\n' + newLine + '\n';
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
+
// Bug 27.15 — unify the path convention. config.toml (written by upstream
|
|
499
|
+
// manifest-generator) declares artifact paths with a `{project-root}/` prefix;
|
|
500
|
+
// previously writeRepoLayoutConfig emitted a THIRD variant: bare relative
|
|
501
|
+
// `_bmad-output/...`. Emit the same `{project-root}/`-prefixed form here so
|
|
502
|
+
// ma-agents stops adding a divergent third convention. config.toml is the
|
|
503
|
+
// install-owned source of truth; the per-module config.yaml is the
|
|
504
|
+
// upstream-stripped read copy. Fully aligning the upstream stripper
|
|
505
|
+
// (official-modules.js) is a separate upstream follow-up; vendored node_modules
|
|
506
|
+
// is not edited here.
|
|
507
|
+
function canonicalArtifactPath(suffix) {
|
|
508
|
+
return `{project-root}/_bmad-output/${suffix}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
498
511
|
function writeRepoLayoutConfig(layout) {
|
|
499
512
|
const configPath = path.join(process.cwd(), '_bmad', 'bmm', 'config.yaml');
|
|
500
513
|
try {
|
|
@@ -522,8 +535,8 @@ function writeRepoLayoutConfig(layout) {
|
|
|
522
535
|
content = writeConfigField(content, 'sprint_backend', 'jira');
|
|
523
536
|
content = writeConfigField(content, 'jira_url', layout.sprintManagement.jiraUrl);
|
|
524
537
|
content = writeConfigField(content, 'jira_project_key', layout.sprintManagement.jiraProjectKey);
|
|
525
|
-
const planningArtifacts = kbPath === '.' ? '
|
|
526
|
-
const implArtifacts = '
|
|
538
|
+
const planningArtifacts = kbPath === '.' ? canonicalArtifactPath('planning-artifacts') : kbPath;
|
|
539
|
+
const implArtifacts = canonicalArtifactPath('implementation-artifacts');
|
|
527
540
|
content = writeConfigField(content, 'planning_artifacts', planningArtifacts);
|
|
528
541
|
content = writeConfigField(content, 'implementation_artifacts', implArtifacts);
|
|
529
542
|
fs.writeFileSync(configPath, content, 'utf-8');
|
|
@@ -534,8 +547,8 @@ function writeRepoLayoutConfig(layout) {
|
|
|
534
547
|
const spPath = spPortable.portable;
|
|
535
548
|
content = writeConfigField(content, 'sprint_backend', 'file-system');
|
|
536
549
|
content = writeConfigField(content, 'sprint_management_path', spPath);
|
|
537
|
-
const planningArtifacts = kbPath === '.' ? '
|
|
538
|
-
const implArtifacts = spPath === '.' ? '
|
|
550
|
+
const planningArtifacts = kbPath === '.' ? canonicalArtifactPath('planning-artifacts') : kbPath;
|
|
551
|
+
const implArtifacts = spPath === '.' ? canonicalArtifactPath('implementation-artifacts') : spPath;
|
|
539
552
|
content = writeConfigField(content, 'planning_artifacts', planningArtifacts);
|
|
540
553
|
content = writeConfigField(content, 'implementation_artifacts', implArtifacts);
|
|
541
554
|
fs.writeFileSync(configPath, content, 'utf-8');
|
|
@@ -831,14 +844,26 @@ async function collectRepoLayout(flags, existingLayout = null) {
|
|
|
831
844
|
* break without them. Retired modules (currently only `wds`) are filtered
|
|
832
845
|
* out unconditionally.
|
|
833
846
|
*/
|
|
834
|
-
async function selectBmadModules({ bmadModulesFlag, bmadModulesPrompt }) {
|
|
847
|
+
async function selectBmadModules({ bmadModulesFlag, bmadModulesPrompt, projectRoot } = {}) {
|
|
835
848
|
const installable = bmad.getInstallableBmadModules().filter(m => !m.retired);
|
|
849
|
+
const root = projectRoot || process.cwd();
|
|
850
|
+
|
|
851
|
+
// Bug 27.13 — persist the resolved selection so a deselection sticks across
|
|
852
|
+
// future installs/reconfigures that do not re-pass --bmad-modules.
|
|
853
|
+
const persist = (modules) => {
|
|
854
|
+
try {
|
|
855
|
+
setBmadModules(root, modules);
|
|
856
|
+
} catch (err) {
|
|
857
|
+
console.warn(chalk.yellow(` Warning: could not persist BMAD module selection: ${err.message}`));
|
|
858
|
+
}
|
|
859
|
+
return modules;
|
|
860
|
+
};
|
|
836
861
|
|
|
837
862
|
// Branch 1: explicit CSV — validate + use as-is.
|
|
838
863
|
if (bmadModulesFlag) {
|
|
839
864
|
const requested = bmadModulesFlag.split(',').map(s => s.trim()).filter(Boolean);
|
|
840
865
|
try {
|
|
841
|
-
return bmad.resolveBmadModules({ requested, available: installable });
|
|
866
|
+
return persist(bmad.resolveBmadModules({ requested, available: installable }));
|
|
842
867
|
} catch (err) {
|
|
843
868
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
844
869
|
process.exit(1);
|
|
@@ -872,11 +897,25 @@ async function selectBmadModules({ bmadModulesFlag, bmadModulesPrompt }) {
|
|
|
872
897
|
|
|
873
898
|
// Belt-and-suspenders: force bmm back in if the user managed to uncheck
|
|
874
899
|
// it despite the (required) tag.
|
|
875
|
-
return bmad.resolveBmadModules({ requested: chosenModules, available: installable });
|
|
900
|
+
return persist(bmad.resolveBmadModules({ requested: chosenModules, available: installable }));
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Branch 3: no flag → use the persisted selection from a prior run if present
|
|
904
|
+
// (bug 27.13 — a previous deselection must survive); otherwise fall back to
|
|
905
|
+
// the original "install every available module" default. The persisted set is
|
|
906
|
+
// intersected with the currently-installable set so a since-retired module
|
|
907
|
+
// does not resurface, and bmm is force-included via resolveBmadModules.
|
|
908
|
+
const persisted = getBmadModules(root);
|
|
909
|
+
if (persisted && persisted.length > 0) {
|
|
910
|
+
const availableIds = new Set(installable.map(m => m.id));
|
|
911
|
+
const stillInstallable = persisted.filter(id => availableIds.has(id));
|
|
912
|
+
try {
|
|
913
|
+
return persist(bmad.resolveBmadModules({ requested: stillInstallable, available: installable }));
|
|
914
|
+
} catch {
|
|
915
|
+
// If the persisted set somehow fails validation, fall through to default.
|
|
916
|
+
}
|
|
876
917
|
}
|
|
877
|
-
|
|
878
|
-
// Branch 3: no flag → install everything available.
|
|
879
|
-
return installable.map(m => m.id);
|
|
918
|
+
return persist(installable.map(m => m.id));
|
|
880
919
|
}
|
|
881
920
|
|
|
882
921
|
// --- Install wizard ---
|
|
@@ -1167,6 +1206,7 @@ async function installWizard(preselectedSkill, preselectedAgents, customPath, fo
|
|
|
1167
1206
|
const bmadModules = await selectBmadModules({
|
|
1168
1207
|
bmadModulesFlag,
|
|
1169
1208
|
bmadModulesPrompt,
|
|
1209
|
+
projectRoot: process.cwd(),
|
|
1170
1210
|
});
|
|
1171
1211
|
|
|
1172
1212
|
if (!bmadInstalled) {
|
|
@@ -1314,6 +1354,20 @@ async function installWizard(preselectedSkill, preselectedAgents, customPath, fo
|
|
|
1314
1354
|
}
|
|
1315
1355
|
}
|
|
1316
1356
|
|
|
1357
|
+
// Bug 27.16 — if BMAD populated the shared `.agents/skills` dir but no
|
|
1358
|
+
// .agents-targeting agent (copilot/kilocode/roo-code) was selected, the
|
|
1359
|
+
// standalone catalog + MANIFEST.yaml were never written there. Backfill them
|
|
1360
|
+
// so the dir is never BMAD-only without the MANIFEST.yaml the instruction
|
|
1361
|
+
// block points agents at. No-op when an .agents agent was selected or no
|
|
1362
|
+
// shared dir exists.
|
|
1363
|
+
if (installScope === 'project') {
|
|
1364
|
+
try {
|
|
1365
|
+
await reconcileAgentsSharedSkills(process.cwd(), selectedAgentIds, installScope);
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
console.log(chalk.yellow(` .agents/skills reconciliation skipped: ${err.message}`));
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1317
1371
|
// Step 6: Update project-context.md with repo layout section (after skills installed project-context.md)
|
|
1318
1372
|
if (installScope === 'project') {
|
|
1319
1373
|
const outputPath = path.join(process.cwd(), '_bmad-output', 'project-context.md');
|
|
@@ -1683,4 +1737,4 @@ if (require.main === module) {
|
|
|
1683
1737
|
});
|
|
1684
1738
|
}
|
|
1685
1739
|
|
|
1686
|
-
module.exports = { parseFlags, collectRepoLayout, readExistingLayout, writeRepoLayoutConfig, writeProjectLayoutYaml, writeConfigField, normalizePath, toPortablePath, resolveStoredPath, ciCloneIfNeeded, showCurrentLayout, handleConfigLayout, yamlEscapeValue };
|
|
1740
|
+
module.exports = { parseFlags, selectBmadModules, collectRepoLayout, readExistingLayout, writeRepoLayoutConfig, writeProjectLayoutYaml, writeConfigField, normalizePath, toPortablePath, resolveStoredPath, ciCloneIfNeeded, showCurrentLayout, handleConfigLayout, yamlEscapeValue };
|
package/lib/bmad.js
CHANGED
|
@@ -278,17 +278,34 @@ function ensurePluginStageGitignoredForProject(projectRoot) {
|
|
|
278
278
|
console.warn(chalk.yellow(` Warning: could not load installer for gitignore policy: ${err.message}`));
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
281
|
-
if (typeof installer.ensurePluginStageIgnored
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
281
|
+
if (typeof installer.ensurePluginStageIgnored === 'function') {
|
|
282
|
+
try {
|
|
283
|
+
installer.ensurePluginStageIgnored(projectRoot);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
// EACCES on read-only `.gitignore`, EROFS on read-only filesystem, etc.
|
|
286
|
+
// Logged so the operator can fix it manually; never fatal to the install.
|
|
287
|
+
console.warn(
|
|
288
|
+
chalk.yellow(
|
|
289
|
+
` Warning: could not update ${path.join(projectRoot, '.gitignore')} with plugin-stage entry: ${err.message}`
|
|
290
|
+
)
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Bug 27.11 — also ensure the regenerated, installer-managed `_bmad/` tree is
|
|
296
|
+
// gitignored. Same project-root contract and same non-fatal failure policy as
|
|
297
|
+
// the plugin-stage entry above. `_bmad-output/` is intentionally left tracked
|
|
298
|
+
// (see ensureBmadOutputTracked) and is not matched by these patterns.
|
|
299
|
+
if (typeof installer.ensureBmadDirIgnored === 'function') {
|
|
300
|
+
try {
|
|
301
|
+
installer.ensureBmadDirIgnored(projectRoot);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.warn(
|
|
304
|
+
chalk.yellow(
|
|
305
|
+
` Warning: could not update ${path.join(projectRoot, '.gitignore')} with _bmad/ entry: ${err.message}`
|
|
306
|
+
)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
292
309
|
}
|
|
293
310
|
}
|
|
294
311
|
|
|
@@ -847,6 +864,11 @@ async function installBmad(modules = ['bmm', 'bmb'], tools = [], projectRoot = p
|
|
|
847
864
|
try {
|
|
848
865
|
runCommand(command, { cwd: projectRoot, env: buildChildSpawnEnv() });
|
|
849
866
|
bmadInvokeSucceeded = true;
|
|
867
|
+
// Bug 27.14 — repair any module artifact path that upstream's
|
|
868
|
+
// suffix-match output-folder resolution contaminated (e.g. gds paths
|
|
869
|
+
// pinned to bmb's "skills" instead of the chosen output folder). Runs
|
|
870
|
+
// after bmad-method writes the config and before downstream consumers.
|
|
871
|
+
normalizeModuleOutputFolders(projectRoot);
|
|
850
872
|
// F1a — on-prem persona phase-prefix pass. Runs BEFORE
|
|
851
873
|
// deployMethodology so a methodology-deploy failure doesn't prevent
|
|
852
874
|
// the prefix from being applied (they are independent concerns).
|
|
@@ -953,6 +975,10 @@ async function runMigration(modules, tools, projectRoot, force, { userName, comm
|
|
|
953
975
|
return false;
|
|
954
976
|
}
|
|
955
977
|
|
|
978
|
+
// Bug 27.14 — repair contaminated module artifact paths on the migration
|
|
979
|
+
// path too (same upstream suffix-match resolution applies to updates).
|
|
980
|
+
normalizeModuleOutputFolders(projectRoot);
|
|
981
|
+
|
|
956
982
|
// Step 3: Merge user customizations
|
|
957
983
|
if (backedUpFiles.length > 0) {
|
|
958
984
|
console.log(chalk.cyan(' Step 3/4: Merging user customizations...'));
|
|
@@ -1808,6 +1834,237 @@ function readCanonicalBmadConfig(projectRoot) {
|
|
|
1808
1834
|
}
|
|
1809
1835
|
}
|
|
1810
1836
|
|
|
1837
|
+
// ── Bug 27.14 — output-folder propagation normalization ──────────────────────
|
|
1838
|
+
//
|
|
1839
|
+
// Upstream bmad-method's resolveConfigValue (official-modules.js) resolves the
|
|
1840
|
+
// `{output_folder}` token via a `endsWith('_output_folder')` suffix match over
|
|
1841
|
+
// all collected answers. When bmb (answer key `bmb_..._output_folder` = "skills")
|
|
1842
|
+
// and gds are co-installed, gds's `{output_folder}` resolves alphabetically to
|
|
1843
|
+
// bmb's "skills" value, so gds artifact paths become `{project-root}/skills/...`
|
|
1844
|
+
// instead of `{project-root}/_bmad-output/...`.
|
|
1845
|
+
//
|
|
1846
|
+
// ma-agents owns no part of that resolution (it lives in vendored node_modules),
|
|
1847
|
+
// so this is a post-install WORK-AROUND: after bmad-method writes the config,
|
|
1848
|
+
// re-read `[core] output_folder` and rewrite any contaminated module artifact
|
|
1849
|
+
// path back onto the canonical output folder. We deliberately scope the rewrite
|
|
1850
|
+
// to the well-known core artifact keys so we never touch a module's INTENTIONAL
|
|
1851
|
+
// own-folder values (e.g. bmb's `bmad_builder_output_folder = ".../skills"` or
|
|
1852
|
+
// wds's `design_artifacts = ".../design-artifacts"`).
|
|
1853
|
+
//
|
|
1854
|
+
// Upstream follow-up (do NOT edit node_modules here): patch resolveConfigValue
|
|
1855
|
+
// to require an exact `output_folder` / `core_output_folder` key instead of the
|
|
1856
|
+
// `endsWith` suffix match. Tracked as an upstream follow-up.
|
|
1857
|
+
|
|
1858
|
+
// Artifact keys that must always live under `[core] output_folder`. These are
|
|
1859
|
+
// the canonical bmm/gds planning+implementation+architecture artifact keys plus
|
|
1860
|
+
// tea's test_artifacts. Module-specific own-folder keys (bmb_*_output_folder,
|
|
1861
|
+
// design_artifacts, project_knowledge, *_output sub-paths) are intentionally
|
|
1862
|
+
// excluded.
|
|
1863
|
+
const OUTPUT_FOLDER_ARTIFACT_KEYS = [
|
|
1864
|
+
'planning_artifacts',
|
|
1865
|
+
'architecture_artifacts',
|
|
1866
|
+
'implementation_artifacts',
|
|
1867
|
+
'test_artifacts',
|
|
1868
|
+
];
|
|
1869
|
+
|
|
1870
|
+
/**
|
|
1871
|
+
* Given an artifact path value and the canonical output folder, return a
|
|
1872
|
+
* corrected value if the path's output-folder segment differs from the
|
|
1873
|
+
* canonical one, or `null` if no change is needed.
|
|
1874
|
+
*
|
|
1875
|
+
* Handles both conventions:
|
|
1876
|
+
* - `{project-root}/<seg>/<suffix...>` (config.toml style)
|
|
1877
|
+
* - `<seg>/<suffix...>` (config.yaml stripped style)
|
|
1878
|
+
*
|
|
1879
|
+
* The leading `<seg>` (the first path segment after an optional `{project-root}/`
|
|
1880
|
+
* prefix) is the resolved output folder; if it !== `outputFolder` it is rewritten.
|
|
1881
|
+
* Only single-segment output folders are handled (the bmad default), which covers
|
|
1882
|
+
* the observed `skills` vs `_bmad-output` contamination.
|
|
1883
|
+
*/
|
|
1884
|
+
function correctArtifactPath(value, outputFolder) {
|
|
1885
|
+
if (typeof value !== 'string' || !value) return null;
|
|
1886
|
+
const ROOT = '{project-root}/';
|
|
1887
|
+
let prefix = '';
|
|
1888
|
+
let rest = value;
|
|
1889
|
+
if (rest.startsWith(ROOT)) {
|
|
1890
|
+
prefix = ROOT;
|
|
1891
|
+
rest = rest.slice(ROOT.length);
|
|
1892
|
+
}
|
|
1893
|
+
const slash = rest.indexOf('/');
|
|
1894
|
+
if (slash === -1) return null; // no segment/suffix split — leave as-is
|
|
1895
|
+
const seg = rest.slice(0, slash);
|
|
1896
|
+
const suffix = rest.slice(slash); // includes leading '/'
|
|
1897
|
+
if (seg === outputFolder) return null; // already canonical
|
|
1898
|
+
return `${prefix}${outputFolder}${suffix}`;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
* Rewrite contaminated artifact paths inside `_bmad/config.toml`. Returns the
|
|
1903
|
+
* number of lines changed. Uses line-based editing (no TOML dependency) so we
|
|
1904
|
+
* preserve comments, ordering, and formatting exactly.
|
|
1905
|
+
*/
|
|
1906
|
+
function normalizeTomlOutputFolders(tomlPath, outputFolder) {
|
|
1907
|
+
if (!fs.existsSync(tomlPath)) return 0;
|
|
1908
|
+
let raw;
|
|
1909
|
+
try {
|
|
1910
|
+
raw = fs.readFileSync(tomlPath, 'utf-8');
|
|
1911
|
+
} catch {
|
|
1912
|
+
return 0;
|
|
1913
|
+
}
|
|
1914
|
+
const usesCrlf = /\r\n/.test(raw);
|
|
1915
|
+
const eol = usesCrlf ? '\r\n' : '\n';
|
|
1916
|
+
const lines = raw.split(/\r?\n/);
|
|
1917
|
+
let changed = 0;
|
|
1918
|
+
let inModuleSection = false;
|
|
1919
|
+
|
|
1920
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1921
|
+
const line = lines[i];
|
|
1922
|
+
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
1923
|
+
if (sectionMatch) {
|
|
1924
|
+
const section = sectionMatch[1].trim();
|
|
1925
|
+
inModuleSection = section.startsWith('modules.');
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
if (!inModuleSection) continue;
|
|
1929
|
+
|
|
1930
|
+
// key = "value" (only quoted string values are artifact paths)
|
|
1931
|
+
const kvMatch = line.match(/^(\s*)([A-Za-z0-9_]+)(\s*=\s*)"([^"]*)"(\s*(?:#.*)?)$/);
|
|
1932
|
+
if (!kvMatch) continue;
|
|
1933
|
+
const [, indent, key, eq, val, trailer] = kvMatch;
|
|
1934
|
+
if (!OUTPUT_FOLDER_ARTIFACT_KEYS.includes(key)) continue;
|
|
1935
|
+
const corrected = correctArtifactPath(val, outputFolder);
|
|
1936
|
+
if (corrected === null) continue;
|
|
1937
|
+
lines[i] = `${indent}${key}${eq}"${corrected}"${trailer}`;
|
|
1938
|
+
changed++;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
if (changed > 0) {
|
|
1942
|
+
fs.writeFileSync(tomlPath, lines.join(eol), 'utf-8');
|
|
1943
|
+
}
|
|
1944
|
+
return changed;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/**
|
|
1948
|
+
* Rewrite contaminated artifact paths inside a per-module `_bmad/<mod>/config.yaml`.
|
|
1949
|
+
* Returns the number of keys changed. Uses js-yaml so we keep schema fidelity.
|
|
1950
|
+
*/
|
|
1951
|
+
function normalizeYamlOutputFolders(yamlPath, outputFolder) {
|
|
1952
|
+
if (!fs.existsSync(yamlPath)) return 0;
|
|
1953
|
+
let raw;
|
|
1954
|
+
try {
|
|
1955
|
+
raw = fs.readFileSync(yamlPath, 'utf-8');
|
|
1956
|
+
} catch {
|
|
1957
|
+
return 0;
|
|
1958
|
+
}
|
|
1959
|
+
let doc;
|
|
1960
|
+
try {
|
|
1961
|
+
doc = yaml.parse(raw);
|
|
1962
|
+
} catch {
|
|
1963
|
+
return 0;
|
|
1964
|
+
}
|
|
1965
|
+
if (!doc || typeof doc !== 'object') return 0;
|
|
1966
|
+
|
|
1967
|
+
let changed = 0;
|
|
1968
|
+
for (const key of OUTPUT_FOLDER_ARTIFACT_KEYS) {
|
|
1969
|
+
if (typeof doc[key] !== 'string') continue;
|
|
1970
|
+
const corrected = correctArtifactPath(doc[key], outputFolder);
|
|
1971
|
+
if (corrected === null) continue;
|
|
1972
|
+
doc[key] = corrected;
|
|
1973
|
+
changed++;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (changed > 0) {
|
|
1977
|
+
// Preserve the leading comment header (everything up to the first
|
|
1978
|
+
// non-comment, non-blank line) so the "Generated by BMAD installer"
|
|
1979
|
+
// banner survives the rewrite.
|
|
1980
|
+
const headerLines = [];
|
|
1981
|
+
for (const l of raw.split(/\r?\n/)) {
|
|
1982
|
+
if (l.trim() === '' || l.trimStart().startsWith('#')) {
|
|
1983
|
+
headerLines.push(l);
|
|
1984
|
+
} else {
|
|
1985
|
+
break;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
const header = headerLines.length > 0 ? headerLines.join('\n') + '\n' : '';
|
|
1989
|
+
fs.writeFileSync(yamlPath, header + yaml.stringify(doc), 'utf-8');
|
|
1990
|
+
}
|
|
1991
|
+
return changed;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* Bug 27.14 — post-install pass that re-points any contaminated module artifact
|
|
1996
|
+
* path at the canonical `[core] output_folder`. Reads the output folder from
|
|
1997
|
+
* `_bmad/config.toml` (the install-owned source of truth), then normalizes both
|
|
1998
|
+
* `_bmad/config.toml` and every `_bmad/<mod>/config.yaml`.
|
|
1999
|
+
*
|
|
2000
|
+
* Idempotent and non-fatal: a second run finds nothing to change, and any I/O
|
|
2001
|
+
* error is logged and swallowed (config normalization must never abort an
|
|
2002
|
+
* otherwise-green install).
|
|
2003
|
+
*
|
|
2004
|
+
* @param {string} projectRoot - Absolute path to the project root.
|
|
2005
|
+
* @returns {number} Total number of artifact paths rewritten (for tests/logging).
|
|
2006
|
+
*/
|
|
2007
|
+
function normalizeModuleOutputFolders(projectRoot) {
|
|
2008
|
+
if (typeof projectRoot !== 'string' || !projectRoot) return 0;
|
|
2009
|
+
const tomlPath = path.join(projectRoot, BMAD_DIR, 'config.toml');
|
|
2010
|
+
if (!fs.existsSync(tomlPath)) return 0;
|
|
2011
|
+
|
|
2012
|
+
let tomlRaw;
|
|
2013
|
+
try {
|
|
2014
|
+
tomlRaw = fs.readFileSync(tomlPath, 'utf-8');
|
|
2015
|
+
} catch {
|
|
2016
|
+
return 0;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Extract `[core] output_folder = "..."`. Only the value inside the [core]
|
|
2020
|
+
// section counts; module sections may carry their own output_folder-like keys.
|
|
2021
|
+
let outputFolder = null;
|
|
2022
|
+
let inCore = false;
|
|
2023
|
+
for (const line of tomlRaw.split(/\r?\n/)) {
|
|
2024
|
+
const section = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
2025
|
+
if (section) {
|
|
2026
|
+
inCore = section[1].trim() === 'core';
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
if (!inCore) continue;
|
|
2030
|
+
const kv = line.match(/^\s*output_folder\s*=\s*"([^"]*)"/);
|
|
2031
|
+
if (kv) {
|
|
2032
|
+
outputFolder = kv[1].trim();
|
|
2033
|
+
break;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (!outputFolder) return 0; // nothing canonical to normalize against
|
|
2037
|
+
|
|
2038
|
+
let total = 0;
|
|
2039
|
+
try {
|
|
2040
|
+
total += normalizeTomlOutputFolders(tomlPath, outputFolder);
|
|
2041
|
+
|
|
2042
|
+
// Walk each `_bmad/<mod>/config.yaml`.
|
|
2043
|
+
const bmadDir = path.join(projectRoot, BMAD_DIR);
|
|
2044
|
+
let entries = [];
|
|
2045
|
+
try {
|
|
2046
|
+
entries = fs.readdirSync(bmadDir, { withFileTypes: true });
|
|
2047
|
+
} catch {
|
|
2048
|
+
entries = [];
|
|
2049
|
+
}
|
|
2050
|
+
for (const entry of entries) {
|
|
2051
|
+
if (!entry.isDirectory()) continue;
|
|
2052
|
+
const cfg = path.join(bmadDir, entry.name, 'config.yaml');
|
|
2053
|
+
total += normalizeYamlOutputFolders(cfg, outputFolder);
|
|
2054
|
+
}
|
|
2055
|
+
} catch (err) {
|
|
2056
|
+
console.warn(chalk.yellow(` Warning: output-folder normalization skipped: ${err.message}`));
|
|
2057
|
+
return total;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
if (total > 0) {
|
|
2061
|
+
console.log(chalk.green(
|
|
2062
|
+
` Normalized ${total} module artifact path(s) to output folder "${outputFolder}/" (bug 27.14 work-around)`
|
|
2063
|
+
));
|
|
2064
|
+
}
|
|
2065
|
+
return total;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1811
2068
|
/**
|
|
1812
2069
|
* Migration shim: normalize the project layout so that canonical config lives
|
|
1813
2070
|
* at `_bmad/bmm/config.yaml` (v6.3.0) instead of `_bmad/_config/manifest.yaml`
|
|
@@ -2840,6 +3097,8 @@ module.exports = {
|
|
|
2840
3097
|
isGeneratedClineWrapper,
|
|
2841
3098
|
// Story 22.7 — canonical config (_bmad/bmm/config.yaml) helpers
|
|
2842
3099
|
readCanonicalBmadConfig,
|
|
3100
|
+
normalizeModuleOutputFolders,
|
|
3101
|
+
correctArtifactPath,
|
|
2843
3102
|
ensureCanonicalConfigLocation,
|
|
2844
3103
|
CANONICAL_CONFIG_REL,
|
|
2845
3104
|
LEGACY_MANIFEST_REL,
|
package/lib/installer.js
CHANGED
|
@@ -166,7 +166,12 @@ function composeInstructionBlock({ profile, projectRoot } = {}) {
|
|
|
166
166
|
composed += '\n\n' + onprem.replace(/\s+$/, '');
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
// Bug 27.10 — the source templates are authored with CRLF line endings, but the
|
|
170
|
+
// segment seams and terminator above are hand-written LF. That mix yields a block
|
|
171
|
+
// whose body lines are CRLF while the joins are LF. Normalize to pure LF once here
|
|
172
|
+
// so the composed block is EOL-consistent regardless of how templates were checked
|
|
173
|
+
// out on Windows, and so downstream marker-merge (which assumes LF) stays stable.
|
|
174
|
+
return composed.replace(/\r\n/g, '\n') + '\n';
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
/**
|
|
@@ -245,6 +250,17 @@ function resolveBmadOutputDirs(projectRoot) {
|
|
|
245
250
|
const CLAUDE_CODE_HOOK_ID = 'ma-agents-verify-manifest';
|
|
246
251
|
const CLAUDE_CODE_SETTINGS_FILE = '.claude/settings.json';
|
|
247
252
|
|
|
253
|
+
// Bug 27.8 — the SessionStart hook must point at a path that exists in a real
|
|
254
|
+
// (npx/node_modules) install, not at the package's own `lib/` which is never
|
|
255
|
+
// copied into the target. The dependency-free hook script is copied into the
|
|
256
|
+
// target's `.claude/hooks/` and the command resolves it via $CLAUDE_PROJECT_DIR
|
|
257
|
+
// (the target project root). The legacy `lib/hooks/...` command string is kept
|
|
258
|
+
// here only so existing installs get it removed/migrated on the next run.
|
|
259
|
+
const CLAUDE_CODE_HOOK_SOURCE = path.join(__dirname, 'hooks', 'claude-code', 'verify-manifest.js');
|
|
260
|
+
const CLAUDE_CODE_HOOK_TARGET_REL = '.claude/hooks/verify-manifest.js';
|
|
261
|
+
const CLAUDE_CODE_HOOK_COMMAND = `node "$CLAUDE_PROJECT_DIR/${CLAUDE_CODE_HOOK_TARGET_REL}"`;
|
|
262
|
+
const CLAUDE_CODE_HOOK_LEGACY_COMMAND = `node "$CLAUDE_PROJECT_DIR/lib/hooks/claude-code/verify-manifest.js"`;
|
|
263
|
+
|
|
248
264
|
function getPackageVersion() {
|
|
249
265
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
250
266
|
return pkg.version;
|
|
@@ -397,13 +413,91 @@ function ensurePluginStageIgnored(projectRoot) {
|
|
|
397
413
|
);
|
|
398
414
|
}
|
|
399
415
|
|
|
416
|
+
// Bug 27.11 — the installer writes the `_bmad/` module tree which is
|
|
417
|
+
// "Installer-managed. Regenerated on every install — treat as read-only."
|
|
418
|
+
// Leaving it tracked guarantees churn and merge conflicts on every reinstall.
|
|
419
|
+
// This sister-policy to PLUGIN_STAGE_PATTERNS ensures `_bmad/` is gitignored.
|
|
420
|
+
//
|
|
421
|
+
// IMPORTANT: this targets ONLY the regenerated `_bmad/` tree. The
|
|
422
|
+
// `_bmad-output/` directory holds tracked project knowledge and is
|
|
423
|
+
// deliberately kept OUT of .gitignore (see ensureBmadOutputTracked) — the
|
|
424
|
+
// patterns below never match it because git treats `_bmad/` (trailing-slash
|
|
425
|
+
// anchored) and `_bmad-output` as distinct entries.
|
|
426
|
+
const BMAD_DIR_NAME = '_bmad';
|
|
427
|
+
const BMAD_DIR_PATTERNS = [
|
|
428
|
+
BMAD_DIR_NAME, // _bmad
|
|
429
|
+
`${BMAD_DIR_NAME}/`, // _bmad/
|
|
430
|
+
`/${BMAD_DIR_NAME}`, // /_bmad
|
|
431
|
+
`/${BMAD_DIR_NAME}/`, // /_bmad/
|
|
432
|
+
];
|
|
433
|
+
const BMAD_DIR_CANONICAL_ENTRY = `${BMAD_DIR_NAME}/`;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Ensure `_bmad/` is present in the target project's `.gitignore`. Mirrors the
|
|
437
|
+
* append-only, idempotent, EOL-preserving contract of `ensurePluginStageIgnored`.
|
|
438
|
+
*
|
|
439
|
+
* - If `.gitignore` is absent, create it with the single canonical entry.
|
|
440
|
+
* - If any active (non-comment) line already matches any BMAD_DIR_PATTERNS
|
|
441
|
+
* variant, do nothing.
|
|
442
|
+
* - Otherwise, append `BMAD_DIR_CANONICAL_ENTRY` preserving existing EOL style.
|
|
443
|
+
*
|
|
444
|
+
* Defensive: no-op on falsy/non-string projectRoot, matching the sister helper.
|
|
445
|
+
*
|
|
446
|
+
* @param {string} projectRoot - Absolute path to the target project root.
|
|
447
|
+
*/
|
|
448
|
+
function ensureBmadDirIgnored(projectRoot) {
|
|
449
|
+
if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
454
|
+
|
|
455
|
+
let content = '';
|
|
456
|
+
let fileExists = true;
|
|
457
|
+
try {
|
|
458
|
+
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
459
|
+
} catch (err) {
|
|
460
|
+
if (err.code !== 'ENOENT') throw err;
|
|
461
|
+
fileExists = false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (fileExists) {
|
|
465
|
+
const alreadyPresent = content.split(/\r?\n/).some(rawLine => {
|
|
466
|
+
const line = rawLine.trim();
|
|
467
|
+
if (!line || line.startsWith('#')) return false; // skip blanks & comments
|
|
468
|
+
return BMAD_DIR_PATTERNS.includes(line);
|
|
469
|
+
});
|
|
470
|
+
if (alreadyPresent) return; // idempotent no-op
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const usesCrlf = fileExists && /\r\n/.test(content);
|
|
474
|
+
const eol = usesCrlf ? '\r\n' : '\n';
|
|
475
|
+
|
|
476
|
+
let next = content;
|
|
477
|
+
if (next && !next.endsWith('\n') && !next.endsWith('\r\n')) {
|
|
478
|
+
next += eol; // ensure the append lands on its own line
|
|
479
|
+
}
|
|
480
|
+
next += BMAD_DIR_CANONICAL_ENTRY + eol;
|
|
481
|
+
|
|
482
|
+
fs.writeFileSync(gitignorePath, next, 'utf-8');
|
|
483
|
+
console.log(
|
|
484
|
+
chalk.green(
|
|
485
|
+
`${BMAD_DIR_CANONICAL_ENTRY} added to .gitignore (installer-managed BMAD tree — regenerated on every install)`
|
|
486
|
+
)
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
400
490
|
function ensureManifest(installPath, agentId, scope) {
|
|
401
491
|
let manifest = readManifest(installPath);
|
|
402
492
|
if (!manifest) {
|
|
493
|
+
// Bug 27.9 — when bootstrapped before any agent is known (e.g. profile
|
|
494
|
+
// bootstrap via setProfile → ensureManifest(root, null, 'project')), agentId
|
|
495
|
+
// is null. Mirror the migrate-branch guard so we never write the malformed
|
|
496
|
+
// `agent: null` / `agents: [null]` shape; an empty agents array is valid.
|
|
403
497
|
manifest = {
|
|
404
498
|
manifestVersion: MANIFEST_VERSION,
|
|
405
|
-
agent: agentId,
|
|
406
|
-
agents: [agentId],
|
|
499
|
+
agent: agentId || null,
|
|
500
|
+
agents: agentId ? [agentId] : [],
|
|
407
501
|
scope: scope,
|
|
408
502
|
skills: {}
|
|
409
503
|
};
|
|
@@ -1265,6 +1359,116 @@ async function performInstall(skillId, skill, agent, installPath) {
|
|
|
1265
1359
|
return true;
|
|
1266
1360
|
}
|
|
1267
1361
|
|
|
1362
|
+
// --- Bug 27.16 — .agents/skills pipeline-consistency reconciliation ----------
|
|
1363
|
+
//
|
|
1364
|
+
// Two pipelines write skills: ma-agents' own standalone loop (installSkill →
|
|
1365
|
+
// performInstall + generateSkillsManifest — the ONLY writer of MANIFEST.yaml),
|
|
1366
|
+
// and bmad-method's PluginResolver (via --custom-source) which routes
|
|
1367
|
+
// copilot/roo/kilo to the shared `.agents/skills/` dir. The standalone loop only
|
|
1368
|
+
// targets `.agents/skills` when a copilot/kilocode/roo-code agent is selected.
|
|
1369
|
+
//
|
|
1370
|
+
// When the user selects only claude-code/cline/opencode, BMAD still writes the
|
|
1371
|
+
// BMAD-family skills into `.agents/skills` (175 of them in the Workshop repro)
|
|
1372
|
+
// but the standalone catalog and MANIFEST.yaml are never written there — leaving
|
|
1373
|
+
// a dir that looks installed but is silently partial AND missing the
|
|
1374
|
+
// MANIFEST.yaml the stamped instruction block points agents at.
|
|
1375
|
+
//
|
|
1376
|
+
// This pass makes `.agents/skills` pipeline-consistent: if BMAD populated it but
|
|
1377
|
+
// no `.agents`-targeting agent was selected (so the standalone loop skipped it),
|
|
1378
|
+
// install the full standalone catalog there and generate MANIFEST.yaml. The set
|
|
1379
|
+
// of `.agents`-targeting agent ids is derived from agents whose project skills
|
|
1380
|
+
// dir resolves to `.agents/skills`.
|
|
1381
|
+
|
|
1382
|
+
const AGENTS_SHARED_SKILLS_DIRNAME = '.agents';
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* Return the list of agent ids whose project-level skills dir is the shared
|
|
1386
|
+
* `.agents/skills/` directory (copilot, kilocode, roo-code). Derived from the
|
|
1387
|
+
* agent registry's `skillsDir` so a future registry change surfaces here.
|
|
1388
|
+
*/
|
|
1389
|
+
function getAgentsSharedSkillAgentIds() {
|
|
1390
|
+
return listAgents()
|
|
1391
|
+
.filter(a => a.skillsDir === path.join(AGENTS_SHARED_SKILLS_DIRNAME, 'skills') ||
|
|
1392
|
+
a.skillsDir === `${AGENTS_SHARED_SKILLS_DIRNAME}/skills`)
|
|
1393
|
+
.map(a => a.id);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Bug 27.16 — reconcile a BMAD-populated `.agents/skills` directory that the
|
|
1398
|
+
* standalone loop never targeted. No-op unless ALL of:
|
|
1399
|
+
* - scope === 'project'
|
|
1400
|
+
* - `<projectRoot>/.agents/skills` exists on disk (BMAD created it)
|
|
1401
|
+
* - no `.agents`-targeting agent was among the selected agents
|
|
1402
|
+
* - the dir has no MANIFEST.yaml (the standalone-loop signature)
|
|
1403
|
+
*
|
|
1404
|
+
* When triggered, installs the full standalone catalog into `.agents/skills`
|
|
1405
|
+
* (using a representative `.agents`-targeting agent for template resolution),
|
|
1406
|
+
* records the skills into that dir's `.ma-agents.json`, and generates
|
|
1407
|
+
* MANIFEST.yaml — so the dir is never BMAD-only without a manifest.
|
|
1408
|
+
*
|
|
1409
|
+
* Idempotent: once MANIFEST.yaml exists the early-return fires.
|
|
1410
|
+
*
|
|
1411
|
+
* @param {string} projectRoot
|
|
1412
|
+
* @param {string[]} selectedAgentIds - agent ids the user selected this run
|
|
1413
|
+
* @param {string} scope - 'project' | 'global'
|
|
1414
|
+
* @returns {Promise<{reconciled: boolean, installed: number}>}
|
|
1415
|
+
*/
|
|
1416
|
+
async function reconcileAgentsSharedSkills(projectRoot, selectedAgentIds = [], scope = 'project') {
|
|
1417
|
+
const result = { reconciled: false, installed: 0 };
|
|
1418
|
+
if (scope !== 'project') return result;
|
|
1419
|
+
if (typeof projectRoot !== 'string' || !projectRoot) return result;
|
|
1420
|
+
|
|
1421
|
+
const sharedDir = path.join(projectRoot, AGENTS_SHARED_SKILLS_DIRNAME, 'skills');
|
|
1422
|
+
if (!fs.existsSync(sharedDir)) return result; // BMAD never populated it
|
|
1423
|
+
|
|
1424
|
+
const sharedAgentIds = getAgentsSharedSkillAgentIds();
|
|
1425
|
+
const selectedSet = new Set(selectedAgentIds || []);
|
|
1426
|
+
const anySharedSelected = sharedAgentIds.some(id => selectedSet.has(id));
|
|
1427
|
+
if (anySharedSelected) return result; // standalone loop already targeted it
|
|
1428
|
+
|
|
1429
|
+
// If a MANIFEST.yaml already exists, the dir is not standalone-orphaned.
|
|
1430
|
+
if (fs.existsSync(path.join(sharedDir, 'MANIFEST.yaml'))) return result;
|
|
1431
|
+
|
|
1432
|
+
// Pick a representative `.agents`-targeting agent for template resolution.
|
|
1433
|
+
const repAgent = sharedAgentIds.map(id => getAgent(id)).find(Boolean);
|
|
1434
|
+
if (!repAgent) return result; // registry has no such agent — nothing to do
|
|
1435
|
+
|
|
1436
|
+
console.log(chalk.yellow(
|
|
1437
|
+
` Note: ${sharedDir} was populated by BMAD but carries no MANIFEST.yaml ` +
|
|
1438
|
+
`(no .agents-targeting agent selected) — backfilling the standalone catalog (bug 27.16).`
|
|
1439
|
+
));
|
|
1440
|
+
|
|
1441
|
+
const skills = listSkills();
|
|
1442
|
+
const manifest = ensureManifest(sharedDir, repAgent.id, scope);
|
|
1443
|
+
const now = new Date().toISOString();
|
|
1444
|
+
|
|
1445
|
+
for (const skill of skills) {
|
|
1446
|
+
try {
|
|
1447
|
+
const ok = await performInstall(skill.id, skill, repAgent, sharedDir);
|
|
1448
|
+
if (!ok) continue;
|
|
1449
|
+
const existing = manifest.skills[skill.id];
|
|
1450
|
+
manifest.skills[skill.id] = {
|
|
1451
|
+
version: skill.version,
|
|
1452
|
+
installedAt: existing ? existing.installedAt : now,
|
|
1453
|
+
updatedAt: now,
|
|
1454
|
+
installerVersion: getPackageVersion(),
|
|
1455
|
+
agentVersion: repAgent.version
|
|
1456
|
+
};
|
|
1457
|
+
result.installed++;
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
console.log(chalk.yellow(` x ${skill.id} skipped in .agents/skills backfill: ${err.message}`));
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
writeManifest(sharedDir, manifest);
|
|
1464
|
+
await generateSkillsManifest(sharedDir);
|
|
1465
|
+
result.reconciled = true;
|
|
1466
|
+
console.log(chalk.green(
|
|
1467
|
+
` Backfilled ${result.installed} standalone skill(s) + MANIFEST.yaml into ${sharedDir}`
|
|
1468
|
+
));
|
|
1469
|
+
return result;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1268
1472
|
// --- Install with upgrade detection ---
|
|
1269
1473
|
|
|
1270
1474
|
async function installSkill(skillId, agentIds, customPath = '', scope = 'project', options = {}) {
|
|
@@ -1882,7 +2086,12 @@ function getStatus(agentIds, customPath = '', scope = 'project') {
|
|
|
1882
2086
|
|
|
1883
2087
|
for (const { path: checkPath, scope: checkScope } of pathsToCheck) {
|
|
1884
2088
|
const manifest = readManifest(checkPath);
|
|
1885
|
-
|
|
2089
|
+
// Bug 27.12 — never treat the PROFILE-ONLY project-root manifest as a
|
|
2090
|
+
// skill inventory. The per-agent skills manifests are authoritative; a
|
|
2091
|
+
// manifest carrying the `project-root` scope discriminator is a
|
|
2092
|
+
// profile/settings store and must be skipped here.
|
|
2093
|
+
if (!manifest || manifest.scope === 'project-root' ||
|
|
2094
|
+
!manifest.skills || Object.keys(manifest.skills).length === 0) {
|
|
1886
2095
|
continue;
|
|
1887
2096
|
}
|
|
1888
2097
|
|
|
@@ -1924,27 +2133,49 @@ async function deployClaudeCodeHook(projectRoot) {
|
|
|
1924
2133
|
settings.hooks.SessionStart = [];
|
|
1925
2134
|
}
|
|
1926
2135
|
|
|
1927
|
-
//
|
|
1928
|
-
|
|
2136
|
+
// Bug 27.8 — copy the self-contained hook into the target so the command
|
|
2137
|
+
// resolves in any install (npx/node_modules/dev-repo). The legacy command
|
|
2138
|
+
// string (which pointed at the never-copied package `lib/`) is migrated to
|
|
2139
|
+
// the new command on every run.
|
|
2140
|
+
const hookTargetPath = path.join(projectRoot, CLAUDE_CODE_HOOK_TARGET_REL);
|
|
2141
|
+
await fs.ensureDir(path.dirname(hookTargetPath));
|
|
2142
|
+
await fs.copy(CLAUDE_CODE_HOOK_SOURCE, hookTargetPath, { overwrite: true });
|
|
2143
|
+
|
|
2144
|
+
const hookCommand = CLAUDE_CODE_HOOK_COMMAND;
|
|
2145
|
+
let migrated = false;
|
|
2146
|
+
for (const group of settings.hooks.SessionStart) {
|
|
2147
|
+
if (!group.hooks) continue;
|
|
2148
|
+
for (const h of group.hooks) {
|
|
2149
|
+
if (h.command === CLAUDE_CODE_HOOK_LEGACY_COMMAND || h._id === CLAUDE_CODE_HOOK_ID) {
|
|
2150
|
+
h.command = hookCommand;
|
|
2151
|
+
h._id = CLAUDE_CODE_HOOK_ID;
|
|
2152
|
+
migrated = true;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
1929
2157
|
const alreadyInstalled = settings.hooks.SessionStart.some(group =>
|
|
1930
2158
|
group.hooks && group.hooks.some(h => h.command === hookCommand)
|
|
1931
2159
|
);
|
|
1932
2160
|
|
|
1933
|
-
if (alreadyInstalled) {
|
|
1934
|
-
|
|
2161
|
+
if (!alreadyInstalled) {
|
|
2162
|
+
settings.hooks.SessionStart.push({
|
|
2163
|
+
matcher: 'startup',
|
|
2164
|
+
hooks: [
|
|
2165
|
+
{
|
|
2166
|
+
type: 'command',
|
|
2167
|
+
command: hookCommand,
|
|
2168
|
+
_id: CLAUDE_CODE_HOOK_ID
|
|
2169
|
+
}
|
|
2170
|
+
]
|
|
2171
|
+
});
|
|
2172
|
+
} else if (!migrated) {
|
|
2173
|
+
// Hook entry already current and file copied — still ensure settings exist.
|
|
2174
|
+
await fs.ensureDir(path.dirname(settingsPath));
|
|
2175
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
2176
|
+
return;
|
|
1935
2177
|
}
|
|
1936
2178
|
|
|
1937
|
-
settings.hooks.SessionStart.push({
|
|
1938
|
-
matcher: 'startup',
|
|
1939
|
-
hooks: [
|
|
1940
|
-
{
|
|
1941
|
-
type: 'command',
|
|
1942
|
-
command: hookCommand,
|
|
1943
|
-
_id: CLAUDE_CODE_HOOK_ID
|
|
1944
|
-
}
|
|
1945
|
-
]
|
|
1946
|
-
});
|
|
1947
|
-
|
|
1948
2179
|
await fs.ensureDir(path.dirname(settingsPath));
|
|
1949
2180
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
1950
2181
|
console.log(chalk.cyan(' + Deployed Claude Code verify-manifest hook'));
|
|
@@ -1956,6 +2187,15 @@ async function deployClaudeCodeHook(projectRoot) {
|
|
|
1956
2187
|
async function removeClaudeCodeHook(projectRoot) {
|
|
1957
2188
|
const settingsPath = path.join(projectRoot, CLAUDE_CODE_SETTINGS_FILE);
|
|
1958
2189
|
|
|
2190
|
+
// Bug 27.8 — remove the copied hook file first, before any settings.json
|
|
2191
|
+
// early-return, so a missing/corrupt settings.json never orphans the file.
|
|
2192
|
+
const hookTargetPath = path.join(projectRoot, CLAUDE_CODE_HOOK_TARGET_REL);
|
|
2193
|
+
try {
|
|
2194
|
+
await fs.remove(hookTargetPath);
|
|
2195
|
+
} catch {
|
|
2196
|
+
// best-effort cleanup — never fail uninstall over a missing file
|
|
2197
|
+
}
|
|
2198
|
+
|
|
1959
2199
|
if (!fs.existsSync(settingsPath)) return;
|
|
1960
2200
|
|
|
1961
2201
|
let settings;
|
|
@@ -1967,10 +2207,15 @@ async function removeClaudeCodeHook(projectRoot) {
|
|
|
1967
2207
|
|
|
1968
2208
|
if (!settings.hooks || !settings.hooks.SessionStart) return;
|
|
1969
2209
|
|
|
1970
|
-
|
|
2210
|
+
// Match both the current command and the legacy package-relative command so
|
|
2211
|
+
// older installs get cleaned up too.
|
|
1971
2212
|
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(group => {
|
|
1972
2213
|
if (!group.hooks) return true;
|
|
1973
|
-
group.hooks = group.hooks.filter(h =>
|
|
2214
|
+
group.hooks = group.hooks.filter(h =>
|
|
2215
|
+
h.command !== CLAUDE_CODE_HOOK_COMMAND &&
|
|
2216
|
+
h.command !== CLAUDE_CODE_HOOK_LEGACY_COMMAND &&
|
|
2217
|
+
h._id !== CLAUDE_CODE_HOOK_ID
|
|
2218
|
+
);
|
|
1974
2219
|
return group.hooks.length > 0;
|
|
1975
2220
|
});
|
|
1976
2221
|
|
|
@@ -2009,6 +2254,9 @@ module.exports = {
|
|
|
2009
2254
|
removeClaudeCodeHook,
|
|
2010
2255
|
ensureBmadOutputTracked,
|
|
2011
2256
|
ensurePluginStageIgnored,
|
|
2257
|
+
ensureBmadDirIgnored,
|
|
2258
|
+
reconcileAgentsSharedSkills,
|
|
2259
|
+
getAgentsSharedSkillAgentIds,
|
|
2012
2260
|
generateSkillsManifest,
|
|
2013
2261
|
generateProjectContext,
|
|
2014
2262
|
generateRepoLayoutSection,
|
package/lib/profile.js
CHANGED
|
@@ -23,6 +23,29 @@ const path = require('path');
|
|
|
23
23
|
const MANIFEST_FILE = '.ma-agents.json';
|
|
24
24
|
const VALID_PROFILES = ['on-prem', 'standard'];
|
|
25
25
|
|
|
26
|
+
// Bug 27.12 — the project-root .ma-agents.json is a PROFILE/settings store, not
|
|
27
|
+
// a skill inventory. Per-agent skills dirs (e.g. .claude/skills/.ma-agents.json)
|
|
28
|
+
// own the installed-skill inventory and remain the authoritative source read by
|
|
29
|
+
// getStatus/uninstall/reconcile. The root bootstrap therefore emits ONLY the
|
|
30
|
+
// fields it owns (manifestVersion + a scope discriminator) and NOT a `skills`/
|
|
31
|
+
// `agents` shape it would never back-fill — so the root file can no longer
|
|
32
|
+
// masquerade as an empty skill inventory (`skills: {}`).
|
|
33
|
+
const ROOT_MANIFEST_VERSION = '1.2.0';
|
|
34
|
+
const ROOT_SCOPE = 'project-root';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bootstrap a fresh PROFILE-ONLY root manifest object (not persisted here).
|
|
38
|
+
* Intentionally omits `skills` and `agents`: those belong to per-agent skills
|
|
39
|
+
* manifests. The `scope: 'project-root'` discriminator makes the file's role
|
|
40
|
+
* explicit for any reader.
|
|
41
|
+
*/
|
|
42
|
+
function bootstrapRootManifest() {
|
|
43
|
+
return {
|
|
44
|
+
manifestVersion: ROOT_MANIFEST_VERSION,
|
|
45
|
+
scope: ROOT_SCOPE,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
/**
|
|
27
50
|
* Reads the persisted profile from .ma-agents.json at the given project root.
|
|
28
51
|
* Returns undefined if the file does not exist, is malformed, or the "profile"
|
|
@@ -73,10 +96,12 @@ function setProfile(projectRoot, value) {
|
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
if (!manifest || typeof manifest !== 'object') {
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
// Bug 27.12 — bootstrap a PROFILE-ONLY root manifest. Previously this used
|
|
100
|
+
// ensureManifest(root, null, 'project'), which stamped an empty `skills: {}`
|
|
101
|
+
// / `agents: []` shape the root file never owns or back-fills, making it
|
|
102
|
+
// look like an (empty) skill inventory. The per-agent skills manifests are
|
|
103
|
+
// the authoritative inventory; the root file stores profile/settings only.
|
|
104
|
+
manifest = bootstrapRootManifest();
|
|
80
105
|
}
|
|
81
106
|
|
|
82
107
|
// Migrate 1.1.0 (or missing version) → 1.2.0, since the "profile" field is 1.2.0-only.
|
|
@@ -127,4 +152,70 @@ function clearProfile(projectRoot) {
|
|
|
127
152
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
128
153
|
}
|
|
129
154
|
|
|
130
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Bug 27.13 — durable BMAD-module selection.
|
|
157
|
+
*
|
|
158
|
+
* Reads the persisted BMAD module set from .ma-agents.json (the `bmadModules`
|
|
159
|
+
* field). Returns undefined when the file is absent/malformed or the field is
|
|
160
|
+
* missing, so callers can distinguish "never persisted" (→ install default set)
|
|
161
|
+
* from an explicit empty selection. A persisted value is always returned as an
|
|
162
|
+
* array of strings.
|
|
163
|
+
*/
|
|
164
|
+
function getBmadModules(projectRoot) {
|
|
165
|
+
const manifestPath = path.join(projectRoot, MANIFEST_FILE);
|
|
166
|
+
if (!fs.existsSync(manifestPath)) return undefined;
|
|
167
|
+
let manifest;
|
|
168
|
+
try {
|
|
169
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
170
|
+
} catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
if (!manifest || typeof manifest !== 'object') return undefined;
|
|
174
|
+
if (!Object.prototype.hasOwnProperty.call(manifest, 'bmadModules')) return undefined;
|
|
175
|
+
const value = manifest.bmadModules;
|
|
176
|
+
if (!Array.isArray(value)) return undefined;
|
|
177
|
+
// Normalize to a clean, de-duplicated array of non-empty strings.
|
|
178
|
+
return [...new Set(value.filter(m => typeof m === 'string' && m.length > 0))];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Bug 27.13 — persists the selected BMAD module set into .ma-agents.json,
|
|
183
|
+
* preserving all other fields. Creates the file via the standard ensureManifest
|
|
184
|
+
* bootstrap path if absent (mirroring setProfile). The choice then survives
|
|
185
|
+
* subsequent installs/reconfigures that do not re-pass --bmad-modules.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} projectRoot
|
|
188
|
+
* @param {string[]} modules - module ids to persist (e.g. ['bmm','tea','bmb'])
|
|
189
|
+
*/
|
|
190
|
+
function setBmadModules(projectRoot, modules) {
|
|
191
|
+
if (!Array.isArray(modules)) {
|
|
192
|
+
throw new Error(`setBmadModules expects an array of module ids (got ${JSON.stringify(modules)})`);
|
|
193
|
+
}
|
|
194
|
+
const cleaned = [...new Set(modules.filter(m => typeof m === 'string' && m.length > 0))];
|
|
195
|
+
|
|
196
|
+
const manifestPath = path.join(projectRoot, MANIFEST_FILE);
|
|
197
|
+
let manifest;
|
|
198
|
+
if (fs.existsSync(manifestPath)) {
|
|
199
|
+
try {
|
|
200
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
201
|
+
} catch {
|
|
202
|
+
manifest = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
206
|
+
// Bug 27.12 — profile-only root bootstrap (see setProfile / bootstrapRootManifest).
|
|
207
|
+
manifest = bootstrapRootManifest();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
manifest.bmadModules = cleaned;
|
|
211
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
getProfile,
|
|
216
|
+
setProfile,
|
|
217
|
+
resolveProfile,
|
|
218
|
+
clearProfile,
|
|
219
|
+
getBmadModules,
|
|
220
|
+
setBmadModules,
|
|
221
|
+
};
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
## Git Workflow (ALL file-changing tasks)
|
|
2
|
-
|
|
3
|
-
These rules apply to EVERY task that modifies a tracked file. There is no
|
|
4
|
-
"small fix exemption" — a one-line edit follows the same workflow as a feature.
|
|
5
|
-
|
|
6
|
-
- **Worktree first.** Before changing any file, create a fresh isolated worktree
|
|
7
|
-
on a new feature branch: `git worktree add ../<project>-<branch> -b <branch>`.
|
|
8
|
-
The scripted form is `skills/git-workflow-skill/scripts/start-feature.sh`. All
|
|
9
|
-
file-writing happens inside that worktree — never in the main working tree.
|
|
10
|
-
- **Never commit directly to the trunk.** Do NOT commit to `main`, `dev`,
|
|
11
|
-
`master`, or whatever the trunk branch is. Commits land only on your feature
|
|
12
|
-
branch inside the worktree.
|
|
13
|
-
- **Conventional Commits.** Every commit message uses a Conventional Commits
|
|
14
|
-
prefix. The allowed prefixes are: `feat`, `fix`, `chore`, `docs`, `refactor`,
|
|
15
|
-
`test`, `ci`.
|
|
16
|
-
- **PR is the only path into the trunk.** Integrate exclusively by opening a pull
|
|
17
|
-
request with `gh pr create`. Never merge directly and never auto-merge a PR
|
|
18
|
-
without explicit human approval.
|
|
19
|
-
- **Self-review before opening the PR.** Run the `code-review` skill on your own
|
|
20
|
-
changes and address its findings before you open the PR.
|
|
21
|
-
- **Clean up after merge.** Once the PR is merged, remove the worktree with
|
|
22
|
-
`git worktree remove` and delete the feature branch.
|
|
23
|
-
|
|
24
|
-
Full procedure: see `skills/git-workflow-skill/SKILL.md`, and the helper scripts
|
|
25
|
-
`scripts/start-feature.sh` / `scripts/finish-feature.sh`.
|
|
1
|
+
## Git Workflow (ALL file-changing tasks)
|
|
2
|
+
|
|
3
|
+
These rules apply to EVERY task that modifies a tracked file. There is no
|
|
4
|
+
"small fix exemption" — a one-line edit follows the same workflow as a feature.
|
|
5
|
+
|
|
6
|
+
- **Worktree first.** Before changing any file, create a fresh isolated worktree
|
|
7
|
+
on a new feature branch: `git worktree add ../<project>-<branch> -b <branch>`.
|
|
8
|
+
The scripted form is `skills/git-workflow-skill/scripts/start-feature.sh`. All
|
|
9
|
+
file-writing happens inside that worktree — never in the main working tree.
|
|
10
|
+
- **Never commit directly to the trunk.** Do NOT commit to `main`, `dev`,
|
|
11
|
+
`master`, or whatever the trunk branch is. Commits land only on your feature
|
|
12
|
+
branch inside the worktree.
|
|
13
|
+
- **Conventional Commits.** Every commit message uses a Conventional Commits
|
|
14
|
+
prefix. The allowed prefixes are: `feat`, `fix`, `chore`, `docs`, `refactor`,
|
|
15
|
+
`test`, `ci`.
|
|
16
|
+
- **PR is the only path into the trunk.** Integrate exclusively by opening a pull
|
|
17
|
+
request with `gh pr create`. Never merge directly and never auto-merge a PR
|
|
18
|
+
without explicit human approval.
|
|
19
|
+
- **Self-review before opening the PR.** Run the `code-review` skill on your own
|
|
20
|
+
changes and address its findings before you open the PR.
|
|
21
|
+
- **Clean up after merge.** Once the PR is merged, remove the worktree with
|
|
22
|
+
`git worktree remove` and delete the feature branch.
|
|
23
|
+
|
|
24
|
+
Full procedure: see `skills/git-workflow-skill/SKILL.md`, and the helper scripts
|
|
25
|
+
`scripts/start-feature.sh` / `scripts/finish-feature.sh`.
|
|
@@ -1,86 +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.
|
|
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.
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
# AI Agent Skills - Planning Instruction
|
|
2
|
-
|
|
3
|
-
You have access to a library of skills in your skills directory. Before starting any task:
|
|
4
|
-
|
|
5
|
-
1. Read the skill manifest at {{MANIFEST_PATH}}
|
|
6
|
-
2. Based on the task description, select which skills are relevant
|
|
7
|
-
3. Read only the selected skill files
|
|
8
|
-
4. Then proceed with the task
|
|
9
|
-
|
|
10
|
-
Always load skills marked with always_load: true.
|
|
11
|
-
Do not load skills that are not relevant to the current task.
|
|
12
|
-
|
|
13
|
-
## Respond in TEXT vs. create FILES
|
|
14
|
-
|
|
15
|
-
Choose your response medium deliberately. Defaulting to file creation when the user asked a question is a common failure mode — especially for coding agents running in web UIs.
|
|
16
|
-
|
|
17
|
-
- **Create or modify FILES when the user's request contains file-action keywords:** `create`, `write`, `generate`, `build`, `implement` (and obvious synonyms such as `add`, `produce`, `refactor`, `fix`, `update <file>`). These signal a concrete artifact is expected.
|
|
18
|
-
- **Respond in TEXT when the request contains text-response keywords:** `what do you think`, `how should we`, `discuss`, `opinion` (and obvious synonyms such as `explain`, `why`, `should I`, `compare`, `recommend`). These signal that a conversation is expected, not a deliverable.
|
|
19
|
-
- **If unsure, respond in TEXT.** A text answer can always be followed by file creation on confirmation; an unwanted file cannot be cleanly undone.
|
|
20
|
-
- **Never create `response.md`, `output.md`, or any similarly named scratch file as a reply.** A reply belongs in the chat transcript, not on disk.
|
|
21
|
-
- **Confirm file paths before writing.** When you are about to create or modify a file whose path the user has not explicitly named, state the intended path in text and wait for confirmation, unless the path is unambiguous from the task context.
|
|
22
|
-
|
|
23
|
-
## BMAD phase discipline
|
|
24
|
-
|
|
25
|
-
BMAD-METHOD organizes work into declared phases (analysis, planning, architecture, story-creation, implementation, review). Respect the currently declared phase.
|
|
26
|
-
|
|
27
|
-
- **Do not skip ahead to implementation during planning.** If the project is in a planning phase — or the user has asked for requirements, architecture, or a story — produce planning artifacts, not code.
|
|
28
|
-
- **Do not retroactively plan after you have already coded.** If implementation has already started, flag the gap instead of fabricating back-dated planning documents.
|
|
29
|
-
- The declared phase is established by the active skill, the story status, or an explicit statement from the user. When none of these is available, ask before assuming.
|
|
1
|
+
# AI Agent Skills - Planning Instruction
|
|
2
|
+
|
|
3
|
+
You have access to a library of skills in your skills directory. Before starting any task:
|
|
4
|
+
|
|
5
|
+
1. Read the skill manifest at {{MANIFEST_PATH}}
|
|
6
|
+
2. Based on the task description, select which skills are relevant
|
|
7
|
+
3. Read only the selected skill files
|
|
8
|
+
4. Then proceed with the task
|
|
9
|
+
|
|
10
|
+
Always load skills marked with always_load: true.
|
|
11
|
+
Do not load skills that are not relevant to the current task.
|
|
12
|
+
|
|
13
|
+
## Respond in TEXT vs. create FILES
|
|
14
|
+
|
|
15
|
+
Choose your response medium deliberately. Defaulting to file creation when the user asked a question is a common failure mode — especially for coding agents running in web UIs.
|
|
16
|
+
|
|
17
|
+
- **Create or modify FILES when the user's request contains file-action keywords:** `create`, `write`, `generate`, `build`, `implement` (and obvious synonyms such as `add`, `produce`, `refactor`, `fix`, `update <file>`). These signal a concrete artifact is expected.
|
|
18
|
+
- **Respond in TEXT when the request contains text-response keywords:** `what do you think`, `how should we`, `discuss`, `opinion` (and obvious synonyms such as `explain`, `why`, `should I`, `compare`, `recommend`). These signal that a conversation is expected, not a deliverable.
|
|
19
|
+
- **If unsure, respond in TEXT.** A text answer can always be followed by file creation on confirmation; an unwanted file cannot be cleanly undone.
|
|
20
|
+
- **Never create `response.md`, `output.md`, or any similarly named scratch file as a reply.** A reply belongs in the chat transcript, not on disk.
|
|
21
|
+
- **Confirm file paths before writing.** When you are about to create or modify a file whose path the user has not explicitly named, state the intended path in text and wait for confirmation, unless the path is unambiguous from the task context.
|
|
22
|
+
|
|
23
|
+
## BMAD phase discipline
|
|
24
|
+
|
|
25
|
+
BMAD-METHOD organizes work into declared phases (analysis, planning, architecture, story-creation, implementation, review). Respect the currently declared phase.
|
|
26
|
+
|
|
27
|
+
- **Do not skip ahead to implementation during planning.** If the project is in a planning phase — or the user has asked for requirements, architecture, or a story — produce planning artifacts, not code.
|
|
28
|
+
- **Do not retroactively plan after you have already coded.** If implementation has already started, flag the gap instead of fabricating back-dated planning documents.
|
|
29
|
+
- The declared phase is established by the active skill, the story status, or an explicit statement from the user. When none of these is available, ask before assuming.
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "Git Workflow",
|
|
3
|
-
"description": "MANDATORY worktree-based workflow for ALL file-changing activities. Enforces isolated feature branches, conventional commits, and PR-based merging.",
|
|
4
|
-
"version": "2.1.0",
|
|
5
|
-
"author": "AI Agent Skills",
|
|
6
|
-
"tags": [
|
|
7
|
-
"git",
|
|
8
|
-
"worktrees",
|
|
9
|
-
"workflow",
|
|
10
|
-
"branching",
|
|
11
|
-
"conventional-commits",
|
|
12
|
-
"pull-requests",
|
|
13
|
-
"multi-agent"
|
|
14
|
-
],
|
|
15
|
-
"applies_when": [
|
|
16
|
-
"committing changes",
|
|
17
|
-
"creating branches or PRs",
|
|
18
|
-
"any code writing or modification task"
|
|
19
|
-
],
|
|
20
|
-
"always_load": true
|
|
21
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "Git Workflow",
|
|
3
|
+
"description": "MANDATORY worktree-based workflow for ALL file-changing activities. Enforces isolated feature branches, conventional commits, and PR-based merging.",
|
|
4
|
+
"version": "2.1.0",
|
|
5
|
+
"author": "AI Agent Skills",
|
|
6
|
+
"tags": [
|
|
7
|
+
"git",
|
|
8
|
+
"worktrees",
|
|
9
|
+
"workflow",
|
|
10
|
+
"branching",
|
|
11
|
+
"conventional-commits",
|
|
12
|
+
"pull-requests",
|
|
13
|
+
"multi-agent"
|
|
14
|
+
],
|
|
15
|
+
"applies_when": [
|
|
16
|
+
"committing changes",
|
|
17
|
+
"creating branches or PRs",
|
|
18
|
+
"any code writing or modification task"
|
|
19
|
+
],
|
|
20
|
+
"always_load": true
|
|
21
|
+
}
|