opencode-swarm 7.83.0 → 7.84.0
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/README.md +3 -1
- package/dist/cli/capability-probe-jevmgwmf.js +18 -0
- package/dist/cli/config-doctor-4tcdd9vt.js +35 -0
- package/dist/cli/dispatch-k86d928w.js +477 -0
- package/dist/cli/evidence-summary-service-g2znnd33.js +320 -0
- package/dist/cli/explorer-gz70sm9b.js +16 -0
- package/dist/cli/gate-evidence-y8zn7fe2.js +29 -0
- package/dist/cli/guardrail-explain-tcamcdfy.js +30 -0
- package/dist/cli/guardrail-log-fd14n96q.js +15 -0
- package/dist/cli/index-293f68mj.js +13538 -0
- package/dist/cli/index-8ra2qpk8.js +29027 -0
- package/dist/cli/index-a76rekgs.js +67 -0
- package/dist/cli/index-a82d6d87.js +1241 -0
- package/dist/cli/index-b9v501fr.js +371 -0
- package/dist/cli/index-bcp79s17.js +1673 -0
- package/dist/cli/index-ckntc5gf.js +91 -0
- package/dist/cli/index-d9fbxaqd.js +2314 -0
- package/dist/cli/index-e7h9bb6v.js +233 -0
- package/dist/cli/index-e8pk68cc.js +540 -0
- package/dist/cli/index-eb85wtx9.js +242 -0
- package/dist/cli/index-f8r50m3h.js +14505 -0
- package/dist/cli/index-fjwwrwr5.js +37 -0
- package/dist/cli/index-hz59hg4h.js +452 -0
- package/dist/cli/index-j710h2ge.js +412 -0
- package/dist/cli/index-jfgr5gye.js +110 -0
- package/dist/cli/index-jtqkh8jf.js +119 -0
- package/dist/cli/index-p0arc26j.js +28 -0
- package/dist/cli/index-p0ye10nd.js +222 -0
- package/dist/cli/index-pv2xmc9k.js +2391 -0
- package/dist/cli/index-red8fm8p.js +2914 -0
- package/dist/cli/index-wg3r6acj.js +2042 -0
- package/dist/cli/index-xw0bcy0v.js +583 -0
- package/dist/cli/index-yhsmmv2z.js +339 -0
- package/dist/cli/index-yx44zd0p.js +40 -0
- package/dist/cli/index-zfsbaaqh.js +29 -0
- package/dist/cli/index.js +73 -69708
- package/dist/cli/knowledge-store-n4x6zyk7.js +73 -0
- package/dist/cli/pending-delegations-pz61mrsz.js +255 -0
- package/dist/cli/pr-subscriptions-y1nn36e5.js +33 -0
- package/dist/cli/schema-c2dbzhm8.js +168 -0
- package/dist/cli/skill-generator-a5ehggyg.js +55 -0
- package/dist/cli/task-envelope-qn0qtnh0.js +90 -0
- package/dist/cli/telemetry-9bbyxrvn.js +20 -0
- package/dist/cli/workspace-snapshot-w58jr2ga.js +90 -0
- package/dist/commands/guardrail-explain.d.ts +1 -0
- package/dist/commands/guardrail-log.d.ts +1 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/registry.d.ts +14 -0
- package/dist/hooks/guardrails/audit-log.d.ts +114 -0
- package/dist/index.js +3569 -2366
- package/dist/services/diagnose-service.d.ts +5 -0
- package/dist/services/guardrail-explain-service.d.ts +42 -0
- package/dist/services/guardrail-log-service.d.ts +10 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1241 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
ALL_AGENT_NAMES,
|
|
4
|
+
PluginConfigSchema,
|
|
5
|
+
stripKnownSwarmPrefix
|
|
6
|
+
} from "./index-wg3r6acj.js";
|
|
7
|
+
import {
|
|
8
|
+
log
|
|
9
|
+
} from "./index-yx44zd0p.js";
|
|
10
|
+
|
|
11
|
+
// src/services/config-doctor.ts
|
|
12
|
+
import * as crypto from "crypto";
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as os from "os";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
var KNOWN_TOP_LEVEL_KEYS = new Set(Object.keys(PluginConfigSchema.shape));
|
|
17
|
+
var DEPRECATED_FIELDS = new Map([
|
|
18
|
+
[
|
|
19
|
+
"skill_improver.model",
|
|
20
|
+
{
|
|
21
|
+
message: "deprecated",
|
|
22
|
+
replacement: "agents.skill_improver.model",
|
|
23
|
+
isDefaultValue: (v) => v === null
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
"skill_improver.fallback_models",
|
|
28
|
+
{
|
|
29
|
+
message: "deprecated",
|
|
30
|
+
replacement: "agents.skill_improver.fallback_models",
|
|
31
|
+
isDefaultValue: (v) => Array.isArray(v) && v.length === 0
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
[
|
|
35
|
+
"spec_writer.model",
|
|
36
|
+
{
|
|
37
|
+
message: "deprecated",
|
|
38
|
+
replacement: "agents.spec_writer.model",
|
|
39
|
+
isDefaultValue: (v) => v === null
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
"spec_writer.fallback_models",
|
|
44
|
+
{
|
|
45
|
+
message: "deprecated",
|
|
46
|
+
replacement: "agents.spec_writer.fallback_models",
|
|
47
|
+
isDefaultValue: (v) => Array.isArray(v) && v.length === 0
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
]);
|
|
51
|
+
function levenshteinDistance(a, b) {
|
|
52
|
+
const al = a.length;
|
|
53
|
+
const bl = b.length;
|
|
54
|
+
const matrix = [];
|
|
55
|
+
for (let i = 0;i <= al; i++) {
|
|
56
|
+
matrix[i] = [i];
|
|
57
|
+
}
|
|
58
|
+
for (let j = 0;j <= bl; j++) {
|
|
59
|
+
matrix[0][j] = j;
|
|
60
|
+
}
|
|
61
|
+
for (let i = 1;i <= al; i++) {
|
|
62
|
+
for (let j = 1;j <= bl; j++) {
|
|
63
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
64
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return matrix[al][bl];
|
|
68
|
+
}
|
|
69
|
+
function emitObjectTypeMismatch(key, value, findings) {
|
|
70
|
+
if (value !== undefined && (typeof value !== "object" || Array.isArray(value) || value === null)) {
|
|
71
|
+
findings.push({
|
|
72
|
+
id: `invalid-${key}-type`,
|
|
73
|
+
title: `Invalid ${key} type`,
|
|
74
|
+
description: `"${key}" must be an object, got ${typeof value}`,
|
|
75
|
+
severity: "error",
|
|
76
|
+
path: key,
|
|
77
|
+
currentValue: value,
|
|
78
|
+
autoFixable: false
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function getUserConfigDir() {
|
|
83
|
+
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
84
|
+
}
|
|
85
|
+
function getConfigPaths(directory) {
|
|
86
|
+
const userConfigPath = path.join(getUserConfigDir(), "opencode", "opencode-swarm.json");
|
|
87
|
+
const projectConfigPath = path.join(directory, ".opencode", "opencode-swarm.json");
|
|
88
|
+
return { userConfigPath, projectConfigPath };
|
|
89
|
+
}
|
|
90
|
+
function computeHash(content) {
|
|
91
|
+
return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
|
|
92
|
+
}
|
|
93
|
+
function isValidConfigPath(configPath, directory) {
|
|
94
|
+
const normalizedPath = configPath.replace(/\\/g, "/");
|
|
95
|
+
const pathParts = normalizedPath.split("/");
|
|
96
|
+
for (const part of pathParts) {
|
|
97
|
+
if (part === ".." || part === "") {
|
|
98
|
+
if (part === "..") {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const { userConfigPath, projectConfigPath } = getConfigPaths(directory);
|
|
104
|
+
try {
|
|
105
|
+
const resolvedConfig = path.resolve(configPath);
|
|
106
|
+
const resolvedUser = path.resolve(userConfigPath);
|
|
107
|
+
const resolvedProject = path.resolve(projectConfigPath);
|
|
108
|
+
if (resolvedConfig !== resolvedUser && resolvedConfig !== resolvedProject) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
if (fs.existsSync(resolvedConfig)) {
|
|
113
|
+
const realConfig = fs.realpathSync(resolvedConfig);
|
|
114
|
+
if (realConfig !== resolvedConfig) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {}
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function atomicWriteFileSync(filePath, content) {
|
|
125
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
126
|
+
fs.writeFileSync(tmpPath, content, "utf-8");
|
|
127
|
+
try {
|
|
128
|
+
fs.renameSync(tmpPath, filePath);
|
|
129
|
+
} catch {
|
|
130
|
+
try {
|
|
131
|
+
fs.unlinkSync(filePath);
|
|
132
|
+
} catch {}
|
|
133
|
+
fs.renameSync(tmpPath, filePath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function createConfigBackup(directory) {
|
|
137
|
+
const { userConfigPath, projectConfigPath } = getConfigPaths(directory);
|
|
138
|
+
let configPath = projectConfigPath;
|
|
139
|
+
let content = null;
|
|
140
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
141
|
+
try {
|
|
142
|
+
content = fs.readFileSync(projectConfigPath, "utf-8");
|
|
143
|
+
} catch (error) {
|
|
144
|
+
log("[ConfigDoctor] project config read failed", {
|
|
145
|
+
error: error instanceof Error ? error.message : String(error)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (content === null && fs.existsSync(userConfigPath)) {
|
|
150
|
+
configPath = userConfigPath;
|
|
151
|
+
try {
|
|
152
|
+
content = fs.readFileSync(userConfigPath, "utf-8");
|
|
153
|
+
} catch (error) {
|
|
154
|
+
log("[ConfigDoctor] user config read failed", {
|
|
155
|
+
error: error instanceof Error ? error.message : String(error)
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (content === null) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
createdAt: Date.now(),
|
|
164
|
+
configPath,
|
|
165
|
+
content,
|
|
166
|
+
contentHash: computeHash(content)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function writeBackupArtifact(directory, backup) {
|
|
170
|
+
const swarmDir = path.join(directory, ".swarm");
|
|
171
|
+
if (!fs.existsSync(swarmDir)) {
|
|
172
|
+
fs.mkdirSync(swarmDir, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
const backupFilename = `config-backup-${backup.createdAt}.json`;
|
|
175
|
+
const backupPath = path.join(swarmDir, backupFilename);
|
|
176
|
+
const artifact = {
|
|
177
|
+
createdAt: backup.createdAt,
|
|
178
|
+
configPath: backup.configPath,
|
|
179
|
+
contentHash: backup.contentHash,
|
|
180
|
+
content: backup.content,
|
|
181
|
+
preview: backup.content.substring(0, 500) + (backup.content.length > 500 ? "..." : "")
|
|
182
|
+
};
|
|
183
|
+
atomicWriteFileSync(backupPath, JSON.stringify(artifact, null, 2));
|
|
184
|
+
return backupPath;
|
|
185
|
+
}
|
|
186
|
+
function restoreFromBackup(backupPath, directory) {
|
|
187
|
+
if (!fs.existsSync(backupPath)) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const swarmDir = path.resolve(path.join(directory, ".swarm"));
|
|
191
|
+
const resolvedBackup = path.resolve(backupPath);
|
|
192
|
+
if (!resolvedBackup.startsWith(swarmDir + path.sep) && resolvedBackup !== swarmDir) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const artifact = JSON.parse(fs.readFileSync(backupPath, "utf-8"));
|
|
197
|
+
if (!artifact.content || !artifact.configPath || !artifact.contentHash) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (!isValidConfigPath(artifact.configPath, directory)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const computedHash = computeHash(artifact.content);
|
|
204
|
+
const storedHash = artifact.contentHash;
|
|
205
|
+
const isLegacyHash = /^\d+$/.test(storedHash);
|
|
206
|
+
if (!isLegacyHash && computedHash !== storedHash) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
log("[ConfigDoctor] Warning: restoring from backup with legacy numeric hash (pre-SHA-256). Consider re-backing up.", {});
|
|
210
|
+
const targetPath = artifact.configPath;
|
|
211
|
+
const targetDir = path.dirname(targetPath);
|
|
212
|
+
if (!fs.existsSync(targetDir)) {
|
|
213
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
214
|
+
}
|
|
215
|
+
atomicWriteFileSync(targetPath, artifact.content);
|
|
216
|
+
return targetPath;
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function readConfigFromFile(directory) {
|
|
222
|
+
const { userConfigPath, projectConfigPath } = getConfigPaths(directory);
|
|
223
|
+
let configPath = projectConfigPath;
|
|
224
|
+
let configContent = null;
|
|
225
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
226
|
+
configPath = projectConfigPath;
|
|
227
|
+
configContent = fs.readFileSync(projectConfigPath, "utf-8");
|
|
228
|
+
} else if (fs.existsSync(userConfigPath)) {
|
|
229
|
+
configPath = userConfigPath;
|
|
230
|
+
configContent = fs.readFileSync(userConfigPath, "utf-8");
|
|
231
|
+
}
|
|
232
|
+
if (configContent === null) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const config = JSON.parse(configContent);
|
|
237
|
+
return { config, configPath };
|
|
238
|
+
} catch (error) {
|
|
239
|
+
log(`[ConfigDoctor] Failed to parse config file: ${configPath}`, {
|
|
240
|
+
error: error instanceof Error ? error.message : String(error)
|
|
241
|
+
});
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function validateConfigKey(path2, value) {
|
|
246
|
+
const findings = [];
|
|
247
|
+
for (const [depPath, depInfo] of DEPRECATED_FIELDS) {
|
|
248
|
+
if (path2 === depPath && !depInfo.isDefaultValue(value)) {
|
|
249
|
+
findings.push({
|
|
250
|
+
id: "deprecated-field",
|
|
251
|
+
title: `Deprecated config field: ${depPath}`,
|
|
252
|
+
description: `Config field "${depPath}" is deprecated. Replacement: ${depInfo.replacement}.`,
|
|
253
|
+
severity: "info",
|
|
254
|
+
path: depPath,
|
|
255
|
+
currentValue: value,
|
|
256
|
+
autoFixable: false
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
switch (path2) {
|
|
261
|
+
case "agents": {
|
|
262
|
+
if (value !== undefined) {
|
|
263
|
+
findings.push({
|
|
264
|
+
id: "deprecated-agents-config",
|
|
265
|
+
title: "Deprecated agents configuration",
|
|
266
|
+
description: 'The "agents" field is deprecated. Use "swarms" instead for multi-swarm support.',
|
|
267
|
+
severity: "warn",
|
|
268
|
+
path: "agents",
|
|
269
|
+
currentValue: value,
|
|
270
|
+
autoFixable: false,
|
|
271
|
+
proposedFix: {
|
|
272
|
+
type: "remove",
|
|
273
|
+
path: "agents",
|
|
274
|
+
description: "Remove deprecated agents config - use swarms instead",
|
|
275
|
+
risk: "low"
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "guardrails.enabled": {
|
|
282
|
+
if (value === false) {
|
|
283
|
+
findings.push({
|
|
284
|
+
id: "guardrails-disabled",
|
|
285
|
+
title: "Guardrails disabled",
|
|
286
|
+
description: "Guardrails have been explicitly disabled. This removes safety limits.",
|
|
287
|
+
severity: "error",
|
|
288
|
+
path: "guardrails.enabled",
|
|
289
|
+
currentValue: value,
|
|
290
|
+
autoFixable: false
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
case "guardrails.profiles": {
|
|
296
|
+
const profiles = value;
|
|
297
|
+
if (profiles) {
|
|
298
|
+
const validAgents = new Set(ALL_AGENT_NAMES);
|
|
299
|
+
for (const [agentName, profile] of Object.entries(profiles)) {
|
|
300
|
+
if (!validAgents.has(agentName)) {
|
|
301
|
+
findings.push({
|
|
302
|
+
id: "unknown-agent-profile",
|
|
303
|
+
title: "Unknown agent profile",
|
|
304
|
+
description: `Profile for unknown agent "${agentName}" will be ignored.`,
|
|
305
|
+
severity: "info",
|
|
306
|
+
path: `guardrails.profiles.${agentName}`,
|
|
307
|
+
currentValue: profile,
|
|
308
|
+
autoFixable: true,
|
|
309
|
+
proposedFix: {
|
|
310
|
+
type: "remove",
|
|
311
|
+
path: `guardrails.profiles.${agentName}`,
|
|
312
|
+
description: `Remove unknown agent profile "${agentName}"`,
|
|
313
|
+
risk: "low"
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case "automation.mode": {
|
|
322
|
+
const validModes = ["manual", "hybrid", "auto"];
|
|
323
|
+
if (value !== undefined && !validModes.includes(value)) {
|
|
324
|
+
findings.push({
|
|
325
|
+
id: "invalid-automation-mode",
|
|
326
|
+
title: "Invalid automation mode",
|
|
327
|
+
description: `Invalid automation mode "${value}". Valid: ${validModes.join(", ")}`,
|
|
328
|
+
severity: "error",
|
|
329
|
+
path: "automation.mode",
|
|
330
|
+
currentValue: value,
|
|
331
|
+
autoFixable: true,
|
|
332
|
+
proposedFix: {
|
|
333
|
+
type: "update",
|
|
334
|
+
path: "automation.mode",
|
|
335
|
+
value: "manual",
|
|
336
|
+
description: 'Reset to safe default "manual"',
|
|
337
|
+
risk: "low"
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "automation.capabilities": {
|
|
344
|
+
const caps = value;
|
|
345
|
+
if (caps) {
|
|
346
|
+
const capabilityNames = [
|
|
347
|
+
"plan_sync",
|
|
348
|
+
"phase_preflight",
|
|
349
|
+
"config_doctor_on_startup",
|
|
350
|
+
"evidence_auto_summaries",
|
|
351
|
+
"decision_drift_detection"
|
|
352
|
+
];
|
|
353
|
+
for (const [name, capValue] of Object.entries(caps)) {
|
|
354
|
+
if (capabilityNames.includes(name) && typeof capValue !== "boolean") {
|
|
355
|
+
findings.push({
|
|
356
|
+
id: "invalid-capability-type",
|
|
357
|
+
title: "Invalid capability type",
|
|
358
|
+
description: `Capability "${name}" must be boolean, got ${typeof capValue}`,
|
|
359
|
+
severity: "error",
|
|
360
|
+
path: `automation.capabilities.${name}`,
|
|
361
|
+
currentValue: capValue,
|
|
362
|
+
autoFixable: true,
|
|
363
|
+
proposedFix: {
|
|
364
|
+
type: "update",
|
|
365
|
+
path: `automation.capabilities.${name}`,
|
|
366
|
+
value: false,
|
|
367
|
+
description: `Reset capability "${name}" to false`,
|
|
368
|
+
risk: "low"
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case "hooks": {
|
|
377
|
+
emitObjectTypeMismatch("hooks", value, findings);
|
|
378
|
+
if (value !== undefined && typeof value === "object" && !Array.isArray(value) && value !== null) {
|
|
379
|
+
const hooks = value;
|
|
380
|
+
const validHooks = [
|
|
381
|
+
"system_enhancer",
|
|
382
|
+
"compaction",
|
|
383
|
+
"agent_activity",
|
|
384
|
+
"delegation_tracker",
|
|
385
|
+
"agent_awareness_max_chars",
|
|
386
|
+
"delegation_gate",
|
|
387
|
+
"delegation_max_chars"
|
|
388
|
+
];
|
|
389
|
+
for (const hookName of Object.keys(hooks)) {
|
|
390
|
+
if (!validHooks.includes(hookName)) {
|
|
391
|
+
findings.push({
|
|
392
|
+
id: "unknown-hook-field",
|
|
393
|
+
title: "Unknown hook configuration",
|
|
394
|
+
description: `Unknown hook "${hookName}" will be ignored.`,
|
|
395
|
+
severity: "info",
|
|
396
|
+
path: `hooks.${hookName}`,
|
|
397
|
+
currentValue: hooks[hookName],
|
|
398
|
+
autoFixable: true,
|
|
399
|
+
proposedFix: {
|
|
400
|
+
type: "remove",
|
|
401
|
+
path: `hooks.${hookName}`,
|
|
402
|
+
description: `Remove unknown hook "${hookName}"`,
|
|
403
|
+
risk: "low"
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case "max_iterations": {
|
|
412
|
+
const numValue = value;
|
|
413
|
+
if (typeof numValue === "number") {
|
|
414
|
+
if (numValue < 1 || numValue > 10) {
|
|
415
|
+
findings.push({
|
|
416
|
+
id: "out-of-bounds-iterations",
|
|
417
|
+
title: "max_iterations out of bounds",
|
|
418
|
+
description: `max_iterations must be 1-10, got ${numValue}`,
|
|
419
|
+
severity: "error",
|
|
420
|
+
path: "max_iterations",
|
|
421
|
+
currentValue: numValue,
|
|
422
|
+
autoFixable: true,
|
|
423
|
+
proposedFix: {
|
|
424
|
+
type: "update",
|
|
425
|
+
path: "max_iterations",
|
|
426
|
+
value: Math.max(1, Math.min(10, numValue)),
|
|
427
|
+
description: "Clamp to valid range 1-10",
|
|
428
|
+
risk: "low"
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
case "qa_retry_limit": {
|
|
436
|
+
const numValue = value;
|
|
437
|
+
if (typeof numValue === "number") {
|
|
438
|
+
if (numValue < 1 || numValue > 10) {
|
|
439
|
+
findings.push({
|
|
440
|
+
id: "out-of-bounds-retry-limit",
|
|
441
|
+
title: "qa_retry_limit out of bounds",
|
|
442
|
+
description: `qa_retry_limit must be 1-10, got ${numValue}`,
|
|
443
|
+
severity: "error",
|
|
444
|
+
path: "qa_retry_limit",
|
|
445
|
+
currentValue: numValue,
|
|
446
|
+
autoFixable: true,
|
|
447
|
+
proposedFix: {
|
|
448
|
+
type: "update",
|
|
449
|
+
path: "qa_retry_limit",
|
|
450
|
+
value: Math.max(1, Math.min(10, numValue)),
|
|
451
|
+
description: "Clamp to valid range 1-10",
|
|
452
|
+
risk: "low"
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
case "swarms": {
|
|
460
|
+
if (value !== undefined) {
|
|
461
|
+
if (typeof value !== "object" || Array.isArray(value) || value === null) {
|
|
462
|
+
findings.push({
|
|
463
|
+
id: "invalid-swarms-type",
|
|
464
|
+
title: "Invalid swarms type",
|
|
465
|
+
description: `"swarms" must be an object, got ${typeof value}`,
|
|
466
|
+
severity: "error",
|
|
467
|
+
path: "swarms",
|
|
468
|
+
currentValue: value,
|
|
469
|
+
autoFixable: false
|
|
470
|
+
});
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
const swarms = value;
|
|
474
|
+
if (Object.keys(swarms).length === 0) {
|
|
475
|
+
findings.push({
|
|
476
|
+
id: "empty-swarms",
|
|
477
|
+
title: "Empty swarms configuration",
|
|
478
|
+
description: 'The "swarms" field is an empty object. No swarm configurations are defined.',
|
|
479
|
+
severity: "info",
|
|
480
|
+
path: "swarms",
|
|
481
|
+
autoFixable: false
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
for (const swarmId of Object.keys(swarms)) {
|
|
485
|
+
if (swarmId.includes("..") || swarmId.includes("/") || swarmId.includes("\\") || swarmId.includes("\x00")) {
|
|
486
|
+
findings.push({
|
|
487
|
+
id: "swarm-id-path-traversal",
|
|
488
|
+
title: "Path traversal in swarm ID",
|
|
489
|
+
description: `Swarm ID "${swarmId}" contains path traversal characters.`,
|
|
490
|
+
severity: "error",
|
|
491
|
+
path: `swarms.${swarmId}`,
|
|
492
|
+
autoFixable: false
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const validAgents = new Set(ALL_AGENT_NAMES);
|
|
497
|
+
for (const [swarmId, swarmConfig] of Object.entries(swarms)) {
|
|
498
|
+
const swarm = swarmConfig;
|
|
499
|
+
if (swarm.agents && typeof swarm.agents === "object") {
|
|
500
|
+
for (const [agentName] of Object.entries(swarm.agents)) {
|
|
501
|
+
const baseName = stripKnownSwarmPrefix(agentName);
|
|
502
|
+
if (baseName !== agentName && agentName.startsWith(`${swarmId}_`) && validAgents.has(baseName)) {
|
|
503
|
+
findings.push({
|
|
504
|
+
id: "prefixed-swarm-agent-override",
|
|
505
|
+
title: "Prefixed agent override is ignored",
|
|
506
|
+
description: `Agent "${agentName}" in swarm "${swarmId}" uses a generated agent name. ` + `Per-swarm overrides must use the canonical key "${baseName}", e.g. ` + `"swarms.${swarmId}.agents.${baseName}.model". Otherwise the override is ignored and the agent falls back to its default model.`,
|
|
507
|
+
severity: "warn",
|
|
508
|
+
path: `swarms.${swarmId}.agents.${agentName}`,
|
|
509
|
+
currentValue: swarm.agents[agentName],
|
|
510
|
+
autoFixable: false
|
|
511
|
+
});
|
|
512
|
+
} else if (!validAgents.has(baseName)) {
|
|
513
|
+
findings.push({
|
|
514
|
+
id: "unknown-swarm-agent",
|
|
515
|
+
title: "Unknown agent in swarm",
|
|
516
|
+
description: `Agent "${agentName}" in swarm "${swarmId}" may not be recognized.`,
|
|
517
|
+
severity: "info",
|
|
518
|
+
path: `swarms.${swarmId}.agents.${agentName}`,
|
|
519
|
+
currentValue: swarm.agents[agentName],
|
|
520
|
+
autoFixable: false
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case "default_agent": {
|
|
530
|
+
if (value !== undefined && typeof value !== "string") {
|
|
531
|
+
findings.push({
|
|
532
|
+
id: "invalid-default_agent-type",
|
|
533
|
+
title: "Invalid default_agent type",
|
|
534
|
+
description: `"default_agent" must be a string, got ${typeof value}`,
|
|
535
|
+
severity: "error",
|
|
536
|
+
path: "default_agent",
|
|
537
|
+
currentValue: value,
|
|
538
|
+
autoFixable: false
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
case "auto_select_architect": {
|
|
544
|
+
if (value !== undefined && typeof value !== "boolean" && typeof value !== "string") {
|
|
545
|
+
findings.push({
|
|
546
|
+
id: "invalid-auto_select_architect-type",
|
|
547
|
+
title: "Invalid auto_select_architect type",
|
|
548
|
+
description: `"auto_select_architect" must be a boolean or string, got ${typeof value}`,
|
|
549
|
+
severity: "error",
|
|
550
|
+
path: "auto_select_architect",
|
|
551
|
+
currentValue: value,
|
|
552
|
+
autoFixable: false
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
case "pipeline": {
|
|
558
|
+
emitObjectTypeMismatch("pipeline", value, findings);
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case "phase_complete": {
|
|
562
|
+
emitObjectTypeMismatch("phase_complete", value, findings);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
case "execution_mode": {
|
|
566
|
+
const validModes = ["strict", "balanced", "fast"];
|
|
567
|
+
if (value !== undefined && !validModes.includes(value)) {
|
|
568
|
+
findings.push({
|
|
569
|
+
id: "invalid-execution_mode-type",
|
|
570
|
+
title: "Invalid execution_mode",
|
|
571
|
+
description: `"execution_mode" must be one of: ${validModes.join(", ")}, got "${value}"`,
|
|
572
|
+
severity: "error",
|
|
573
|
+
path: "execution_mode",
|
|
574
|
+
currentValue: value,
|
|
575
|
+
autoFixable: false
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
case "inject_phase_reminders": {
|
|
581
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
582
|
+
findings.push({
|
|
583
|
+
id: "invalid-inject_phase_reminders-type",
|
|
584
|
+
title: "Invalid inject_phase_reminders type",
|
|
585
|
+
description: `"inject_phase_reminders" must be a boolean, got ${typeof value}`,
|
|
586
|
+
severity: "error",
|
|
587
|
+
path: "inject_phase_reminders",
|
|
588
|
+
currentValue: value,
|
|
589
|
+
autoFixable: false
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
case "gates": {
|
|
595
|
+
emitObjectTypeMismatch("gates", value, findings);
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
case "context_budget": {
|
|
599
|
+
emitObjectTypeMismatch("context_budget", value, findings);
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
case "guardrails": {
|
|
603
|
+
emitObjectTypeMismatch("guardrails", value, findings);
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
case "watchdog": {
|
|
607
|
+
emitObjectTypeMismatch("watchdog", value, findings);
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
case "self_review": {
|
|
611
|
+
emitObjectTypeMismatch("self_review", value, findings);
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
case "tool_filter": {
|
|
615
|
+
emitObjectTypeMismatch("tool_filter", value, findings);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
case "authority": {
|
|
619
|
+
emitObjectTypeMismatch("authority", value, findings);
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
case "plan_cursor": {
|
|
623
|
+
emitObjectTypeMismatch("plan_cursor", value, findings);
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
case "context_map": {
|
|
627
|
+
emitObjectTypeMismatch("context_map", value, findings);
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
case "evidence": {
|
|
631
|
+
emitObjectTypeMismatch("evidence", value, findings);
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
case "summaries": {
|
|
635
|
+
emitObjectTypeMismatch("summaries", value, findings);
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
case "review_passes": {
|
|
639
|
+
emitObjectTypeMismatch("review_passes", value, findings);
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case "adversarial_detection": {
|
|
643
|
+
emitObjectTypeMismatch("adversarial_detection", value, findings);
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
case "adversarial_testing": {
|
|
647
|
+
emitObjectTypeMismatch("adversarial_testing", value, findings);
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case "integration_analysis": {
|
|
651
|
+
emitObjectTypeMismatch("integration_analysis", value, findings);
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
case "docs": {
|
|
655
|
+
emitObjectTypeMismatch("docs", value, findings);
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
case "design_docs": {
|
|
659
|
+
emitObjectTypeMismatch("design_docs", value, findings);
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
case "ui_review": {
|
|
663
|
+
emitObjectTypeMismatch("ui_review", value, findings);
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case "compaction_advisory": {
|
|
667
|
+
emitObjectTypeMismatch("compaction_advisory", value, findings);
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
case "lint": {
|
|
671
|
+
emitObjectTypeMismatch("lint", value, findings);
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
case "secretscan": {
|
|
675
|
+
emitObjectTypeMismatch("secretscan", value, findings);
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
case "checkpoint": {
|
|
679
|
+
emitObjectTypeMismatch("checkpoint", value, findings);
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
case "automation": {
|
|
683
|
+
emitObjectTypeMismatch("automation", value, findings);
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case "knowledge": {
|
|
687
|
+
emitObjectTypeMismatch("knowledge", value, findings);
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
case "memory": {
|
|
691
|
+
emitObjectTypeMismatch("memory", value, findings);
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
case "curator": {
|
|
695
|
+
emitObjectTypeMismatch("curator", value, findings);
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
case "architectural_supervision": {
|
|
699
|
+
emitObjectTypeMismatch("architectural_supervision", value, findings);
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case "knowledge_application": {
|
|
703
|
+
emitObjectTypeMismatch("knowledge_application", value, findings);
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
case "skillPropagation": {
|
|
707
|
+
emitObjectTypeMismatch("skillPropagation", value, findings);
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
case "skill_improver": {
|
|
711
|
+
emitObjectTypeMismatch("skill_improver", value, findings);
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
case "spec_writer": {
|
|
715
|
+
emitObjectTypeMismatch("spec_writer", value, findings);
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
case "tool_output": {
|
|
719
|
+
emitObjectTypeMismatch("tool_output", value, findings);
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
case "slop_detector": {
|
|
723
|
+
emitObjectTypeMismatch("slop_detector", value, findings);
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
case "todo_gate": {
|
|
727
|
+
emitObjectTypeMismatch("todo_gate", value, findings);
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
case "incremental_verify": {
|
|
731
|
+
emitObjectTypeMismatch("incremental_verify", value, findings);
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
case "compaction_service": {
|
|
735
|
+
emitObjectTypeMismatch("compaction_service", value, findings);
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
case "prm": {
|
|
739
|
+
emitObjectTypeMismatch("prm", value, findings);
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
case "council": {
|
|
743
|
+
emitObjectTypeMismatch("council", value, findings);
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
case "parallelization": {
|
|
747
|
+
emitObjectTypeMismatch("parallelization", value, findings);
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
case "worktree": {
|
|
751
|
+
emitObjectTypeMismatch("worktree", value, findings);
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case "turbo": {
|
|
755
|
+
emitObjectTypeMismatch("turbo", value, findings);
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
case "turbo_mode": {
|
|
759
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
760
|
+
findings.push({
|
|
761
|
+
id: "invalid-turbo_mode-type",
|
|
762
|
+
title: "Invalid turbo_mode type",
|
|
763
|
+
description: `"turbo_mode" must be a boolean, got ${typeof value}`,
|
|
764
|
+
severity: "error",
|
|
765
|
+
path: "turbo_mode",
|
|
766
|
+
currentValue: value,
|
|
767
|
+
autoFixable: false
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
case "quiet": {
|
|
773
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
774
|
+
findings.push({
|
|
775
|
+
id: "invalid-quiet-type",
|
|
776
|
+
title: "Invalid quiet type",
|
|
777
|
+
description: `"quiet" must be a boolean, got ${typeof value}`,
|
|
778
|
+
severity: "error",
|
|
779
|
+
path: "quiet",
|
|
780
|
+
currentValue: value,
|
|
781
|
+
autoFixable: false
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
case "version_check": {
|
|
787
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
788
|
+
findings.push({
|
|
789
|
+
id: "invalid-version_check-type",
|
|
790
|
+
title: "Invalid version_check type",
|
|
791
|
+
description: `"version_check" must be a boolean, got ${typeof value}`,
|
|
792
|
+
severity: "error",
|
|
793
|
+
path: "version_check",
|
|
794
|
+
currentValue: value,
|
|
795
|
+
autoFixable: false
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
case "full_auto": {
|
|
801
|
+
emitObjectTypeMismatch("full_auto", value, findings);
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
case "pr_monitor": {
|
|
805
|
+
emitObjectTypeMismatch("pr_monitor", value, findings);
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
case "external_skills": {
|
|
809
|
+
emitObjectTypeMismatch("external_skills", value, findings);
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
default: {
|
|
813
|
+
const topLevel = path2.split(".")[0];
|
|
814
|
+
if (KNOWN_TOP_LEVEL_KEYS.has(topLevel)) {
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
const MAX_SUGGESTION_KEY_LENGTH = 100;
|
|
818
|
+
const lowerTopLevel = topLevel.toLowerCase();
|
|
819
|
+
let suggestion;
|
|
820
|
+
let matchCount = 0;
|
|
821
|
+
if (lowerTopLevel.length <= MAX_SUGGESTION_KEY_LENGTH) {
|
|
822
|
+
for (const knownKey of KNOWN_TOP_LEVEL_KEYS) {
|
|
823
|
+
if (levenshteinDistance(lowerTopLevel, knownKey.toLowerCase()) <= 2) {
|
|
824
|
+
matchCount++;
|
|
825
|
+
if (matchCount === 1) {
|
|
826
|
+
suggestion = knownKey;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (matchCount === 1 && suggestion) {
|
|
832
|
+
findings.push({
|
|
833
|
+
id: "unknown-config-key",
|
|
834
|
+
title: `Unknown config key: ${topLevel}`,
|
|
835
|
+
description: `Unknown config key "${path2}" is not in the schema. Did you mean "${suggestion}"?`,
|
|
836
|
+
severity: "warn",
|
|
837
|
+
path: path2,
|
|
838
|
+
currentValue: value,
|
|
839
|
+
autoFixable: false
|
|
840
|
+
});
|
|
841
|
+
} else {
|
|
842
|
+
findings.push({
|
|
843
|
+
id: "unknown-config-key",
|
|
844
|
+
title: `Unknown config key: ${topLevel}`,
|
|
845
|
+
description: `Unknown config key "${path2}" is not in the schema.`,
|
|
846
|
+
severity: "warn",
|
|
847
|
+
path: path2,
|
|
848
|
+
currentValue: value,
|
|
849
|
+
autoFixable: false
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return findings;
|
|
856
|
+
}
|
|
857
|
+
function walkConfigAndValidate(obj, path2, findings, visited = new WeakSet) {
|
|
858
|
+
if (obj === null || obj === undefined) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (path2 && typeof obj === "object" && !Array.isArray(obj)) {
|
|
862
|
+
const keyFindings = validateConfigKey(path2, obj);
|
|
863
|
+
findings.push(...keyFindings);
|
|
864
|
+
}
|
|
865
|
+
if (typeof obj !== "object") {
|
|
866
|
+
const keyFindings = validateConfigKey(path2, obj);
|
|
867
|
+
findings.push(...keyFindings);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (visited.has(obj)) {
|
|
871
|
+
findings.push({
|
|
872
|
+
id: "circular-reference",
|
|
873
|
+
title: `Circular reference detected at ${path2}`,
|
|
874
|
+
description: `Config value at "${path2}" contains a circular reference. Validation stopped at this path to prevent stack overflow.`,
|
|
875
|
+
severity: "error",
|
|
876
|
+
path: path2,
|
|
877
|
+
currentValue: "[circular]",
|
|
878
|
+
autoFixable: false
|
|
879
|
+
});
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
visited.add(obj);
|
|
883
|
+
if (Array.isArray(obj)) {
|
|
884
|
+
const arrayFindings = validateConfigKey(path2, obj);
|
|
885
|
+
findings.push(...arrayFindings);
|
|
886
|
+
obj.forEach((item, index) => {
|
|
887
|
+
walkConfigAndValidate(item, `${path2}[${index}]`, findings, visited);
|
|
888
|
+
});
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
892
|
+
const newPath = path2 ? `${path2}.${key}` : key;
|
|
893
|
+
walkConfigAndValidate(value, newPath, findings, visited);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function runConfigDoctor(config, directory) {
|
|
897
|
+
const findings = [];
|
|
898
|
+
walkConfigAndValidate(config, "", findings);
|
|
899
|
+
const summary = {
|
|
900
|
+
info: findings.filter((f) => f.severity === "info").length,
|
|
901
|
+
warn: findings.filter((f) => f.severity === "warn").length,
|
|
902
|
+
error: findings.filter((f) => f.severity === "error").length
|
|
903
|
+
};
|
|
904
|
+
const hasAutoFixableIssues = findings.some((f) => f.autoFixable && f.proposedFix?.risk === "low");
|
|
905
|
+
const { userConfigPath, projectConfigPath } = getConfigPaths(directory);
|
|
906
|
+
let configSource = "defaults";
|
|
907
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
908
|
+
configSource = projectConfigPath;
|
|
909
|
+
} else if (fs.existsSync(userConfigPath)) {
|
|
910
|
+
configSource = userConfigPath;
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
findings,
|
|
914
|
+
summary,
|
|
915
|
+
hasAutoFixableIssues,
|
|
916
|
+
timestamp: Date.now(),
|
|
917
|
+
configSource
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
var DANGEROUS_PATH_SEGMENTS = new Set([
|
|
921
|
+
"__proto__",
|
|
922
|
+
"constructor",
|
|
923
|
+
"prototype"
|
|
924
|
+
]);
|
|
925
|
+
function isDangerousPathSegment(segment) {
|
|
926
|
+
return DANGEROUS_PATH_SEGMENTS.has(segment);
|
|
927
|
+
}
|
|
928
|
+
function isPathSafe(fixPath) {
|
|
929
|
+
const segments = fixPath.split(".");
|
|
930
|
+
for (const segment of segments) {
|
|
931
|
+
if (isDangerousPathSegment(segment)) {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
function applySafeAutoFixes(directory, result) {
|
|
938
|
+
const appliedFixes = [];
|
|
939
|
+
let updatedConfigPath = null;
|
|
940
|
+
const { userConfigPath, projectConfigPath } = getConfigPaths(directory);
|
|
941
|
+
let configPath = projectConfigPath;
|
|
942
|
+
let configContent;
|
|
943
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
944
|
+
configPath = projectConfigPath;
|
|
945
|
+
configContent = fs.readFileSync(projectConfigPath, "utf-8");
|
|
946
|
+
} else if (fs.existsSync(userConfigPath)) {
|
|
947
|
+
configPath = userConfigPath;
|
|
948
|
+
configContent = fs.readFileSync(userConfigPath, "utf-8");
|
|
949
|
+
} else {
|
|
950
|
+
return { appliedFixes, updatedConfigPath: null };
|
|
951
|
+
}
|
|
952
|
+
let config;
|
|
953
|
+
try {
|
|
954
|
+
config = JSON.parse(configContent);
|
|
955
|
+
} catch {
|
|
956
|
+
return { appliedFixes, updatedConfigPath: null };
|
|
957
|
+
}
|
|
958
|
+
const safeFixes = result.findings.filter((f) => f.autoFixable && f.proposedFix?.risk === "low");
|
|
959
|
+
for (const finding of safeFixes) {
|
|
960
|
+
const fix = finding.proposedFix;
|
|
961
|
+
if (!fix)
|
|
962
|
+
continue;
|
|
963
|
+
if (!isPathSafe(fix.path)) {
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
const pathParts = fix.path.split(".");
|
|
967
|
+
let current = config;
|
|
968
|
+
let navigated = true;
|
|
969
|
+
for (let i = 0;i < pathParts.length - 1; i++) {
|
|
970
|
+
const part = pathParts[i];
|
|
971
|
+
if (current === null || current === undefined) {
|
|
972
|
+
navigated = false;
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
if (typeof current !== "object" || Array.isArray(current)) {
|
|
976
|
+
navigated = false;
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
const obj = current;
|
|
980
|
+
if (obj[part] === undefined) {
|
|
981
|
+
obj[part] = {};
|
|
982
|
+
} else if (obj[part] === null) {
|
|
983
|
+
navigated = false;
|
|
984
|
+
break;
|
|
985
|
+
} else if (typeof obj[part] !== "object") {
|
|
986
|
+
navigated = false;
|
|
987
|
+
break;
|
|
988
|
+
}
|
|
989
|
+
current = obj[part];
|
|
990
|
+
}
|
|
991
|
+
if (!navigated) {
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
const lastPart = pathParts[pathParts.length - 1];
|
|
995
|
+
switch (fix.type) {
|
|
996
|
+
case "remove":
|
|
997
|
+
if (current !== null && current !== undefined && typeof current === "object") {
|
|
998
|
+
delete current[lastPart];
|
|
999
|
+
appliedFixes.push(fix);
|
|
1000
|
+
}
|
|
1001
|
+
break;
|
|
1002
|
+
case "update":
|
|
1003
|
+
if (current !== null && current !== undefined && typeof current === "object") {
|
|
1004
|
+
current[lastPart] = fix.value;
|
|
1005
|
+
appliedFixes.push(fix);
|
|
1006
|
+
}
|
|
1007
|
+
break;
|
|
1008
|
+
case "add":
|
|
1009
|
+
if (current !== null && current !== undefined && typeof current === "object") {
|
|
1010
|
+
current[lastPart] = fix.value;
|
|
1011
|
+
appliedFixes.push(fix);
|
|
1012
|
+
}
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (appliedFixes.length > 0) {
|
|
1017
|
+
const configDir = path.dirname(configPath);
|
|
1018
|
+
if (!fs.existsSync(configDir)) {
|
|
1019
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1020
|
+
}
|
|
1021
|
+
atomicWriteFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1022
|
+
updatedConfigPath = configPath;
|
|
1023
|
+
}
|
|
1024
|
+
return { appliedFixes, updatedConfigPath };
|
|
1025
|
+
}
|
|
1026
|
+
function readDoctorArtifact(directory) {
|
|
1027
|
+
try {
|
|
1028
|
+
const artifactPath = path.join(directory, ".swarm", "config-doctor.json");
|
|
1029
|
+
if (!fs.existsSync(artifactPath)) {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
const content = fs.readFileSync(artifactPath, "utf-8");
|
|
1033
|
+
const artifact = JSON.parse(content);
|
|
1034
|
+
const summary = artifact.summary;
|
|
1035
|
+
if (!summary || typeof summary !== "object") {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
const infoVal = summary.info;
|
|
1039
|
+
const warnVal = summary.warn;
|
|
1040
|
+
const errorVal = summary.error;
|
|
1041
|
+
if (typeof infoVal !== "number" || !Number.isFinite(infoVal) || typeof warnVal !== "number" || !Number.isFinite(warnVal) || typeof errorVal !== "number" || !Number.isFinite(errorVal)) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
const ts = artifact.timestamp;
|
|
1045
|
+
if (typeof ts !== "number" || !Number.isFinite(ts)) {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
const findingsCount = infoVal + warnVal + errorVal;
|
|
1049
|
+
const findings = artifact.findings;
|
|
1050
|
+
const autoFixableCount = Array.isArray(findings) ? findings.filter((f) => f.autoFixable === true).length : 0;
|
|
1051
|
+
return {
|
|
1052
|
+
timestamp: new Date(ts).toISOString(),
|
|
1053
|
+
findingsCount,
|
|
1054
|
+
autoFixableCount
|
|
1055
|
+
};
|
|
1056
|
+
} catch {
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function writeDoctorArtifact(directory, result) {
|
|
1061
|
+
const swarmDir = path.join(directory, ".swarm");
|
|
1062
|
+
if (!fs.existsSync(swarmDir)) {
|
|
1063
|
+
fs.mkdirSync(swarmDir, { recursive: true });
|
|
1064
|
+
}
|
|
1065
|
+
const artifactFilename = "config-doctor.json";
|
|
1066
|
+
const artifactPath = path.join(swarmDir, artifactFilename);
|
|
1067
|
+
const guiOutput = {
|
|
1068
|
+
timestamp: result.timestamp,
|
|
1069
|
+
summary: result.summary,
|
|
1070
|
+
hasAutoFixableIssues: result.hasAutoFixableIssues,
|
|
1071
|
+
configSource: result.configSource,
|
|
1072
|
+
findings: result.findings.map((f) => ({
|
|
1073
|
+
id: f.id,
|
|
1074
|
+
title: f.title,
|
|
1075
|
+
description: f.description,
|
|
1076
|
+
severity: f.severity,
|
|
1077
|
+
path: f.path,
|
|
1078
|
+
autoFixable: f.autoFixable,
|
|
1079
|
+
proposedFix: f.proposedFix ? {
|
|
1080
|
+
type: f.proposedFix.type,
|
|
1081
|
+
path: f.proposedFix.path,
|
|
1082
|
+
description: f.proposedFix.description,
|
|
1083
|
+
risk: f.proposedFix.risk
|
|
1084
|
+
} : null
|
|
1085
|
+
}))
|
|
1086
|
+
};
|
|
1087
|
+
atomicWriteFileSync(artifactPath, JSON.stringify(guiOutput, null, 2));
|
|
1088
|
+
return artifactPath;
|
|
1089
|
+
}
|
|
1090
|
+
function shouldRunOnStartup(automationConfig) {
|
|
1091
|
+
if (!automationConfig) {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
if (automationConfig.mode === "manual") {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
return automationConfig.capabilities?.config_doctor_on_startup === true;
|
|
1098
|
+
}
|
|
1099
|
+
async function runConfigDoctorWithFixes(directory, config, autoFix = false) {
|
|
1100
|
+
const result = runConfigDoctor(config, directory);
|
|
1101
|
+
const artifactPath = writeDoctorArtifact(directory, result);
|
|
1102
|
+
if (!autoFix) {
|
|
1103
|
+
return {
|
|
1104
|
+
result,
|
|
1105
|
+
backupPath: null,
|
|
1106
|
+
appliedFixes: [],
|
|
1107
|
+
updatedConfigPath: null,
|
|
1108
|
+
artifactPath
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
const backup = createConfigBackup(directory);
|
|
1112
|
+
let backupPath = null;
|
|
1113
|
+
if (backup) {
|
|
1114
|
+
backupPath = writeBackupArtifact(directory, backup);
|
|
1115
|
+
}
|
|
1116
|
+
const { appliedFixes, updatedConfigPath } = applySafeAutoFixes(directory, result);
|
|
1117
|
+
if (appliedFixes.length > 0) {
|
|
1118
|
+
const freshConfig = readConfigFromFile(directory);
|
|
1119
|
+
if (freshConfig) {
|
|
1120
|
+
const newResult = runConfigDoctor(freshConfig.config, directory);
|
|
1121
|
+
writeDoctorArtifact(directory, newResult);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return {
|
|
1125
|
+
result,
|
|
1126
|
+
backupPath,
|
|
1127
|
+
appliedFixes,
|
|
1128
|
+
updatedConfigPath,
|
|
1129
|
+
artifactPath
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function detectStraySwarmDirs(projectRoot) {
|
|
1133
|
+
const findings = [];
|
|
1134
|
+
const SKIP_DIRS = new Set([
|
|
1135
|
+
"node_modules",
|
|
1136
|
+
".git",
|
|
1137
|
+
"dist",
|
|
1138
|
+
".cache",
|
|
1139
|
+
".next",
|
|
1140
|
+
"coverage",
|
|
1141
|
+
".turbo",
|
|
1142
|
+
".vercel",
|
|
1143
|
+
".terraform",
|
|
1144
|
+
"__pycache__",
|
|
1145
|
+
".tox"
|
|
1146
|
+
]);
|
|
1147
|
+
const MAX_DEPTH = 10;
|
|
1148
|
+
const MAX_CONTENTS_ENTRIES = 20;
|
|
1149
|
+
function walk(dir, depth) {
|
|
1150
|
+
if (depth > MAX_DEPTH)
|
|
1151
|
+
return;
|
|
1152
|
+
let entries;
|
|
1153
|
+
try {
|
|
1154
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1155
|
+
} catch {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
for (const entry of entries) {
|
|
1159
|
+
if (!entry.isDirectory())
|
|
1160
|
+
continue;
|
|
1161
|
+
const name = entry.name;
|
|
1162
|
+
const fullPath = path.join(dir, name);
|
|
1163
|
+
if (SKIP_DIRS.has(name))
|
|
1164
|
+
continue;
|
|
1165
|
+
const gitPath = path.join(fullPath, ".git");
|
|
1166
|
+
try {
|
|
1167
|
+
const gitStat = fs.statSync(gitPath);
|
|
1168
|
+
if (gitStat.isFile() || gitStat.isDirectory())
|
|
1169
|
+
continue;
|
|
1170
|
+
} catch {}
|
|
1171
|
+
if (name === ".swarm") {
|
|
1172
|
+
const parentDir = path.dirname(fullPath);
|
|
1173
|
+
if (parentDir === projectRoot)
|
|
1174
|
+
continue;
|
|
1175
|
+
let contents = [];
|
|
1176
|
+
try {
|
|
1177
|
+
contents = fs.readdirSync(fullPath);
|
|
1178
|
+
} catch {
|
|
1179
|
+
contents = ["<unreadable>"];
|
|
1180
|
+
}
|
|
1181
|
+
findings.push({
|
|
1182
|
+
path: path.relative(projectRoot, fullPath).replace(/\\/g, "/"),
|
|
1183
|
+
absolutePath: fullPath,
|
|
1184
|
+
contents: contents.slice(0, MAX_CONTENTS_ENTRIES),
|
|
1185
|
+
totalEntries: contents.length
|
|
1186
|
+
});
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
walk(fullPath, depth + 1);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
walk(projectRoot, 0);
|
|
1193
|
+
return findings;
|
|
1194
|
+
}
|
|
1195
|
+
function removeStraySwarmDir(projectRoot, strayPath) {
|
|
1196
|
+
let canonicalRoot;
|
|
1197
|
+
let canonicalStray;
|
|
1198
|
+
try {
|
|
1199
|
+
canonicalRoot = fs.realpathSync(projectRoot);
|
|
1200
|
+
canonicalStray = fs.realpathSync(path.isAbsolute(strayPath) ? strayPath : path.resolve(projectRoot, strayPath));
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
return {
|
|
1203
|
+
success: false,
|
|
1204
|
+
message: `Failed to resolve paths: ${err instanceof Error ? err.message : String(err)}`
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
const rootSwarm = path.join(canonicalRoot, ".swarm");
|
|
1208
|
+
if (canonicalStray === rootSwarm || canonicalStray === canonicalRoot) {
|
|
1209
|
+
return {
|
|
1210
|
+
success: false,
|
|
1211
|
+
message: "Refusing to remove root .swarm/ directory"
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
if (!canonicalStray.startsWith(canonicalRoot + path.sep)) {
|
|
1215
|
+
return {
|
|
1216
|
+
success: false,
|
|
1217
|
+
message: "Path is outside project root \u2014 refusing to remove"
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
const normalizedStray = canonicalStray.replace(/\\/g, "/");
|
|
1221
|
+
if (!normalizedStray.endsWith("/.swarm")) {
|
|
1222
|
+
return {
|
|
1223
|
+
success: false,
|
|
1224
|
+
message: "Path is not a .swarm directory \u2014 refusing to remove"
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
try {
|
|
1228
|
+
fs.rmSync(canonicalStray, { recursive: true, force: true });
|
|
1229
|
+
return {
|
|
1230
|
+
success: true,
|
|
1231
|
+
message: `Removed stray .swarm directory: ${canonicalStray}`
|
|
1232
|
+
};
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
return {
|
|
1235
|
+
success: false,
|
|
1236
|
+
message: `Failed to remove: ${err instanceof Error ? err.message : String(err)}`
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
export { getConfigPaths, createConfigBackup, writeBackupArtifact, restoreFromBackup, runConfigDoctor, applySafeAutoFixes, readDoctorArtifact, writeDoctorArtifact, shouldRunOnStartup, runConfigDoctorWithFixes, detectStraySwarmDirs, removeStraySwarmDir };
|