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.
- package/CHANGELOG.md +59 -0
- package/bin/scene-capability-engine.js +78 -4
- package/docs/command-reference.md +5 -0
- package/docs/developer-guide.md +1 -1
- package/docs/releases/README.md +6 -0
- package/docs/releases/v3.6.40.md +19 -0
- package/docs/releases/v3.6.41.md +20 -0
- package/docs/releases/v3.6.42.md +19 -0
- package/docs/releases/v3.6.43.md +17 -0
- package/docs/releases/v3.6.44.md +17 -0
- package/docs/releases/v3.6.45.md +18 -0
- package/docs/spec-collaboration-guide.md +1 -1
- package/docs/zh/releases/README.md +6 -0
- package/docs/zh/releases/v3.6.40.md +19 -0
- package/docs/zh/releases/v3.6.41.md +20 -0
- package/docs/zh/releases/v3.6.42.md +19 -0
- package/docs/zh/releases/v3.6.43.md +17 -0
- package/docs/zh/releases/v3.6.44.md +17 -0
- package/docs/zh/releases/v3.6.45.md +18 -0
- package/lib/adoption/adoption-logger.js +1 -1
- package/lib/adoption/adoption-strategy.js +29 -29
- package/lib/adoption/detection-engine.js +16 -13
- package/lib/adoption/smart-orchestrator.js +3 -3
- package/lib/adoption/strategy-selector.js +19 -15
- package/lib/adoption/template-sync.js +3 -3
- package/lib/auto/autonomous-engine.js +5 -5
- package/lib/auto/handoff-release-gate-history-loaders-service.js +24 -4
- package/lib/auto/handoff-run-service.js +37 -0
- package/lib/backup/backup-system.js +10 -10
- package/lib/collab/collab-manager.js +8 -5
- package/lib/collab/dependency-manager.js +1 -1
- package/lib/commands/adopt.js +2 -2
- package/lib/commands/auto.js +239 -97
- package/lib/commands/collab.js +10 -4
- package/lib/commands/status.js +3 -3
- package/lib/commands/studio.js +8 -0
- package/lib/repo/config-manager.js +2 -2
- package/lib/spec/bootstrap/context-collector.js +5 -4
- package/lib/spec-gate/rules/default-rules.js +8 -8
- package/lib/upgrade/migration-engine.js +5 -5
- package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +3 -3
- package/lib/utils/tool-detector.js +4 -4
- package/lib/utils/validation.js +6 -6
- package/lib/workspace/collab-governance-audit.js +575 -0
- package/lib/workspace/multi/workspace-context-resolver.js +3 -3
- package/lib/workspace/multi/workspace-registry.js +3 -3
- package/lib/workspace/multi/workspace-state-manager.js +3 -3
- package/lib/workspace/spec-delivery-audit.js +553 -0
- 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
|
|
304
|
-
const
|
|
303
|
+
const scePath = path.join(projectPath, '.sce');
|
|
304
|
+
const sceExists = await pathExists(scePath);
|
|
305
305
|
|
|
306
|
-
if (!
|
|
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(
|
|
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(
|
|
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
|
|
29
|
+
const scePath = path.join(projectPath, '.sce');
|
|
30
30
|
|
|
31
31
|
// 1. Ensure backups/ directory exists
|
|
32
|
-
const backupsPath = path.join(
|
|
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(
|
|
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
|
|
68
|
-
if (await fs.pathExists(
|
|
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
|
|
75
|
+
const sceFiles = [
|
|
76
76
|
'.sce/steering',
|
|
77
77
|
'.sce/specs',
|
|
78
78
|
'.sce/tools'
|
|
79
79
|
];
|
|
80
80
|
|
|
81
|
-
for (const file of
|
|
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`);
|
package/lib/utils/validation.js
CHANGED
|
@@ -21,11 +21,11 @@ async function validateProjectStructure(projectPath) {
|
|
|
21
21
|
const warnings = [];
|
|
22
22
|
|
|
23
23
|
try {
|
|
24
|
-
const
|
|
24
|
+
const scePath = path.join(projectPath, '.sce');
|
|
25
25
|
|
|
26
26
|
// Check if .sce/ directory exists
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
};
|