bmad-fh 6.0.0-alpha.23 → 6.0.0-alpha.23.02a963fa
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/.github/workflows/publish-multi-artifact.yaml +6 -2
- package/eslint.config.mjs +2 -2
- package/package.json +2 -2
- package/src/bmm/module.yaml +2 -2
- package/src/core/lib/scope/artifact-resolver.js +26 -26
- package/src/core/lib/scope/event-logger.js +34 -45
- package/src/core/lib/scope/index.js +3 -3
- package/src/core/lib/scope/scope-context.js +22 -28
- package/src/core/lib/scope/scope-initializer.js +29 -31
- package/src/core/lib/scope/scope-manager.js +21 -21
- package/src/core/lib/scope/scope-migrator.js +44 -52
- package/src/core/lib/scope/scope-sync.js +42 -48
- package/src/core/lib/scope/scope-validator.js +16 -21
- package/src/core/lib/scope/state-lock.js +37 -43
- package/src/core/module.yaml +2 -2
- package/test/test-scope-e2e.js +65 -76
- package/test/test-scope-system.js +66 -72
- package/tools/cli/commands/scope.js +73 -73
- package/tools/cli/scripts/migrate-workflows.js +43 -51
|
@@ -28,10 +28,14 @@ jobs:
|
|
|
28
28
|
- name: Install dependencies
|
|
29
29
|
run: npm ci
|
|
30
30
|
|
|
31
|
-
- name:
|
|
31
|
+
- name: Set version with commit hash
|
|
32
32
|
id: version
|
|
33
33
|
run: |
|
|
34
|
-
|
|
34
|
+
BASE_VERSION=$(node -p "require('./package.json').version")
|
|
35
|
+
SHORT_SHA=$(git rev-parse --short HEAD)
|
|
36
|
+
NEW_VERSION="${BASE_VERSION}.${SHORT_SHA}"
|
|
37
|
+
npm version "${NEW_VERSION}" --no-git-tag-version
|
|
38
|
+
echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
|
|
35
39
|
|
|
36
40
|
- name: Publish to NPM
|
|
37
41
|
env:
|
package/eslint.config.mjs
CHANGED
|
@@ -81,9 +81,9 @@ export default [
|
|
|
81
81
|
},
|
|
82
82
|
},
|
|
83
83
|
|
|
84
|
-
// CLI scripts under tools
|
|
84
|
+
// CLI scripts under tools/**, test/**, and src/core/lib/**
|
|
85
85
|
{
|
|
86
|
-
files: ['tools/**/*.js', 'tools/**/*.mjs', 'test/**/*.js'],
|
|
86
|
+
files: ['tools/**/*.js', 'tools/**/*.mjs', 'test/**/*.js', 'src/core/lib/**/*.js'],
|
|
87
87
|
rules: {
|
|
88
88
|
// Allow CommonJS patterns for Node CLI scripts
|
|
89
89
|
'unicorn/prefer-module': 'off',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "bmad-fh",
|
|
4
|
-
"version": "6.0.0-alpha.23",
|
|
4
|
+
"version": "6.0.0-alpha.23.02a963fa",
|
|
5
5
|
"description": "Breakthrough Method of Agile AI-driven Development",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agile",
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"bmad:install": "node tools/cli/bmad-cli.js install",
|
|
27
|
-
"publish:multi-artifact": "npm publish --tag multi-artifact",
|
|
28
27
|
"bundle": "node tools/cli/bundlers/bundle-web.js all",
|
|
29
28
|
"docs:build": "node tools/build-docs.js",
|
|
30
29
|
"docs:dev": "astro dev --root website",
|
|
@@ -40,6 +39,7 @@
|
|
|
40
39
|
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
|
|
41
40
|
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
|
42
41
|
"prepare": "husky",
|
|
42
|
+
"publish:multi-artifact": "npm publish --tag multi-artifact",
|
|
43
43
|
"rebundle": "node tools/cli/bundlers/bundle-web.js rebundle",
|
|
44
44
|
"release:major": "gh workflow run \"Manual Release\" -f version_bump=major",
|
|
45
45
|
"release:minor": "gh workflow run \"Manual Release\" -f version_bump=minor",
|
package/src/bmm/module.yaml
CHANGED
|
@@ -34,13 +34,13 @@ user_skill_level:
|
|
|
34
34
|
scope:
|
|
35
35
|
default: ""
|
|
36
36
|
result: "{value}"
|
|
37
|
-
runtime: true
|
|
37
|
+
runtime: true # Indicates this is resolved at runtime, not during install
|
|
38
38
|
|
|
39
39
|
# Scope-aware path helper - resolves to output_folder/scope or just output_folder
|
|
40
40
|
scope_path:
|
|
41
41
|
default: "{output_folder}"
|
|
42
42
|
result: "{value}"
|
|
43
|
-
runtime: true
|
|
43
|
+
runtime: true # Updated at runtime when scope is active
|
|
44
44
|
|
|
45
45
|
planning_artifacts: # Phase 1-3 artifacts
|
|
46
46
|
prompt: "Where should planning artifacts be stored? (Brainstorming, Briefs, PRDs, UX Designs, Architecture, Epics)"
|
|
@@ -3,15 +3,15 @@ const path = require('node:path');
|
|
|
3
3
|
/**
|
|
4
4
|
* Resolves and enforces scope-based artifact access
|
|
5
5
|
* Implements read-any/write-own access model
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* @class ArtifactResolver
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* @example
|
|
10
10
|
* const resolver = new ArtifactResolver({
|
|
11
11
|
* currentScope: 'auth',
|
|
12
12
|
* basePath: '/path/to/_bmad-output'
|
|
13
13
|
* });
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
15
|
* if (resolver.canWrite('/path/to/_bmad-output/auth/file.md')) {
|
|
16
16
|
* // Write operation allowed
|
|
17
17
|
* }
|
|
@@ -52,7 +52,7 @@ class ArtifactResolver {
|
|
|
52
52
|
extractScopeFromPath(filePath) {
|
|
53
53
|
// Normalize path
|
|
54
54
|
const normalizedPath = path.normalize(filePath);
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
// Find the base path in the file path
|
|
57
57
|
const baseIndex = normalizedPath.indexOf(this.basePath);
|
|
58
58
|
if (baseIndex === -1) {
|
|
@@ -61,16 +61,16 @@ class ArtifactResolver {
|
|
|
61
61
|
|
|
62
62
|
// Get the relative path from base
|
|
63
63
|
const relativePath = normalizedPath.slice(Math.max(0, baseIndex + this.basePath.length + 1));
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
// Split to get the first segment (scope name)
|
|
66
66
|
const segments = relativePath.split(path.sep).filter(Boolean);
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
if (segments.length === 0) {
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const firstSegment = segments[0];
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
// Check if it's a reserved path
|
|
75
75
|
if (this.reservedPaths.includes(firstSegment)) {
|
|
76
76
|
return firstSegment; // Return the reserved path name
|
|
@@ -109,7 +109,7 @@ class ArtifactResolver {
|
|
|
109
109
|
// Read is always allowed for all paths
|
|
110
110
|
return {
|
|
111
111
|
allowed: true,
|
|
112
|
-
reason: 'Read access is always allowed in read-any model'
|
|
112
|
+
reason: 'Read access is always allowed in read-any model',
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -124,7 +124,7 @@ class ArtifactResolver {
|
|
|
124
124
|
return {
|
|
125
125
|
allowed: true,
|
|
126
126
|
reason: 'No scope active, operating in legacy mode',
|
|
127
|
-
warning: null
|
|
127
|
+
warning: null,
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -135,7 +135,7 @@ class ArtifactResolver {
|
|
|
135
135
|
return {
|
|
136
136
|
allowed: false,
|
|
137
137
|
reason: `Cannot write directly to '${this.sharedPath}'. Use: bmad scope sync-up`,
|
|
138
|
-
warning: null
|
|
138
|
+
warning: null,
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -144,7 +144,7 @@ class ArtifactResolver {
|
|
|
144
144
|
return {
|
|
145
145
|
allowed: false,
|
|
146
146
|
reason: `Cannot write to reserved path '${targetScope}'`,
|
|
147
|
-
warning: null
|
|
147
|
+
warning: null,
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -153,7 +153,7 @@ class ArtifactResolver {
|
|
|
153
153
|
return {
|
|
154
154
|
allowed: true,
|
|
155
155
|
reason: `Write allowed to current scope '${this.currentScope}'`,
|
|
156
|
-
warning: null
|
|
156
|
+
warning: null,
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -164,31 +164,31 @@ class ArtifactResolver {
|
|
|
164
164
|
return {
|
|
165
165
|
allowed: false,
|
|
166
166
|
reason: `Cannot write to scope '${targetScope}' while in scope '${this.currentScope}'`,
|
|
167
|
-
warning: null
|
|
167
|
+
warning: null,
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
case 'warn': {
|
|
172
172
|
return {
|
|
173
173
|
allowed: true,
|
|
174
174
|
reason: 'Write allowed with warning in warn mode',
|
|
175
|
-
warning: `Warning: Writing to scope '${targetScope}' from scope '${this.currentScope}'
|
|
175
|
+
warning: `Warning: Writing to scope '${targetScope}' from scope '${this.currentScope}'`,
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
case 'permissive': {
|
|
180
180
|
return {
|
|
181
181
|
allowed: true,
|
|
182
182
|
reason: 'Write allowed in permissive mode',
|
|
183
|
-
warning: null
|
|
183
|
+
warning: null,
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
|
-
|
|
186
|
+
|
|
187
187
|
default: {
|
|
188
188
|
return {
|
|
189
189
|
allowed: false,
|
|
190
190
|
reason: 'Unknown isolation mode',
|
|
191
|
-
warning: null
|
|
191
|
+
warning: null,
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
194
|
}
|
|
@@ -198,7 +198,7 @@ class ArtifactResolver {
|
|
|
198
198
|
return {
|
|
199
199
|
allowed: true,
|
|
200
200
|
reason: 'Path is outside scope system',
|
|
201
|
-
warning: null
|
|
201
|
+
warning: null,
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -209,11 +209,11 @@ class ArtifactResolver {
|
|
|
209
209
|
*/
|
|
210
210
|
validateWrite(filePath) {
|
|
211
211
|
const result = this.canWrite(filePath);
|
|
212
|
-
|
|
212
|
+
|
|
213
213
|
if (!result.allowed) {
|
|
214
214
|
throw new Error(result.reason);
|
|
215
215
|
}
|
|
216
|
-
|
|
216
|
+
|
|
217
217
|
if (result.warning) {
|
|
218
218
|
console.warn(result.warning);
|
|
219
219
|
}
|
|
@@ -227,7 +227,7 @@ class ArtifactResolver {
|
|
|
227
227
|
*/
|
|
228
228
|
resolveScopePath(relativePath, scopeId = null) {
|
|
229
229
|
const scope = scopeId || this.currentScope;
|
|
230
|
-
|
|
230
|
+
|
|
231
231
|
if (!scope) {
|
|
232
232
|
// No scope - return path relative to base
|
|
233
233
|
return path.join(this.basePath, relativePath);
|
|
@@ -254,7 +254,7 @@ class ArtifactResolver {
|
|
|
254
254
|
currentScope: this.currentScope ? path.join(this.basePath, this.currentScope) : null,
|
|
255
255
|
shared: path.join(this.basePath, this.sharedPath),
|
|
256
256
|
allScopes: `${this.basePath}/*`,
|
|
257
|
-
description: 'Read access is allowed to all scopes and shared directories'
|
|
257
|
+
description: 'Read access is allowed to all scopes and shared directories',
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
260
|
|
|
@@ -266,13 +266,13 @@ class ArtifactResolver {
|
|
|
266
266
|
if (!this.currentScope) {
|
|
267
267
|
return {
|
|
268
268
|
all: this.basePath,
|
|
269
|
-
description: 'No scope active - all paths writable (legacy mode)'
|
|
269
|
+
description: 'No scope active - all paths writable (legacy mode)',
|
|
270
270
|
};
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
return {
|
|
274
274
|
currentScope: path.join(this.basePath, this.currentScope),
|
|
275
|
-
description: `Write access limited to scope '${this.currentScope}'
|
|
275
|
+
description: `Write access limited to scope '${this.currentScope}'`,
|
|
276
276
|
};
|
|
277
277
|
}
|
|
278
278
|
|
|
@@ -6,12 +6,12 @@ const { StateLock } = require('./state-lock');
|
|
|
6
6
|
/**
|
|
7
7
|
* Logs and tracks events across scopes
|
|
8
8
|
* Handles event logging and subscription notifications
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
10
|
* @class EventLogger
|
|
11
11
|
* @requires fs-extra
|
|
12
12
|
* @requires yaml
|
|
13
13
|
* @requires StateLock
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
15
|
* @example
|
|
16
16
|
* const logger = new EventLogger({ projectRoot: '/path/to/project' });
|
|
17
17
|
* await logger.logEvent('artifact_created', 'auth', { artifact: 'prd.md' });
|
|
@@ -47,19 +47,19 @@ class EventLogger {
|
|
|
47
47
|
await fs.ensureDir(this.eventsPath);
|
|
48
48
|
|
|
49
49
|
// Create event-log.yaml if not exists
|
|
50
|
-
if (!await fs.pathExists(this.eventLogPath)) {
|
|
50
|
+
if (!(await fs.pathExists(this.eventLogPath))) {
|
|
51
51
|
const eventLog = {
|
|
52
52
|
version: 1,
|
|
53
|
-
events: []
|
|
53
|
+
events: [],
|
|
54
54
|
};
|
|
55
55
|
await fs.writeFile(this.eventLogPath, yaml.stringify(eventLog), 'utf8');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// Create subscriptions.yaml if not exists
|
|
59
|
-
if (!await fs.pathExists(this.subscriptionsPath)) {
|
|
59
|
+
if (!(await fs.pathExists(this.subscriptionsPath))) {
|
|
60
60
|
const subscriptions = {
|
|
61
61
|
version: 1,
|
|
62
|
-
subscriptions: {}
|
|
62
|
+
subscriptions: {},
|
|
63
63
|
};
|
|
64
64
|
await fs.writeFile(this.subscriptionsPath, yaml.stringify(subscriptions), 'utf8');
|
|
65
65
|
}
|
|
@@ -88,7 +88,7 @@ class EventLogger {
|
|
|
88
88
|
type,
|
|
89
89
|
scope: scopeId,
|
|
90
90
|
timestamp: new Date().toISOString(),
|
|
91
|
-
data
|
|
91
|
+
data,
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
return this.stateLock.withLock(this.eventLogPath, async () => {
|
|
@@ -123,23 +123,23 @@ class EventLogger {
|
|
|
123
123
|
|
|
124
124
|
// Filter by scope
|
|
125
125
|
if (scopeId) {
|
|
126
|
-
events = events.filter(e => e.scope === scopeId);
|
|
126
|
+
events = events.filter((e) => e.scope === scopeId);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// Filter by type
|
|
130
130
|
if (options.type) {
|
|
131
|
-
events = events.filter(e => e.type === options.type);
|
|
131
|
+
events = events.filter((e) => e.type === options.type);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
// Filter by time range
|
|
135
135
|
if (options.since) {
|
|
136
136
|
const sinceDate = new Date(options.since);
|
|
137
|
-
events = events.filter(e => new Date(e.timestamp) >= sinceDate);
|
|
137
|
+
events = events.filter((e) => new Date(e.timestamp) >= sinceDate);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
if (options.until) {
|
|
141
141
|
const untilDate = new Date(options.until);
|
|
142
|
-
events = events.filter(e => new Date(e.timestamp) <= untilDate);
|
|
142
|
+
events = events.filter((e) => new Date(e.timestamp) <= untilDate);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// Limit results
|
|
@@ -169,21 +169,19 @@ class EventLogger {
|
|
|
169
169
|
if (!subs.subscriptions[subscriberScope]) {
|
|
170
170
|
subs.subscriptions[subscriberScope] = {
|
|
171
171
|
watch: [],
|
|
172
|
-
notify: true
|
|
172
|
+
notify: true,
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
// Add or update watch entry
|
|
177
|
-
const existingWatch = subs.subscriptions[subscriberScope].watch.find(
|
|
178
|
-
w => w.scope === watchScope
|
|
179
|
-
);
|
|
177
|
+
const existingWatch = subs.subscriptions[subscriberScope].watch.find((w) => w.scope === watchScope);
|
|
180
178
|
|
|
181
179
|
if (existingWatch) {
|
|
182
180
|
existingWatch.patterns = patterns;
|
|
183
181
|
} else {
|
|
184
182
|
subs.subscriptions[subscriberScope].watch.push({
|
|
185
183
|
scope: watchScope,
|
|
186
|
-
patterns
|
|
184
|
+
patterns,
|
|
187
185
|
});
|
|
188
186
|
}
|
|
189
187
|
|
|
@@ -206,8 +204,7 @@ class EventLogger {
|
|
|
206
204
|
const subs = yaml.parse(content);
|
|
207
205
|
|
|
208
206
|
if (subs.subscriptions[subscriberScope]) {
|
|
209
|
-
subs.subscriptions[subscriberScope].watch =
|
|
210
|
-
subs.subscriptions[subscriberScope].watch.filter(w => w.scope !== watchScope);
|
|
207
|
+
subs.subscriptions[subscriberScope].watch = subs.subscriptions[subscriberScope].watch.filter((w) => w.scope !== watchScope);
|
|
211
208
|
}
|
|
212
209
|
|
|
213
210
|
await fs.writeFile(this.subscriptionsPath, yaml.stringify(subs), 'utf8');
|
|
@@ -239,38 +236,34 @@ class EventLogger {
|
|
|
239
236
|
async getPendingNotifications(scopeId, since = null) {
|
|
240
237
|
try {
|
|
241
238
|
const subs = await this.getSubscriptions(scopeId);
|
|
242
|
-
|
|
239
|
+
|
|
243
240
|
if (!subs.notify || subs.watch.length === 0) {
|
|
244
241
|
return [];
|
|
245
242
|
}
|
|
246
243
|
|
|
247
244
|
const notifications = [];
|
|
248
|
-
|
|
245
|
+
|
|
249
246
|
for (const watch of subs.watch) {
|
|
250
247
|
const events = await this.getEvents(watch.scope, {
|
|
251
|
-
since: since || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() // Last 24h default
|
|
248
|
+
since: since || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // Last 24h default
|
|
252
249
|
});
|
|
253
250
|
|
|
254
251
|
for (const event of events) {
|
|
255
252
|
// Check if event matches any pattern
|
|
256
|
-
const matches = watch.patterns.some(pattern =>
|
|
257
|
-
this.matchesPattern(event.data?.artifact, pattern)
|
|
258
|
-
);
|
|
253
|
+
const matches = watch.patterns.some((pattern) => this.matchesPattern(event.data?.artifact, pattern));
|
|
259
254
|
|
|
260
255
|
if (matches || watch.patterns.includes('*')) {
|
|
261
256
|
notifications.push({
|
|
262
257
|
...event,
|
|
263
258
|
watchedBy: scopeId,
|
|
264
|
-
pattern: watch.patterns
|
|
259
|
+
pattern: watch.patterns,
|
|
265
260
|
});
|
|
266
261
|
}
|
|
267
262
|
}
|
|
268
263
|
}
|
|
269
264
|
|
|
270
265
|
// Sort by timestamp
|
|
271
|
-
notifications.sort((a, b) =>
|
|
272
|
-
new Date(a.timestamp) - new Date(b.timestamp)
|
|
273
|
-
);
|
|
266
|
+
notifications.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
274
267
|
|
|
275
268
|
return notifications;
|
|
276
269
|
} catch {
|
|
@@ -287,10 +280,8 @@ class EventLogger {
|
|
|
287
280
|
matchesPattern(artifact, pattern) {
|
|
288
281
|
if (!artifact) return false;
|
|
289
282
|
if (pattern === '*') return true;
|
|
290
|
-
|
|
291
|
-
const regexPattern = pattern
|
|
292
|
-
.replaceAll('.', String.raw`\.`)
|
|
293
|
-
.replaceAll('*', '.*');
|
|
283
|
+
|
|
284
|
+
const regexPattern = pattern.replaceAll('.', String.raw`\.`).replaceAll('*', '.*');
|
|
294
285
|
const regex = new RegExp(regexPattern);
|
|
295
286
|
return regex.test(artifact);
|
|
296
287
|
}
|
|
@@ -309,7 +300,7 @@ class EventLogger {
|
|
|
309
300
|
SYNC_UP: 'sync_up',
|
|
310
301
|
SYNC_DOWN: 'sync_down',
|
|
311
302
|
WORKFLOW_STARTED: 'workflow_started',
|
|
312
|
-
WORKFLOW_COMPLETED: 'workflow_completed'
|
|
303
|
+
WORKFLOW_COMPLETED: 'workflow_completed',
|
|
313
304
|
};
|
|
314
305
|
|
|
315
306
|
/**
|
|
@@ -321,7 +312,7 @@ class EventLogger {
|
|
|
321
312
|
async logArtifactCreated(scopeId, artifact, metadata = {}) {
|
|
322
313
|
return this.logEvent(EventLogger.EventTypes.ARTIFACT_CREATED, scopeId, {
|
|
323
314
|
artifact,
|
|
324
|
-
...metadata
|
|
315
|
+
...metadata,
|
|
325
316
|
});
|
|
326
317
|
}
|
|
327
318
|
|
|
@@ -334,7 +325,7 @@ class EventLogger {
|
|
|
334
325
|
async logArtifactUpdated(scopeId, artifact, metadata = {}) {
|
|
335
326
|
return this.logEvent(EventLogger.EventTypes.ARTIFACT_UPDATED, scopeId, {
|
|
336
327
|
artifact,
|
|
337
|
-
...metadata
|
|
328
|
+
...metadata,
|
|
338
329
|
});
|
|
339
330
|
}
|
|
340
331
|
|
|
@@ -347,7 +338,7 @@ class EventLogger {
|
|
|
347
338
|
async logArtifactPromoted(scopeId, artifact, sharedPath) {
|
|
348
339
|
return this.logEvent(EventLogger.EventTypes.ARTIFACT_PROMOTED, scopeId, {
|
|
349
340
|
artifact,
|
|
350
|
-
shared_path: sharedPath
|
|
341
|
+
shared_path: sharedPath,
|
|
351
342
|
});
|
|
352
343
|
}
|
|
353
344
|
|
|
@@ -358,14 +349,12 @@ class EventLogger {
|
|
|
358
349
|
* @param {object} result - Sync result
|
|
359
350
|
*/
|
|
360
351
|
async logSync(type, scopeId, result) {
|
|
361
|
-
const eventType = type === 'up'
|
|
362
|
-
|
|
363
|
-
: EventLogger.EventTypes.SYNC_DOWN;
|
|
364
|
-
|
|
352
|
+
const eventType = type === 'up' ? EventLogger.EventTypes.SYNC_UP : EventLogger.EventTypes.SYNC_DOWN;
|
|
353
|
+
|
|
365
354
|
return this.logEvent(eventType, scopeId, {
|
|
366
355
|
files_count: result.promoted?.length || result.pulled?.length || 0,
|
|
367
356
|
conflicts_count: result.conflicts?.length || 0,
|
|
368
|
-
errors_count: result.errors?.length || 0
|
|
357
|
+
errors_count: result.errors?.length || 0,
|
|
369
358
|
});
|
|
370
359
|
}
|
|
371
360
|
|
|
@@ -376,13 +365,13 @@ class EventLogger {
|
|
|
376
365
|
*/
|
|
377
366
|
async getStats(scopeId = null) {
|
|
378
367
|
const events = await this.getEvents(scopeId);
|
|
379
|
-
|
|
368
|
+
|
|
380
369
|
const stats = {
|
|
381
370
|
total: events.length,
|
|
382
371
|
byType: {},
|
|
383
372
|
byScope: {},
|
|
384
373
|
last24h: 0,
|
|
385
|
-
lastEvent: null
|
|
374
|
+
lastEvent: null,
|
|
386
375
|
};
|
|
387
376
|
|
|
388
377
|
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
@@ -390,10 +379,10 @@ class EventLogger {
|
|
|
390
379
|
for (const event of events) {
|
|
391
380
|
// Count by type
|
|
392
381
|
stats.byType[event.type] = (stats.byType[event.type] || 0) + 1;
|
|
393
|
-
|
|
382
|
+
|
|
394
383
|
// Count by scope
|
|
395
384
|
stats.byScope[event.scope] = (stats.byScope[event.scope] || 0) + 1;
|
|
396
|
-
|
|
385
|
+
|
|
397
386
|
// Count recent
|
|
398
387
|
if (new Date(event.timestamp) >= oneDayAgo) {
|
|
399
388
|
stats.last24h++;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scope Management Module
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Provides multi-scope parallel artifact system functionality
|
|
5
5
|
* for isolated development workflows.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* @module scope
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -26,5 +26,5 @@ module.exports = {
|
|
|
26
26
|
ArtifactResolver,
|
|
27
27
|
StateLock,
|
|
28
28
|
ScopeSync,
|
|
29
|
-
EventLogger
|
|
29
|
+
EventLogger,
|
|
30
30
|
};
|
|
@@ -5,11 +5,11 @@ const yaml = require('yaml');
|
|
|
5
5
|
/**
|
|
6
6
|
* Manages session-sticky scope context
|
|
7
7
|
* Tracks the current active scope for workflows and agents
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* @class ScopeContext
|
|
10
10
|
* @requires fs-extra
|
|
11
11
|
* @requires yaml
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* @example
|
|
14
14
|
* const context = new ScopeContext({ projectRoot: '/path/to/project' });
|
|
15
15
|
* await context.setScope('auth');
|
|
@@ -40,13 +40,13 @@ class ScopeContext {
|
|
|
40
40
|
*/
|
|
41
41
|
async getCurrentScope() {
|
|
42
42
|
try {
|
|
43
|
-
if (!await fs.pathExists(this.contextFilePath)) {
|
|
43
|
+
if (!(await fs.pathExists(this.contextFilePath))) {
|
|
44
44
|
return null;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const content = await fs.readFile(this.contextFilePath, 'utf8');
|
|
48
48
|
const context = yaml.parse(content);
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
return context?.active_scope || null;
|
|
51
51
|
} catch {
|
|
52
52
|
return null;
|
|
@@ -63,7 +63,7 @@ class ScopeContext {
|
|
|
63
63
|
const context = {
|
|
64
64
|
active_scope: scopeId,
|
|
65
65
|
set_at: new Date().toISOString(),
|
|
66
|
-
set_by: process.env.USER || 'unknown'
|
|
66
|
+
set_by: process.env.USER || 'unknown',
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
await fs.writeFile(this.contextFilePath, yaml.stringify(context), 'utf8');
|
|
@@ -94,7 +94,7 @@ class ScopeContext {
|
|
|
94
94
|
*/
|
|
95
95
|
async getContext() {
|
|
96
96
|
try {
|
|
97
|
-
if (!await fs.pathExists(this.contextFilePath)) {
|
|
97
|
+
if (!(await fs.pathExists(this.contextFilePath))) {
|
|
98
98
|
return null;
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -121,11 +121,11 @@ class ScopeContext {
|
|
|
121
121
|
* @returns {Promise<object>} Merged context object
|
|
122
122
|
*/
|
|
123
123
|
async loadProjectContext(scopeId = null) {
|
|
124
|
-
const scope = scopeId || await this.getCurrentScope();
|
|
124
|
+
const scope = scopeId || (await this.getCurrentScope());
|
|
125
125
|
const context = {
|
|
126
126
|
global: null,
|
|
127
127
|
scope: null,
|
|
128
|
-
merged: ''
|
|
128
|
+
merged: '',
|
|
129
129
|
};
|
|
130
130
|
|
|
131
131
|
try {
|
|
@@ -137,12 +137,7 @@ class ScopeContext {
|
|
|
137
137
|
|
|
138
138
|
// Load scope-specific context if scope is set
|
|
139
139
|
if (scope) {
|
|
140
|
-
const scopeContextPath = path.join(
|
|
141
|
-
this.projectRoot,
|
|
142
|
-
this.outputBase,
|
|
143
|
-
scope,
|
|
144
|
-
'project-context.md'
|
|
145
|
-
);
|
|
140
|
+
const scopeContextPath = path.join(this.projectRoot, this.outputBase, scope, 'project-context.md');
|
|
146
141
|
if (await fs.pathExists(scopeContextPath)) {
|
|
147
142
|
context.scope = await fs.readFile(scopeContextPath, 'utf8');
|
|
148
143
|
}
|
|
@@ -156,7 +151,6 @@ class ScopeContext {
|
|
|
156
151
|
} else if (context.scope) {
|
|
157
152
|
context.merged = context.scope;
|
|
158
153
|
}
|
|
159
|
-
|
|
160
154
|
} catch (error) {
|
|
161
155
|
throw new Error(`Failed to load project context: ${error.message}`);
|
|
162
156
|
}
|
|
@@ -204,26 +198,26 @@ class ScopeContext {
|
|
|
204
198
|
* @returns {Promise<object>} Variables object
|
|
205
199
|
*/
|
|
206
200
|
async getScopeVariables(scopeId) {
|
|
207
|
-
const scope = scopeId || await this.getCurrentScope();
|
|
208
|
-
|
|
201
|
+
const scope = scopeId || (await this.getCurrentScope());
|
|
202
|
+
|
|
209
203
|
if (!scope) {
|
|
210
204
|
return {
|
|
211
205
|
scope: '',
|
|
212
206
|
scope_path: '',
|
|
213
207
|
scope_planning: '',
|
|
214
208
|
scope_implementation: '',
|
|
215
|
-
scope_tests: ''
|
|
209
|
+
scope_tests: '',
|
|
216
210
|
};
|
|
217
211
|
}
|
|
218
212
|
|
|
219
213
|
const basePath = path.join(this.outputBase, scope);
|
|
220
|
-
|
|
214
|
+
|
|
221
215
|
return {
|
|
222
216
|
scope: scope,
|
|
223
217
|
scope_path: basePath,
|
|
224
218
|
scope_planning: path.join(basePath, 'planning-artifacts'),
|
|
225
219
|
scope_implementation: path.join(basePath, 'implementation-artifacts'),
|
|
226
|
-
scope_tests: path.join(basePath, 'tests')
|
|
220
|
+
scope_tests: path.join(basePath, 'tests'),
|
|
227
221
|
};
|
|
228
222
|
}
|
|
229
223
|
|
|
@@ -234,8 +228,8 @@ class ScopeContext {
|
|
|
234
228
|
* @returns {Promise<string>} Context snippet
|
|
235
229
|
*/
|
|
236
230
|
async createContextSnippet(scopeId) {
|
|
237
|
-
const scope = scopeId || await this.getCurrentScope();
|
|
238
|
-
|
|
231
|
+
const scope = scopeId || (await this.getCurrentScope());
|
|
232
|
+
|
|
239
233
|
if (!scope) {
|
|
240
234
|
return '<!-- No scope context active -->';
|
|
241
235
|
}
|
|
@@ -264,14 +258,14 @@ ${context.merged || 'No project context loaded.'}
|
|
|
264
258
|
* @returns {Promise<string>} Shell export statements
|
|
265
259
|
*/
|
|
266
260
|
async exportForShell(scopeId) {
|
|
267
|
-
const scope = scopeId || await this.getCurrentScope();
|
|
268
|
-
|
|
261
|
+
const scope = scopeId || (await this.getCurrentScope());
|
|
262
|
+
|
|
269
263
|
if (!scope) {
|
|
270
264
|
return '# No scope set';
|
|
271
265
|
}
|
|
272
266
|
|
|
273
267
|
const vars = await this.getScopeVariables(scope);
|
|
274
|
-
|
|
268
|
+
|
|
275
269
|
return `
|
|
276
270
|
export BMAD_SCOPE="${vars.scope}"
|
|
277
271
|
export BMAD_SCOPE_PATH="${vars.scope_path}"
|
|
@@ -288,12 +282,12 @@ export BMAD_SCOPE_TESTS="${vars.scope_tests}"
|
|
|
288
282
|
*/
|
|
289
283
|
async updateMetadata(metadata) {
|
|
290
284
|
try {
|
|
291
|
-
const context = await this.getContext() || {};
|
|
292
|
-
|
|
285
|
+
const context = (await this.getContext()) || {};
|
|
286
|
+
|
|
293
287
|
const updated = {
|
|
294
288
|
...context,
|
|
295
289
|
...metadata,
|
|
296
|
-
updated_at: new Date().toISOString()
|
|
290
|
+
updated_at: new Date().toISOString(),
|
|
297
291
|
};
|
|
298
292
|
|
|
299
293
|
await fs.writeFile(this.contextFilePath, yaml.stringify(updated), 'utf8');
|