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/git/commit.ts
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
// the plan for user approval, then executes file-level staging + commit.
|
|
6
6
|
|
|
7
7
|
import type { Platform } from "../platform/types.js";
|
|
8
|
+
import { createWorkflowProgress } from "../platform/progress.js";
|
|
8
9
|
import { VALID_COMMIT_TYPES } from "../release/commit-types.js";
|
|
9
10
|
import { validateCommitMessage } from "./commit-msg.js";
|
|
10
11
|
import { getWorkingTreeStatus } from "./status.js";
|
|
11
12
|
import { discoverCommitConventions } from "./conventions.js";
|
|
13
|
+
import { normalizeLineEndings } from "../text.js";
|
|
12
14
|
import { notifyInfo, notifyError, notifySuccess } from "../notifications/renderer.js";
|
|
13
15
|
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
14
|
-
import {
|
|
16
|
+
import { resolveAllCandidates, createModelBridge } from "../config/model-resolver.js";
|
|
15
17
|
import { loadModelConfig } from "../config/model-config.js";
|
|
16
18
|
|
|
17
19
|
// ── Public types ───────────────────────────────────────────
|
|
@@ -95,129 +97,40 @@ const DIFF_TRUNCATED_LINES = 200;
|
|
|
95
97
|
|
|
96
98
|
// ── Commit progress tracker ────────────────────────────────
|
|
97
99
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
const COMMIT_STEPS = [
|
|
101
|
+
{ key: "check-working-tree", label: "Check working tree" },
|
|
102
|
+
{ key: "stage-changes", label: "Stage changes" },
|
|
103
|
+
{ key: "read-diff", label: "Read diff" },
|
|
104
|
+
{ key: "scan-conventions", label: "Scan conventions" },
|
|
105
|
+
{ key: "ai-analysis", label: "AI analysis" },
|
|
106
|
+
{ key: "review-plan", label: "Review plan" },
|
|
107
|
+
{ key: "execute-commits", label: "Execute commits" },
|
|
108
|
+
] as const;
|
|
101
109
|
|
|
102
|
-
/** A named step in the commit workflow. */
|
|
103
|
-
interface Step {
|
|
104
|
-
label: string;
|
|
105
|
-
status: "pending" | "active" | "done" | "skipped";
|
|
106
|
-
detail?: string;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Rich progress tracker for the commit flow.
|
|
111
|
-
*
|
|
112
|
-
* Uses `setWidget` for a persistent multi-line panel showing all steps,
|
|
113
|
-
* and `setStatus` for the current operation detail in the footer.
|
|
114
|
-
*/
|
|
115
110
|
function createProgress(ctx: any) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{ label: "Execute commits", status: "pending" },
|
|
124
|
-
];
|
|
125
|
-
|
|
126
|
-
let frame = 0;
|
|
127
|
-
let statusDetail = "";
|
|
128
|
-
let timer: ReturnType<typeof setInterval> | null = null;
|
|
129
|
-
|
|
130
|
-
function icon(step: Step): string {
|
|
131
|
-
switch (step.status) {
|
|
132
|
-
case "done": return "✓";
|
|
133
|
-
case "active": return SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
134
|
-
case "skipped": return "–";
|
|
135
|
-
default: return "○";
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function renderWidget(): string[] {
|
|
140
|
-
const lines: string[] = ["┌─ supi:commit ─────────────────────┐"];
|
|
141
|
-
for (const step of steps) {
|
|
142
|
-
const mark = icon(step);
|
|
143
|
-
const detail = step.detail ? ` (${step.detail})` : "";
|
|
144
|
-
lines.push(`│ ${mark} ${step.label}${detail}`);
|
|
145
|
-
}
|
|
146
|
-
lines.push("└───────────────────────────────────┘");
|
|
147
|
-
return lines;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function refresh() {
|
|
151
|
-
frame++;
|
|
152
|
-
ctx.ui.setWidget?.(WIDGET_KEY, renderWidget());
|
|
153
|
-
if (statusDetail) {
|
|
154
|
-
ctx.ui.setStatus?.(STATUS_KEY, `${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${statusDetail}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function startTimer() {
|
|
159
|
-
if (!timer) {
|
|
160
|
-
timer = setInterval(refresh, 80);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function stopTimer() {
|
|
165
|
-
if (timer) {
|
|
166
|
-
clearInterval(timer);
|
|
167
|
-
timer = null;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
111
|
+
const progress = createWorkflowProgress(ctx.ui, {
|
|
112
|
+
title: "supi:commit",
|
|
113
|
+
statusKey: "supi-commit",
|
|
114
|
+
widgetKey: "supi-commit",
|
|
115
|
+
clearStatusKeys: ["supi-model"],
|
|
116
|
+
steps: [...COMMIT_STEPS],
|
|
117
|
+
});
|
|
170
118
|
|
|
171
119
|
return {
|
|
172
|
-
/**
|
|
173
|
-
* Mark a step as active (in progress) with an optional status-bar detail.
|
|
174
|
-
* `stepIndex` is 0-based into the steps array.
|
|
175
|
-
*/
|
|
176
120
|
activate(stepIndex: number, detail?: string) {
|
|
177
|
-
|
|
178
|
-
if (step) {
|
|
179
|
-
step.status = "active";
|
|
180
|
-
step.detail = detail;
|
|
181
|
-
}
|
|
182
|
-
statusDetail = detail ?? step?.label ?? "";
|
|
183
|
-
startTimer();
|
|
184
|
-
refresh();
|
|
121
|
+
progress.activate(COMMIT_STEPS[stepIndex]!.key, detail);
|
|
185
122
|
},
|
|
186
|
-
|
|
187
|
-
/** Update the status-bar detail text without changing the step. */
|
|
188
123
|
detail(text: string) {
|
|
189
|
-
|
|
190
|
-
// Also update the current active step's detail
|
|
191
|
-
const active = steps.find((s) => s.status === "active");
|
|
192
|
-
if (active) active.detail = text;
|
|
124
|
+
progress.detail(text);
|
|
193
125
|
},
|
|
194
|
-
|
|
195
|
-
/** Mark a step as completed. */
|
|
196
126
|
complete(stepIndex: number, detail?: string) {
|
|
197
|
-
|
|
198
|
-
if (step) {
|
|
199
|
-
step.status = "done";
|
|
200
|
-
if (detail !== undefined) step.detail = detail;
|
|
201
|
-
}
|
|
202
|
-
refresh();
|
|
127
|
+
progress.complete(COMMIT_STEPS[stepIndex]!.key, detail);
|
|
203
128
|
},
|
|
204
|
-
|
|
205
|
-
/** Mark a step as skipped. */
|
|
206
129
|
skip(stepIndex: number, detail?: string) {
|
|
207
|
-
|
|
208
|
-
if (step) {
|
|
209
|
-
step.status = "skipped";
|
|
210
|
-
if (detail !== undefined) step.detail = detail;
|
|
211
|
-
}
|
|
212
|
-
refresh();
|
|
130
|
+
progress.skip(COMMIT_STEPS[stepIndex]!.key, detail);
|
|
213
131
|
},
|
|
214
|
-
|
|
215
|
-
/** Tear down: stop animation, clear status bar and widget. */
|
|
216
132
|
dispose() {
|
|
217
|
-
|
|
218
|
-
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
219
|
-
ctx.ui.setStatus?.("supi-model", undefined);
|
|
220
|
-
ctx.ui.setWidget?.(WIDGET_KEY, undefined);
|
|
133
|
+
progress.dispose();
|
|
221
134
|
},
|
|
222
135
|
};
|
|
223
136
|
}
|
|
@@ -268,7 +181,7 @@ export async function analyzeAndCommit(
|
|
|
268
181
|
exec("git", ["diff", "--cached", "--name-only"], { cwd }),
|
|
269
182
|
]);
|
|
270
183
|
|
|
271
|
-
const fileList = filesResult.stdout.trim().split("\n").filter(Boolean);
|
|
184
|
+
const fileList = normalizeLineEndings(filesResult.stdout).trim().split("\n").filter(Boolean);
|
|
272
185
|
if (fileList.length === 0) {
|
|
273
186
|
progress.complete(2, "empty");
|
|
274
187
|
progress.dispose();
|
|
@@ -293,32 +206,41 @@ export async function analyzeAndCommit(
|
|
|
293
206
|
});
|
|
294
207
|
|
|
295
208
|
let plan: CommitPlan | null = null;
|
|
209
|
+
let agentReason: string | undefined;
|
|
296
210
|
|
|
297
211
|
if (platform.capabilities.agentSessions) {
|
|
298
212
|
progress.activate(4, `${fileList.length} file(s)`);
|
|
299
|
-
// Resolve the commit sub-agent model from config (falls back to session default)
|
|
300
213
|
const modelCfg = loadModelConfig(platform.paths, cwd);
|
|
301
214
|
const bridge = createModelBridge(platform);
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
215
|
+
const candidates = resolveAllCandidates("commit", modelRegistry, modelCfg, bridge);
|
|
216
|
+
|
|
217
|
+
for (const candidate of candidates) {
|
|
218
|
+
// Show model override in status bar if not using the main session model
|
|
219
|
+
if (candidate.source !== "main" && candidate.model) {
|
|
220
|
+
const sourceLabel =
|
|
221
|
+
candidate.source === "action" ? "configured for commit" :
|
|
222
|
+
candidate.source === "default" ? "supipowers default" :
|
|
223
|
+
"harness role";
|
|
224
|
+
let detail = sourceLabel;
|
|
225
|
+
if (candidate.thinkingLevel) {
|
|
226
|
+
detail += ` \u00b7 ${candidate.thinkingLevel} thinking`;
|
|
227
|
+
}
|
|
228
|
+
ctx.ui?.setStatus?.("supi-model", `Model: ${candidate.model} (${detail})`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const agentResult = await tryAgentPlan(platform, cwd, prompt, candidate.model);
|
|
232
|
+
if (agentResult.plan) {
|
|
233
|
+
plan = validatePlanFiles(agentResult.plan, fileList);
|
|
234
|
+
progress.complete(4, `${plan.commits.length} commit(s)`);
|
|
235
|
+
break;
|
|
313
236
|
}
|
|
314
|
-
|
|
237
|
+
|
|
238
|
+
// Store last failure reason; try next candidate
|
|
239
|
+
agentReason = agentResult.reason;
|
|
315
240
|
}
|
|
316
|
-
|
|
317
|
-
if (plan) {
|
|
318
|
-
|
|
319
|
-
progress.complete(4, `${plan.commits.length} commit(s)`);
|
|
320
|
-
} else {
|
|
321
|
-
progress.skip(4, "unavailable");
|
|
241
|
+
|
|
242
|
+
if (!plan) {
|
|
243
|
+
progress.skip(4, agentReason ?? "unavailable");
|
|
322
244
|
}
|
|
323
245
|
} else {
|
|
324
246
|
progress.skip(4, "no agent sessions");
|
|
@@ -329,7 +251,10 @@ export async function analyzeAndCommit(
|
|
|
329
251
|
progress.skip(5, "manual");
|
|
330
252
|
progress.skip(6, "manual");
|
|
331
253
|
progress.dispose();
|
|
332
|
-
|
|
254
|
+
const reason = !platform.capabilities.agentSessions
|
|
255
|
+
? "no agent sessions"
|
|
256
|
+
: agentReason;
|
|
257
|
+
return manualFallback(platform, ctx, cwd, fileList, reason);
|
|
333
258
|
}
|
|
334
259
|
|
|
335
260
|
// 6. Present plan for approval
|
|
@@ -363,12 +288,18 @@ export async function analyzeAndCommit(
|
|
|
363
288
|
|
|
364
289
|
// ── Agent interaction ──────────────────────────────────────
|
|
365
290
|
|
|
291
|
+
interface AgentPlanResult {
|
|
292
|
+
plan: CommitPlan | null;
|
|
293
|
+
/** Human-readable reason when plan is null */
|
|
294
|
+
reason?: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
366
297
|
async function tryAgentPlan(
|
|
367
298
|
platform: Platform,
|
|
368
299
|
cwd: string,
|
|
369
300
|
prompt: string,
|
|
370
301
|
model?: string,
|
|
371
|
-
): Promise<
|
|
302
|
+
): Promise<AgentPlanResult> {
|
|
372
303
|
let session: Awaited<ReturnType<Platform["createAgentSession"]>> | null = null;
|
|
373
304
|
try {
|
|
374
305
|
session = await platform.createAgentSession({ cwd, hasUI: false, ...(model ? { model } : {}) });
|
|
@@ -388,7 +319,7 @@ async function tryAgentPlan(
|
|
|
388
319
|
.reverse()
|
|
389
320
|
.find((m: any) => m.role === "assistant");
|
|
390
321
|
|
|
391
|
-
if (!lastAssistant) return null;
|
|
322
|
+
if (!lastAssistant) return { plan: null, reason: "no assistant response" };
|
|
392
323
|
|
|
393
324
|
const text =
|
|
394
325
|
typeof lastAssistant.content === "string"
|
|
@@ -400,9 +331,15 @@ async function tryAgentPlan(
|
|
|
400
331
|
.join("\n")
|
|
401
332
|
: "";
|
|
402
333
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
334
|
+
if (!text) return { plan: null, reason: "empty agent response" };
|
|
335
|
+
|
|
336
|
+
const plan = parseCommitPlan(text);
|
|
337
|
+
if (!plan) return { plan: null, reason: diagnoseParseFailure(text) };
|
|
338
|
+
|
|
339
|
+
return { plan };
|
|
340
|
+
} catch (err) {
|
|
341
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
342
|
+
return { plan: null, reason: message };
|
|
406
343
|
} finally {
|
|
407
344
|
if (session) {
|
|
408
345
|
try {
|
|
@@ -421,13 +358,16 @@ async function manualFallback(
|
|
|
421
358
|
ctx: any,
|
|
422
359
|
cwd: string,
|
|
423
360
|
fileList: string[],
|
|
361
|
+
reason?: string,
|
|
424
362
|
): Promise<CommitResult | null> {
|
|
425
363
|
const exec = platform.exec.bind(platform);
|
|
426
364
|
|
|
427
365
|
notifyInfo(
|
|
428
366
|
ctx,
|
|
429
367
|
"AI commit unavailable",
|
|
430
|
-
|
|
368
|
+
reason
|
|
369
|
+
? `${reason} \u2014 enter a commit message manually`
|
|
370
|
+
: "Enter a commit message manually",
|
|
431
371
|
);
|
|
432
372
|
|
|
433
373
|
const message = await ctx.ui.input("Commit message (empty to abort)", {
|
|
@@ -581,6 +521,7 @@ interface PromptInput {
|
|
|
581
521
|
/** Exported for testing */
|
|
582
522
|
export function buildAnalysisPrompt(input: PromptInput): string {
|
|
583
523
|
const { diff, stat, fileList, conventions, userContext } = input;
|
|
524
|
+
const normalizedDiff = normalizeLineEndings(diff);
|
|
584
525
|
|
|
585
526
|
const parts: string[] = [
|
|
586
527
|
"You are a commit message generator. Analyze the following code changes and produce a commit plan.",
|
|
@@ -602,12 +543,12 @@ export function buildAnalysisPrompt(input: PromptInput): string {
|
|
|
602
543
|
}
|
|
603
544
|
|
|
604
545
|
// Diff content — truncate for large diffs
|
|
605
|
-
const diffBytes = Buffer.byteLength(
|
|
546
|
+
const diffBytes = Buffer.byteLength(normalizedDiff, "utf8");
|
|
606
547
|
|
|
607
548
|
if (diffBytes <= DIFF_FULL_LIMIT) {
|
|
608
|
-
parts.push("**Full diff:**", "```",
|
|
549
|
+
parts.push("**Full diff:**", "```", normalizedDiff, "```", "");
|
|
609
550
|
} else if (diffBytes <= DIFF_STAT_ONLY_LIMIT) {
|
|
610
|
-
const truncated =
|
|
551
|
+
const truncated = normalizedDiff.split("\n").slice(0, DIFF_TRUNCATED_LINES).join("\n");
|
|
611
552
|
parts.push(
|
|
612
553
|
"**Diff (truncated — too large for full inclusion):**",
|
|
613
554
|
"```",
|
|
@@ -650,6 +591,47 @@ export function buildAnalysisPrompt(input: PromptInput): string {
|
|
|
650
591
|
|
|
651
592
|
// ── Plan parsing ───────────────────────────────────────────
|
|
652
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Produce a human-readable reason why parseCommitPlan returned null.
|
|
596
|
+
* Used for diagnostics — never shown raw to the user.
|
|
597
|
+
*/
|
|
598
|
+
function diagnoseParseFailure(text: string): string {
|
|
599
|
+
const fenceRe = /```json\s*\n([\s\S]*?)```/;
|
|
600
|
+
const match = fenceRe.exec(text);
|
|
601
|
+
if (!match) return "no JSON code block in response";
|
|
602
|
+
|
|
603
|
+
let parsed: any;
|
|
604
|
+
try {
|
|
605
|
+
parsed = JSON.parse(match[1]);
|
|
606
|
+
} catch {
|
|
607
|
+
return "JSON parse error in response";
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (!parsed.commits || !Array.isArray(parsed.commits)) return "missing commits array";
|
|
611
|
+
if (parsed.commits.length === 0) return "empty commits array";
|
|
612
|
+
|
|
613
|
+
for (const c of parsed.commits) {
|
|
614
|
+
if (!c.type) return "commit missing type";
|
|
615
|
+
if (!c.summary) return "commit missing summary";
|
|
616
|
+
if (!Array.isArray(c.files) || c.files.length === 0) return "commit missing files";
|
|
617
|
+
if (!(VALID_COMMIT_TYPES as readonly string[]).includes(c.type)) {
|
|
618
|
+
return `invalid commit type: ${c.type}`;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Check for duplicate files across groups
|
|
623
|
+
const seen = new Set<string>();
|
|
624
|
+
for (const group of parsed.commits) {
|
|
625
|
+
for (const file of group.files) {
|
|
626
|
+
if (seen.has(file)) return `duplicate file across commits: ${file}`;
|
|
627
|
+
seen.add(file);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return "unknown parse failure";
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
|
|
653
635
|
/** Exported for testing */
|
|
654
636
|
export function parseCommitPlan(text: string): CommitPlan | null {
|
|
655
637
|
// Look for ```json ... ``` fenced block
|
package/src/git/conventions.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/git/conventions.ts — Discover commit message conventions from repo docs and config
|
|
2
2
|
|
|
3
3
|
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { isAbsolute, join } from "node:path";
|
|
5
5
|
|
|
6
6
|
type ExecFn = (
|
|
7
7
|
cmd: string,
|
|
@@ -110,7 +110,7 @@ export async function discoverCommitConventions(
|
|
|
110
110
|
);
|
|
111
111
|
if (result.code === 0 && result.stdout.trim()) {
|
|
112
112
|
const templatePath = result.stdout.trim();
|
|
113
|
-
const absPath = templatePath
|
|
113
|
+
const absPath = isAbsolute(templatePath)
|
|
114
114
|
? templatePath
|
|
115
115
|
: join(cwd, templatePath);
|
|
116
116
|
const content = readFileSafe(absPath);
|
package/src/git/status.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { normalizeLineEndings } from "../text.js";
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
type ExecFn = (cmd: string, args: string[], opts?: { cwd?: string }) => Promise<{ stdout: string; code: number }>;
|
|
2
5
|
|
|
3
6
|
export interface WorkingTreeStatus {
|
|
@@ -16,7 +19,7 @@ export async function getWorkingTreeStatus(exec: ExecFn, cwd: string): Promise<W
|
|
|
16
19
|
if (result.code !== 0) {
|
|
17
20
|
return { dirty: false, files: [] };
|
|
18
21
|
}
|
|
19
|
-
const files = result.stdout
|
|
22
|
+
const files = normalizeLineEndings(result.stdout)
|
|
20
23
|
.split("\n")
|
|
21
24
|
.filter(Boolean)
|
|
22
25
|
.map((line) => line.slice(3)); // strip 2-char status + space
|
package/src/lsp/bridge.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// src/lsp/bridge.ts
|
|
2
|
+
import type { GateExecutionContext, GateIssue } from "../types.js";
|
|
3
|
+
import { runStructuredAgentSession } from "../quality/ai-session.js";
|
|
4
|
+
import { stripMarkdownCodeFence } from "../text.js";
|
|
2
5
|
|
|
3
6
|
export interface DiagnosticsResult {
|
|
4
7
|
file: string;
|
|
@@ -12,25 +15,148 @@ export interface Diagnostic {
|
|
|
12
15
|
column: number;
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
function isDiagnostic(value: unknown): value is Diagnostic {
|
|
19
|
+
return (
|
|
20
|
+
typeof value === "object" &&
|
|
21
|
+
value !== null &&
|
|
22
|
+
["error", "warning", "info", "hint"].includes((value as Diagnostic).severity) &&
|
|
23
|
+
typeof (value as Diagnostic).message === "string" &&
|
|
24
|
+
typeof (value as Diagnostic).line === "number" &&
|
|
25
|
+
typeof (value as Diagnostic).column === "number"
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isDiagnosticsResult(value: unknown): value is DiagnosticsResult {
|
|
30
|
+
return (
|
|
31
|
+
typeof value === "object" &&
|
|
32
|
+
value !== null &&
|
|
33
|
+
typeof (value as DiagnosticsResult).file === "string" &&
|
|
34
|
+
Array.isArray((value as DiagnosticsResult).diagnostics) &&
|
|
35
|
+
(value as DiagnosticsResult).diagnostics.every((diagnostic) => isDiagnostic(diagnostic))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeDiagnosticSeverity(severity: Diagnostic["severity"]): GateIssue["severity"] {
|
|
40
|
+
return severity === "hint" ? "info" : severity;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatDiagnosticDetail(diagnostic: Diagnostic): string | undefined {
|
|
44
|
+
return diagnostic.column > 0 ? `column ${diagnostic.column}` : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractJsonArray(raw: string): unknown | null {
|
|
48
|
+
const trimmed = raw.trim();
|
|
49
|
+
|
|
50
|
+
// 1. Try direct parse (no fence)
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(trimmed);
|
|
53
|
+
if (Array.isArray(parsed)) return parsed;
|
|
54
|
+
} catch {}
|
|
55
|
+
|
|
56
|
+
// 2. Strip markdown code fence and retry
|
|
57
|
+
const stripped = stripMarkdownCodeFence(trimmed);
|
|
58
|
+
if (stripped !== trimmed) {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(stripped);
|
|
61
|
+
if (Array.isArray(parsed)) return parsed;
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. Extract first JSON array from prose via bracket matching
|
|
66
|
+
const start = trimmed.indexOf("[");
|
|
67
|
+
if (start === -1) return null;
|
|
68
|
+
|
|
69
|
+
let depth = 0;
|
|
70
|
+
for (let i = start; i < trimmed.length; i++) {
|
|
71
|
+
if (trimmed[i] === "[") depth++;
|
|
72
|
+
else if (trimmed[i] === "]") depth--;
|
|
73
|
+
if (depth === 0) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(trimmed.slice(start, i + 1));
|
|
76
|
+
if (Array.isArray(parsed)) return parsed;
|
|
77
|
+
} catch {}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseLspDiagnosticsResults(raw: string): DiagnosticsResult[] | null {
|
|
86
|
+
const parsed = extractJsonArray(raw);
|
|
87
|
+
if (!Array.isArray(parsed)) return null;
|
|
88
|
+
return parsed.every((entry) => isDiagnosticsResult(entry)) ? parsed : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
15
91
|
/**
|
|
16
|
-
* Request LSP diagnostics
|
|
17
|
-
* triggers the LLM to use the lsp tool. For direct use,
|
|
18
|
-
* we provide a prompt snippet the orchestrator can include
|
|
19
|
-
* in sub-agent assignments.
|
|
92
|
+
* Request LSP diagnostics via a headless agent session that can call the lsp tool.
|
|
20
93
|
*/
|
|
21
|
-
export function buildLspDiagnosticsPrompt(
|
|
22
|
-
|
|
94
|
+
export function buildLspDiagnosticsPrompt(
|
|
95
|
+
scopeFiles: string[],
|
|
96
|
+
fileScope: GateExecutionContext["fileScope"],
|
|
97
|
+
): string {
|
|
98
|
+
const scopeInstruction =
|
|
99
|
+
fileScope === "all-files"
|
|
100
|
+
? [
|
|
101
|
+
'Run repository-wide diagnostics with the lsp tool using action "diagnostics" and file "*".',
|
|
102
|
+
"Return diagnostics for files that report issues.",
|
|
103
|
+
]
|
|
104
|
+
: [
|
|
105
|
+
"Run LSP diagnostics on these files:",
|
|
106
|
+
...scopeFiles.map((file) => `- ${file}`),
|
|
107
|
+
"",
|
|
108
|
+
'Use the lsp tool with action "diagnostics" for each listed file.',
|
|
109
|
+
];
|
|
110
|
+
|
|
23
111
|
return [
|
|
24
|
-
"
|
|
25
|
-
|
|
112
|
+
"You are collecting structured LSP diagnostics for a review quality gate.",
|
|
113
|
+
...scopeInstruction,
|
|
26
114
|
"",
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
115
|
+
"Return JSON only as an array with this exact shape:",
|
|
116
|
+
'[{"file":"path/to/file.ts","diagnostics":[{"severity":"error|warning|info|hint","message":"text","line":1,"column":1}]}]',
|
|
117
|
+
"",
|
|
118
|
+
"Rules:",
|
|
119
|
+
"- Do not wrap the JSON in markdown fences.",
|
|
120
|
+
"- Include line and column numbers exactly as reported by the tool.",
|
|
121
|
+
"- Use an empty array when there are no diagnostics.",
|
|
31
122
|
].join("\n");
|
|
32
123
|
}
|
|
33
124
|
|
|
125
|
+
export async function collectLspDiagnostics(options: {
|
|
126
|
+
cwd: string;
|
|
127
|
+
scopeFiles: string[];
|
|
128
|
+
fileScope: GateExecutionContext["fileScope"];
|
|
129
|
+
createAgentSession: GateExecutionContext["createAgentSession"];
|
|
130
|
+
reviewModel?: GateExecutionContext["reviewModel"];
|
|
131
|
+
}): Promise<GateIssue[]> {
|
|
132
|
+
const sessionResult = await runStructuredAgentSession(options.createAgentSession, {
|
|
133
|
+
cwd: options.cwd,
|
|
134
|
+
prompt: buildLspDiagnosticsPrompt(options.scopeFiles, options.fileScope),
|
|
135
|
+
model: options.reviewModel?.model,
|
|
136
|
+
thinkingLevel: options.reviewModel?.thinkingLevel ?? null,
|
|
137
|
+
timeoutMs: 120_000,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (sessionResult.status !== "ok") {
|
|
141
|
+
throw new Error(sessionResult.error);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const parsed = parseLspDiagnosticsResults(sessionResult.finalText);
|
|
145
|
+
if (!parsed) {
|
|
146
|
+
throw new Error("LSP diagnostics integration returned invalid JSON.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return parsed.flatMap((result) =>
|
|
150
|
+
result.diagnostics.map<GateIssue>((diagnostic) => ({
|
|
151
|
+
severity: normalizeDiagnosticSeverity(diagnostic.severity),
|
|
152
|
+
message: diagnostic.message,
|
|
153
|
+
file: result.file,
|
|
154
|
+
line: diagnostic.line,
|
|
155
|
+
detail: formatDiagnosticDetail(diagnostic),
|
|
156
|
+
})),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
34
160
|
/**
|
|
35
161
|
* Build a prompt snippet for sub-agents to check references before renaming.
|
|
36
162
|
*/
|