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
@@ -0,0 +1,454 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { Platform, PlatformPaths } from "../platform/types.js";
4
+ import type { DocDriftState, DriftCheckResult, DriftFinding } from "../types.js";
5
+ import { runStructuredAgentSession } from "../quality/ai-session.js";
6
+
7
+ // ── State persistence ─────────────────────────────────────────
8
+
9
+ const STATE_FILENAME = "doc-drift.json";
10
+
11
+ const EMPTY_STATE: DocDriftState = {
12
+ trackedFiles: [],
13
+ lastCommit: null,
14
+ lastRunAt: null,
15
+ };
16
+
17
+ export function statePath(paths: PlatformPaths, cwd: string): string {
18
+ return paths.project(cwd, STATE_FILENAME);
19
+ }
20
+
21
+ export function loadState(paths: PlatformPaths, cwd: string): DocDriftState {
22
+ const file = statePath(paths, cwd);
23
+ try {
24
+ return JSON.parse(fs.readFileSync(file, "utf-8")) as DocDriftState;
25
+ } catch {
26
+ return { ...EMPTY_STATE, trackedFiles: [] };
27
+ }
28
+ }
29
+
30
+ export function saveState(paths: PlatformPaths, cwd: string, state: DocDriftState): void {
31
+ const file = statePath(paths, cwd);
32
+ fs.mkdirSync(path.dirname(file), { recursive: true });
33
+ fs.writeFileSync(file, JSON.stringify(state, null, 2) + "\n");
34
+ }
35
+
36
+ // ── File discovery ────────────────────────────────────────────
37
+
38
+ /** Known documentation file patterns to discover via git ls-files */
39
+ const DOC_GLOBS = ["*.md", "*.txt", "*.rst", "docs/*", "*.mdx"];
40
+
41
+ /**
42
+ * Directory segments that hold agentic workflows, skills, or prompt
43
+ * templates — not project documentation a human would maintain.
44
+ * Matched as path segments (e.g. `src/review/prompts/foo.md` matches `prompts`).
45
+ */
46
+ const AGENTIC_DIRS = [
47
+ "skills",
48
+ "commands",
49
+ "prompts",
50
+ "default-agents",
51
+ "test",
52
+ "tests",
53
+ "__tests__",
54
+ ];
55
+
56
+ /** Filenames that are agentic artifacts regardless of directory */
57
+ const AGENTIC_NAMES = new Set(["SKILL.md", "SYSTEM.md"]);
58
+
59
+ /** Returns true if the file looks like project documentation, not an agentic workflow artifact */
60
+ export function isProjectDoc(filePath: string): boolean {
61
+ const segments = filePath.toLowerCase().split("/");
62
+
63
+ // Exclude if any path segment is a known agentic directory
64
+ for (const seg of segments) {
65
+ for (const dir of AGENTIC_DIRS) {
66
+ if (seg === dir) return false;
67
+ }
68
+ }
69
+
70
+ // Exclude agentic filenames anywhere in the tree
71
+ const basename = path.basename(filePath);
72
+ if (AGENTIC_NAMES.has(basename)) return false;
73
+
74
+ return true;
75
+ }
76
+
77
+ export async function discoverDocFiles(
78
+ platform: Platform,
79
+ cwd: string,
80
+ ): Promise<string[]> {
81
+ const result = await platform.exec(
82
+ "git",
83
+ ["ls-files", "--", ...DOC_GLOBS],
84
+ { cwd },
85
+ );
86
+ if (result.code !== 0) return [];
87
+
88
+ const files = result.stdout
89
+ .split("\n")
90
+ .map((f) => f.trim())
91
+ .filter((f) => f.length > 0 && isProjectDoc(f));
92
+
93
+ // Deduplicate (globs may overlap)
94
+ return [...new Set(files)].sort();
95
+ }
96
+
97
+ // ── Git helpers ───────────────────────────────────────────────
98
+
99
+ export async function getHeadCommit(
100
+ platform: Platform,
101
+ cwd: string,
102
+ ): Promise<string | null> {
103
+ const result = await platform.exec("git", ["rev-parse", "HEAD"], { cwd });
104
+ if (result.code !== 0) return null;
105
+ return result.stdout.trim() || null;
106
+ }
107
+
108
+ export async function getDiffFilesSince(
109
+ platform: Platform,
110
+ cwd: string,
111
+ sinceCommit: string,
112
+ ): Promise<string[]> {
113
+ const result = await platform.exec(
114
+ "git",
115
+ ["diff", "--name-only", `${sinceCommit}..HEAD`],
116
+ { cwd },
117
+ );
118
+ if (result.code !== 0) return [];
119
+ return result.stdout
120
+ .split("\n")
121
+ .map((f) => f.trim())
122
+ .filter((f) => f.length > 0);
123
+ }
124
+
125
+ // ── Doc content reading ───────────────────────────────────────
126
+
127
+ export function readTrackedDocs(cwd: string, files: string[]): Map<string, string> {
128
+ const docs = new Map<string, string>();
129
+ for (const file of files) {
130
+ const abs = path.join(cwd, file);
131
+ try {
132
+ docs.set(file, fs.readFileSync(abs, "utf-8"));
133
+ } catch {
134
+ // File was deleted — still include it so the LLM knows
135
+ docs.set(file, "[FILE DELETED]");
136
+ }
137
+ }
138
+ return docs;
139
+ }
140
+
141
+ // ── Grouping ──────────────────────────────────────────────────
142
+
143
+ export interface DocDriftGroup {
144
+ docs: string[];
145
+ changedFiles: string[];
146
+ }
147
+
148
+ /**
149
+ * Extracts affinity stems from a doc file path.
150
+ * "docs/review.md" → ["review"], "docs/planning/guide.md" → ["planning"]
151
+ * Top-level files (README.md, AGENTS.md) → [] (catch-all group)
152
+ */
153
+ function docAffinityStems(docPath: string): string[] {
154
+ const dir = path.dirname(docPath);
155
+ const stem = path.basename(docPath, path.extname(docPath)).toLowerCase();
156
+
157
+ // Top-level docs have no specific affinity
158
+ if (dir === ".") return [];
159
+
160
+ // Use the first directory segment under docs/ (or the dir itself)
161
+ const segments = dir.split("/").filter((s) => s !== "docs" && s.length > 0);
162
+ const stems: string[] = [];
163
+ if (segments.length > 0) stems.push(segments[0].toLowerCase());
164
+ if (stem !== "index" && stem !== "readme") stems.push(stem);
165
+ return [...new Set(stems)];
166
+ }
167
+
168
+ /**
169
+ * Groups doc files with their related code changes by directory affinity.
170
+ * Unmatched changed files go to the top-level docs group.
171
+ */
172
+ export function groupDocsByAffinity(
173
+ trackedFiles: string[],
174
+ changedFiles: string[],
175
+ ): DocDriftGroup[] {
176
+ // Separate top-level docs from scoped docs
177
+ const topLevelDocs: string[] = [];
178
+ const scopedGroups = new Map<string, { docs: string[]; changedFiles: string[] }>();
179
+
180
+ for (const doc of trackedFiles) {
181
+ const stems = docAffinityStems(doc);
182
+ if (stems.length === 0) {
183
+ topLevelDocs.push(doc);
184
+ } else {
185
+ // Use the first stem as group key
186
+ const key = stems[0];
187
+ if (!scopedGroups.has(key)) {
188
+ scopedGroups.set(key, { docs: [], changedFiles: [] });
189
+ }
190
+ scopedGroups.get(key)!.docs.push(doc);
191
+ }
192
+ }
193
+
194
+ const unmatchedChanges: string[] = [];
195
+
196
+ for (const changed of changedFiles) {
197
+ const changedLower = changed.toLowerCase();
198
+ const changedDir = path.dirname(changedLower);
199
+ const changedSegments = changedDir.split("/");
200
+
201
+ // Find best matching scoped group
202
+ let matched = false;
203
+ for (const [stem, group] of scopedGroups) {
204
+ if (changedSegments.some((s) => s === stem) || changedLower.includes(`/${stem}/`)) {
205
+ group.changedFiles.push(changed);
206
+ matched = true;
207
+ break;
208
+ }
209
+ }
210
+ if (!matched) {
211
+ unmatchedChanges.push(changed);
212
+ }
213
+ }
214
+
215
+ const groups: DocDriftGroup[] = [];
216
+
217
+ // Add scoped groups
218
+ for (const group of scopedGroups.values()) {
219
+ groups.push({ docs: group.docs, changedFiles: group.changedFiles });
220
+ }
221
+
222
+ // Add top-level group with unmatched changes
223
+ if (topLevelDocs.length > 0) {
224
+ groups.push({ docs: topLevelDocs, changedFiles: unmatchedChanges });
225
+ }
226
+
227
+ return groups;
228
+ }
229
+
230
+ // ── Sub-agent prompt ──────────────────────────────────────────
231
+
232
+ export function buildSubAgentPrompt(group: DocDriftGroup, isFirstRun: boolean): string {
233
+ const parts: string[] = [
234
+ `You are a documentation drift checker. Your job is to review tracked documentation files against recent code changes and identify drift.`,
235
+ ``,
236
+ `<critical>`,
237
+ `You MUST read skill://create-readme before assessing documentation quality.`,
238
+ `</critical>`,
239
+ ``,
240
+ `## Your Assignment`,
241
+ ``,
242
+ `Check these documentation files for accuracy against the current codebase:`,
243
+ ];
244
+
245
+ for (const doc of group.docs) {
246
+ parts.push(`- \`${doc}\``);
247
+ }
248
+
249
+ if (!isFirstRun && group.changedFiles.length > 0) {
250
+ parts.push(
251
+ ``,
252
+ `## Code Changes to Consider`,
253
+ ``,
254
+ `These source files changed since the last documentation check:`,
255
+ );
256
+ for (const f of group.changedFiles) {
257
+ parts.push(`- \`${f}\``);
258
+ }
259
+ parts.push(
260
+ ``,
261
+ `Read the documentation files and the changed source files. Compare them to identify:`,
262
+ );
263
+ } else {
264
+ parts.push(
265
+ ``,
266
+ `This is a full documentation audit. Read each documentation file and compare against the current codebase:`,
267
+ );
268
+ }
269
+
270
+ parts.push(
271
+ ``,
272
+ `1. **Factual inaccuracies**: wrong names, missing parameters, removed features, changed behavior, outdated examples`,
273
+ `2. **Missing documentation**: new commands, features, config options, subcommands that exist in code but not in docs`,
274
+ `3. **Structural gaps**: important sections that should exist based on the codebase but don't`,
275
+ ``,
276
+ `Rules:`,
277
+ `- Do NOT flag style, wording, or formatting issues`,
278
+ `- Do NOT suggest restructuring existing content`,
279
+ `- Only flag things that are factually wrong or missing`,
280
+ `- If documentation is accurate, say so`,
281
+ ``,
282
+ `Respond with a JSON object:`,
283
+ `{`,
284
+ ` "findings": [`,
285
+ ` {`,
286
+ ` "file": "path/to/doc.md",`,
287
+ ` "description": "What is wrong or missing",`,
288
+ ` "severity": "info" | "warning" | "error",`,
289
+ ` "relatedFiles": ["path/to/source.ts"]`,
290
+ ` }`,
291
+ ` ],`,
292
+ ` "status": "ok" | "drifted"`,
293
+ `}`,
294
+ ``,
295
+ `Set status to "drifted" if ANY findings exist, "ok" if all docs are accurate.`,
296
+ `Respond ONLY with the JSON object, no other text.`,
297
+ );
298
+
299
+ return parts.join("\n");
300
+ }
301
+
302
+ // ── Response parsing ──────────────────────────────────────────
303
+
304
+ export function parseDriftFindings(text: string): { findings: DriftFinding[]; status: string } {
305
+ try {
306
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
307
+ if (jsonMatch) {
308
+ const parsed = JSON.parse(jsonMatch[0]);
309
+ if (Array.isArray(parsed.findings)) {
310
+ const findings: DriftFinding[] = parsed.findings
311
+ .filter(
312
+ (f: any) =>
313
+ typeof f.file === "string" &&
314
+ typeof f.description === "string",
315
+ )
316
+ .map((f: any) => ({
317
+ file: f.file,
318
+ description: f.description,
319
+ severity: f.severity === "warning" || f.severity === "error" ? f.severity : "info",
320
+ ...(Array.isArray(f.relatedFiles) ? { relatedFiles: f.relatedFiles } : {}),
321
+ }));
322
+ return { findings, status: parsed.status === "ok" ? "ok" : "drifted" };
323
+ }
324
+ }
325
+ } catch {
326
+ // Fall through
327
+ }
328
+
329
+ // Fallback: treat unparseable response as potential drift
330
+ const lower = text.toLowerCase();
331
+ const likelyDrifted =
332
+ lower.includes("inaccura") ||
333
+ lower.includes("outdated") ||
334
+ lower.includes("missing") ||
335
+ lower.includes("drift");
336
+ return {
337
+ findings: likelyDrifted
338
+ ? [{ file: "unknown", description: text.slice(0, 200), severity: "warning" }]
339
+ : [],
340
+ status: likelyDrifted ? "drifted" : "ok",
341
+ };
342
+ }
343
+
344
+ // ── Sub-agent runner ──────────────────────────────────────────
345
+
346
+ async function runDriftSubAgent(
347
+ createAgentSession: Platform["createAgentSession"],
348
+ group: DocDriftGroup,
349
+ isFirstRun: boolean,
350
+ cwd: string,
351
+ ): Promise<{ findings: DriftFinding[]; status: string }> {
352
+ const prompt = buildSubAgentPrompt(group, isFirstRun);
353
+ const result = await runStructuredAgentSession(
354
+ createAgentSession.bind(undefined) as any,
355
+ { cwd, prompt },
356
+ );
357
+ if (result.status !== "ok" || !result.finalText) {
358
+ return { findings: [], status: "error" };
359
+ }
360
+ return parseDriftFindings(result.finalText);
361
+ }
362
+
363
+ // ── Orchestrator ──────────────────────────────────────────────
364
+
365
+ /**
366
+ * Checks tracked docs for drift using parallel sub-agents.
367
+ * Returns null to skip silently, or a DriftCheckResult with per-doc findings.
368
+ */
369
+ export async function checkDocDrift(
370
+ platform: Platform,
371
+ cwd: string,
372
+ ): Promise<DriftCheckResult | null> {
373
+ const state = loadState(platform.paths, cwd);
374
+ if (state.trackedFiles.length === 0) return null;
375
+
376
+ let changedFiles: string[] = [];
377
+ const isFirstRun = !state.lastCommit;
378
+
379
+ if (!isFirstRun) {
380
+ changedFiles = await getDiffFilesSince(platform, cwd, state.lastCommit!);
381
+ if (changedFiles.length === 0) return null;
382
+ }
383
+
384
+ // Group docs with relevant code changes
385
+ const groups = groupDocsByAffinity(state.trackedFiles, changedFiles);
386
+ if (groups.length === 0) return null;
387
+
388
+ // Dispatch parallel sub-agents
389
+ const results = await Promise.all(
390
+ groups.map((group) =>
391
+ runDriftSubAgent(
392
+ platform.createAgentSession.bind(platform),
393
+ group,
394
+ isFirstRun,
395
+ cwd,
396
+ ),
397
+ ),
398
+ );
399
+
400
+ // Aggregate findings
401
+ const allFindings: DriftFinding[] = [];
402
+ for (const result of results) {
403
+ allFindings.push(...result.findings);
404
+ }
405
+
406
+ const drifted = allFindings.length > 0;
407
+ const summary = drifted
408
+ ? `${allFindings.length} finding(s) across ${new Set(allFindings.map((f) => f.file)).size} doc(s)`
409
+ : "All documentation is up to date.";
410
+
411
+ return { drifted, summary, findings: allFindings };
412
+ }
413
+
414
+ // ── Doc fix prompt ────────────────────────────────────────────
415
+
416
+ /**
417
+ * Builds a fix prompt from structured findings. Each finding tells the agent
418
+ * exactly what to fix and in which file, so it only needs to read those files.
419
+ */
420
+ export function buildFixPrompt(findings: DriftFinding[]): string {
421
+ const parts: string[] = [
422
+ `You are a documentation fixer. Fix ONLY the issues listed below.`,
423
+ ``,
424
+ `Rules:`,
425
+ `1. Only fix factual inaccuracies and add missing sections for undocumented features`,
426
+ `2. Do NOT rewrite prose, improve wording, or restructure existing content`,
427
+ `3. Preserve each document's existing formatting, tone, and conventions`,
428
+ `4. Keep it concise — match the style of existing documentation`,
429
+ ``,
430
+ `## Findings to fix`,
431
+ ``,
432
+ ];
433
+
434
+ // Group findings by file for clarity
435
+ const byFile = new Map<string, DriftFinding[]>();
436
+ for (const f of findings) {
437
+ if (!byFile.has(f.file)) byFile.set(f.file, []);
438
+ byFile.get(f.file)!.push(f);
439
+ }
440
+
441
+ for (const [file, fileFindings] of byFile) {
442
+ parts.push(`### \`${file}\``);
443
+ for (const f of fileFindings) {
444
+ parts.push(`- [${f.severity}] ${f.description}`);
445
+ if (f.relatedFiles?.length) {
446
+ parts.push(` Related source: ${f.relatedFiles.join(", ")}`);
447
+ }
448
+ }
449
+ parts.push(``);
450
+ }
451
+
452
+ parts.push(`Read each file listed above, apply the fixes, and write the corrected files.`);
453
+ return parts.join("\n");
454
+ }
@@ -0,0 +1,66 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { Platform } from "../platform/types.js";
4
+
5
+ const INLINE_COMMENTS_JQ =
6
+ '.[] | {id, path, line: .line, body, user: .user.login, userType: .user.type, createdAt: .created_at, updatedAt: .updated_at, inReplyToId: .in_reply_to_id, diffHunk: .diff_hunk, state: "COMMENTED"}';
7
+
8
+ const REVIEW_COMMENTS_JQ =
9
+ '.[] | select(.body != null and .body != "") | {id, path: null, line: null, body, user: .user.login, userType: .user.type, createdAt: .submitted_at, updatedAt: .submitted_at, inReplyToId: null, diffHunk: null, state}';
10
+
11
+ /**
12
+ * Fetch all review comments for a PR and write them as JSONL to outputPath.
13
+ *
14
+ * Calls `gh api` directly instead of shelling out to a bash script, avoiding
15
+ * Windows path resolution failures when Bun.spawn invokes bash.exe without
16
+ * a proper MSYS2 shell context.
17
+ *
18
+ * @returns Error message if both API calls failed, undefined on success.
19
+ */
20
+ export async function fetchPrComments(
21
+ platform: Platform,
22
+ repo: string,
23
+ prNumber: number,
24
+ outputPath: string,
25
+ cwd: string,
26
+ ): Promise<string | undefined> {
27
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
28
+
29
+ // Fetch inline review comments (code-level)
30
+ const inlineResult = await platform.exec(
31
+ "gh",
32
+ [
33
+ "api",
34
+ "--paginate",
35
+ `repos/${repo}/pulls/${prNumber}/comments`,
36
+ "--jq",
37
+ INLINE_COMMENTS_JQ,
38
+ ],
39
+ { cwd },
40
+ );
41
+
42
+ // Write what we got — empty string on failure (mirrors || true in the original script)
43
+ fs.writeFileSync(outputPath, inlineResult.code === 0 ? inlineResult.stdout : "");
44
+
45
+ // Fetch review-level comments (top-level reviews with body text)
46
+ const reviewResult = await platform.exec(
47
+ "gh",
48
+ [
49
+ "api",
50
+ "--paginate",
51
+ `repos/${repo}/pulls/${prNumber}/reviews`,
52
+ "--jq",
53
+ REVIEW_COMMENTS_JQ,
54
+ ],
55
+ { cwd },
56
+ );
57
+
58
+ if (reviewResult.code === 0 && reviewResult.stdout) {
59
+ fs.appendFileSync(outputPath, reviewResult.stdout);
60
+ }
61
+
62
+ // Both calls failed — gh is broken, not just empty results
63
+ if (inlineResult.code !== 0 && reviewResult.code !== 0) {
64
+ return inlineResult.stderr || reviewResult.stderr || "gh api calls failed";
65
+ }
66
+ }
@@ -1,6 +1,7 @@
1
1
  // src/git/commit-msg.ts — Commit message validation against conventional commit format
2
2
 
3
3
  import { VALID_COMMIT_TYPES } from "../release/commit-types.js";
4
+ import { normalizeLineEndings } from "../text.js";
4
5
 
5
6
  // Same regex shape as changelog.ts CONVENTIONAL_PREFIX
6
7
  const CONVENTIONAL_RE = /^([a-z]+)(?:\(([^)]+)\))?(!)?:\s+(.+)$/;
@@ -20,7 +21,7 @@ export interface CommitValidation {
20
21
  }
21
22
 
22
23
  export function validateCommitMessage(message: string): CommitValidation {
23
- const firstLine = message.split("\n")[0].trim();
24
+ const firstLine = normalizeLineEndings(message).split("\n")[0].trim();
24
25
 
25
26
  if (!firstLine) {
26
27
  return { valid: false, error: "Commit message is empty." };