scene-capability-engine 3.6.44 → 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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.6.45] - 2026-03-13
11
+
12
+ ### Added
13
+ - Added collaboration governance auditing via `lib/workspace/collab-governance-audit.js` and the new `sce workspace collab-governance-audit` command to check collaboration Git boundaries, runtime-state tracking drift, multi-agent config presence/validity, legacy `.kiro*` references, and steering boundary hygiene.
14
+ - Added Spec `122-00-sce-collab-governance-audit` with requirements, design, tasks, and a dogfooded delivery manifest to formalize the new co-work governance capability.
15
+ - Added unit/integration coverage for collaboration governance drift scenarios, including missing ignore rules, tracked runtime files, legacy naming references, and strict CLI failure behavior.
16
+
17
+ ### Changed
18
+ - Expanded the repository `.gitignore` collaboration boundary rules to cover coordination logs, machine identity, spec lock directories, `tasks.md.lock`, and steering lock/pending runtime files.
19
+ - Seeded `.sce/config/multi-agent.json` as the canonical project-level multi-agent config baseline for governance auditing and future co-work enablement.
20
+
21
+ ### Fixed
22
+ - Removed `.sce/steering/CURRENT_CONTEXT.md` from Git tracking so the repository now conforms to its own runtime/personal-state governance boundary.
23
+
10
24
  ## [3.6.44] - 2026-03-13
11
25
 
12
26
  ### Fixed
@@ -34,6 +34,7 @@ const {
34
34
  } = require('../lib/workspace/legacy-kiro-migrator');
35
35
  const { auditSceTracking } = require('../lib/workspace/sce-tracking-audit');
36
36
  const { auditSpecDeliverySync } = require('../lib/workspace/spec-delivery-audit');
37
+ const { auditCollabGovernance } = require('../lib/workspace/collab-governance-audit');
37
38
  const { applyTakeoverBaseline } = require('../lib/workspace/takeover-baseline');
38
39
 
39
40
  const i18n = getI18n();
@@ -170,7 +171,9 @@ function isLegacyMigrationAllowlistedCommand(args) {
170
171
 
171
172
  if (command === 'workspace') {
172
173
  const subcommand = args[commandIndex + 1];
173
- return subcommand === 'legacy-scan' || subcommand === 'legacy-migrate';
174
+ return subcommand === 'legacy-scan'
175
+ || subcommand === 'legacy-migrate'
176
+ || subcommand === 'collab-governance-audit';
174
177
  }
175
178
 
176
179
  return false;
@@ -198,7 +201,7 @@ function isTakeoverAutoApplySkippedCommand(args) {
198
201
  }
199
202
 
200
203
  const subcommand = args[commandIndex + 1];
201
- return subcommand === 'takeover-audit';
204
+ return subcommand === 'takeover-audit' || subcommand === 'collab-governance-audit';
202
205
  }
203
206
 
204
207
  /**
@@ -784,6 +787,37 @@ workspaceCmd
784
787
  console.log(chalk.gray(`Conflict files: ${report.conflict_files}`));
785
788
  });
786
789
 
790
+ workspaceCmd
791
+ .command('collab-governance-audit')
792
+ .description('Audit collaboration governance boundaries, runtime git hygiene, and legacy naming drift')
793
+ .option('--json', 'Output in JSON format')
794
+ .option('--strict', 'Exit non-zero when collaboration governance violations are found')
795
+ .action(async (options) => {
796
+ const report = await auditCollabGovernance(process.cwd());
797
+
798
+ if (options.json) {
799
+ console.log(JSON.stringify(report, null, 2));
800
+ } else if (report.passed) {
801
+ console.log(chalk.green('✓ Collaboration governance audit passed.'));
802
+ console.log(chalk.gray(`Missing ignore rules: ${report.summary.missing_gitignore_rules}`));
803
+ console.log(chalk.gray(`Legacy references: ${report.summary.legacy_reference_count}`));
804
+ if (report.warnings.length > 0) {
805
+ report.warnings.forEach((item) => console.log(chalk.gray(` - ${item}`)));
806
+ }
807
+ } else {
808
+ console.log(chalk.red('✖ Collaboration governance audit failed.'));
809
+ report.violations.forEach((item) => console.log(chalk.gray(` - ${item}`)));
810
+ if (report.warnings.length > 0) {
811
+ console.log(chalk.yellow('Warnings:'));
812
+ report.warnings.forEach((item) => console.log(chalk.gray(` - ${item}`)));
813
+ }
814
+ }
815
+
816
+ if (!report.passed && options.strict) {
817
+ process.exitCode = 1;
818
+ }
819
+ });
820
+
787
821
  workspaceCmd
788
822
  .command('delivery-audit')
789
823
  .description('Audit spec delivery manifests against git tracking and upstream sync state')
@@ -347,6 +347,11 @@ sce workspace legacy-migrate --dry-run --json
347
347
  sce workspace tracking-audit
348
348
  sce workspace tracking-audit --json
349
349
 
350
+ # Audit collaboration governance boundaries and legacy naming drift
351
+ sce workspace collab-governance-audit
352
+ sce workspace collab-governance-audit --json
353
+ sce workspace collab-governance-audit --strict
354
+
350
355
  # Audit takeover baseline drift (non-mutating)
351
356
  sce workspace takeover-audit
352
357
  sce workspace takeover-audit --json
@@ -9,6 +9,7 @@ This directory stores release-facing documents:
9
9
  ## Archived Versions
10
10
 
11
11
  - [Release checklist](../release-checklist.md)
12
+ - [v3.6.45 release notes](./v3.6.45.md)
12
13
  - [v3.6.44 release notes](./v3.6.44.md)
13
14
  - [v3.6.43 release notes](./v3.6.43.md)
14
15
  - [v3.6.42 release notes](./v3.6.42.md)
@@ -0,0 +1,18 @@
1
+ # v3.6.45 Release Notes
2
+
3
+ Release date: 2026-03-13
4
+
5
+ ## Highlights
6
+
7
+ - Added `sce workspace collab-governance-audit` to convert collaboration governance drift into an executable audit, covering `.gitignore` boundaries, runtime-state tracking, `multi-agent.json`, legacy `.kiro*` references, and `.sce/steering/` boundary hygiene.
8
+ - Brought the repository itself into compliance by extending the collaboration ignore rules, seeding `.sce/config/multi-agent.json`, and removing `.sce/steering/CURRENT_CONTEXT.md` from Git tracking while preserving the local file.
9
+
10
+ ## Verification
11
+
12
+ - `npx jest tests/unit/workspace/collab-governance-audit.test.js tests/integration/workspace-collab-governance-audit-cli.integration.test.js --runInBand`
13
+ - `node bin/sce.js workspace collab-governance-audit --strict`
14
+
15
+ ## Release Notes
16
+
17
+ - This patch is intentionally audit-first. It adds a reusable governance gate for co-work drift, rather than introducing auto-migration or hidden cross-machine sync behavior.
18
+ - The shipped baseline now reflects the intended runtime boundary: collaboration runtime files stay local, while shared config and specs remain versioned.
@@ -9,6 +9,7 @@
9
9
  ## 历史版本归档
10
10
 
11
11
  - [发布检查清单](../release-checklist.md)
12
+ - [v3.6.45 发布说明](./v3.6.45.md)
12
13
  - [v3.6.44 发布说明](./v3.6.44.md)
13
14
  - [v3.6.43 发布说明](./v3.6.43.md)
14
15
  - [v3.6.42 发布说明](./v3.6.42.md)
@@ -0,0 +1,18 @@
1
+ # v3.6.45 发布说明
2
+
3
+ 发布日期:2026-03-13
4
+
5
+ ## 重点变化
6
+
7
+ - 新增 `sce workspace collab-governance-audit`,把协作治理漂移变成可执行审计,覆盖 `.gitignore` 边界、运行态文件误入 Git、`multi-agent.json`、遗留 `.kiro*` 引用,以及 `.sce/steering/` 核心区治理。
8
+ - 同步把仓库自身基线校正到合规状态:补齐协作 ignore 规则、落地 `.sce/config/multi-agent.json`,并将 `.sce/steering/CURRENT_CONTEXT.md` 从 Git 跟踪中移除但保留本地文件。
9
+
10
+ ## 验证
11
+
12
+ - `npx jest tests/unit/workspace/collab-governance-audit.test.js tests/integration/workspace-collab-governance-audit-cli.integration.test.js --runInBand`
13
+ - `node bin/sce.js workspace collab-governance-audit --strict`
14
+
15
+ ## 发布说明
16
+
17
+ - 这个补丁版刻意采用 audit-first 策略:先把 co-work 治理漂移显式化并形成可复用门禁,而不是直接引入自动迁移或隐式同步。
18
+ - 本次发版后的仓库基线与能力目标一致:协作运行态文件留在本地,共享配置和 spec 继续纳入版本化治理。
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.6.44",
3
+ "version": "3.6.45",
4
4
  "description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {