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 +14 -0
- package/bin/scene-capability-engine.js +36 -2
- package/docs/command-reference.md +5 -0
- package/docs/releases/README.md +1 -0
- package/docs/releases/v3.6.45.md +18 -0
- package/docs/zh/releases/README.md +1 -0
- package/docs/zh/releases/v3.6.45.md +18 -0
- package/lib/workspace/collab-governance-audit.js +575 -0
- package/package.json +1 -1
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'
|
|
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
|
package/docs/releases/README.md
CHANGED
|
@@ -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.
|
|
@@ -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