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.
Files changed (137) hide show
  1. package/README.md +118 -56
  2. package/bin/install.ts +48 -128
  3. package/package.json +11 -3
  4. package/skills/code-review/SKILL.md +137 -40
  5. package/skills/context-mode/SKILL.md +67 -56
  6. package/skills/creating-supi-agents/SKILL.md +204 -0
  7. package/skills/debugging/SKILL.md +86 -40
  8. package/skills/fix-pr/SKILL.md +96 -65
  9. package/skills/planning/SKILL.md +103 -46
  10. package/skills/qa-strategy/SKILL.md +68 -46
  11. package/skills/receiving-code-review/SKILL.md +60 -53
  12. package/skills/release/SKILL.md +111 -39
  13. package/skills/tdd/SKILL.md +118 -67
  14. package/skills/verification/SKILL.md +71 -37
  15. package/src/bootstrap.ts +27 -5
  16. package/src/commands/agents.ts +249 -0
  17. package/src/commands/ai-review.ts +1113 -0
  18. package/src/commands/config.ts +224 -95
  19. package/src/commands/doctor.ts +19 -13
  20. package/src/commands/fix-pr.ts +8 -11
  21. package/src/commands/generate.ts +200 -0
  22. package/src/commands/model-picker.ts +5 -15
  23. package/src/commands/model.ts +4 -5
  24. package/src/commands/optimize-context.ts +202 -0
  25. package/src/commands/plan.ts +148 -92
  26. package/src/commands/qa.ts +14 -23
  27. package/src/commands/release.ts +504 -275
  28. package/src/commands/review.ts +643 -86
  29. package/src/commands/status.ts +44 -17
  30. package/src/commands/supi.ts +69 -41
  31. package/src/commands/update.ts +57 -2
  32. package/src/config/defaults.ts +6 -39
  33. package/src/config/loader.ts +388 -40
  34. package/src/config/model-resolver.ts +26 -22
  35. package/src/config/schema.ts +113 -48
  36. package/src/context/analyzer.ts +61 -2
  37. package/src/context/optimizer.ts +199 -0
  38. package/src/context-mode/compressor.ts +14 -11
  39. package/src/context-mode/detector.ts +16 -54
  40. package/src/context-mode/event-extractor.ts +45 -16
  41. package/src/context-mode/event-store.ts +225 -16
  42. package/src/context-mode/hooks.ts +195 -22
  43. package/src/context-mode/knowledge/chunker.ts +235 -0
  44. package/src/context-mode/knowledge/store.ts +187 -0
  45. package/src/context-mode/routing.ts +12 -23
  46. package/src/context-mode/sandbox/executor.ts +183 -0
  47. package/src/context-mode/sandbox/runners.ts +40 -0
  48. package/src/context-mode/snapshot-builder.ts +243 -7
  49. package/src/context-mode/tools.ts +440 -0
  50. package/src/context-mode/web/fetcher.ts +117 -0
  51. package/src/context-mode/web/html-to-md.ts +293 -0
  52. package/src/debug/logger.ts +107 -0
  53. package/src/deps/registry.ts +0 -20
  54. package/src/docs/drift.ts +454 -0
  55. package/src/fix-pr/fetch-comments.ts +66 -0
  56. package/src/git/commit-msg.ts +2 -1
  57. package/src/git/commit.ts +123 -141
  58. package/src/git/conventions.ts +2 -2
  59. package/src/git/status.ts +4 -1
  60. package/src/lsp/bridge.ts +138 -12
  61. package/src/planning/approval-flow.ts +125 -19
  62. package/src/planning/plan-writer-prompt.ts +4 -11
  63. package/src/planning/planning-ask-tool.ts +81 -0
  64. package/src/planning/prompt-builder.ts +9 -169
  65. package/src/planning/system-prompt.ts +290 -0
  66. package/src/platform/omp.ts +50 -4
  67. package/src/platform/progress.ts +182 -0
  68. package/src/platform/test-utils.ts +4 -1
  69. package/src/platform/tui-colors.ts +30 -0
  70. package/src/platform/types.ts +1 -0
  71. package/src/qa/detect-app-type.ts +102 -0
  72. package/src/qa/discover-routes.ts +353 -0
  73. package/src/quality/ai-session.ts +96 -0
  74. package/src/quality/ai-setup.ts +86 -0
  75. package/src/quality/gates/ai-review.ts +129 -0
  76. package/src/quality/gates/build.ts +8 -0
  77. package/src/quality/gates/command.ts +150 -0
  78. package/src/quality/gates/format.ts +28 -0
  79. package/src/quality/gates/lint.ts +22 -0
  80. package/src/quality/gates/lsp-diagnostics.ts +84 -0
  81. package/src/quality/gates/test-suite.ts +8 -0
  82. package/src/quality/gates/typecheck.ts +22 -0
  83. package/src/quality/registry.ts +25 -0
  84. package/src/quality/review-gates.ts +33 -0
  85. package/src/quality/runner.ts +268 -0
  86. package/src/quality/schemas.ts +48 -0
  87. package/src/quality/setup.ts +227 -0
  88. package/src/release/changelog.ts +7 -3
  89. package/src/release/channels/custom.ts +43 -0
  90. package/src/release/channels/gitea.ts +35 -0
  91. package/src/release/channels/github.ts +35 -0
  92. package/src/release/channels/gitlab.ts +35 -0
  93. package/src/release/channels/registry.ts +52 -0
  94. package/src/release/channels/types.ts +27 -0
  95. package/src/release/detector.ts +10 -63
  96. package/src/release/executor.ts +61 -51
  97. package/src/release/prompt.ts +38 -38
  98. package/src/release/version.ts +129 -10
  99. package/src/review/agent-loader.ts +331 -0
  100. package/src/review/consolidator.ts +180 -0
  101. package/src/review/default-agents/correctness.md +72 -0
  102. package/src/review/default-agents/maintainability.md +64 -0
  103. package/src/review/default-agents/security.md +67 -0
  104. package/src/review/fixer.ts +219 -0
  105. package/src/review/multi-agent-runner.ts +135 -0
  106. package/src/review/output.ts +147 -0
  107. package/src/review/prompts/agent-review-wrapper.md +36 -0
  108. package/src/review/prompts/fix-findings.md +32 -0
  109. package/src/review/prompts/fix-output-schema.md +18 -0
  110. package/src/review/prompts/invalid-output-retry.md +22 -0
  111. package/src/review/prompts/output-instructions.md +14 -0
  112. package/src/review/prompts/review-output-schema.md +38 -0
  113. package/src/review/prompts/single-review.md +53 -0
  114. package/src/review/prompts/validation-review.md +30 -0
  115. package/src/review/runner.ts +128 -0
  116. package/src/review/scope.ts +353 -0
  117. package/src/review/template.ts +15 -0
  118. package/src/review/types.ts +296 -0
  119. package/src/review/validator.ts +160 -0
  120. package/src/storage/plans.ts +5 -3
  121. package/src/storage/reports.ts +50 -7
  122. package/src/storage/review-sessions.ts +117 -0
  123. package/src/text.ts +19 -0
  124. package/src/types.ts +336 -26
  125. package/src/utils/paths.ts +39 -0
  126. package/src/visual/companion.ts +5 -3
  127. package/src/visual/start-server.ts +101 -0
  128. package/src/visual/stop-server.ts +39 -0
  129. package/bin/ctx-mode-wrapper.mjs +0 -66
  130. package/src/config/profiles.ts +0 -64
  131. package/src/context-mode/installer.ts +0 -38
  132. package/src/quality/ai-review-gate.ts +0 -43
  133. package/src/quality/gate-runner.ts +0 -67
  134. package/src/quality/lsp-gate.ts +0 -24
  135. package/src/quality/test-gate.ts +0 -39
  136. package/src/visual/scripts/start-server.sh +0 -98
  137. package/src/visual/scripts/stop-server.sh +0 -21
@@ -1,116 +1,673 @@
1
1
  import type { Platform } from "../platform/types.js";
2
- import { loadConfig } from "../config/loader.js";
3
- import { listProfiles, resolveProfile } from "../config/profiles.js";
4
- import { buildReviewPrompt } from "../quality/gate-runner.js";
5
- import { isLspAvailable } from "../lsp/detector.js";
6
- import { notifyInfo, notifyWarning } from "../notifications/renderer.js";
2
+ import {
3
+ inspectConfig,
4
+ inspectQualityGateRecovery,
5
+ loadConfig,
6
+ removeQualityGatesConfig,
7
+ } from "../config/loader.js";
8
+ import { notifyInfo, notifyError } from "../notifications/renderer.js";
7
9
  import { modelRegistry } from "../config/model-registry-instance.js";
8
10
  import { resolveModelForAction, createModelBridge, applyModelOverride } from "../config/model-resolver.js";
9
11
  import { loadModelConfig } from "../config/model-config.js";
12
+ import { createWorkflowProgress } from "../platform/progress.js";
13
+ import { runQualityGates, type ReviewRunEvent } from "../quality/runner.js";
14
+ import {
15
+ interactivelySaveGateSetup,
16
+ setupGates,
17
+ } from "../quality/setup.js";
18
+ import type {
19
+ ConfigScope,
20
+ GateFilters,
21
+ GateId,
22
+ GateResult,
23
+ QualityGatesConfig,
24
+ ReviewReport,
25
+ SupipowersConfig,
26
+ } from "../types.js";
27
+ import { CANONICAL_GATE_ORDER, GATE_DISPLAY_NAMES } from "../quality/registry.js";
28
+ import { REVIEW_GATE_REGISTRY } from "../quality/review-gates.js";
29
+ import { saveReviewReport } from "../storage/reports.js";
10
30
 
11
31
  modelRegistry.register({
12
- id: "review",
32
+ id: "checks",
13
33
  category: "command",
14
- label: "Review",
34
+ label: "Checks",
15
35
  harnessRoleHint: "slow",
16
36
  });
17
37
 
18
- export function registerReviewCommand(platform: Platform): void {
19
- platform.registerCommand("supi:review", {
20
- description: "Run quality gates at chosen depth (quick/thorough/full-regression)",
21
- async handler(args: string | undefined, ctx: any) {
22
- // Resolve and apply model override early — before any logic that might fail
23
- const modelCfg = loadModelConfig(platform.paths, ctx.cwd);
24
- const bridge = createModelBridge(platform);
25
- const resolved = resolveModelForAction("review", modelRegistry, modelCfg, bridge);
26
- await applyModelOverride(platform, ctx, "review", resolved);
27
-
28
- const config = loadConfig(platform.paths, ctx.cwd);
29
-
30
- let profileOverride: string | undefined;
31
- if (args?.includes("--quick")) profileOverride = "quick";
32
- else if (args?.includes("--thorough")) profileOverride = "thorough";
33
- else if (args?.includes("--full")) profileOverride = "full-regression";
34
- else if (args?.includes("--profile")) {
35
- const match = args.match(/--profile\s+(\S+)/);
36
- if (match) profileOverride = match[1];
37
- }
38
+ function createReviewSteps() {
39
+ return [
40
+ { key: "load-config", label: "Load config" },
41
+ { key: "repair-config", label: "Repair invalid config" },
42
+ { key: "discover-scope", label: "Discover review scope" },
43
+ ...CANONICAL_GATE_ORDER.map((gateId) => ({
44
+ key: gateStepKey(gateId),
45
+ label: GATE_DISPLAY_NAMES[gateId],
46
+ })),
47
+ { key: "save-report", label: "Save report" },
48
+ ];
49
+ }
50
+
51
+ export interface ChecksCommandDependencies {
52
+ loadModelConfig: typeof loadModelConfig;
53
+ createModelBridge: typeof createModelBridge;
54
+ resolveModelForAction: typeof resolveModelForAction;
55
+ applyModelOverride: typeof applyModelOverride;
56
+ inspectConfig: typeof inspectConfig;
57
+ inspectQualityGateRecovery: typeof inspectQualityGateRecovery;
58
+ loadConfig: typeof loadConfig;
59
+ removeQualityGatesConfig: typeof removeQualityGatesConfig;
60
+ setupGates: typeof setupGates;
61
+ interactivelySaveGateSetup: typeof interactivelySaveGateSetup;
62
+ runQualityGates: typeof runQualityGates;
63
+ saveReviewReport: typeof saveReviewReport;
64
+ notifyInfo: typeof notifyInfo;
65
+ }
66
+
67
+ const CHECKS_COMMAND_DEPENDENCIES: ChecksCommandDependencies = {
68
+ loadModelConfig,
69
+ createModelBridge,
70
+ resolveModelForAction,
71
+ applyModelOverride,
72
+ inspectConfig,
73
+ inspectQualityGateRecovery,
74
+ loadConfig,
75
+ removeQualityGatesConfig,
76
+ setupGates,
77
+ interactivelySaveGateSetup,
78
+ runQualityGates,
79
+ saveReviewReport,
80
+ notifyInfo,
81
+ };
82
+
83
+ function tokenizeGateList(raw: string): string[] {
84
+ return raw
85
+ .split(/[\s,]+/)
86
+ .map((part) => part.trim())
87
+ .filter((part) => part.length > 0);
88
+ }
89
+
90
+ function extractFlagValues(args: string | undefined, flag: "--only" | "--skip"): string[] {
91
+ if (!args) {
92
+ return [];
93
+ }
94
+
95
+ const escapedFlag = flag.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
96
+ const match = args.match(new RegExp(`${escapedFlag}\\s+([^]+?)(?=\\s--\\w+|$)`));
97
+ if (!match) {
98
+ return [];
99
+ }
100
+
101
+ return tokenizeGateList(match[1]);
102
+ }
103
+
104
+ function assertKnownGateIds(gateIds: string[]): GateId[] {
105
+ const uniqueGateIds = [...new Set(gateIds)];
106
+
107
+ for (const gateId of uniqueGateIds) {
108
+ if (!CANONICAL_GATE_ORDER.includes(gateId as GateId)) {
109
+ throw new Error(`Unknown gate id: ${gateId}`);
110
+ }
111
+ }
112
+
113
+ return uniqueGateIds as GateId[];
114
+ }
38
115
 
39
- // If no flag provided and UI is available, let the user pick
40
- if (!profileOverride && ctx.hasUI) {
41
- const profiles = listProfiles(platform.paths, ctx.cwd);
42
- const choice = await ctx.ui.select(
43
- "Review profile",
44
- profiles,
45
- {
46
- initialIndex: profiles.indexOf(config.defaultProfile),
47
- helpText: "Select review depth · Esc to cancel",
48
- },
49
- );
50
- if (!choice) return;
51
- profileOverride = choice;
116
+ export function parseGateFilters(args: string | undefined): GateFilters {
117
+ const only = assertKnownGateIds(extractFlagValues(args, "--only"));
118
+ const skip = assertKnownGateIds(extractFlagValues(args, "--skip"));
119
+
120
+ if (only.length > 0 && skip.length > 0) {
121
+ throw new Error("--only and --skip are mutually exclusive.");
122
+ }
123
+
124
+ return {
125
+ ...(only.length > 0 ? { only } : {}),
126
+ ...(skip.length > 0 ? { skip } : {}),
127
+ };
128
+ }
129
+
130
+ function getEnabledGateIds(gates: QualityGatesConfig): GateId[] {
131
+ return CANONICAL_GATE_ORDER.filter((gateId) => gates[gateId]?.enabled === true);
132
+ }
133
+
134
+ function validateGateSelection(enabledGateIds: GateId[], filters: GateFilters): void {
135
+ if (enabledGateIds.length === 0) {
136
+ throw new Error("No quality gates configured. Run /supi:config → Setup quality gates.");
137
+ }
138
+
139
+ if (filters.only) {
140
+ for (const gateId of filters.only) {
141
+ if (!enabledGateIds.includes(gateId)) {
142
+ throw new Error(`Gate ${gateId} is not configured or is disabled.`);
52
143
  }
144
+ }
145
+ }
146
+
147
+ const selectedGateIds = filters.only?.length
148
+ ? enabledGateIds.filter((gateId) => filters.only?.includes(gateId))
149
+ : enabledGateIds;
150
+ const skippedGateIds = new Set(filters.skip ?? []);
151
+ const runnableGateIds = selectedGateIds.filter((gateId) => !skippedGateIds.has(gateId));
152
+
153
+ if (runnableGateIds.length === 0) {
154
+ throw new Error("The current filters leave no selected gates to run.");
155
+ }
156
+ }
157
+
158
+ function describeScope(scope: ConfigScope): string {
159
+ return scope === "global" ? "global" : "project";
160
+ }
161
+
162
+ function buildRecoveryDetail(scopes: ConfigScope[]): string {
163
+ const removed = scopes.map((scope) => `- Removed quality.gates from ${describeScope(scope)} config`).join("\n");
164
+ return [
165
+ removed,
166
+ "",
167
+ "Supipowers opened quality-gate setup so you can save a fresh configuration.",
168
+ ].join("\n");
169
+ }
170
+
171
+ function gateStepKey(gateId: GateId): string {
172
+ return `gate-${gateId}`;
173
+ }
174
+
175
+ function truncateDetail(detail: string, maxLength = 48): string {
176
+ const normalized = detail.replace(/\s+/g, " ").trim();
177
+ if (normalized.length <= maxLength) {
178
+ return normalized;
179
+ }
180
+
181
+ return `${normalized.slice(0, maxLength - 1)}…`;
182
+ }
183
+
184
+ function formatScopeDetail(event: Extract<ReviewRunEvent, { type: "scope-discovered" }>): string {
185
+ if (event.fileScope === "changed-files") {
186
+ return `${event.changedFiles} changed file(s)`;
187
+ }
188
+
189
+ return `all files (${event.scopeFiles})`;
190
+ }
191
+
192
+ function createReviewProgress(ctx: any) {
193
+ const progress = createWorkflowProgress(ctx.ui, {
194
+ title: "supi:checks",
195
+ statusKey: "supi-review",
196
+ statusLabel: "Running checks...",
197
+ widgetKey: "supi-review",
198
+ clearStatusKeys: ["supi-model"],
199
+ steps: createReviewSteps(),
200
+ });
201
+ let activeStepKey: string | null = null;
202
+
203
+ function activate(stepKey: string, detail?: string) {
204
+ activeStepKey = stepKey;
205
+ progress.activate(stepKey, detail ? truncateDetail(detail) : undefined);
206
+ }
207
+
208
+ function finish(stepKey: string, status: "done" | "skipped" | "failed" | "blocked", detail?: string) {
209
+ if (activeStepKey === stepKey) {
210
+ activeStepKey = null;
211
+ }
212
+
213
+ const nextDetail = detail ? truncateDetail(detail) : undefined;
214
+ switch (status) {
215
+ case "done":
216
+ progress.complete(stepKey, nextDetail);
217
+ return;
218
+ case "skipped":
219
+ progress.skip(stepKey, nextDetail);
220
+ return;
221
+ case "failed":
222
+ progress.fail(stepKey, nextDetail);
223
+ return;
224
+ case "blocked":
225
+ progress.block(stepKey, nextDetail);
226
+ return;
227
+ }
228
+ }
53
229
 
54
- const profile = resolveProfile(platform.paths, ctx.cwd, config, profileOverride);
55
- const lsp = isLspAvailable(platform.getActiveTools());
230
+ function skipIfPending(stepKey: string, detail: string) {
231
+ if (progress.getStatus(stepKey) === "pending") {
232
+ finish(stepKey, "skipped", detail);
233
+ }
234
+ }
56
235
 
57
- if (!lsp && profile.gates.lspDiagnostics) {
58
- notifyWarning(
59
- ctx,
60
- "LSP not available",
61
- "Review will continue without LSP diagnostics. Run /supi:config for setup."
62
- );
236
+ function hideIfPending(stepKey: string) {
237
+ if (progress.getStatus(stepKey) === "pending") {
238
+ progress.hide(stepKey);
239
+ }
240
+ }
241
+
242
+ return {
243
+ startLoadingConfig() {
244
+ activate("load-config", "Loading checks config");
245
+ },
246
+ completeLoadingConfig() {
247
+ finish("load-config", "done", "loaded");
248
+ hideIfPending("repair-config");
249
+ },
250
+ blockLoadingConfig(detail: string) {
251
+ finish("load-config", "blocked", detail);
252
+ },
253
+ failLoadingConfig(detail: string) {
254
+ finish("load-config", "failed", detail);
255
+ },
256
+ startRepair(detail: string) {
257
+ activate("repair-config", detail);
258
+ },
259
+ updateRepair(detail: string) {
260
+ if (activeStepKey === "repair-config") {
261
+ progress.detail(truncateDetail(detail));
262
+ return;
63
263
  }
264
+ activate("repair-config", detail);
265
+ },
266
+ completeRepair(detail: string) {
267
+ finish("repair-config", "done", detail);
268
+ },
269
+ skipRepair(detail: string) {
270
+ finish("repair-config", "skipped", detail);
271
+ },
272
+ failRepair(detail: string) {
273
+ finish("repair-config", "failed", detail);
274
+ },
275
+ configureGateSteps(gates: QualityGatesConfig, filters: GateFilters) {
276
+ const selectedOnly = new Set(filters.only ?? []);
277
+ const skipped = new Set(filters.skip ?? []);
278
+ const hasOnlyFilter = selectedOnly.size > 0;
64
279
 
65
- let changedFiles: string[] = [];
66
- try {
67
- const result = await platform.exec("git", ["diff", "--name-only", "HEAD"], { cwd: ctx.cwd });
68
- if (result.code === 0) {
69
- changedFiles = result.stdout
70
- .split("\n")
71
- .map((f) => f.trim())
72
- .filter((f) => f.length > 0);
280
+ for (const gateId of CANONICAL_GATE_ORDER) {
281
+ if (gates[gateId]?.enabled !== true) {
282
+ progress.hide(gateStepKey(gateId));
283
+ continue;
284
+ }
285
+ if (hasOnlyFilter && !selectedOnly.has(gateId)) {
286
+ progress.hide(gateStepKey(gateId));
287
+ continue;
73
288
  }
74
- } catch {
75
- // If git fails, we'll review without file filtering
289
+ if (skipped.has(gateId)) {
290
+ progress.hide(gateStepKey(gateId));
291
+ }
292
+ }
293
+ },
294
+ startScopeDiscovery() {
295
+ activate("discover-scope", "Inspecting repository");
296
+ },
297
+ handleRunnerEvent(event: ReviewRunEvent) {
298
+ if (event.type === "scope-discovered") {
299
+ finish("discover-scope", "done", formatScopeDetail(event));
300
+ return;
76
301
  }
77
302
 
78
- if (changedFiles.length === 0) {
79
- try {
80
- const result = await platform.exec("git", ["diff", "--name-only", "--cached"], { cwd: ctx.cwd });
81
- if (result.code === 0) {
82
- changedFiles = result.stdout
83
- .split("\n")
84
- .map((f) => f.trim())
85
- .filter((f) => f.length > 0);
86
- }
87
- } catch {
88
- // continue without
89
- }
303
+ if (event.type === "gate-started") {
304
+ activate(gateStepKey(event.gateId), "running");
305
+ return;
90
306
  }
91
307
 
92
- if (changedFiles.length === 0) {
93
- notifyInfo(ctx, "No changed files detected", "Reviewing all files in scope");
308
+ if (event.type === "gate-skipped") {
309
+ finish(gateStepKey(event.gateId), "skipped", event.reason);
310
+ return;
94
311
  }
95
312
 
96
- const reviewPrompt = buildReviewPrompt({
97
- profile,
98
- changedFiles,
99
- testCommand: config.qa.command,
100
- lspAvailable: lsp,
101
- });
313
+ const status =
314
+ event.status === "passed"
315
+ ? "done"
316
+ : event.status === "failed"
317
+ ? "failed"
318
+ : event.status === "blocked"
319
+ ? "blocked"
320
+ : "skipped";
321
+ finish(gateStepKey(event.gateId), status, event.summary);
322
+ },
323
+ startSavingReport() {
324
+ activate("save-report", "Writing report");
325
+ },
326
+ completeSavingReport(status: ReviewReport["overallStatus"]) {
327
+ finish("save-report", "done", status);
328
+ },
329
+ cancelRemaining(detail: string) {
330
+ skipIfPending("discover-scope", detail);
331
+ for (const gateId of CANONICAL_GATE_ORDER) {
332
+ skipIfPending(gateStepKey(gateId), detail);
333
+ }
334
+ skipIfPending("save-report", detail);
335
+ },
336
+ failActive(detail: string) {
337
+ if (!activeStepKey) {
338
+ return;
339
+ }
340
+ finish(activeStepKey, "failed", detail);
341
+ },
342
+ dispose() {
343
+ progress.dispose();
344
+ },
345
+ };
346
+ }
347
+
348
+ async function recoverInvalidQualityGateConfig(
349
+ platform: Platform,
350
+ ctx: any,
351
+ deps: ChecksCommandDependencies,
352
+ reviewProgress: ReturnType<typeof createReviewProgress>,
353
+ ): Promise<
354
+ | { status: "unrecoverable" }
355
+ | { status: "cancelled" }
356
+ | { status: "recovered"; config: SupipowersConfig }
357
+ > {
358
+ const recovery = deps.inspectQualityGateRecovery(platform.paths, ctx.cwd);
359
+ const recoverableScopes = recovery.scopes
360
+ .filter((scope) => scope.recoverableInvalidQualityGates)
361
+ .map((scope) => scope.scope);
362
+ const hasBlockingParseErrors = recovery.scopes.some((scope) => scope.parseError !== null);
363
+ const hasBlockingValidationErrors = recovery.scopes.some(
364
+ (scope) => scope.otherValidationErrors.length > 0,
365
+ );
366
+
367
+ if (recoverableScopes.length === 0 || hasBlockingParseErrors || hasBlockingValidationErrors) {
368
+ reviewProgress.failLoadingConfig("invalid config");
369
+ reviewProgress.skipRepair("not recoverable");
370
+ return { status: "unrecoverable" };
371
+ }
372
+
373
+ reviewProgress.blockLoadingConfig("invalid quality.gates");
374
+ reviewProgress.startRepair(`cleaning ${recoverableScopes.join(" + ")}`);
375
+
376
+ for (const scope of recoverableScopes) {
377
+ deps.removeQualityGatesConfig(platform.paths, ctx.cwd, scope);
378
+ }
379
+
380
+ deps.notifyInfo(
381
+ ctx,
382
+ "Removed invalid review config",
383
+ buildRecoveryDetail(recoverableScopes),
384
+ );
385
+
386
+ const setupResult = await deps.setupGates(
387
+ platform,
388
+ ctx.cwd,
389
+ deps.inspectConfig(platform.paths, ctx.cwd),
390
+ { mode: "deterministic" },
391
+ );
392
+ if (setupResult.status !== "proposed") {
393
+ reviewProgress.failRepair("setup proposal failed");
394
+ throw new Error(
395
+ setupResult.errors?.join("\n") ?? "Unable to build a valid quality-gate setup proposal.",
396
+ );
397
+ }
398
+
399
+ reviewProgress.updateRepair("waiting for setup");
400
+ const saveResult = await deps.interactivelySaveGateSetup(
401
+ ctx,
402
+ platform.paths,
403
+ ctx.cwd,
404
+ setupResult.proposal,
405
+ );
406
+ if (saveResult !== "saved") {
407
+ reviewProgress.skipRepair("cancelled");
408
+ reviewProgress.cancelRemaining("cancelled");
409
+ deps.notifyInfo(
410
+ ctx,
411
+ "Checks cancelled",
412
+ "Removed invalid quality.gates config, but setup was cancelled. Checks did not run.",
413
+ );
414
+ return { status: "cancelled" };
415
+ }
416
+
417
+ const config = deps.loadConfig(platform.paths, ctx.cwd);
418
+ reviewProgress.completeRepair("reconfigured");
419
+ return {
420
+ status: "recovered",
421
+ config,
422
+ };
423
+ }
102
424
 
103
- notifyInfo(ctx, `Review started`, `profile: ${profile.name}`);
425
+ export function buildReviewSummary(report: ReviewReport, reportPath: string): string {
426
+ const orderedGates = [...report.gates].sort(
427
+ (left, right) =>
428
+ CANONICAL_GATE_ORDER.indexOf(left.gate) - CANONICAL_GATE_ORDER.indexOf(right.gate),
429
+ );
104
430
 
431
+ return [
432
+ `passed: ${report.summary.passed} | failed: ${report.summary.failed} | blocked: ${report.summary.blocked} | skipped: ${report.summary.skipped}`,
433
+ ...orderedGates.map((gate) => `${gate.gate}: ${gate.status}`),
434
+ `saved: ${reportPath}`,
435
+ ].join("\n");
436
+ }
437
+
438
+ function getFailedGates(report: ReviewReport): GateResult[] {
439
+ return report.gates
440
+ .filter((gate) => gate.status === "failed" || gate.status === "blocked")
441
+ .sort(
442
+ (left, right) =>
443
+ CANONICAL_GATE_ORDER.indexOf(left.gate) - CANONICAL_GATE_ORDER.indexOf(right.gate),
444
+ );
445
+ }
446
+
447
+ /**
448
+ * Build a compact, user-facing failure summary.
449
+ * Only error-severity issues are shown; passed gates and info/warnings are omitted.
450
+ */
451
+ export function buildFailureSummary(failedGates: GateResult[]): string {
452
+ const sections: string[] = [];
453
+
454
+ for (const gate of failedGates) {
455
+ const label = GATE_DISPLAY_NAMES[gate.gate] ?? gate.gate;
456
+ const errors = gate.issues.filter((issue) => issue.severity === "error");
457
+
458
+ if (errors.length === 0) {
459
+ // Gate failed/blocked but reported no error-level issues — show the summary
460
+ sections.push(`${label}: ${gate.summary}`);
461
+ continue;
462
+ }
463
+
464
+ const lines = errors.map((issue) => {
465
+ const loc = issue.file
466
+ ? issue.line
467
+ ? `${issue.file}:${issue.line}`
468
+ : issue.file
469
+ : undefined;
470
+ return loc ? ` ${loc} — ${issue.message}` : ` ${issue.message}`;
471
+ });
472
+
473
+ sections.push(`${label} (${errors.length} error${errors.length === 1 ? "" : "s"}):\n${lines.join("\n")}`);
474
+ }
475
+
476
+ return sections.join("\n\n");
477
+ }
478
+
479
+ /**
480
+ * Build a steer prompt that gives the LLM full failure context to fix issues.
481
+ */
482
+ // Patterns that identify passing-test lines across common test runners.
483
+ // Lines matching these are noise when sending failure context to the LLM.
484
+ const PASSING_TEST_PATTERNS = [
485
+ // bun:test, vitest
486
+ /^\s*\(pass\)\s/,
487
+ // jest, mocha — ✓ or √ prefix
488
+ /^\s*[✓√]\s/,
489
+ // jest file-level PASS
490
+ /^\s*PASS\s/,
491
+ // pytest — lines ending with PASSED or PASSED in brackets
492
+ /\bPASSED\s*$/,
493
+ // pytest compact dot-progress lines (all dots, no F/E)
494
+ /^\s*[\.]+\s*$/,
495
+ // pytest short summary: "X passed" without failures
496
+ /^\s*=+\s*\d+\s+passed(?!.*failed).*=+\s*$/,
497
+ ];
498
+
499
+ /**
500
+ * Strip passing-test lines from raw test runner output.
501
+ * Keeps failures, errors, stack traces, and summary lines.
502
+ */
503
+ export function filterTestRunnerOutput(raw: string): string {
504
+ const lines = raw.split("\n");
505
+ const filtered = lines.filter(
506
+ (line) => !PASSING_TEST_PATTERNS.some((pattern) => pattern.test(line)),
507
+ );
508
+
509
+ // Collapse runs of 3+ blank lines into 1
510
+ const collapsed: string[] = [];
511
+ let blankRun = 0;
512
+ for (const line of filtered) {
513
+ if (line.trim() === "") {
514
+ blankRun++;
515
+ if (blankRun <= 1) collapsed.push(line);
516
+ } else {
517
+ blankRun = 0;
518
+ collapsed.push(line);
519
+ }
520
+ }
521
+
522
+ return collapsed.join("\n").trim();
523
+ }
105
524
 
106
- platform.sendMessage(
107
- {
108
- customType: "supi-review",
109
- content: [{ type: "text", text: reviewPrompt }],
110
- display: "none",
111
- },
112
- { deliverAs: "steer", triggerTurn: true }
525
+ function buildFixPrompt(failedGates: GateResult[]): string {
526
+ const sections: string[] = [];
527
+
528
+ for (const gate of failedGates) {
529
+ const label = GATE_DISPLAY_NAMES[gate.gate] ?? gate.gate;
530
+ const errors = gate.issues.filter((issue) => issue.severity === "error");
531
+
532
+ const issueLines = errors.map((issue) => {
533
+ const parts = [issue.message];
534
+ if (issue.file) parts.push(`file: ${issue.file}${issue.line ? `:${issue.line}` : ""}`);
535
+ if (issue.detail) {
536
+ const detail = gate.gate === "test-suite"
537
+ ? filterTestRunnerOutput(issue.detail)
538
+ : issue.detail;
539
+ parts.push(`detail:\n${detail}`);
540
+ }
541
+ return `- ${parts.join(" | ")}`;
542
+ });
543
+
544
+ sections.push([
545
+ `## ${label} (${gate.status})`,
546
+ gate.summary,
547
+ ...(issueLines.length > 0 ? ["", ...issueLines] : []),
548
+ ].join("\n"));
549
+ }
550
+
551
+ const gateNames = failedGates.map((g) => GATE_DISPLAY_NAMES[g.gate] ?? g.gate);
552
+ const rerunCmd = `/supi:checks --only ${failedGates.map((g) => g.gate).join(" ")}`;
553
+
554
+ return [
555
+ "Quality checks found failures that need fixing.",
556
+ `Fix the issues below, then run \`${rerunCmd}\` to validate your fixes.`,
557
+ "",
558
+ ...sections,
559
+ ].join("\n");
560
+ }
561
+
562
+ export async function handleChecks(
563
+ platform: Platform,
564
+ ctx: any,
565
+ args: string | undefined,
566
+ deps: ChecksCommandDependencies = CHECKS_COMMAND_DEPENDENCIES,
567
+ ): Promise<void> {
568
+ const reviewProgress = createReviewProgress(ctx);
569
+ let modelCleanup: (() => Promise<void>) | undefined;
570
+
571
+ try {
572
+ const modelCfg = deps.loadModelConfig(platform.paths, ctx.cwd);
573
+ const bridge = deps.createModelBridge(platform);
574
+ const resolved = deps.resolveModelForAction("checks", modelRegistry, modelCfg, bridge);
575
+ modelCleanup = await deps.applyModelOverride(platform, ctx, "checks", resolved);
576
+
577
+ reviewProgress.startLoadingConfig();
578
+
579
+ let config: SupipowersConfig;
580
+ try {
581
+ config = deps.loadConfig(platform.paths, ctx.cwd);
582
+ reviewProgress.completeLoadingConfig();
583
+ } catch (error) {
584
+ const recovered = await recoverInvalidQualityGateConfig(platform, ctx, deps, reviewProgress);
585
+ if (recovered.status === "unrecoverable") {
586
+ throw error;
587
+ }
588
+ if (recovered.status === "cancelled") {
589
+ return;
590
+ }
591
+ config = recovered.config;
592
+ }
593
+
594
+ const enabledGateIds = getEnabledGateIds(config.quality.gates);
595
+ const filters = parseGateFilters(args);
596
+ reviewProgress.configureGateSteps(config.quality.gates, filters);
597
+ validateGateSelection(enabledGateIds, filters);
598
+
599
+ reviewProgress.startScopeDiscovery();
600
+ const report = await deps.runQualityGates({
601
+ platform,
602
+ cwd: ctx.cwd,
603
+ gates: config.quality.gates,
604
+ filters,
605
+ reviewModel: resolved,
606
+ gateRegistry: REVIEW_GATE_REGISTRY,
607
+ onEvent: (event) => reviewProgress.handleRunnerEvent(event),
608
+ });
609
+
610
+ reviewProgress.startSavingReport();
611
+ const reportPath = deps.saveReviewReport(platform.paths, ctx.cwd, report);
612
+ reviewProgress.completeSavingReport(report.overallStatus);
613
+
614
+ // Dispose widget before showing any TUI dialogs
615
+ reviewProgress.dispose();
616
+
617
+ const failedGates = getFailedGates(report);
618
+
619
+ if (failedGates.length === 0) {
620
+ deps.notifyInfo(
621
+ ctx,
622
+ `Checks complete: ${report.overallStatus}`,
623
+ buildReviewSummary(report, reportPath),
113
624
  );
625
+ return;
626
+ }
627
+
628
+ // Show compact failure summary
629
+ const failureNames = failedGates.map((g) => GATE_DISPLAY_NAMES[g.gate] ?? g.gate);
630
+ deps.notifyInfo(
631
+ ctx,
632
+ `Checks complete: ${report.overallStatus}`,
633
+ buildFailureSummary(failedGates),
634
+ );
635
+
636
+ // Offer to fix
637
+ const FIX_NOW = `Yes, fix ${failureNames.join(", ")}`;
638
+ const SAVE_ONLY = "No, just save for later";
639
+ const choice = await ctx.ui.select(
640
+ `${failedGates.length} check${failedGates.length === 1 ? "" : "s"} failed — do you want to fix now?`,
641
+ [FIX_NOW, SAVE_ONLY],
642
+ );
643
+
644
+ if (choice === FIX_NOW) {
645
+ platform.sendUserMessage(buildFixPrompt(failedGates));
646
+ }
647
+ } catch (error) {
648
+ reviewProgress.failActive((error as Error).message);
649
+ throw error;
650
+ } finally {
651
+ await modelCleanup?.();
652
+ reviewProgress.dispose();
653
+ }
654
+ }
655
+
656
+ export function handleChecksCommand(platform: Platform, ctx: any, args?: string): void {
657
+ handleChecks(platform, ctx, args).catch((error) => {
658
+ notifyError(ctx, "Checks failed", (error as Error).message);
659
+ });
660
+ }
661
+
662
+ export function registerChecksCommand(platform: Platform): void {
663
+ platform.registerCommand("supi:checks", {
664
+ description: "Run configured quality gates",
665
+ async handler(args: string | undefined, ctx: any) {
666
+ try {
667
+ await handleChecks(platform, ctx, args);
668
+ } catch (error) {
669
+ notifyError(ctx, "Checks failed", (error as Error).message);
670
+ }
114
671
  },
115
672
  });
116
673
  }