supipowers 1.2.6 → 1.5.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 +118 -56
- package/bin/install.ts +48 -128
- package/package.json +11 -3
- package/skills/code-review/SKILL.md +137 -40
- package/skills/context-mode/SKILL.md +67 -56
- package/skills/creating-supi-agents/SKILL.md +204 -0
- package/skills/debugging/SKILL.md +86 -40
- package/skills/fix-pr/SKILL.md +96 -65
- package/skills/planning/SKILL.md +103 -46
- package/skills/qa-strategy/SKILL.md +68 -46
- package/skills/receiving-code-review/SKILL.md +60 -53
- package/skills/release/SKILL.md +111 -39
- package/skills/tdd/SKILL.md +118 -67
- package/skills/verification/SKILL.md +71 -37
- package/src/bootstrap.ts +27 -5
- package/src/commands/agents.ts +249 -0
- package/src/commands/ai-review.ts +1113 -0
- package/src/commands/config.ts +224 -95
- package/src/commands/doctor.ts +19 -13
- package/src/commands/fix-pr.ts +8 -11
- package/src/commands/generate.ts +200 -0
- package/src/commands/model-picker.ts +5 -15
- package/src/commands/model.ts +4 -5
- package/src/commands/optimize-context.ts +202 -0
- package/src/commands/plan.ts +148 -92
- package/src/commands/qa.ts +14 -23
- package/src/commands/release.ts +504 -275
- package/src/commands/review.ts +643 -86
- package/src/commands/status.ts +44 -17
- package/src/commands/supi.ts +69 -41
- package/src/commands/update.ts +57 -2
- package/src/config/defaults.ts +6 -39
- package/src/config/loader.ts +388 -40
- package/src/config/model-resolver.ts +26 -22
- package/src/config/schema.ts +113 -48
- package/src/context/analyzer.ts +61 -2
- package/src/context/optimizer.ts +199 -0
- package/src/context-mode/compressor.ts +14 -11
- package/src/context-mode/detector.ts +16 -54
- package/src/context-mode/event-extractor.ts +45 -16
- package/src/context-mode/event-store.ts +225 -16
- package/src/context-mode/hooks.ts +195 -22
- package/src/context-mode/knowledge/chunker.ts +235 -0
- package/src/context-mode/knowledge/store.ts +187 -0
- package/src/context-mode/routing.ts +12 -23
- package/src/context-mode/sandbox/executor.ts +183 -0
- package/src/context-mode/sandbox/runners.ts +40 -0
- package/src/context-mode/snapshot-builder.ts +243 -7
- package/src/context-mode/tools.ts +440 -0
- package/src/context-mode/web/fetcher.ts +117 -0
- package/src/context-mode/web/html-to-md.ts +293 -0
- package/src/debug/logger.ts +107 -0
- package/src/deps/registry.ts +0 -20
- package/src/docs/drift.ts +454 -0
- package/src/fix-pr/fetch-comments.ts +66 -0
- package/src/git/commit-msg.ts +2 -1
- package/src/git/commit.ts +123 -141
- package/src/git/conventions.ts +2 -2
- package/src/git/status.ts +4 -1
- package/src/lsp/bridge.ts +138 -12
- package/src/planning/approval-flow.ts +125 -19
- package/src/planning/plan-writer-prompt.ts +4 -11
- package/src/planning/planning-ask-tool.ts +81 -0
- package/src/planning/prompt-builder.ts +9 -169
- package/src/planning/system-prompt.ts +290 -0
- package/src/platform/omp.ts +50 -4
- package/src/platform/progress.ts +182 -0
- package/src/platform/test-utils.ts +4 -1
- package/src/platform/tui-colors.ts +30 -0
- package/src/platform/types.ts +1 -0
- package/src/qa/detect-app-type.ts +102 -0
- package/src/qa/discover-routes.ts +353 -0
- package/src/quality/ai-session.ts +96 -0
- package/src/quality/ai-setup.ts +86 -0
- package/src/quality/gates/ai-review.ts +129 -0
- package/src/quality/gates/build.ts +8 -0
- package/src/quality/gates/command.ts +150 -0
- package/src/quality/gates/format.ts +28 -0
- package/src/quality/gates/lint.ts +22 -0
- package/src/quality/gates/lsp-diagnostics.ts +84 -0
- package/src/quality/gates/test-suite.ts +8 -0
- package/src/quality/gates/typecheck.ts +22 -0
- package/src/quality/registry.ts +25 -0
- package/src/quality/review-gates.ts +33 -0
- package/src/quality/runner.ts +268 -0
- package/src/quality/schemas.ts +48 -0
- package/src/quality/setup.ts +227 -0
- package/src/release/changelog.ts +7 -3
- package/src/release/channels/custom.ts +43 -0
- package/src/release/channels/gitea.ts +35 -0
- package/src/release/channels/github.ts +35 -0
- package/src/release/channels/gitlab.ts +35 -0
- package/src/release/channels/registry.ts +52 -0
- package/src/release/channels/types.ts +27 -0
- package/src/release/detector.ts +10 -63
- package/src/release/executor.ts +61 -51
- package/src/release/prompt.ts +38 -38
- package/src/release/version.ts +129 -10
- package/src/review/agent-loader.ts +331 -0
- package/src/review/consolidator.ts +180 -0
- package/src/review/default-agents/correctness.md +72 -0
- package/src/review/default-agents/maintainability.md +64 -0
- package/src/review/default-agents/security.md +67 -0
- package/src/review/fixer.ts +219 -0
- package/src/review/multi-agent-runner.ts +135 -0
- package/src/review/output.ts +147 -0
- package/src/review/prompts/agent-review-wrapper.md +36 -0
- package/src/review/prompts/fix-findings.md +32 -0
- package/src/review/prompts/fix-output-schema.md +18 -0
- package/src/review/prompts/invalid-output-retry.md +22 -0
- package/src/review/prompts/output-instructions.md +14 -0
- package/src/review/prompts/review-output-schema.md +38 -0
- package/src/review/prompts/single-review.md +53 -0
- package/src/review/prompts/validation-review.md +30 -0
- package/src/review/runner.ts +128 -0
- package/src/review/scope.ts +353 -0
- package/src/review/template.ts +15 -0
- package/src/review/types.ts +296 -0
- package/src/review/validator.ts +160 -0
- package/src/storage/plans.ts +5 -3
- package/src/storage/reports.ts +50 -7
- package/src/storage/review-sessions.ts +117 -0
- package/src/text.ts +19 -0
- package/src/types.ts +336 -26
- package/src/utils/paths.ts +39 -0
- package/src/visual/companion.ts +5 -3
- package/src/visual/start-server.ts +101 -0
- package/src/visual/stop-server.ts +39 -0
- package/bin/ctx-mode-wrapper.mjs +0 -66
- package/src/config/profiles.ts +0 -64
- package/src/context-mode/installer.ts +0 -38
- package/src/quality/ai-review-gate.ts +0 -43
- package/src/quality/gate-runner.ts +0 -67
- package/src/quality/lsp-gate.ts +0 -24
- package/src/quality/test-gate.ts +0 -39
- package/src/visual/scripts/start-server.sh +0 -98
- package/src/visual/scripts/stop-server.sh +0 -21
package/src/config/loader.ts
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
// src/config/loader.ts
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ConfigScope,
|
|
6
|
+
SupipowersConfig,
|
|
7
|
+
ReleaseChannel,
|
|
8
|
+
QualityGatesConfig,
|
|
9
|
+
} from "../types.js";
|
|
5
10
|
import type { PlatformPaths } from "../platform/types.js";
|
|
6
11
|
import { DEFAULT_CONFIG } from "./defaults.js";
|
|
12
|
+
import {
|
|
13
|
+
collectConfigValidationErrors,
|
|
14
|
+
type ConfigParseError,
|
|
15
|
+
type ConfigValidationError,
|
|
16
|
+
type InspectionLoadResult,
|
|
17
|
+
} from "./schema.js";
|
|
18
|
+
|
|
19
|
+
export interface ScopedConfigInspection {
|
|
20
|
+
scope: ConfigScope;
|
|
21
|
+
path: string;
|
|
22
|
+
data: Record<string, unknown> | null;
|
|
23
|
+
parseError: ConfigParseError | null;
|
|
24
|
+
validationErrors: ConfigValidationError[];
|
|
25
|
+
qualityGateValidationErrors: ConfigValidationError[];
|
|
26
|
+
otherValidationErrors: ConfigValidationError[];
|
|
27
|
+
hasOwnQualityGates: boolean;
|
|
28
|
+
recoverableInvalidQualityGates: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface QualityGateRecoveryInspection {
|
|
32
|
+
scopes: ScopedConfigInspection[];
|
|
33
|
+
}
|
|
7
34
|
|
|
8
35
|
function getProjectConfigPath(paths: PlatformPaths, cwd: string): string {
|
|
9
36
|
return paths.project(cwd, "config.json");
|
|
@@ -13,19 +40,39 @@ function getGlobalConfigPath(paths: PlatformPaths): string {
|
|
|
13
40
|
return paths.global("config.json");
|
|
14
41
|
}
|
|
15
42
|
|
|
16
|
-
function
|
|
43
|
+
function getConfigPath(paths: PlatformPaths, cwd: string, scope: ConfigScope): string {
|
|
44
|
+
return scope === "global" ? getGlobalConfigPath(paths) : getProjectConfigPath(paths, cwd);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readJsonFile(
|
|
48
|
+
source: ConfigParseError["source"],
|
|
49
|
+
filePath: string,
|
|
50
|
+
): { data: Record<string, unknown> | null; error: ConfigParseError | null } {
|
|
17
51
|
try {
|
|
18
|
-
if (!fs.existsSync(filePath))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
52
|
+
if (!fs.existsSync(filePath)) {
|
|
53
|
+
return { data: null, error: null };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
data: JSON.parse(fs.readFileSync(filePath, "utf-8")) as Record<string, unknown>,
|
|
58
|
+
error: null,
|
|
59
|
+
};
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
data: null,
|
|
63
|
+
error: {
|
|
64
|
+
source,
|
|
65
|
+
path: filePath,
|
|
66
|
+
message: (error as Error).message,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
22
69
|
}
|
|
23
70
|
}
|
|
24
71
|
|
|
25
72
|
/** Deep merge source into target. Source values override target. */
|
|
26
73
|
export function deepMerge<T extends object>(
|
|
27
74
|
target: T,
|
|
28
|
-
source: Record<string, unknown
|
|
75
|
+
source: Record<string, unknown>,
|
|
29
76
|
): T {
|
|
30
77
|
const result = { ...target } as Record<string, unknown>;
|
|
31
78
|
for (const key of Object.keys(source)) {
|
|
@@ -41,7 +88,7 @@ export function deepMerge<T extends object>(
|
|
|
41
88
|
) {
|
|
42
89
|
result[key] = deepMerge(
|
|
43
90
|
targetVal as Record<string, unknown>,
|
|
44
|
-
sourceVal as Record<string, unknown
|
|
91
|
+
sourceVal as Record<string, unknown>,
|
|
45
92
|
);
|
|
46
93
|
} else {
|
|
47
94
|
result[key] = sourceVal;
|
|
@@ -50,58 +97,359 @@ export function deepMerge<T extends object>(
|
|
|
50
97
|
return result as T;
|
|
51
98
|
}
|
|
52
99
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
100
|
+
function hasOwnNestedProperty(
|
|
101
|
+
value: Record<string, unknown>,
|
|
102
|
+
topLevelKey: string,
|
|
103
|
+
nestedKey: string,
|
|
104
|
+
): boolean {
|
|
105
|
+
if (!(topLevelKey in value)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const nested = value[topLevelKey];
|
|
110
|
+
return !!nested && typeof nested === "object" && nestedKey in (nested as Record<string, unknown>);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function applyConfigOverride(
|
|
114
|
+
base: Record<string, unknown>,
|
|
115
|
+
override: Record<string, unknown>,
|
|
116
|
+
): Record<string, unknown> {
|
|
117
|
+
const merged = deepMerge(base, override) as Record<string, unknown>;
|
|
118
|
+
|
|
119
|
+
if (hasOwnNestedProperty(override, "quality", "gates")) {
|
|
120
|
+
const mergedQuality =
|
|
121
|
+
merged.quality && typeof merged.quality === "object" && !Array.isArray(merged.quality)
|
|
122
|
+
? (merged.quality as Record<string, unknown>)
|
|
123
|
+
: {};
|
|
124
|
+
const overrideQuality = override.quality as Record<string, unknown>;
|
|
125
|
+
mergedQuality.gates = overrideQuality.gates;
|
|
126
|
+
merged.quality = mergedQuality;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return merged;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Known legacy config shapes are normalized before validation. */
|
|
133
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
134
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
135
|
+
? (value as Record<string, unknown>)
|
|
136
|
+
: null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeReleaseChannels(
|
|
140
|
+
existingChannels: unknown,
|
|
141
|
+
pipeline: string | null,
|
|
142
|
+
): ReleaseChannel[] {
|
|
143
|
+
if (Array.isArray(existingChannels)) {
|
|
144
|
+
const channels = existingChannels.filter(
|
|
145
|
+
(channel): channel is string => typeof channel === "string" && channel.length > 0,
|
|
146
|
+
);
|
|
147
|
+
if (channels.length > 0) {
|
|
148
|
+
return [...new Set(channels)];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof pipeline === "string" && pipeline.length > 0) return [pipeline];
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function legacyGatesFromProfile(
|
|
157
|
+
profileName: string | null,
|
|
158
|
+
legacyTestCommand: string | null,
|
|
159
|
+
): QualityGatesConfig | null {
|
|
160
|
+
const gates: QualityGatesConfig = {};
|
|
161
|
+
|
|
162
|
+
if (profileName === "quick" || profileName === "thorough" || profileName === "full-regression") {
|
|
163
|
+
gates["lsp-diagnostics"] = { enabled: true };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (legacyTestCommand) {
|
|
167
|
+
gates["test-suite"] = { enabled: true, command: legacyTestCommand };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return Object.keys(gates).length > 0 ? gates : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Migrate config from older shapes to the canonical schema. */
|
|
174
|
+
function migrateConfig(config: Record<string, unknown>): Record<string, unknown> {
|
|
175
|
+
const migrated = structuredClone(config) as Record<string, unknown>;
|
|
176
|
+
const legacyProfile = typeof migrated.defaultProfile === "string" ? migrated.defaultProfile : null;
|
|
177
|
+
delete migrated.defaultProfile;
|
|
178
|
+
delete migrated.orchestration;
|
|
179
|
+
|
|
180
|
+
const qa = asRecord(migrated.qa);
|
|
181
|
+
const legacyTestCommand =
|
|
182
|
+
qa && typeof qa.command === "string" && qa.command.trim().length > 0
|
|
183
|
+
? qa.command.trim()
|
|
184
|
+
: null;
|
|
185
|
+
if (qa && "command" in qa) {
|
|
186
|
+
delete qa.command;
|
|
187
|
+
migrated.qa = qa;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const release = asRecord(migrated.release);
|
|
191
|
+
if (release) {
|
|
192
|
+
if ("pipeline" in release || "channels" in release) {
|
|
193
|
+
const pipeline = typeof release.pipeline === "string" ? release.pipeline : null;
|
|
194
|
+
release.channels = normalizeReleaseChannels(release.channels, pipeline);
|
|
195
|
+
delete release.pipeline;
|
|
196
|
+
}
|
|
197
|
+
// Strip legacy "npm" values that may linger in saved configs
|
|
198
|
+
if (Array.isArray(release.channels)) {
|
|
199
|
+
release.channels = (release.channels as string[]).filter((c) => c !== "npm");
|
|
200
|
+
}
|
|
201
|
+
migrated.release = release;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const quality = asRecord(migrated.quality);
|
|
205
|
+
if (quality) {
|
|
206
|
+
const gates = asRecord(quality.gates);
|
|
207
|
+
if (gates) {
|
|
208
|
+
// Strip legacy ai-review gate — removed from the schema in the checks/review split.
|
|
209
|
+
delete gates["ai-review"];
|
|
210
|
+
}
|
|
211
|
+
if (!gates || Object.keys(gates).length === 0) {
|
|
212
|
+
const legacyGates = legacyGatesFromProfile(legacyProfile, legacyTestCommand);
|
|
213
|
+
if (legacyGates) {
|
|
214
|
+
quality.gates = legacyGates as unknown as Record<string, unknown>;
|
|
215
|
+
}
|
|
216
|
+
} else if (legacyTestCommand && !("test-suite" in gates)) {
|
|
217
|
+
gates["test-suite"] = { enabled: true, command: legacyTestCommand };
|
|
218
|
+
quality.gates = gates;
|
|
219
|
+
}
|
|
220
|
+
migrated.quality = quality;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
migrated.version = DEFAULT_CONFIG.version;
|
|
224
|
+
return migrated;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function mergeConfigLayers(
|
|
228
|
+
defaults: SupipowersConfig,
|
|
229
|
+
globalData: Record<string, unknown> | null,
|
|
230
|
+
projectData: Record<string, unknown> | null,
|
|
231
|
+
): Record<string, unknown> {
|
|
232
|
+
let merged = structuredClone(defaults) as unknown as Record<string, unknown>;
|
|
233
|
+
|
|
234
|
+
if (globalData) {
|
|
235
|
+
merged = applyConfigOverride(merged, globalData);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (projectData) {
|
|
239
|
+
merged = applyConfigOverride(merged, projectData);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// The config schema changed without a version bump, so normalize known
|
|
243
|
+
// legacy fields on every load before strict validation runs.
|
|
244
|
+
merged = migrateConfig(merged);
|
|
245
|
+
|
|
246
|
+
return merged;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function inspectScopeConfig(
|
|
250
|
+
paths: PlatformPaths,
|
|
251
|
+
cwd: string,
|
|
252
|
+
scope: ConfigScope,
|
|
253
|
+
): ScopedConfigInspection {
|
|
254
|
+
const filePath = getConfigPath(paths, cwd, scope);
|
|
255
|
+
const readResult = readJsonFile(scope, filePath);
|
|
256
|
+
|
|
257
|
+
if (readResult.error) {
|
|
258
|
+
return {
|
|
259
|
+
scope,
|
|
260
|
+
path: filePath,
|
|
261
|
+
data: null,
|
|
262
|
+
parseError: readResult.error,
|
|
263
|
+
validationErrors: [],
|
|
264
|
+
qualityGateValidationErrors: [],
|
|
265
|
+
otherValidationErrors: [],
|
|
266
|
+
hasOwnQualityGates: false,
|
|
267
|
+
recoverableInvalidQualityGates: false,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const hasOwnQualityGates = !!readResult.data && hasOwnNestedProperty(readResult.data, "quality", "gates");
|
|
272
|
+
const mergedConfig =
|
|
273
|
+
scope === "global"
|
|
274
|
+
? mergeConfigLayers(DEFAULT_CONFIG, readResult.data, null)
|
|
275
|
+
: mergeConfigLayers(DEFAULT_CONFIG, null, readResult.data);
|
|
276
|
+
const validationErrors = collectConfigValidationErrors(mergedConfig);
|
|
277
|
+
const qualityGateValidationErrors = hasOwnQualityGates
|
|
278
|
+
? validationErrors.filter((error) => error.path === "quality.gates" || error.path.startsWith("quality.gates."))
|
|
279
|
+
: [];
|
|
280
|
+
const otherValidationErrors = validationErrors.filter(
|
|
281
|
+
(error) => !qualityGateValidationErrors.includes(error),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
scope,
|
|
286
|
+
path: filePath,
|
|
287
|
+
data: readResult.data,
|
|
288
|
+
parseError: null,
|
|
289
|
+
validationErrors,
|
|
290
|
+
qualityGateValidationErrors,
|
|
291
|
+
otherValidationErrors,
|
|
292
|
+
hasOwnQualityGates,
|
|
293
|
+
recoverableInvalidQualityGates:
|
|
294
|
+
hasOwnQualityGates && qualityGateValidationErrors.length > 0 && otherValidationErrors.length === 0,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function removeQualityGatesFromRecord(config: Record<string, unknown>): Record<string, unknown> {
|
|
299
|
+
const next = structuredClone(config) as Record<string, unknown>;
|
|
300
|
+
const quality = asRecord(next.quality);
|
|
301
|
+
if (!quality || !("gates" in quality)) {
|
|
302
|
+
return next;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
delete quality.gates;
|
|
306
|
+
if (Object.keys(quality).length === 0) {
|
|
307
|
+
delete next.quality;
|
|
308
|
+
} else {
|
|
309
|
+
next.quality = quality;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return next;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function writeRawConfigFile(filePath: string, config: Record<string, unknown>): void {
|
|
316
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
317
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function inspectQualityGateRecovery(
|
|
321
|
+
paths: PlatformPaths,
|
|
322
|
+
cwd: string,
|
|
323
|
+
): QualityGateRecoveryInspection {
|
|
324
|
+
return {
|
|
325
|
+
scopes: (["global", "project"] as ConfigScope[]).map((scope) =>
|
|
326
|
+
inspectScopeConfig(paths, cwd, scope),
|
|
327
|
+
),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
58
330
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
331
|
+
export function writeQualityGatesConfig(
|
|
332
|
+
paths: PlatformPaths,
|
|
333
|
+
cwd: string,
|
|
334
|
+
scope: ConfigScope,
|
|
335
|
+
gates: QualityGatesConfig,
|
|
336
|
+
): void {
|
|
337
|
+
const configPath = getConfigPath(paths, cwd, scope);
|
|
338
|
+
const current = readJsonFile(scope, configPath);
|
|
339
|
+
if (current.error) {
|
|
340
|
+
throw new Error(`${scope} config ${configPath}: ${current.error.message}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const next = current.data ? structuredClone(current.data) as Record<string, unknown> : {};
|
|
344
|
+
const quality =
|
|
345
|
+
next.quality && typeof next.quality === "object" && !Array.isArray(next.quality)
|
|
346
|
+
? { ...(next.quality as Record<string, unknown>) }
|
|
347
|
+
: {};
|
|
348
|
+
|
|
349
|
+
quality.gates = gates;
|
|
350
|
+
next.quality = quality;
|
|
351
|
+
writeRawConfigFile(configPath, next);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function removeQualityGatesConfig(
|
|
355
|
+
paths: PlatformPaths,
|
|
356
|
+
cwd: string,
|
|
357
|
+
scope: ConfigScope,
|
|
358
|
+
): boolean {
|
|
359
|
+
const configPath = getConfigPath(paths, cwd, scope);
|
|
360
|
+
const current = readJsonFile(scope, configPath);
|
|
361
|
+
if (current.error) {
|
|
362
|
+
throw new Error(`${scope} config ${configPath}: ${current.error.message}`);
|
|
363
|
+
}
|
|
364
|
+
if (!current.data || !hasOwnNestedProperty(current.data, "quality", "gates")) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
writeRawConfigFile(configPath, removeQualityGatesFromRecord(current.data));
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function formatConfigErrors(result: InspectionLoadResult): string {
|
|
373
|
+
const messages = [
|
|
374
|
+
...result.parseErrors.map(
|
|
375
|
+
(error) => `${error.source} config ${error.path}: ${error.message}`,
|
|
376
|
+
),
|
|
377
|
+
...result.validationErrors.map(
|
|
378
|
+
(error) => `${error.path}: ${error.message}`,
|
|
379
|
+
),
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
return messages.join("\n") || "Unknown config error";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function inspectConfig(paths: PlatformPaths, cwd: string): InspectionLoadResult {
|
|
386
|
+
const globalRead = readJsonFile("global", getGlobalConfigPath(paths));
|
|
387
|
+
const projectRead = readJsonFile("project", getProjectConfigPath(paths, cwd));
|
|
388
|
+
const mergedConfig = mergeConfigLayers(DEFAULT_CONFIG, globalRead.data, projectRead.data);
|
|
389
|
+
const parseErrors = [globalRead.error, projectRead.error].filter(
|
|
390
|
+
(error): error is ConfigParseError => error !== null,
|
|
391
|
+
);
|
|
392
|
+
const validationErrors = collectConfigValidationErrors(mergedConfig);
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
mergedConfig,
|
|
396
|
+
effectiveConfig:
|
|
397
|
+
parseErrors.length === 0 && validationErrors.length === 0
|
|
398
|
+
? (mergedConfig as unknown as SupipowersConfig)
|
|
399
|
+
: null,
|
|
400
|
+
parseErrors,
|
|
401
|
+
validationErrors,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Load config with global -> project layering over defaults. */
|
|
406
|
+
export function loadConfig(paths: PlatformPaths, cwd: string): SupipowersConfig {
|
|
407
|
+
const result = inspectConfig(paths, cwd);
|
|
62
408
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
config = migrateConfig(config);
|
|
66
|
-
// Persist migrated config if project-level exists
|
|
67
|
-
if (projectData) saveConfig(paths, cwd, config);
|
|
409
|
+
if (!result.effectiveConfig) {
|
|
410
|
+
throw new Error(formatConfigErrors(result));
|
|
68
411
|
}
|
|
69
412
|
|
|
70
|
-
return
|
|
413
|
+
return result.effectiveConfig;
|
|
71
414
|
}
|
|
72
415
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const raw = config as Record<string, any>;
|
|
416
|
+
function assertValidConfig(data: unknown): void {
|
|
417
|
+
const validationErrors = collectConfigValidationErrors(data);
|
|
76
418
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const pipeline = raw.release.pipeline as string | null;
|
|
80
|
-
let channels: ReleaseChannel[] = [];
|
|
81
|
-
if (pipeline === "npm") channels = ["npm"];
|
|
82
|
-
else if (pipeline === "github") channels = ["github"];
|
|
83
|
-
// "manual" and null both map to empty array
|
|
84
|
-
raw.release = { channels };
|
|
419
|
+
if (validationErrors.length === 0) {
|
|
420
|
+
return;
|
|
85
421
|
}
|
|
86
422
|
|
|
87
|
-
|
|
423
|
+
throw new Error(
|
|
424
|
+
validationErrors
|
|
425
|
+
.map((error) => `${error.path}: ${error.message}`)
|
|
426
|
+
.join("\n"),
|
|
427
|
+
);
|
|
88
428
|
}
|
|
89
429
|
|
|
90
|
-
|
|
430
|
+
|
|
431
|
+
/** Save project-level config. */
|
|
91
432
|
export function saveConfig(paths: PlatformPaths, cwd: string, config: SupipowersConfig): void {
|
|
433
|
+
assertValidConfig(config);
|
|
434
|
+
|
|
92
435
|
const configPath = getProjectConfigPath(paths, cwd);
|
|
93
436
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
94
437
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
95
438
|
}
|
|
96
439
|
|
|
97
|
-
/** Update specific config fields (deep merge into current) */
|
|
440
|
+
/** Update specific config fields (deep merge into current). */
|
|
98
441
|
export function updateConfig(
|
|
99
442
|
paths: PlatformPaths,
|
|
100
443
|
cwd: string,
|
|
101
|
-
updates: Record<string, unknown
|
|
444
|
+
updates: Record<string, unknown>,
|
|
102
445
|
): SupipowersConfig {
|
|
103
446
|
const current = loadConfig(paths, cwd);
|
|
104
|
-
const updated =
|
|
105
|
-
|
|
106
|
-
|
|
447
|
+
const updated = applyConfigOverride(
|
|
448
|
+
structuredClone(current) as unknown as Record<string, unknown>,
|
|
449
|
+
updates,
|
|
450
|
+
);
|
|
451
|
+
assertValidConfig(updated);
|
|
452
|
+
|
|
453
|
+
saveConfig(paths, cwd, updated as unknown as SupipowersConfig);
|
|
454
|
+
return updated as unknown as SupipowersConfig;
|
|
107
455
|
}
|
|
@@ -123,31 +123,33 @@ export function createModelBridge(platform: Platform): ModelPlatformBridge {
|
|
|
123
123
|
* @param ctx - Command handler context (must have modelRegistry.getAvailable())
|
|
124
124
|
* @param actionId - The action being configured (e.g. "plan", "review") — used in notification
|
|
125
125
|
* @param resolved - The resolved model from resolveModelForAction()
|
|
126
|
-
* @returns
|
|
126
|
+
* @returns cleanup function — call in `finally` to clear the status bar and restore the
|
|
127
|
+
* original model. Safe to call multiple times (idempotent). Returns a no-op if nothing
|
|
128
|
+
* was applied.
|
|
127
129
|
*/
|
|
128
130
|
export async function applyModelOverride(
|
|
129
131
|
platform: Platform,
|
|
130
132
|
ctx: any,
|
|
131
133
|
actionId: string,
|
|
132
134
|
resolved: ResolvedModel,
|
|
133
|
-
): Promise<
|
|
135
|
+
): Promise<() => Promise<void>> {
|
|
134
136
|
// Skip if resolution fell through to the main session model (nothing to change)
|
|
135
|
-
if (resolved.source === "main") return
|
|
137
|
+
if (resolved.source === "main") return async () => {};
|
|
136
138
|
|
|
137
139
|
const modelId = resolved.model;
|
|
138
|
-
if (!modelId) return
|
|
140
|
+
if (!modelId) return async () => {};
|
|
139
141
|
|
|
140
142
|
// Apply thinking level (independent of model switch success)
|
|
141
143
|
if (resolved.thinkingLevel && platform.setThinkingLevel) {
|
|
142
144
|
platform.setThinkingLevel(resolved.thinkingLevel);
|
|
143
145
|
}
|
|
144
146
|
|
|
145
|
-
if (!platform.setModel) return
|
|
147
|
+
if (!platform.setModel) return async () => {};
|
|
146
148
|
|
|
147
149
|
// Resolve string model ID to full OMP Model object via the context's model registry.
|
|
148
150
|
// OMP's setModel expects a Model object (with provider, id, api, etc.), not a string.
|
|
149
151
|
const available = ctx.modelRegistry?.getAvailable?.() as any[] | undefined;
|
|
150
|
-
if (!available) return
|
|
152
|
+
if (!available) return async () => {};
|
|
151
153
|
|
|
152
154
|
const modelObj = available.find((m: any) => {
|
|
153
155
|
if (!m?.id) return false;
|
|
@@ -156,7 +158,7 @@ export async function applyModelOverride(
|
|
|
156
158
|
return modelId.includes("/") ? false : m.id === modelId;
|
|
157
159
|
});
|
|
158
160
|
|
|
159
|
-
if (!modelObj) return
|
|
161
|
+
if (!modelObj) return async () => {};
|
|
160
162
|
|
|
161
163
|
// Save current model so we can restore after the agent turn completes.
|
|
162
164
|
// OMP's extension API setModel() persists to settings (calls session.setModel,
|
|
@@ -165,7 +167,7 @@ export async function applyModelOverride(
|
|
|
165
167
|
const originalModel = ctx.model;
|
|
166
168
|
|
|
167
169
|
const applied = await platform.setModel(modelObj);
|
|
168
|
-
if (!applied) return
|
|
170
|
+
if (!applied) return async () => {};
|
|
169
171
|
|
|
170
172
|
// Show persistent model override info in the footer status bar.
|
|
171
173
|
// ctx.ui.notify() is transient and gets immediately replaced by progress widgets;
|
|
@@ -182,19 +184,21 @@ export async function applyModelOverride(
|
|
|
182
184
|
}
|
|
183
185
|
ctx.ui?.setStatus?.(STATUS_KEY, `Model: ${displayName} (${detail})`);
|
|
184
186
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
187
|
+
// Cleanup: clear the status bar entry and restore the original model.
|
|
188
|
+
// Idempotent — safe to call multiple times.
|
|
189
|
+
let cleaned = false;
|
|
190
|
+
const cleanup = async () => {
|
|
191
|
+
if (cleaned) return;
|
|
192
|
+
cleaned = true;
|
|
193
|
+
ctx.ui?.setStatus?.(STATUS_KEY, undefined);
|
|
194
|
+
if (originalModel) {
|
|
195
|
+
await platform.setModel!(originalModel);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Safety net for agent-driven commands: if the caller hands off to an OMP agent
|
|
200
|
+
// session and never calls cleanup explicitly, agent_end will still clear the status.
|
|
201
|
+
platform.on("agent_end", cleanup);
|
|
198
202
|
|
|
199
|
-
return
|
|
203
|
+
return cleanup;
|
|
200
204
|
}
|