ma-agents 3.16.0 → 3.17.0-beta.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/README.md +85 -4
- package/bin/cli.js +323 -6
- package/lib/agents.js +22 -15
- package/lib/bmad-cache/cache-manifest.json +2 -2
- package/lib/bmad-extension/.claude-plugin/marketplace.json.template +2 -15
- package/lib/bmad-extension/skills/module-help.csv +0 -13
- package/lib/bmad-extension-plugin/.claude-plugin/marketplace.json +3 -16
- package/lib/bmad-extension-plugin/skills/module-help.csv +0 -13
- package/lib/bmad.js +116 -1
- package/lib/custom-marketplace.js +507 -0
- package/lib/installer.js +19 -7
- package/lib/knowledge-atlas/dist/atlas.cjs +14651 -14651
- package/lib/knowledge-atlas/src/render/vendor.ts +43 -43
- package/lib/knowledge-atlas/vendor/drawio-viewer.min.js +7563 -7563
- package/lib/knowledge-atlas/vendor/mermaid.min.js +2314 -2314
- package/lib/profile.js +201 -0
- package/lib/skill-authoring.js +3 -1
- package/package.json +3 -3
- package/scripts/build-bmad-cache.js +127 -43
- package/lib/bmad-extension/skills/ma-agent-ml/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ma-agent-ml/SKILL.md +0 -59
- package/lib/bmad-extension/skills/ma-agent-ml/bmad-skill-manifest.yaml +0 -11
- package/lib/bmad-extension/skills/ml-advise/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-advise/SKILL.md +0 -76
- package/lib/bmad-extension/skills/ml-advise/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-advise/skill.json +0 -7
- package/lib/bmad-extension/skills/ml-analysis/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-analysis/SKILL.md +0 -60
- package/lib/bmad-extension/skills/ml-analysis/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-analysis/skill.json +0 -7
- package/lib/bmad-extension/skills/ml-architecture/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-architecture/SKILL.md +0 -55
- package/lib/bmad-extension/skills/ml-architecture/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-architecture/skill.json +0 -7
- package/lib/bmad-extension/skills/ml-detailed-design/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-detailed-design/SKILL.md +0 -67
- package/lib/bmad-extension/skills/ml-detailed-design/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-detailed-design/skill.json +0 -7
- package/lib/bmad-extension/skills/ml-eda/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-eda/SKILL.md +0 -56
- package/lib/bmad-extension/skills/ml-eda/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-eda/scripts/baseline_classifier.py +0 -522
- package/lib/bmad-extension/skills/ml-eda/scripts/class_weights_calculator.py +0 -295
- package/lib/bmad-extension/skills/ml-eda/scripts/clustering_explorer.py +0 -383
- package/lib/bmad-extension/skills/ml-eda/scripts/eda_analyzer.py +0 -654
- package/lib/bmad-extension/skills/ml-eda/skill.json +0 -7
- package/lib/bmad-extension/skills/ml-experiment/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-experiment/SKILL.md +0 -74
- package/lib/bmad-extension/skills/ml-experiment/assets/advanced_trainer_configs.py +0 -430
- package/lib/bmad-extension/skills/ml-experiment/assets/quick_trainer_setup.py +0 -233
- package/lib/bmad-extension/skills/ml-experiment/assets/template_datamodule.py +0 -219
- package/lib/bmad-extension/skills/ml-experiment/assets/template_gnn_module.py +0 -341
- package/lib/bmad-extension/skills/ml-experiment/assets/template_lightning_module.py +0 -158
- package/lib/bmad-extension/skills/ml-experiment/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-experiment/skill.json +0 -7
- package/lib/bmad-extension/skills/ml-hparam/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-hparam/SKILL.md +0 -81
- package/lib/bmad-extension/skills/ml-hparam/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-hparam/skill.json +0 -7
- package/lib/bmad-extension/skills/ml-ideation/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-ideation/SKILL.md +0 -50
- package/lib/bmad-extension/skills/ml-ideation/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-ideation/scripts/validate_ml_prd.py +0 -287
- package/lib/bmad-extension/skills/ml-ideation/skill.json +0 -7
- package/lib/bmad-extension/skills/ml-infra/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-infra/SKILL.md +0 -58
- package/lib/bmad-extension/skills/ml-infra/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-infra/skill.json +0 -7
- package/lib/bmad-extension/skills/ml-retrospective/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-retrospective/SKILL.md +0 -63
- package/lib/bmad-extension/skills/ml-retrospective/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-retrospective/skill.json +0 -7
- package/lib/bmad-extension/skills/ml-revision/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-revision/SKILL.md +0 -82
- package/lib/bmad-extension/skills/ml-revision/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-revision/skill.json +0 -7
- package/lib/bmad-extension/skills/ml-techspec/.gitkeep +0 -0
- package/lib/bmad-extension/skills/ml-techspec/SKILL.md +0 -80
- package/lib/bmad-extension/skills/ml-techspec/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension/skills/ml-techspec/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ma-agent-ml/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ma-agent-ml/SKILL.md +0 -59
- package/lib/bmad-extension-plugin/skills/ma-agent-ml/bmad-skill-manifest.yaml +0 -11
- package/lib/bmad-extension-plugin/skills/ml-advise/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-advise/SKILL.md +0 -76
- package/lib/bmad-extension-plugin/skills/ml-advise/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-advise/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ml-analysis/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-analysis/SKILL.md +0 -60
- package/lib/bmad-extension-plugin/skills/ml-analysis/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-analysis/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ml-architecture/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-architecture/SKILL.md +0 -55
- package/lib/bmad-extension-plugin/skills/ml-architecture/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-architecture/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ml-detailed-design/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-detailed-design/SKILL.md +0 -67
- package/lib/bmad-extension-plugin/skills/ml-detailed-design/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-detailed-design/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ml-eda/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-eda/SKILL.md +0 -56
- package/lib/bmad-extension-plugin/skills/ml-eda/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-eda/scripts/baseline_classifier.py +0 -522
- package/lib/bmad-extension-plugin/skills/ml-eda/scripts/class_weights_calculator.py +0 -295
- package/lib/bmad-extension-plugin/skills/ml-eda/scripts/clustering_explorer.py +0 -383
- package/lib/bmad-extension-plugin/skills/ml-eda/scripts/eda_analyzer.py +0 -654
- package/lib/bmad-extension-plugin/skills/ml-eda/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ml-experiment/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-experiment/SKILL.md +0 -74
- package/lib/bmad-extension-plugin/skills/ml-experiment/assets/advanced_trainer_configs.py +0 -430
- package/lib/bmad-extension-plugin/skills/ml-experiment/assets/quick_trainer_setup.py +0 -233
- package/lib/bmad-extension-plugin/skills/ml-experiment/assets/template_datamodule.py +0 -219
- package/lib/bmad-extension-plugin/skills/ml-experiment/assets/template_gnn_module.py +0 -341
- package/lib/bmad-extension-plugin/skills/ml-experiment/assets/template_lightning_module.py +0 -158
- package/lib/bmad-extension-plugin/skills/ml-experiment/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-experiment/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ml-hparam/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-hparam/SKILL.md +0 -81
- package/lib/bmad-extension-plugin/skills/ml-hparam/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-hparam/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ml-ideation/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-ideation/SKILL.md +0 -50
- package/lib/bmad-extension-plugin/skills/ml-ideation/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-ideation/scripts/validate_ml_prd.py +0 -287
- package/lib/bmad-extension-plugin/skills/ml-ideation/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ml-infra/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-infra/SKILL.md +0 -58
- package/lib/bmad-extension-plugin/skills/ml-infra/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-infra/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ml-retrospective/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-retrospective/SKILL.md +0 -63
- package/lib/bmad-extension-plugin/skills/ml-retrospective/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-retrospective/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ml-revision/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-revision/SKILL.md +0 -82
- package/lib/bmad-extension-plugin/skills/ml-revision/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-revision/skill.json +0 -7
- package/lib/bmad-extension-plugin/skills/ml-techspec/.gitkeep +0 -0
- package/lib/bmad-extension-plugin/skills/ml-techspec/SKILL.md +0 -80
- package/lib/bmad-extension-plugin/skills/ml-techspec/bmad-skill-manifest.yaml +0 -3
- package/lib/bmad-extension-plugin/skills/ml-techspec/skill.json +0 -7
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Story 30.6a — Custom marketplace source: thin adapter over bmad-method's
|
|
5
|
+
* `CustomModuleManager`.
|
|
6
|
+
*
|
|
7
|
+
* Scope (per the 30.5 spike hand-off design, `_bmad-output/implementation-
|
|
8
|
+
* artifacts/epic-runs/epic-30/30-5-spike-findings.md` §5): enumerate ONLY.
|
|
9
|
+
* This module does NOT prune/stage/install anything — that lands in Story
|
|
10
|
+
* 30.6b, which consumes `enumerateCustomSource()`'s return value (the
|
|
11
|
+
* discovered plugin list) and the wizard's `selectedCodes` (see
|
|
12
|
+
* `bin/cli.js#promptCustomMarketplace`).
|
|
13
|
+
*
|
|
14
|
+
* NFR17 — pinned-internal coupling.
|
|
15
|
+
* bmad-method's `package.json` has no `exports` map, so a deep require of
|
|
16
|
+
* `tools/installer/modules/custom-module-manager.js` is reachable but is an
|
|
17
|
+
* installer-internal surface, not a documented/supported API. This is a
|
|
18
|
+
* single-call-site (blast-radius-of-one) wrapper around that surface, per the
|
|
19
|
+
* spike's mitigation list. `test/custom-marketplace-surface-drift.test.js`
|
|
20
|
+
* fails loudly if the deep path, exported class, consumed methods, or the
|
|
21
|
+
* installed bmad-method version drift from what this file was written
|
|
22
|
+
* against.
|
|
23
|
+
*
|
|
24
|
+
* FR241 — zero token handling. Enumeration relies entirely on bmad-method's
|
|
25
|
+
* own `cloneRepo()`, which sets `GIT_TERMINAL_PROMPT=0` (never prompts) and
|
|
26
|
+
* spreads `process.env` (ambient SSH agent / credential helper / git config
|
|
27
|
+
* creds). ma-agents never reads, stores, or logs a credential here.
|
|
28
|
+
*
|
|
29
|
+
* Story 30.6b — prune + stage the user's selected subset (consumes
|
|
30
|
+
* `enumerateCustomSource()`'s output + the wizard's `selectedCodes`). This
|
|
31
|
+
* still lives in the SAME file as 30.6a's enumerate adapter (not a new
|
|
32
|
+
* module) because staging a selection also needs the pinned-internal
|
|
33
|
+
* `resolvePlugin` method — keeping every deep-require consumer in one file
|
|
34
|
+
* preserves the single-call-site NFR17 mitigation. `stageCustomMarketplaceSubset()`
|
|
35
|
+
* only stages to a local directory; it never invokes bmad-method itself — the
|
|
36
|
+
* actual `--custom-source` install is `lib/bmad.js#installCustomMarketplaceSubset()`,
|
|
37
|
+
* which reuses `buildBmadArgs`/`runCommand` (no hand-rolled child-process/git logic).
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const path = require('node:path');
|
|
41
|
+
const fs = require('fs-extra');
|
|
42
|
+
|
|
43
|
+
/** Deep, pinned-internal require path (NFR17). Single call site. */
|
|
44
|
+
const CUSTOM_MODULE_MANAGER_PATH = 'bmad-method/tools/installer/modules/custom-module-manager.js';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The `CustomModuleManager` methods the custom-marketplace feature depends on.
|
|
48
|
+
* Any drift in this set (renamed/removed on a bmad-method pin bump) must fail
|
|
49
|
+
* the surface-drift test. 6a's `enumerateCustomSource()` calls `resolveSource`
|
|
50
|
+
* + `discoverModules` (and `parseSource` transitively); `resolvePlugin` is the
|
|
51
|
+
* staging primitive Story 30.6b will consume — it is guarded here now so the
|
|
52
|
+
* drift test catches a regression BEFORE 6b lands, per the 30.5 spike
|
|
53
|
+
* mitigation list (the four methods it names as the consumed surface).
|
|
54
|
+
*/
|
|
55
|
+
const REQUIRED_METHODS = ['parseSource', 'resolveSource', 'discoverModules', 'resolvePlugin'];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* NFR71 — custom-source content is pass-through and outside ma-agents' sealed
|
|
59
|
+
* -bundle guarantee. Shown once, before the user is offered the multiselect
|
|
60
|
+
* (i.e. before anything from the source could be installed).
|
|
61
|
+
*/
|
|
62
|
+
const NFR71_WARNING =
|
|
63
|
+
'Custom marketplace content is outside ma-agents\' sealed-bundle guarantee — ' +
|
|
64
|
+
'it is installed as-is from the source you provided and is not vetted by ma-agents.';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Error type raised by this adapter for any enumeration failure (unreachable
|
|
68
|
+
* source, invalid input, drifted surface). Callers should catch this and
|
|
69
|
+
* print `err.message` only — never `err.stack` — to keep the wizard's
|
|
70
|
+
* "fail clean" contract (no raw stack-dump on an unreachable source).
|
|
71
|
+
*/
|
|
72
|
+
class CustomMarketplaceError extends Error {
|
|
73
|
+
constructor(message, { cause } = {}) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = 'CustomMarketplaceError';
|
|
76
|
+
if (cause !== undefined) this.cause = cause;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Deep-require bmad-method's CustomModuleManager (NFR17 single call site).
|
|
82
|
+
*
|
|
83
|
+
* @returns {{ CustomModuleManager: Function }}
|
|
84
|
+
* @throws {CustomMarketplaceError} if the pinned-internal surface is not
|
|
85
|
+
* reachable at all (module moved/renamed on a bmad-method version bump).
|
|
86
|
+
*/
|
|
87
|
+
function loadCustomModuleManagerModule() {
|
|
88
|
+
try {
|
|
89
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
90
|
+
return require(CUSTOM_MODULE_MANAGER_PATH);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new CustomMarketplaceError(
|
|
93
|
+
`bmad-method's CustomModuleManager is not reachable at the pinned-internal ` +
|
|
94
|
+
`path "${CUSTOM_MODULE_MANAGER_PATH}" (NFR17). This usually means the ` +
|
|
95
|
+
`installed bmad-method version moved or renamed the module — re-run the ` +
|
|
96
|
+
`surface-drift test (test/custom-marketplace-surface-drift.test.js) after ` +
|
|
97
|
+
`any bmad-method version bump.`,
|
|
98
|
+
{ cause: err }
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Assert the pinned-internal surface this adapter depends on is present and
|
|
105
|
+
* shaped as expected. Used by both this module (defensively, before use) and
|
|
106
|
+
* the drift/smoke test (explicitly, as its own assertion).
|
|
107
|
+
*
|
|
108
|
+
* @returns {{ version: string, mgr: import('bmad-method/tools/installer/modules/custom-module-manager').CustomModuleManager }}
|
|
109
|
+
* @throws {CustomMarketplaceError} on any drift — missing class, missing
|
|
110
|
+
* method, or a method that is no longer a function.
|
|
111
|
+
*/
|
|
112
|
+
function assertCustomModuleManagerSurface() {
|
|
113
|
+
const mod = loadCustomModuleManagerModule();
|
|
114
|
+
if (typeof mod.CustomModuleManager !== 'function') {
|
|
115
|
+
throw new CustomMarketplaceError(
|
|
116
|
+
`bmad-method's "${CUSTOM_MODULE_MANAGER_PATH}" no longer exports a ` +
|
|
117
|
+
`CustomModuleManager class (NFR17 surface drift). Cannot enumerate custom ` +
|
|
118
|
+
`marketplace sources until ma-agents' adapter (lib/custom-marketplace.js) ` +
|
|
119
|
+
`is updated for the new shape.`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let mgr;
|
|
124
|
+
try {
|
|
125
|
+
mgr = new mod.CustomModuleManager();
|
|
126
|
+
} catch (err) {
|
|
127
|
+
throw new CustomMarketplaceError(
|
|
128
|
+
`bmad-method's CustomModuleManager constructor threw (NFR17 surface drift): ${err.message}`,
|
|
129
|
+
{ cause: err }
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const missing = REQUIRED_METHODS.filter((m) => typeof mgr[m] !== 'function');
|
|
134
|
+
if (missing.length > 0) {
|
|
135
|
+
throw new CustomMarketplaceError(
|
|
136
|
+
`bmad-method's CustomModuleManager is missing expected method(s): ` +
|
|
137
|
+
`${missing.join(', ')} (NFR17 surface drift). The custom-marketplace ` +
|
|
138
|
+
`feature depends on: ${REQUIRED_METHODS.join(', ')}.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let version = null;
|
|
143
|
+
try {
|
|
144
|
+
// eslint-disable-next-line global-require
|
|
145
|
+
version = require('bmad-method/package.json').version;
|
|
146
|
+
} catch {
|
|
147
|
+
// Best-effort only — absence doesn't invalidate the surface check above.
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { version, mgr };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Enumerate the plugins declared by a custom marketplace source.
|
|
155
|
+
*
|
|
156
|
+
* Thin wrapper over `CustomModuleManager#resolveSource` +
|
|
157
|
+
* `CustomModuleManager#discoverModules` (FR240). Performs NO staging, no
|
|
158
|
+
* pruning, no install — see the module doc-comment above. Authentication is
|
|
159
|
+
* whatever bmad-method's own `cloneRepo()` uses (ambient git creds; zero
|
|
160
|
+
* token handling — FR241).
|
|
161
|
+
*
|
|
162
|
+
* @param {string} source - A local filesystem path OR a Git URL (HTTPS/SSH)
|
|
163
|
+
* pointing at a repository. If it contains `.claude-plugin/marketplace.json`
|
|
164
|
+
* ("discovery" mode) every declared plugin is enumerated; otherwise
|
|
165
|
+
* ("direct" mode, FR240) the source itself is treated as a single module.
|
|
166
|
+
* @returns {Promise<{ mode: 'discovery'|'direct', plugins: Array<Object>, rootDir: string, sourceUrl: string|null }>}
|
|
167
|
+
* @throws {CustomMarketplaceError} on invalid input, an unreachable/unauthorized
|
|
168
|
+
* remote, or a drifted pinned-internal surface. Always a clean, human-readable
|
|
169
|
+
* message — never a raw upstream stack.
|
|
170
|
+
*/
|
|
171
|
+
async function enumerateCustomSource(source) {
|
|
172
|
+
if (!source || typeof source !== 'string' || !source.trim()) {
|
|
173
|
+
throw new CustomMarketplaceError('A custom marketplace source (path or Git URL) is required.');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { mgr } = assertCustomModuleManagerSurface();
|
|
177
|
+
|
|
178
|
+
let resolved;
|
|
179
|
+
try {
|
|
180
|
+
// skipInstall: true — this is enumeration only, never run a source's own
|
|
181
|
+
// `npm install` as a side effect of browsing it.
|
|
182
|
+
// silent: true — ma-agents owns all user-facing output; suppress
|
|
183
|
+
// bmad-method's own spinner/log lines here.
|
|
184
|
+
resolved = await mgr.resolveSource(source, { skipInstall: true, silent: true });
|
|
185
|
+
} catch (err) {
|
|
186
|
+
// Fail clean (NFR71 posture + spike §3 "fails clean" contract): a
|
|
187
|
+
// human-readable one-liner, no upstream stack surfaced to the wizard.
|
|
188
|
+
throw new CustomMarketplaceError(
|
|
189
|
+
`Could not reach custom marketplace source "${source}": ${err.message}`,
|
|
190
|
+
{ cause: err }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (resolved.mode === 'discovery') {
|
|
195
|
+
let plugins;
|
|
196
|
+
try {
|
|
197
|
+
plugins = await mgr.discoverModules(resolved.marketplace, resolved.sourceUrl);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
throw new CustomMarketplaceError(
|
|
200
|
+
`Could not read the plugin list from "${source}": ${err.message}`,
|
|
201
|
+
{ cause: err }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return { mode: 'discovery', plugins, rootDir: resolved.rootDir, sourceUrl: resolved.sourceUrl };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// FR240 — "direct" mode (no marketplace.json at the source root): treat the
|
|
208
|
+
// whole source as a single module so the wizard's multiselect still has
|
|
209
|
+
// exactly one, well-described choice. 30.6b decides how a direct-mode
|
|
210
|
+
// selection stages; 6a only surfaces it.
|
|
211
|
+
const singlePlugin = {
|
|
212
|
+
code: path.basename(resolved.rootDir),
|
|
213
|
+
name: path.basename(resolved.rootDir),
|
|
214
|
+
description: '(direct-mode source — no marketplace.json; the whole source is one module)',
|
|
215
|
+
source: '.',
|
|
216
|
+
skills: [],
|
|
217
|
+
type: 'custom-direct',
|
|
218
|
+
trustTier: 'unverified',
|
|
219
|
+
builtIn: false,
|
|
220
|
+
isExternal: true,
|
|
221
|
+
};
|
|
222
|
+
return { mode: 'direct', plugins: [singlePlugin], rootDir: resolved.rootDir, sourceUrl: resolved.sourceUrl };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Story 30.6b — prune + stage the selected subset ───────────────────────
|
|
226
|
+
|
|
227
|
+
/** Project-relative stage dir for the pruned custom-marketplace subset. Sibling of
|
|
228
|
+
* `lib/bmad.js#PLUGIN_STAGE_DIR_NAME` (the bundled-plugin stage) but a distinct
|
|
229
|
+
* directory: the two stages are unrelated content and must never merge. */
|
|
230
|
+
const CUSTOM_MARKETPLACE_STAGE_DIR_NAME = '.ma-agents-custom-marketplace-stage';
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* FR240 direct-mode gap-fill: `enumerateCustomSource()`'s synthetic direct-mode
|
|
234
|
+
* plugin object always carries `skills: []` (6a only needs it to render ONE
|
|
235
|
+
* multiselect choice). bmad-method's own `PluginResolver.resolve()` returns `[]`
|
|
236
|
+
* immediately when `plugin.skills` is empty, so staging a direct-mode selection
|
|
237
|
+
* as-is would resolve to zero installable skills. bmad-method's own CLI
|
|
238
|
+
* (`ui.js#_resolveCustomSourcesCli`, direct-mode branch) works around this by
|
|
239
|
+
* scanning the source root for immediate subdirectories containing `SKILL.md`
|
|
240
|
+
* and using THOSE directory names as `plugin.skills` before calling
|
|
241
|
+
* `resolvePlugin`. This mirrors that scan exactly so 30.6b's direct-mode
|
|
242
|
+
* staging matches what bmad-method's own non-interactive install would do.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} rootDir - Absolute path to the direct-mode source root.
|
|
245
|
+
* @returns {Promise<string[]>} Directory names (not paths) that contain a SKILL.md.
|
|
246
|
+
*/
|
|
247
|
+
async function scanDirectModeSkillDirs(rootDir) {
|
|
248
|
+
let entries;
|
|
249
|
+
try {
|
|
250
|
+
entries = await fs.readdir(rootDir, { withFileTypes: true });
|
|
251
|
+
} catch {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
const skillDirs = [];
|
|
255
|
+
for (const entry of entries) {
|
|
256
|
+
if (!entry.isDirectory()) continue;
|
|
257
|
+
const skillMd = path.join(rootDir, entry.name, 'SKILL.md');
|
|
258
|
+
if (await fs.pathExists(skillMd)) {
|
|
259
|
+
skillDirs.push(entry.name);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return skillDirs;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Resolve every selected plugin's installable skill/module layout via
|
|
267
|
+
* bmad-method's own `CustomModuleManager#resolvePlugin` (the pinned-internal
|
|
268
|
+
* method 30.6a already guards in `REQUIRED_METHODS`, reserved for this story).
|
|
269
|
+
*
|
|
270
|
+
* @param {Object} ctx
|
|
271
|
+
* @param {'discovery'|'direct'} ctx.mode
|
|
272
|
+
* @param {string} ctx.rootDir - Absolute path returned by `enumerateCustomSource`.
|
|
273
|
+
* @param {string|null} ctx.sourceUrl - Absolute/null, as returned by `enumerateCustomSource`.
|
|
274
|
+
* @param {Array<Object>} ctx.plugins - The FULL enumerated plugin list (30.6a's return value).
|
|
275
|
+
* @param {string[]} ctx.selectedCodes - The user's chosen subset of plugin codes.
|
|
276
|
+
* @returns {Promise<Array<{ plugin: Object, rawPlugin: Object, resolved: Array<Object> }>>}
|
|
277
|
+
* @throws {CustomMarketplaceError} if no enumerated plugin matches `selectedCodes`.
|
|
278
|
+
*/
|
|
279
|
+
async function resolveSelectedPlugins({ mode, rootDir, sourceUrl, plugins, selectedCodes }) {
|
|
280
|
+
const { mgr } = assertCustomModuleManagerSurface();
|
|
281
|
+
const selectedSet = new Set(selectedCodes);
|
|
282
|
+
const selectedPlugins = (plugins || []).filter((p) => selectedSet.has(p.code));
|
|
283
|
+
|
|
284
|
+
if (selectedPlugins.length === 0) {
|
|
285
|
+
throw new CustomMarketplaceError(
|
|
286
|
+
`None of the selected codes (${[...selectedSet].join(', ') || '(none)'}) matched an ` +
|
|
287
|
+
`enumerated plugin — nothing to stage.`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Local sources pass their rootDir as `localPath` (manifest tracking only);
|
|
292
|
+
// URL sources pass `null` here since the clone metadata already carries the
|
|
293
|
+
// origin. Mirrors resolvePlugin's own call sites (custom-module-manager.js,
|
|
294
|
+
// ui.js#_resolveCustomSourcesCli).
|
|
295
|
+
const localPath = sourceUrl ? null : rootDir;
|
|
296
|
+
|
|
297
|
+
const resolvedByPlugin = [];
|
|
298
|
+
for (const plugin of selectedPlugins) {
|
|
299
|
+
let rawPlugin;
|
|
300
|
+
if (mode === 'discovery') {
|
|
301
|
+
rawPlugin = plugin.rawPlugin;
|
|
302
|
+
} else {
|
|
303
|
+
// FR240 direct mode — see scanDirectModeSkillDirs() doc comment.
|
|
304
|
+
const skillDirNames = await scanDirectModeSkillDirs(rootDir);
|
|
305
|
+
rawPlugin = {
|
|
306
|
+
name: plugin.code,
|
|
307
|
+
description: plugin.description || '',
|
|
308
|
+
version: plugin.version || null,
|
|
309
|
+
source: '.',
|
|
310
|
+
skills: skillDirNames,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const resolved = await mgr.resolvePlugin(rootDir, rawPlugin, sourceUrl, localPath);
|
|
314
|
+
resolvedByPlugin.push({ plugin, rawPlugin, resolved });
|
|
315
|
+
}
|
|
316
|
+
return resolvedByPlugin;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* FR238 — pre-flight guard mirroring (and pre-empting, with a friendlier
|
|
321
|
+
* message) bmad-method 6.10.0's `_copyResolvedSkills` fail-loud invariants
|
|
322
|
+
* (`tools/installer/modules/official-modules.js`):
|
|
323
|
+
* (a) the overall resolution must be non-empty (an empty selection would
|
|
324
|
+
* otherwise silently install nothing — worse than a loud abort, since
|
|
325
|
+
* the custom-module install path has no built-in guard for this case);
|
|
326
|
+
* (b) each individual resolved module's skill directories must have
|
|
327
|
+
* UNIQUE leaf (basename) names — `_copyResolvedSkills` flattens skills
|
|
328
|
+
* by leaf name into that module's target dir and throws on a collision.
|
|
329
|
+
* Scope note: uniqueness is checked PER MODULE, matching the upstream guard's
|
|
330
|
+
* exact scope — the same leaf name reused across two DIFFERENT modules is
|
|
331
|
+
* fine (each module installs into its own `bmadDir/<code>/`).
|
|
332
|
+
*
|
|
333
|
+
* @param {Array<{ plugin: Object, rawPlugin: Object, resolved: Array<Object> }>} resolvedByPlugin
|
|
334
|
+
* @param {Object} [ctx]
|
|
335
|
+
* @param {string} [ctx.source] - Original source string, for error messages only.
|
|
336
|
+
* @returns {Array<Object>} The flattened list of resolved modules (all plugins).
|
|
337
|
+
* @throws {CustomMarketplaceError} on any violation above.
|
|
338
|
+
*/
|
|
339
|
+
function assertResolutionNonEmptyAndUniqueLeaves(resolvedByPlugin, { source = '' } = {}) {
|
|
340
|
+
const allModules = resolvedByPlugin.flatMap((r) => r.resolved || []);
|
|
341
|
+
|
|
342
|
+
if (allModules.length === 0) {
|
|
343
|
+
throw new CustomMarketplaceError(
|
|
344
|
+
`Custom marketplace selection from "${source}" resolved to zero installable modules ` +
|
|
345
|
+
`— bmad-method's PluginResolver could not locate module.yaml/skills for any selected ` +
|
|
346
|
+
`plugin. Nothing would be installed; aborting before staging (FR238).`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const mod of allModules) {
|
|
351
|
+
if (!Array.isArray(mod.skillPaths) || mod.skillPaths.length === 0) {
|
|
352
|
+
throw new CustomMarketplaceError(
|
|
353
|
+
`Module '${mod.code}' (from plugin '${mod.pluginName}') resolved to zero skill ` +
|
|
354
|
+
`directories — installing it would create an empty module. Aborting before staging (FR238).`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const seenLeaves = new Map();
|
|
358
|
+
for (const skillPath of mod.skillPaths) {
|
|
359
|
+
const leaf = path.basename(skillPath);
|
|
360
|
+
if (seenLeaves.has(leaf)) {
|
|
361
|
+
throw new CustomMarketplaceError(
|
|
362
|
+
`Module '${mod.code}' has two skill directories that share the leaf name ` +
|
|
363
|
+
`'${leaf}' ('${seenLeaves.get(leaf)}' and '${skillPath}') — they would overwrite ` +
|
|
364
|
+
`each other on install. bmad-method 6.10.0's _copyResolvedSkills fails loud on ` +
|
|
365
|
+
`this exact case; ma-agents checks it first so the failure is clean (FR238).`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
seenLeaves.set(leaf, skillPath);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return allModules;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Prune the enumerated marketplace down to `selectedCodes` and stage the
|
|
377
|
+
* resulting subset's actual content into a project-local directory, ready to
|
|
378
|
+
* be handed to bmad-method as an additional `--custom-source`.
|
|
379
|
+
*
|
|
380
|
+
* Stages ONLY (no bmad-method invocation) — `lib/bmad.js#installCustomMarketplaceSubset`
|
|
381
|
+
* performs the actual install using this function's return value.
|
|
382
|
+
*
|
|
383
|
+
* @param {Object} ctx
|
|
384
|
+
* @param {string} ctx.source - Original source string (path or Git URL), for messages only.
|
|
385
|
+
* @param {'discovery'|'direct'} ctx.mode
|
|
386
|
+
* @param {string} ctx.rootDir
|
|
387
|
+
* @param {string|null} ctx.sourceUrl
|
|
388
|
+
* @param {Array<Object>} ctx.plugins - Full enumerated list (30.6a's return value).
|
|
389
|
+
* @param {string[]} ctx.selectedCodes
|
|
390
|
+
* @param {string} ctx.projectRoot
|
|
391
|
+
* @returns {Promise<{ stagePath: string, customSourceArg: string, resolvedModules: Array<Object> }>}
|
|
392
|
+
* @throws {CustomMarketplaceError} on a drifted surface, an unmatched selection, or a failed
|
|
393
|
+
* FR238 pre-flight check (see assertResolutionNonEmptyAndUniqueLeaves).
|
|
394
|
+
*/
|
|
395
|
+
async function stageCustomMarketplaceSubset({ source, mode, rootDir, sourceUrl, plugins, selectedCodes, projectRoot }) {
|
|
396
|
+
projectRoot = path.resolve(projectRoot);
|
|
397
|
+
|
|
398
|
+
const resolvedByPlugin = await resolveSelectedPlugins({ mode, rootDir, sourceUrl, plugins, selectedCodes });
|
|
399
|
+
const allModules = assertResolutionNonEmptyAndUniqueLeaves(resolvedByPlugin, { source });
|
|
400
|
+
|
|
401
|
+
const stagePath = path.join(projectRoot, CUSTOM_MARKETPLACE_STAGE_DIR_NAME);
|
|
402
|
+
|
|
403
|
+
// Safety pattern mirrors lib/bmad.js#stagePlugin (Story 22.6/22.8): never
|
|
404
|
+
// follow a pre-existing stage path that is a symlink, and wipe any stale
|
|
405
|
+
// stage from a prior failed run before copying fresh content.
|
|
406
|
+
if (fs.existsSync(stagePath)) {
|
|
407
|
+
const stageLstat = fs.lstatSync(stagePath);
|
|
408
|
+
if (stageLstat.isSymbolicLink()) {
|
|
409
|
+
throw new CustomMarketplaceError(
|
|
410
|
+
`Refusing to stage custom marketplace subset — ${stagePath} is a symbolic link. ` +
|
|
411
|
+
`Remove it manually and re-run.`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
fs.removeSync(stagePath);
|
|
416
|
+
} catch (err) {
|
|
417
|
+
throw new CustomMarketplaceError(`Cannot wipe stale stage ${stagePath}: ${err.message}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
fs.mkdirpSync(stagePath);
|
|
421
|
+
|
|
422
|
+
// Copy every resolved file, preserving its path relative to rootDir, so
|
|
423
|
+
// bmad-method's own re-resolution against the stage (a fresh 'local' type
|
|
424
|
+
// source once staged) reproduces the identical result: same skill dirs at
|
|
425
|
+
// the same relative locations, same module.yaml if one was found, and the
|
|
426
|
+
// same synthesized fallback if strategy 5 applies (a pure function of the
|
|
427
|
+
// marketplace.json entry + the copied skill dirs' SKILL.md frontmatter).
|
|
428
|
+
const copyRelativeToRoot = (absSrc) => {
|
|
429
|
+
const rel = path.relative(rootDir, absSrc);
|
|
430
|
+
const dest = path.join(stagePath, rel);
|
|
431
|
+
fs.copySync(absSrc, dest, { overwrite: true, dereference: true });
|
|
432
|
+
};
|
|
433
|
+
try {
|
|
434
|
+
for (const mod of allModules) {
|
|
435
|
+
for (const skillPath of mod.skillPaths) copyRelativeToRoot(skillPath);
|
|
436
|
+
if (mod.moduleYamlPath) copyRelativeToRoot(mod.moduleYamlPath);
|
|
437
|
+
if (mod.moduleHelpCsvPath) copyRelativeToRoot(mod.moduleHelpCsvPath);
|
|
438
|
+
}
|
|
439
|
+
} catch (err) {
|
|
440
|
+
try { fs.removeSync(stagePath); } catch { /* swallow — primary error wins */ }
|
|
441
|
+
throw new CustomMarketplaceError(`Staging the custom marketplace subset failed: ${err.message}`, { cause: err });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Prune: write a marketplace.json containing ONLY the selected plugins'
|
|
445
|
+
// raw entries (direct mode has no original marketplace.json to prune, so
|
|
446
|
+
// this synthesizes the one entry from the direct-mode plugin descriptor).
|
|
447
|
+
const prunedPlugins = resolvedByPlugin.map(({ rawPlugin }) => rawPlugin);
|
|
448
|
+
const marketplaceDir = path.join(stagePath, '.claude-plugin');
|
|
449
|
+
fs.mkdirpSync(marketplaceDir);
|
|
450
|
+
fs.writeFileSync(
|
|
451
|
+
path.join(marketplaceDir, 'marketplace.json'),
|
|
452
|
+
JSON.stringify({ name: path.basename(rootDir), plugins: prunedPlugins }, null, 2)
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
// POSIX-style `./<dir>` satisfies bmad-method's parseSource() local-path
|
|
456
|
+
// detector on both Windows and POSIX (see lib/bmad.js#stagePlugin for the
|
|
457
|
+
// same rationale) — bmad-method's cwd is always projectRoot.
|
|
458
|
+
const customSourceArg = `./${CUSTOM_MARKETPLACE_STAGE_DIR_NAME}`;
|
|
459
|
+
return { stagePath, customSourceArg, resolvedModules: allModules };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ─── Story 30.7 — persist + re-enumerate/reconcile on update ───────────────
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Pure reconcile — no I/O. Compares a freshly re-enumerated plugin list
|
|
466
|
+
* against the PERSISTED selection from a prior install and reports the
|
|
467
|
+
* difference. Newly-appeared plugins are reported only — never added to the
|
|
468
|
+
* returned selection (FR242's "report only" contract); the caller decides
|
|
469
|
+
* what to do with `added`/`removed` (typically: print a report, and re-install
|
|
470
|
+
* only `stillPresent`).
|
|
471
|
+
*
|
|
472
|
+
* @param {Object} ctx
|
|
473
|
+
* @param {string[]} ctx.persistedSelectedCodes - The previously-persisted `selectedCodes`.
|
|
474
|
+
* @param {Array<{ code: string }>} ctx.enumeratedPlugins - Fresh `enumerateCustomSource()` output.
|
|
475
|
+
* @returns {{ added: string[], removed: string[], stillPresent: string[] }}
|
|
476
|
+
* `added` — declared now but not previously selected (report only, not auto-selected).
|
|
477
|
+
* `removed` — previously selected but no longer declared by the source.
|
|
478
|
+
* `stillPresent` — previously selected AND still declared (safe to re-install as-is).
|
|
479
|
+
*/
|
|
480
|
+
function reconcileCustomMarketplaceSelection({ persistedSelectedCodes, enumeratedPlugins }) {
|
|
481
|
+
const persistedSet = new Set(persistedSelectedCodes || []);
|
|
482
|
+
const enumeratedCodes = (enumeratedPlugins || []).map((p) => p.code);
|
|
483
|
+
const enumeratedSet = new Set(enumeratedCodes);
|
|
484
|
+
|
|
485
|
+
const added = enumeratedCodes.filter((c) => !persistedSet.has(c));
|
|
486
|
+
const removed = [...persistedSet].filter((c) => !enumeratedSet.has(c));
|
|
487
|
+
const stillPresent = [...persistedSet].filter((c) => enumeratedSet.has(c));
|
|
488
|
+
|
|
489
|
+
return { added, removed, stillPresent };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
module.exports = {
|
|
493
|
+
CUSTOM_MODULE_MANAGER_PATH,
|
|
494
|
+
REQUIRED_METHODS,
|
|
495
|
+
NFR71_WARNING,
|
|
496
|
+
CustomMarketplaceError,
|
|
497
|
+
assertCustomModuleManagerSurface,
|
|
498
|
+
enumerateCustomSource,
|
|
499
|
+
// Story 30.6b — prune + stage the selected subset
|
|
500
|
+
CUSTOM_MARKETPLACE_STAGE_DIR_NAME,
|
|
501
|
+
scanDirectModeSkillDirs,
|
|
502
|
+
resolveSelectedPlugins,
|
|
503
|
+
assertResolutionNonEmptyAndUniqueLeaves,
|
|
504
|
+
stageCustomMarketplaceSubset,
|
|
505
|
+
// Story 30.7 — persist + reconcile on update
|
|
506
|
+
reconcileCustomMarketplaceSelection,
|
|
507
|
+
};
|
package/lib/installer.js
CHANGED
|
@@ -14,6 +14,11 @@ const TEMPLATE_PATH = path.join(__dirname, 'templates', 'project-context.templat
|
|
|
14
14
|
const UNIVERSAL_INSTRUCTION_TEMPLATE_PATH = path.join(__dirname, 'templates', 'instruction-block-universal.template.md');
|
|
15
15
|
const GIT_INSTRUCTION_TEMPLATE_PATH = path.join(__dirname, 'templates', 'instruction-block-git.template.md');
|
|
16
16
|
const ONPREM_INSTRUCTION_TEMPLATE_PATH = path.join(__dirname, 'templates', 'instruction-block-onprem.template.md');
|
|
17
|
+
// Retained for test reference (test/clinerules.test.js asserts the template's
|
|
18
|
+
// existence + body). Production template selection is now registry-driven via
|
|
19
|
+
// agent.injectionStrategy.framingTemplate + EXTRA_TEMPLATE_DIR (Story 30.9), so
|
|
20
|
+
// this constant is no longer read by install logic — do not reintroduce an
|
|
21
|
+
// agent-id literal that consumes it directly.
|
|
17
22
|
const CLINERULES_TEMPLATE_PATH = path.join(__dirname, 'templates', 'clinerules.template.md');
|
|
18
23
|
const EXTRA_TEMPLATE_DIR = path.join(__dirname, 'templates');
|
|
19
24
|
|
|
@@ -1238,21 +1243,28 @@ async function updateAgentInstructions(agent, projectRoot, opts = {}) {
|
|
|
1238
1243
|
const wrappedInstruction = `${markerStart}${planningInstruction}${markerEnd}`;
|
|
1239
1244
|
const wrappedInstructionWithTrailingNewline = wrappedInstruction + '\n';
|
|
1240
1245
|
|
|
1241
|
-
// Story 21.5 AC #6 —
|
|
1246
|
+
// Story 21.5 AC #6 / Story 30.9 — dual-file drift detection, driven by the
|
|
1247
|
+
// registry's injectionStrategy.dualFileDriftCheck flag rather than a
|
|
1248
|
+
// hardcoded agent-id literal (test/agent-injection-strategy.test.js #3.3).
|
|
1242
1249
|
// Runs BEFORE the file loop so we abort cleanly without partial writes.
|
|
1243
1250
|
// `--yes` does NOT bypass (explicit documented exception, AC #6).
|
|
1244
|
-
if (agent.
|
|
1251
|
+
if (agent.injectionStrategy?.dualFileDriftCheck) {
|
|
1245
1252
|
checkClinerulesDualFileDrift(projectRoot);
|
|
1246
1253
|
}
|
|
1247
1254
|
|
|
1248
|
-
// Story 21.5 AC #1, #2 — optional framing template for fresh
|
|
1249
|
-
// The composer produces the universal body once; the
|
|
1250
|
-
//
|
|
1255
|
+
// Story 21.5 AC #1, #2 / Story 30.9 — optional framing template for fresh
|
|
1256
|
+
// file creation. The composer produces the universal body once; the
|
|
1257
|
+
// framing supplies an agent-flavored header/guidance wrapper. Framing is
|
|
1251
1258
|
// only used when creating a NEW file — existing files go through the normal
|
|
1252
1259
|
// marker-replace path so user content outside markers is preserved (AC #4).
|
|
1260
|
+
// The template name is registry-driven (injectionStrategy.framingTemplate),
|
|
1261
|
+
// not gated on the agent id literal.
|
|
1253
1262
|
let frameworkTemplate = null;
|
|
1254
|
-
if (agent.
|
|
1255
|
-
|
|
1263
|
+
if (agent.injectionStrategy?.framingTemplate) {
|
|
1264
|
+
const framingTemplatePath = path.join(EXTRA_TEMPLATE_DIR, agent.injectionStrategy.framingTemplate);
|
|
1265
|
+
if (fs.existsSync(framingTemplatePath)) {
|
|
1266
|
+
frameworkTemplate = fs.readFileSync(framingTemplatePath, 'utf-8');
|
|
1267
|
+
}
|
|
1256
1268
|
}
|
|
1257
1269
|
|
|
1258
1270
|
for (const fileName of agent.instructionFiles) {
|