scene-capability-engine 3.6.39 → 3.6.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/scene-capability-engine.js +78 -4
  3. package/docs/command-reference.md +5 -0
  4. package/docs/developer-guide.md +1 -1
  5. package/docs/releases/README.md +6 -0
  6. package/docs/releases/v3.6.40.md +19 -0
  7. package/docs/releases/v3.6.41.md +20 -0
  8. package/docs/releases/v3.6.42.md +19 -0
  9. package/docs/releases/v3.6.43.md +17 -0
  10. package/docs/releases/v3.6.44.md +17 -0
  11. package/docs/releases/v3.6.45.md +18 -0
  12. package/docs/spec-collaboration-guide.md +1 -1
  13. package/docs/zh/releases/README.md +6 -0
  14. package/docs/zh/releases/v3.6.40.md +19 -0
  15. package/docs/zh/releases/v3.6.41.md +20 -0
  16. package/docs/zh/releases/v3.6.42.md +19 -0
  17. package/docs/zh/releases/v3.6.43.md +17 -0
  18. package/docs/zh/releases/v3.6.44.md +17 -0
  19. package/docs/zh/releases/v3.6.45.md +18 -0
  20. package/lib/adoption/adoption-logger.js +1 -1
  21. package/lib/adoption/adoption-strategy.js +29 -29
  22. package/lib/adoption/detection-engine.js +16 -13
  23. package/lib/adoption/smart-orchestrator.js +3 -3
  24. package/lib/adoption/strategy-selector.js +19 -15
  25. package/lib/adoption/template-sync.js +3 -3
  26. package/lib/auto/autonomous-engine.js +5 -5
  27. package/lib/auto/handoff-release-gate-history-loaders-service.js +24 -4
  28. package/lib/auto/handoff-run-service.js +37 -0
  29. package/lib/backup/backup-system.js +10 -10
  30. package/lib/collab/collab-manager.js +8 -5
  31. package/lib/collab/dependency-manager.js +1 -1
  32. package/lib/commands/adopt.js +2 -2
  33. package/lib/commands/auto.js +239 -97
  34. package/lib/commands/collab.js +10 -4
  35. package/lib/commands/status.js +3 -3
  36. package/lib/commands/studio.js +8 -0
  37. package/lib/repo/config-manager.js +2 -2
  38. package/lib/spec/bootstrap/context-collector.js +5 -4
  39. package/lib/spec-gate/rules/default-rules.js +8 -8
  40. package/lib/upgrade/migration-engine.js +5 -5
  41. package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +3 -3
  42. package/lib/utils/tool-detector.js +4 -4
  43. package/lib/utils/validation.js +6 -6
  44. package/lib/workspace/collab-governance-audit.js +575 -0
  45. package/lib/workspace/multi/workspace-context-resolver.js +3 -3
  46. package/lib/workspace/multi/workspace-registry.js +3 -3
  47. package/lib/workspace/multi/workspace-state-manager.js +3 -3
  48. package/lib/workspace/spec-delivery-audit.js +553 -0
  49. package/package.json +1 -1
@@ -300,10 +300,10 @@ class MigrationEngine {
300
300
 
301
301
  try {
302
302
  // Check if .sce/ directory exists
303
- const kiroPath = path.join(projectPath, '.sce');
304
- const kiroExists = await pathExists(kiroPath);
303
+ const scePath = path.join(projectPath, '.sce');
304
+ const sceExists = await pathExists(scePath);
305
305
 
306
- if (!kiroExists) {
306
+ if (!sceExists) {
307
307
  errors.push('.sce/ directory not found');
308
308
  return { success: false, errors, warnings };
309
309
  }
@@ -320,7 +320,7 @@ class MigrationEngine {
320
320
  const requiredDirs = ['specs', 'steering', 'tools', 'backups'];
321
321
 
322
322
  for (const dir of requiredDirs) {
323
- const dirPath = path.join(kiroPath, dir);
323
+ const dirPath = path.join(scePath, dir);
324
324
  const exists = await pathExists(dirPath);
325
325
 
326
326
  if (!exists) {
@@ -337,7 +337,7 @@ class MigrationEngine {
337
337
  ];
338
338
 
339
339
  for (const file of requiredSteeringFiles) {
340
- const filePath = path.join(kiroPath, file);
340
+ const filePath = path.join(scePath, file);
341
341
  const exists = await pathExists(filePath);
342
342
 
343
343
  if (!exists) {
@@ -26,10 +26,10 @@ module.exports = {
26
26
  const changes = [];
27
27
 
28
28
  try {
29
- const kiroPath = path.join(projectPath, '.sce');
29
+ const scePath = path.join(projectPath, '.sce');
30
30
 
31
31
  // 1. Ensure backups/ directory exists
32
- const backupsPath = path.join(kiroPath, 'backups');
32
+ const backupsPath = path.join(scePath, 'backups');
33
33
  const backupsExists = await pathExists(backupsPath);
34
34
 
35
35
  if (!backupsExists) {
@@ -39,7 +39,7 @@ module.exports = {
39
39
 
40
40
  // 2. Ensure version.json has correct structure
41
41
  // (This is handled by VersionManager, but we verify it here)
42
- const versionPath = path.join(kiroPath, 'version.json');
42
+ const versionPath = path.join(scePath, 'version.json');
43
43
  const versionExists = await pathExists(versionPath);
44
44
 
45
45
  if (versionExists) {
@@ -64,21 +64,21 @@ async function detectKiroIDE(projectPath) {
64
64
  let confidence = 'low';
65
65
 
66
66
  // Check for .sce directory
67
- const kiroDir = path.join(projectPath, '.sce');
68
- if (await fs.pathExists(kiroDir)) {
67
+ const sceDir = path.join(projectPath, '.sce');
68
+ if (await fs.pathExists(sceDir)) {
69
69
  indicators.push('.sce directory exists');
70
70
  detected = true;
71
71
  confidence = 'medium';
72
72
  }
73
73
 
74
74
  // Check for SCE-specific files
75
- const kiroFiles = [
75
+ const sceFiles = [
76
76
  '.sce/steering',
77
77
  '.sce/specs',
78
78
  '.sce/tools'
79
79
  ];
80
80
 
81
- for (const file of kiroFiles) {
81
+ for (const file of sceFiles) {
82
82
  const filePath = path.join(projectPath, file);
83
83
  if (await fs.pathExists(filePath)) {
84
84
  indicators.push(`${file} exists`);
@@ -21,11 +21,11 @@ async function validateProjectStructure(projectPath) {
21
21
  const warnings = [];
22
22
 
23
23
  try {
24
- const kiroPath = path.join(projectPath, '.sce');
24
+ const scePath = path.join(projectPath, '.sce');
25
25
 
26
26
  // Check if .sce/ directory exists
27
- const kiroExists = await pathExists(kiroPath);
28
- if (!kiroExists) {
27
+ const sceExists = await pathExists(scePath);
28
+ if (!sceExists) {
29
29
  errors.push('.sce/ directory not found');
30
30
  return { success: false, errors, warnings };
31
31
  }
@@ -39,7 +39,7 @@ async function validateProjectStructure(projectPath) {
39
39
  ];
40
40
 
41
41
  for (const dir of requiredDirs) {
42
- const dirPath = path.join(kiroPath, dir.path);
42
+ const dirPath = path.join(scePath, dir.path);
43
43
  const exists = await pathExists(dirPath);
44
44
 
45
45
  if (!exists) {
@@ -60,7 +60,7 @@ async function validateProjectStructure(projectPath) {
60
60
  ];
61
61
 
62
62
  for (const file of requiredSteeringFiles) {
63
- const filePath = path.join(kiroPath, file.path);
63
+ const filePath = path.join(scePath, file.path);
64
64
  const exists = await pathExists(filePath);
65
65
 
66
66
  if (!exists) {
@@ -78,7 +78,7 @@ async function validateProjectStructure(projectPath) {
78
78
  ];
79
79
 
80
80
  for (const file of requiredToolFiles) {
81
- const filePath = path.join(kiroPath, file.path);
81
+ const filePath = path.join(scePath, file.path);
82
82
  const exists = await pathExists(filePath);
83
83
 
84
84
  if (!exists) {
@@ -0,0 +1,575 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const { minimatch } = require('minimatch');
6
+ const { loadGitSnapshot } = require('./spec-delivery-audit');
7
+ const SteeringComplianceChecker = require('../steering/steering-compliance-checker');
8
+
9
+ const MAX_SCAN_BYTES = 256 * 1024;
10
+ const ACTIVE_TEXT_EXTENSIONS = new Set([
11
+ '.cjs',
12
+ '.js',
13
+ '.json',
14
+ '.jsx',
15
+ '.md',
16
+ '.mjs',
17
+ '.ps1',
18
+ '.sh',
19
+ '.ts',
20
+ '.tsx',
21
+ '.txt',
22
+ '.yaml',
23
+ '.yml'
24
+ ]);
25
+
26
+ const ACTIVE_TEXT_SCAN_ROOTS = Object.freeze([
27
+ '.github',
28
+ 'bin',
29
+ 'docs',
30
+ 'lib',
31
+ 'README.md',
32
+ 'README.zh.md',
33
+ 'scripts',
34
+ 'template'
35
+ ]);
36
+
37
+ const ACTIVE_TEXT_SCAN_EXCLUDES = Object.freeze([
38
+ '.git/**',
39
+ '.sce/specs/**',
40
+ 'CHANGELOG.md',
41
+ 'coverage/**',
42
+ 'dist/**',
43
+ 'build/**',
44
+ 'docs/handoffs/**',
45
+ 'docs/releases/**',
46
+ 'docs/zh/releases/**',
47
+ 'node_modules/**',
48
+ 'tests/**'
49
+ ]);
50
+
51
+ const LEGACY_REFERENCE_REGEX = /\.kiro(?:[\\/]|-workspaces\b)/;
52
+ const MULTI_AGENT_CONFIG_REFERENCE = '.sce/config/multi-agent.json';
53
+
54
+ const REQUIRED_GITIGNORE_RULES = Object.freeze([
55
+ { rule: '.sce/steering/CURRENT_CONTEXT.md', sample: '.sce/steering/CURRENT_CONTEXT.md' },
56
+ { rule: '.sce/contexts/.active', sample: '.sce/contexts/.active' },
57
+ { rule: '.sce/contexts/*/CURRENT_CONTEXT.md', sample: '.sce/contexts/alice/CURRENT_CONTEXT.md' },
58
+ { rule: '.sce/config/agent-registry.json', sample: '.sce/config/agent-registry.json' },
59
+ { rule: '.sce/config/coordination-log.json', sample: '.sce/config/coordination-log.json' },
60
+ { rule: '.sce/config/machine-id.json', sample: '.sce/config/machine-id.json' },
61
+ { rule: '.sce/specs/**/.lock', sample: '.sce/specs/demo/.lock' },
62
+ { rule: '.sce/specs/**/locks/', sample: '.sce/specs/demo/locks/1.1.lock' },
63
+ { rule: '.sce/specs/**/tasks.md.lock', sample: '.sce/specs/demo/tasks.md.lock' },
64
+ { rule: '.sce/steering/*.lock', sample: '.sce/steering/CURRENT_CONTEXT.md.lock' },
65
+ { rule: '.sce/steering/*.pending.*', sample: '.sce/steering/CURRENT_CONTEXT.md.pending.agent-1' }
66
+ ]);
67
+
68
+ const RUNTIME_TRACKED_PATTERNS = Object.freeze([
69
+ '.sce/steering/CURRENT_CONTEXT.md',
70
+ '.sce/contexts/.active',
71
+ '.sce/contexts/*/CURRENT_CONTEXT.md',
72
+ '.sce/config/agent-registry.json',
73
+ '.sce/config/coordination-log.json',
74
+ '.sce/config/machine-id.json',
75
+ '.sce/specs/**/.lock',
76
+ '.sce/specs/**/locks/**',
77
+ '.sce/specs/**/tasks.md.lock',
78
+ '.sce/steering/*.lock',
79
+ '.sce/steering/*.pending.*'
80
+ ]);
81
+
82
+ function toRelativePosix(projectRoot, targetPath) {
83
+ return path.relative(projectRoot, targetPath).replace(/\\/g, '/');
84
+ }
85
+
86
+ function normalizeRelativePath(projectRoot, candidate) {
87
+ if (typeof candidate !== 'string' || !candidate.trim()) {
88
+ return null;
89
+ }
90
+
91
+ const absolutePath = path.isAbsolute(candidate)
92
+ ? candidate
93
+ : path.join(projectRoot, candidate);
94
+ const relativePath = toRelativePosix(projectRoot, absolutePath);
95
+ if (!relativePath || relativePath.startsWith('..')) {
96
+ return null;
97
+ }
98
+ return relativePath;
99
+ }
100
+
101
+ function matchesAnyPattern(candidate, patterns) {
102
+ return patterns.some((pattern) => minimatch(candidate, pattern, { dot: true }));
103
+ }
104
+
105
+ function shouldExcludeFromActiveScan(relativePath) {
106
+ return matchesAnyPattern(relativePath, ACTIVE_TEXT_SCAN_EXCLUDES);
107
+ }
108
+
109
+ function isActiveTextCandidate(relativePath) {
110
+ if (!relativePath || shouldExcludeFromActiveScan(relativePath)) {
111
+ return false;
112
+ }
113
+
114
+ const normalized = `${relativePath}`.replace(/\\/g, '/');
115
+ const exactRoot = ACTIVE_TEXT_SCAN_ROOTS.find((item) => !item.includes('/') && item === normalized);
116
+ if (exactRoot) {
117
+ return true;
118
+ }
119
+
120
+ const underRoot = ACTIVE_TEXT_SCAN_ROOTS.some((root) => {
121
+ if (!root.includes('/')) {
122
+ return normalized.startsWith(`${root}/`);
123
+ }
124
+ return normalized === root || normalized.startsWith(`${root}/`);
125
+ });
126
+ if (!underRoot) {
127
+ return false;
128
+ }
129
+
130
+ return ACTIVE_TEXT_EXTENSIONS.has(path.extname(normalized).toLowerCase());
131
+ }
132
+
133
+ function parseGitignore(content) {
134
+ return `${content || ''}`
135
+ .split(/\r?\n/)
136
+ .map((line) => line.trim())
137
+ .filter((line) => line && !line.startsWith('#') && !line.startsWith('!'));
138
+ }
139
+
140
+ function gitignorePatternMatchesSample(pattern, sample) {
141
+ const normalizedPattern = `${pattern || ''}`.replace(/\\/g, '/').trim();
142
+ const normalizedSample = `${sample || ''}`.replace(/\\/g, '/').trim();
143
+ if (!normalizedPattern || !normalizedSample) {
144
+ return false;
145
+ }
146
+
147
+ if (normalizedPattern === normalizedSample) {
148
+ return true;
149
+ }
150
+
151
+ const matchPattern = normalizedPattern.endsWith('/')
152
+ ? `${normalizedPattern}**`
153
+ : normalizedPattern;
154
+ return minimatch(normalizedSample, matchPattern, { dot: true });
155
+ }
156
+
157
+ async function collectCandidateTextFiles(projectRoot, trackedFiles, fileSystem) {
158
+ const results = [];
159
+ const visited = new Set();
160
+
161
+ if (trackedFiles instanceof Set && trackedFiles.size > 0) {
162
+ for (const relativePath of trackedFiles) {
163
+ if (!isActiveTextCandidate(relativePath) || visited.has(relativePath)) {
164
+ continue;
165
+ }
166
+ visited.add(relativePath);
167
+ results.push(relativePath);
168
+ }
169
+ }
170
+
171
+ async function walk(relativePath) {
172
+ const absolutePath = path.join(projectRoot, relativePath);
173
+ let stats;
174
+ try {
175
+ stats = await fileSystem.stat(absolutePath);
176
+ } catch (_error) {
177
+ return;
178
+ }
179
+
180
+ if (stats.isDirectory()) {
181
+ let entries = [];
182
+ try {
183
+ entries = await fileSystem.readdir(absolutePath, { withFileTypes: true });
184
+ } catch (_error) {
185
+ return;
186
+ }
187
+ for (const entry of entries) {
188
+ const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
189
+ if (shouldExcludeFromActiveScan(childRelative)) {
190
+ continue;
191
+ }
192
+ await walk(childRelative);
193
+ }
194
+ return;
195
+ }
196
+
197
+ if (!stats.isFile()) {
198
+ return;
199
+ }
200
+
201
+ if (!isActiveTextCandidate(relativePath) || visited.has(relativePath)) {
202
+ return;
203
+ }
204
+ visited.add(relativePath);
205
+ results.push(relativePath);
206
+ }
207
+
208
+ for (const root of ACTIVE_TEXT_SCAN_ROOTS) {
209
+ const normalized = normalizeRelativePath(projectRoot, root);
210
+ if (!normalized) {
211
+ continue;
212
+ }
213
+ await walk(normalized);
214
+ }
215
+
216
+ return results.sort();
217
+ }
218
+
219
+ function summarizeLine(line) {
220
+ const trimmed = `${line || ''}`.trim();
221
+ if (trimmed.length <= 160) {
222
+ return trimmed;
223
+ }
224
+ return `${trimmed.slice(0, 157)}...`;
225
+ }
226
+
227
+ async function scanActiveTextReferences(projectRoot, trackedFiles, options = {}, dependencies = {}) {
228
+ const fileSystem = dependencies.fileSystem || fs;
229
+ const candidateFiles = Array.isArray(options.candidateFiles)
230
+ ? options.candidateFiles
231
+ : await collectCandidateTextFiles(projectRoot, trackedFiles, fileSystem);
232
+
233
+ const legacyMatches = [];
234
+ const multiAgentConfigReferences = [];
235
+
236
+ for (const relativePath of candidateFiles) {
237
+ const absolutePath = path.join(projectRoot, relativePath);
238
+ let stats;
239
+ try {
240
+ stats = await fileSystem.stat(absolutePath);
241
+ } catch (_error) {
242
+ continue;
243
+ }
244
+ if (!stats.isFile() || stats.size > MAX_SCAN_BYTES) {
245
+ continue;
246
+ }
247
+
248
+ let content;
249
+ try {
250
+ content = await fileSystem.readFile(absolutePath, 'utf8');
251
+ } catch (_error) {
252
+ continue;
253
+ }
254
+
255
+ const lines = content.split(/\r?\n/);
256
+ for (let index = 0; index < lines.length; index += 1) {
257
+ const line = lines[index];
258
+ if (LEGACY_REFERENCE_REGEX.test(line)) {
259
+ legacyMatches.push({
260
+ path: relativePath,
261
+ line: index + 1,
262
+ snippet: summarizeLine(line)
263
+ });
264
+ }
265
+ if (line.includes(MULTI_AGENT_CONFIG_REFERENCE)) {
266
+ multiAgentConfigReferences.push({
267
+ path: relativePath,
268
+ line: index + 1,
269
+ snippet: summarizeLine(line)
270
+ });
271
+ }
272
+ }
273
+ }
274
+
275
+ return {
276
+ candidate_files: candidateFiles,
277
+ legacy_matches: legacyMatches,
278
+ multi_agent_config_references: multiAgentConfigReferences
279
+ };
280
+ }
281
+
282
+ async function inspectGitignore(projectRoot, options = {}, dependencies = {}) {
283
+ const fileSystem = dependencies.fileSystem || fs;
284
+ const gitignorePath = path.join(projectRoot, '.gitignore');
285
+ const exists = await fileSystem.pathExists(gitignorePath);
286
+ const report = {
287
+ file: '.gitignore',
288
+ exists,
289
+ required_rules: REQUIRED_GITIGNORE_RULES.map((item) => item.rule),
290
+ missing_rules: [],
291
+ warnings: [],
292
+ violations: [],
293
+ passed: true,
294
+ reason: 'passed'
295
+ };
296
+
297
+ if (!exists) {
298
+ report.passed = false;
299
+ report.reason = 'missing-gitignore';
300
+ report.violations.push('missing .gitignore');
301
+ return report;
302
+ }
303
+
304
+ const patterns = parseGitignore(await fileSystem.readFile(gitignorePath, 'utf8'));
305
+ report.missing_rules = REQUIRED_GITIGNORE_RULES
306
+ .filter((rule) => !patterns.some((pattern) => gitignorePatternMatchesSample(pattern, rule.sample)))
307
+ .map((rule) => rule.rule);
308
+
309
+ if (report.missing_rules.length > 0) {
310
+ report.passed = false;
311
+ report.reason = 'missing-rules';
312
+ report.violations.push(...report.missing_rules.map((rule) => `missing ignore rule: ${rule}`));
313
+ }
314
+
315
+ return report;
316
+ }
317
+
318
+ function inspectRuntimeTracking(gitSnapshot) {
319
+ const trackedFiles = gitSnapshot && gitSnapshot.tracked_files instanceof Set
320
+ ? [...gitSnapshot.tracked_files]
321
+ : [];
322
+ const trackedRuntimeFiles = trackedFiles
323
+ .filter((relativePath) => matchesAnyPattern(relativePath, RUNTIME_TRACKED_PATTERNS))
324
+ .sort();
325
+
326
+ if (!gitSnapshot || gitSnapshot.available !== true) {
327
+ return {
328
+ available: false,
329
+ tracked_runtime_files: [],
330
+ warnings: ['git repository unavailable; runtime tracking audit is advisory only'],
331
+ violations: [],
332
+ passed: true,
333
+ reason: 'git-unavailable'
334
+ };
335
+ }
336
+
337
+ const passed = trackedRuntimeFiles.length === 0;
338
+ return {
339
+ available: true,
340
+ tracked_runtime_files: trackedRuntimeFiles,
341
+ warnings: [],
342
+ violations: trackedRuntimeFiles.map((item) => `runtime/personal state tracked by git: ${item}`),
343
+ passed,
344
+ reason: passed ? 'passed' : 'tracked-runtime-files'
345
+ };
346
+ }
347
+
348
+ function validateMultiAgentConfig(payload) {
349
+ const violations = [];
350
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
351
+ violations.push('multi-agent config must be a JSON object');
352
+ return violations;
353
+ }
354
+
355
+ if (typeof payload.enabled !== 'boolean') {
356
+ violations.push('multi-agent config must declare boolean field "enabled"');
357
+ }
358
+ if (
359
+ payload.heartbeatIntervalMs !== undefined
360
+ && (!Number.isInteger(payload.heartbeatIntervalMs) || payload.heartbeatIntervalMs <= 0)
361
+ ) {
362
+ violations.push('multi-agent config field "heartbeatIntervalMs" must be a positive integer');
363
+ }
364
+ if (
365
+ payload.heartbeatTimeoutMs !== undefined
366
+ && (!Number.isInteger(payload.heartbeatTimeoutMs) || payload.heartbeatTimeoutMs <= 0)
367
+ ) {
368
+ violations.push('multi-agent config field "heartbeatTimeoutMs" must be a positive integer');
369
+ }
370
+ if (
371
+ Number.isInteger(payload.heartbeatIntervalMs)
372
+ && Number.isInteger(payload.heartbeatTimeoutMs)
373
+ && payload.heartbeatTimeoutMs <= payload.heartbeatIntervalMs
374
+ ) {
375
+ violations.push('"heartbeatTimeoutMs" must be greater than "heartbeatIntervalMs"');
376
+ }
377
+ return violations;
378
+ }
379
+
380
+ async function inspectMultiAgentConfig(projectRoot, scanResult, options = {}, dependencies = {}) {
381
+ const fileSystem = dependencies.fileSystem || fs;
382
+ const configPath = path.join(projectRoot, '.sce', 'config', 'multi-agent.json');
383
+ const exists = await fileSystem.pathExists(configPath);
384
+ const runtimeTracePatterns = [
385
+ '.sce/config/agent-registry.json',
386
+ '.sce/config/coordination-log.json',
387
+ '.sce/specs/**/locks/**',
388
+ '.sce/specs/**/tasks.md.lock',
389
+ '.sce/steering/*.lock',
390
+ '.sce/steering/*.pending.*'
391
+ ];
392
+
393
+ const runtimeTracePaths = [];
394
+ async function walk(relativePath) {
395
+ const absolutePath = path.join(projectRoot, relativePath);
396
+ let entries = [];
397
+ try {
398
+ entries = await fileSystem.readdir(absolutePath, { withFileTypes: true });
399
+ } catch (_error) {
400
+ return;
401
+ }
402
+
403
+ for (const entry of entries) {
404
+ const childRelative = `${relativePath}/${entry.name}`.replace(/\\/g, '/');
405
+ if (entry.isDirectory()) {
406
+ await walk(childRelative);
407
+ } else if (matchesAnyPattern(childRelative, runtimeTracePatterns)) {
408
+ runtimeTracePaths.push(childRelative);
409
+ }
410
+ }
411
+ }
412
+
413
+ for (const root of ['.sce/config', '.sce/specs', '.sce/steering']) {
414
+ if (await fileSystem.pathExists(path.join(projectRoot, root))) {
415
+ await walk(root);
416
+ }
417
+ }
418
+
419
+ const report = {
420
+ file: '.sce/config/multi-agent.json',
421
+ exists,
422
+ valid: false,
423
+ enabled: null,
424
+ runtime_traces: runtimeTracePaths.sort(),
425
+ reference_count: Array.isArray(scanResult?.multi_agent_config_references)
426
+ ? scanResult.multi_agent_config_references.length
427
+ : 0,
428
+ warnings: [],
429
+ violations: [],
430
+ passed: true,
431
+ reason: 'not-configured'
432
+ };
433
+
434
+ if (!exists) {
435
+ if (report.runtime_traces.length > 0) {
436
+ report.passed = false;
437
+ report.reason = 'missing-config';
438
+ report.violations.push('multi-agent runtime traces detected but .sce/config/multi-agent.json is missing');
439
+ } else if (report.reference_count > 0) {
440
+ report.reason = 'missing-config-advisory';
441
+ report.warnings.push('multi-agent config is referenced in active docs/code but project config is not seeded');
442
+ }
443
+ return report;
444
+ }
445
+
446
+ let payload;
447
+ try {
448
+ payload = await fileSystem.readJson(configPath);
449
+ } catch (error) {
450
+ report.passed = false;
451
+ report.reason = 'invalid-json';
452
+ report.violations.push(`invalid multi-agent config: ${error.message}`);
453
+ return report;
454
+ }
455
+
456
+ const validationErrors = validateMultiAgentConfig(payload);
457
+ report.valid = validationErrors.length === 0;
458
+ report.enabled = typeof payload.enabled === 'boolean' ? payload.enabled : null;
459
+ if (validationErrors.length > 0) {
460
+ report.passed = false;
461
+ report.reason = 'invalid-config';
462
+ report.violations.push(...validationErrors);
463
+ return report;
464
+ }
465
+
466
+ report.reason = 'passed';
467
+ return report;
468
+ }
469
+
470
+ function inspectSteeringBoundary(projectRoot) {
471
+ const checker = new SteeringComplianceChecker();
472
+ const steeringPath = path.join(projectRoot, '.sce', 'steering');
473
+ const result = checker.check(steeringPath);
474
+ const violations = Array.isArray(result.violations) ? result.violations : [];
475
+ return {
476
+ path: '.sce/steering',
477
+ exists: fs.existsSync(steeringPath),
478
+ compliant: result.compliant === true,
479
+ violations: violations.map((item) => {
480
+ const relativePath = item.path ? normalizeRelativePath(projectRoot, item.path) : null;
481
+ return {
482
+ type: item.type,
483
+ name: item.name,
484
+ path: relativePath
485
+ };
486
+ }),
487
+ warnings: [],
488
+ passed: result.compliant === true,
489
+ reason: result.compliant === true ? 'passed' : 'boundary-drift'
490
+ };
491
+ }
492
+
493
+ async function auditCollabGovernance(projectRoot = process.cwd(), options = {}, dependencies = {}) {
494
+ const fileSystem = dependencies.fileSystem || fs;
495
+ const gitSnapshot = loadGitSnapshot(projectRoot, { allowNoRemote: true }, dependencies);
496
+ const gitignore = await inspectGitignore(projectRoot, options, dependencies);
497
+ const runtimeTracking = inspectRuntimeTracking(gitSnapshot);
498
+ const scanResult = await scanActiveTextReferences(projectRoot, gitSnapshot.tracked_files, options, dependencies);
499
+ const multiAgent = await inspectMultiAgentConfig(projectRoot, scanResult, options, dependencies);
500
+ const steeringBoundary = inspectSteeringBoundary(projectRoot);
501
+
502
+ const legacyReferences = {
503
+ matches: scanResult.legacy_matches,
504
+ warnings: [],
505
+ violations: scanResult.legacy_matches.map((item) => `legacy .kiro reference: ${item.path}:${item.line}`),
506
+ passed: scanResult.legacy_matches.length === 0,
507
+ reason: scanResult.legacy_matches.length === 0 ? 'passed' : 'legacy-references'
508
+ };
509
+
510
+ const report = {
511
+ mode: 'workspace-collab-governance-audit',
512
+ generated_at: new Date().toISOString(),
513
+ root: projectRoot,
514
+ git: {
515
+ available: gitSnapshot.available === true,
516
+ branch: gitSnapshot.branch,
517
+ upstream: gitSnapshot.upstream,
518
+ has_target_remote: gitSnapshot.has_target_remote === true,
519
+ warnings: gitSnapshot.available === true ? gitSnapshot.warnings : []
520
+ },
521
+ gitignore,
522
+ runtime_tracking: runtimeTracking,
523
+ multi_agent: multiAgent,
524
+ legacy_references: legacyReferences,
525
+ steering_boundary: steeringBoundary,
526
+ summary: {
527
+ missing_gitignore_rules: gitignore.missing_rules.length,
528
+ tracked_runtime_files: runtimeTracking.tracked_runtime_files.length,
529
+ multi_agent_warnings: multiAgent.warnings.length,
530
+ multi_agent_violations: multiAgent.violations.length,
531
+ legacy_reference_count: legacyReferences.matches.length,
532
+ steering_boundary_violations: steeringBoundary.violations.length
533
+ },
534
+ warnings: [],
535
+ violations: [],
536
+ passed: true,
537
+ reason: 'passed'
538
+ };
539
+
540
+ report.warnings.push(...gitignore.warnings);
541
+ report.warnings.push(...(gitSnapshot.available === true ? gitSnapshot.warnings : []));
542
+ report.warnings.push(...runtimeTracking.warnings);
543
+ report.warnings.push(...multiAgent.warnings);
544
+ report.warnings.push(...legacyReferences.warnings);
545
+ report.warnings.push(...steeringBoundary.warnings);
546
+
547
+ report.violations.push(...gitignore.violations);
548
+ report.violations.push(...runtimeTracking.violations);
549
+ report.violations.push(...multiAgent.violations);
550
+ report.violations.push(...legacyReferences.violations);
551
+ report.violations.push(
552
+ ...steeringBoundary.violations.map((item) => `steering boundary violation: ${item.path || item.name}`)
553
+ );
554
+
555
+ report.passed = report.violations.length === 0;
556
+ report.reason = report.passed
557
+ ? (report.warnings.length > 0 ? 'warnings' : 'passed')
558
+ : 'violations';
559
+
560
+ return report;
561
+ }
562
+
563
+ module.exports = {
564
+ ACTIVE_TEXT_SCAN_EXCLUDES,
565
+ ACTIVE_TEXT_SCAN_ROOTS,
566
+ MULTI_AGENT_CONFIG_REFERENCE,
567
+ REQUIRED_GITIGNORE_RULES,
568
+ RUNTIME_TRACKED_PATTERNS,
569
+ auditCollabGovernance,
570
+ inspectGitignore,
571
+ inspectMultiAgentConfig,
572
+ inspectRuntimeTracking,
573
+ scanActiveTextReferences,
574
+ validateMultiAgentConfig
575
+ };