supipowers 0.1.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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +194 -0
  3. package/bin/install.mjs +220 -0
  4. package/package.json +38 -0
  5. package/skills/code-review/SKILL.md +45 -0
  6. package/skills/debugging/SKILL.md +23 -0
  7. package/skills/planning/SKILL.md +54 -0
  8. package/skills/qa-strategy/SKILL.md +32 -0
  9. package/src/commands/config.ts +70 -0
  10. package/src/commands/plan.ts +85 -0
  11. package/src/commands/qa.ts +52 -0
  12. package/src/commands/release.ts +60 -0
  13. package/src/commands/review.ts +84 -0
  14. package/src/commands/run.ts +175 -0
  15. package/src/commands/status.ts +51 -0
  16. package/src/commands/supi.ts +42 -0
  17. package/src/config/defaults.ts +72 -0
  18. package/src/config/loader.ts +101 -0
  19. package/src/config/profiles.ts +64 -0
  20. package/src/config/schema.ts +42 -0
  21. package/src/index.ts +28 -0
  22. package/src/lsp/bridge.ts +59 -0
  23. package/src/lsp/detector.ts +38 -0
  24. package/src/lsp/setup-guide.ts +81 -0
  25. package/src/notifications/renderer.ts +67 -0
  26. package/src/notifications/types.ts +19 -0
  27. package/src/orchestrator/batch-scheduler.ts +59 -0
  28. package/src/orchestrator/conflict-resolver.ts +38 -0
  29. package/src/orchestrator/dispatcher.ts +106 -0
  30. package/src/orchestrator/prompts.ts +123 -0
  31. package/src/orchestrator/result-collector.ts +72 -0
  32. package/src/qa/detector.ts +61 -0
  33. package/src/qa/report.ts +22 -0
  34. package/src/qa/runner.ts +46 -0
  35. package/src/quality/ai-review-gate.ts +43 -0
  36. package/src/quality/gate-runner.ts +67 -0
  37. package/src/quality/lsp-gate.ts +24 -0
  38. package/src/quality/test-gate.ts +39 -0
  39. package/src/release/analyzer.ts +22 -0
  40. package/src/release/notes.ts +26 -0
  41. package/src/release/publisher.ts +33 -0
  42. package/src/storage/plans.ts +129 -0
  43. package/src/storage/reports.ts +36 -0
  44. package/src/storage/runs.ts +124 -0
  45. package/src/types.ts +142 -0
@@ -0,0 +1,106 @@
1
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
2
+ import type { PlanTask, AgentResult, AgentStatus, SupipowersConfig } from "../types.js";
3
+ import { buildTaskPrompt, buildFixPrompt } from "./prompts.js";
4
+ import { isLspAvailable } from "../lsp/detector.js";
5
+ import { notifySuccess, notifyWarning, notifyError } from "../notifications/renderer.js";
6
+
7
+ export interface DispatchOptions {
8
+ pi: ExtensionAPI;
9
+ ctx: { cwd: string; ui: { notify(msg: string, type?: "info" | "warning" | "error"): void } };
10
+ task: PlanTask;
11
+ planContext: string;
12
+ config: SupipowersConfig;
13
+ lspAvailable: boolean;
14
+ }
15
+
16
+ export async function dispatchAgent(options: DispatchOptions): Promise<AgentResult> {
17
+ const { pi, ctx, task, planContext, config, lspAvailable } = options;
18
+ const startTime = Date.now();
19
+
20
+ const prompt = buildTaskPrompt(task, planContext, config, lspAvailable);
21
+
22
+ try {
23
+ const result = await executeSubAgent(pi, prompt, task, config);
24
+
25
+ const agentResult: AgentResult = {
26
+ taskId: task.id,
27
+ status: result.status,
28
+ output: result.output,
29
+ concerns: result.concerns,
30
+ filesChanged: result.filesChanged,
31
+ duration: Date.now() - startTime,
32
+ };
33
+
34
+ switch (agentResult.status) {
35
+ case "done":
36
+ notifySuccess(ctx, `Task ${task.id} completed`, task.name);
37
+ break;
38
+ case "done_with_concerns":
39
+ notifyWarning(ctx, `Task ${task.id} done with concerns`, agentResult.concerns);
40
+ break;
41
+ case "blocked":
42
+ notifyError(ctx, `Task ${task.id} blocked`, agentResult.output);
43
+ break;
44
+ }
45
+
46
+ return agentResult;
47
+ } catch (error) {
48
+ const agentResult: AgentResult = {
49
+ taskId: task.id,
50
+ status: "blocked",
51
+ output: `Agent error: ${error instanceof Error ? error.message : String(error)}`,
52
+ filesChanged: [],
53
+ duration: Date.now() - startTime,
54
+ };
55
+ notifyError(ctx, `Task ${task.id} failed`, agentResult.output);
56
+ return agentResult;
57
+ }
58
+ }
59
+
60
+ interface SubAgentResult {
61
+ status: AgentStatus;
62
+ output: string;
63
+ concerns?: string;
64
+ filesChanged: string[];
65
+ }
66
+
67
+ async function executeSubAgent(
68
+ pi: ExtensionAPI,
69
+ prompt: string,
70
+ task: PlanTask,
71
+ config: SupipowersConfig
72
+ ): Promise<SubAgentResult> {
73
+ throw new Error(
74
+ "Sub-agent dispatch requires OMP runtime. " +
75
+ "This will be connected to createAgentSession during integration."
76
+ );
77
+ }
78
+
79
+ export async function dispatchFixAgent(
80
+ options: DispatchOptions & { previousOutput: string; failureReason: string }
81
+ ): Promise<AgentResult> {
82
+ const { pi, ctx, task, config, lspAvailable, previousOutput, failureReason } = options;
83
+ const startTime = Date.now();
84
+
85
+ const prompt = buildFixPrompt(task, previousOutput, failureReason, lspAvailable);
86
+
87
+ try {
88
+ const result = await executeSubAgent(pi, prompt, task, config);
89
+ return {
90
+ taskId: task.id,
91
+ status: result.status,
92
+ output: result.output,
93
+ concerns: result.concerns,
94
+ filesChanged: result.filesChanged,
95
+ duration: Date.now() - startTime,
96
+ };
97
+ } catch (error) {
98
+ return {
99
+ taskId: task.id,
100
+ status: "blocked",
101
+ output: `Fix agent error: ${error instanceof Error ? error.message : String(error)}`,
102
+ filesChanged: [],
103
+ duration: Date.now() - startTime,
104
+ };
105
+ }
106
+ }
@@ -0,0 +1,123 @@
1
+ // src/orchestrator/prompts.ts
2
+ import type { PlanTask, SupipowersConfig } from "../types.js";
3
+ import { buildLspValidationPrompt } from "../lsp/bridge.js";
4
+
5
+ /** Build the system prompt for a sub-agent executing a task */
6
+ export function buildTaskPrompt(
7
+ task: PlanTask,
8
+ planContext: string,
9
+ config: SupipowersConfig,
10
+ lspAvailable: boolean
11
+ ): string {
12
+ const sections: string[] = [
13
+ "# Task Assignment",
14
+ "",
15
+ `## Task: ${task.name}`,
16
+ "",
17
+ task.description,
18
+ "",
19
+ "## Target Files",
20
+ ...task.files.map((f) => `- ${f}`),
21
+ "",
22
+ "## Acceptance Criteria",
23
+ task.criteria,
24
+ "",
25
+ "## Context",
26
+ planContext,
27
+ "",
28
+ "## Instructions",
29
+ "1. Read the target files to understand current state",
30
+ "2. Implement the changes described above",
31
+ "3. Ensure acceptance criteria are met",
32
+ "4. Report your status when done",
33
+ "",
34
+ "Report one of these statuses:",
35
+ "- DONE: Task completed successfully, all criteria met",
36
+ "- DONE_WITH_CONCERNS: Completed but with caveats (explain what)",
37
+ "- BLOCKED: Cannot complete (explain why and what's needed)",
38
+ ];
39
+
40
+ if (lspAvailable) {
41
+ sections.push(
42
+ "",
43
+ "## LSP Available",
44
+ "You have access to the LSP tool. Use it to:",
45
+ "- Check diagnostics after making changes",
46
+ "- Find references before renaming symbols",
47
+ "- Validate your work has no type errors",
48
+ "",
49
+ buildLspValidationPrompt(task.files)
50
+ );
51
+ }
52
+
53
+ return sections.join("\n");
54
+ }
55
+
56
+ /** Build prompt for a fix agent */
57
+ export function buildFixPrompt(
58
+ task: PlanTask,
59
+ previousOutput: string,
60
+ failureReason: string,
61
+ lspAvailable: boolean
62
+ ): string {
63
+ const sections: string[] = [
64
+ "# Fix Assignment",
65
+ "",
66
+ `## Original Task: ${task.name}`,
67
+ "",
68
+ "## What Went Wrong",
69
+ failureReason,
70
+ "",
71
+ "## Previous Agent Output",
72
+ previousOutput,
73
+ "",
74
+ "## Target Files",
75
+ ...task.files.map((f) => `- ${f}`),
76
+ "",
77
+ "## Acceptance Criteria",
78
+ task.criteria,
79
+ "",
80
+ "## Instructions",
81
+ "1. Understand what the previous agent attempted",
82
+ "2. Identify and fix the issue",
83
+ "3. Verify the acceptance criteria are now met",
84
+ "4. Report your status",
85
+ ];
86
+
87
+ if (lspAvailable) {
88
+ sections.push("", buildLspValidationPrompt(task.files));
89
+ }
90
+
91
+ return sections.join("\n");
92
+ }
93
+
94
+ /** Build prompt for a merge/conflict resolution agent */
95
+ export function buildMergePrompt(
96
+ conflictingFiles: string[],
97
+ agentOutputs: { taskName: string; output: string }[]
98
+ ): string {
99
+ const sections: string[] = [
100
+ "# Merge Assignment",
101
+ "",
102
+ "Multiple agents edited the same files. Resolve the conflicts.",
103
+ "",
104
+ "## Conflicting Files",
105
+ ...conflictingFiles.map((f) => `- ${f}`),
106
+ "",
107
+ "## Agent Outputs",
108
+ ];
109
+
110
+ for (const { taskName, output } of agentOutputs) {
111
+ sections.push(`### ${taskName}`, output, "");
112
+ }
113
+
114
+ sections.push(
115
+ "## Instructions",
116
+ "1. Read each conflicting file",
117
+ "2. Understand what each agent intended",
118
+ "3. Merge the changes so both intents are preserved",
119
+ "4. If changes are incompatible, report BLOCKED with explanation"
120
+ );
121
+
122
+ return sections.join("\n");
123
+ }
@@ -0,0 +1,72 @@
1
+ import type { AgentResult, RunBatch } from "../types.js";
2
+
3
+ export interface BatchSummary {
4
+ batchIndex: number;
5
+ total: number;
6
+ done: number;
7
+ doneWithConcerns: number;
8
+ blocked: number;
9
+ allPassed: boolean;
10
+ concerns: string[];
11
+ blockers: string[];
12
+ filesChanged: string[];
13
+ }
14
+
15
+ export function summarizeBatch(
16
+ batch: RunBatch,
17
+ results: AgentResult[]
18
+ ): BatchSummary {
19
+ const batchResults = results.filter((r) => batch.taskIds.includes(r.taskId));
20
+
21
+ const done = batchResults.filter((r) => r.status === "done").length;
22
+ const doneWithConcerns = batchResults.filter(
23
+ (r) => r.status === "done_with_concerns"
24
+ ).length;
25
+ const blocked = batchResults.filter((r) => r.status === "blocked").length;
26
+
27
+ return {
28
+ batchIndex: batch.index,
29
+ total: batch.taskIds.length,
30
+ done,
31
+ doneWithConcerns,
32
+ blocked,
33
+ allPassed: blocked === 0,
34
+ concerns: batchResults
35
+ .filter((r) => r.concerns)
36
+ .map((r) => r.concerns!),
37
+ blockers: batchResults
38
+ .filter((r) => r.status === "blocked")
39
+ .map((r) => r.output),
40
+ filesChanged: batchResults.flatMap((r) => r.filesChanged),
41
+ };
42
+ }
43
+
44
+ export function detectConflicts(results: AgentResult[]): string[] {
45
+ const fileCounts = new Map<string, number>();
46
+ for (const result of results) {
47
+ for (const file of result.filesChanged) {
48
+ fileCounts.set(file, (fileCounts.get(file) ?? 0) + 1);
49
+ }
50
+ }
51
+ return [...fileCounts.entries()]
52
+ .filter(([, count]) => count > 1)
53
+ .map(([file]) => file);
54
+ }
55
+
56
+ export function buildRunSummary(allResults: AgentResult[]): {
57
+ totalTasks: number;
58
+ done: number;
59
+ doneWithConcerns: number;
60
+ blocked: number;
61
+ totalFilesChanged: number;
62
+ totalDuration: number;
63
+ } {
64
+ return {
65
+ totalTasks: allResults.length,
66
+ done: allResults.filter((r) => r.status === "done").length,
67
+ doneWithConcerns: allResults.filter((r) => r.status === "done_with_concerns").length,
68
+ blocked: allResults.filter((r) => r.status === "blocked").length,
69
+ totalFilesChanged: new Set(allResults.flatMap((r) => r.filesChanged)).size,
70
+ totalDuration: allResults.reduce((sum, r) => sum + r.duration, 0),
71
+ };
72
+ }
@@ -0,0 +1,61 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { updateConfig, loadConfig } from "../config/loader.js";
4
+
5
+ export interface DetectedFramework {
6
+ name: string;
7
+ command: string;
8
+ }
9
+
10
+ const FRAMEWORK_SIGNATURES: { name: string; files: string[]; command: string }[] = [
11
+ { name: "vitest", files: ["vitest.config.ts", "vitest.config.js", "vitest.config.mts"], command: "npx vitest run" },
12
+ { name: "jest", files: ["jest.config.ts", "jest.config.js", "jest.config.mjs"], command: "npx jest" },
13
+ { name: "mocha", files: [".mocharc.yml", ".mocharc.json", ".mocharc.js"], command: "npx mocha" },
14
+ { name: "pytest", files: ["pytest.ini", "pyproject.toml", "conftest.py"], command: "pytest" },
15
+ { name: "cargo-test", files: ["Cargo.toml"], command: "cargo test" },
16
+ { name: "go-test", files: ["go.mod"], command: "go test ./..." },
17
+ ];
18
+
19
+ export function detectFramework(cwd: string): DetectedFramework | null {
20
+ const pkgPath = path.join(cwd, "package.json");
21
+ if (fs.existsSync(pkgPath)) {
22
+ try {
23
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
24
+ if (pkg.scripts?.test && pkg.scripts.test !== 'echo "Error: no test specified" && exit 1') {
25
+ const testScript = pkg.scripts.test;
26
+ for (const sig of FRAMEWORK_SIGNATURES) {
27
+ if (testScript.includes(sig.name)) {
28
+ return { name: sig.name, command: "npm test" };
29
+ }
30
+ }
31
+ return { name: "npm-test", command: "npm test" };
32
+ }
33
+ } catch {
34
+ // continue to file-based detection
35
+ }
36
+ }
37
+
38
+ for (const sig of FRAMEWORK_SIGNATURES) {
39
+ for (const file of sig.files) {
40
+ if (fs.existsSync(path.join(cwd, file))) {
41
+ return { name: sig.name, command: sig.command };
42
+ }
43
+ }
44
+ }
45
+
46
+ return null;
47
+ }
48
+
49
+ export function detectAndCache(cwd: string): DetectedFramework | null {
50
+ const config = loadConfig(cwd);
51
+
52
+ if (config.qa.framework && config.qa.command) {
53
+ return { name: config.qa.framework, command: config.qa.command };
54
+ }
55
+
56
+ const detected = detectFramework(cwd);
57
+ if (detected) {
58
+ updateConfig(cwd, { qa: { framework: detected.name, command: detected.command } });
59
+ }
60
+ return detected;
61
+ }
@@ -0,0 +1,22 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export interface QaReport {
5
+ timestamp: string;
6
+ framework: string;
7
+ scope: string;
8
+ totalTests: number;
9
+ passed: number;
10
+ failed: number;
11
+ skipped: number;
12
+ failures: { name: string; file: string; error: string }[];
13
+ }
14
+
15
+ export function saveQaReport(cwd: string, report: QaReport): string {
16
+ const dir = path.join(cwd, ".omp", "supipowers", "reports");
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ const filename = `qa-${report.timestamp.slice(0, 10)}.json`;
19
+ const filePath = path.join(dir, filename);
20
+ fs.writeFileSync(filePath, JSON.stringify(report, null, 2) + "\n");
21
+ return filePath;
22
+ }
@@ -0,0 +1,46 @@
1
+ export function buildQaRunPrompt(
2
+ command: string,
3
+ scope: "all" | "changed" | "e2e",
4
+ changedFiles?: string[]
5
+ ): string {
6
+ const sections: string[] = ["# QA Pipeline", ""];
7
+
8
+ switch (scope) {
9
+ case "all":
10
+ sections.push(`Run the full test suite: \`${command}\``);
11
+ break;
12
+ case "changed":
13
+ sections.push(
14
+ "Run tests related to changed files only:",
15
+ ...(changedFiles ?? []).map((f) => `- ${f}`),
16
+ "",
17
+ `Base command: \`${command}\``,
18
+ "Filter to only tests relevant to the files above."
19
+ );
20
+ break;
21
+ case "e2e":
22
+ sections.push(
23
+ "Run end-to-end tests only.",
24
+ "Use Playwright or the configured E2E framework.",
25
+ "Command: `npx playwright test`"
26
+ );
27
+ break;
28
+ }
29
+
30
+ sections.push(
31
+ "",
32
+ "Report results in this format:",
33
+ "- Total tests: N",
34
+ "- Passed: N",
35
+ "- Failed: N",
36
+ "- Skipped: N",
37
+ "",
38
+ "For each failure, include:",
39
+ "- Test name",
40
+ "- File path",
41
+ "- Error message",
42
+ "- Stack trace (first 5 lines)"
43
+ );
44
+
45
+ return sections.join("\n");
46
+ }
@@ -0,0 +1,43 @@
1
+ import type { GateResult } from "../types.js";
2
+
3
+ export function buildAiReviewPrompt(
4
+ changedFiles: string[],
5
+ depth: "quick" | "deep"
6
+ ): string {
7
+ const depthInstructions =
8
+ depth === "quick"
9
+ ? "Do a quick scan: check for obvious bugs, security issues, and naming problems."
10
+ : [
11
+ "Do a thorough review covering:",
12
+ "- Correctness and edge cases",
13
+ "- Security vulnerabilities (OWASP top 10)",
14
+ "- Performance concerns",
15
+ "- Code clarity and maintainability",
16
+ "- Error handling completeness",
17
+ "- Test coverage gaps",
18
+ ].join("\n");
19
+
20
+ return [
21
+ "Review the following changed files:",
22
+ ...changedFiles.map((f) => `- ${f}`),
23
+ "",
24
+ depthInstructions,
25
+ "",
26
+ "For each issue found, report:",
27
+ "- Severity: error | warning | info",
28
+ "- File and line number",
29
+ "- Description of the issue",
30
+ "- Suggested fix",
31
+ ].join("\n");
32
+ }
33
+
34
+ export function createAiReviewResult(
35
+ issues: { severity: "error" | "warning" | "info"; message: string; file?: string; line?: number }[]
36
+ ): GateResult {
37
+ const hasErrors = issues.some((i) => i.severity === "error");
38
+ return {
39
+ gate: "ai-review",
40
+ passed: !hasErrors,
41
+ issues,
42
+ };
43
+ }
@@ -0,0 +1,67 @@
1
+ import type { Profile, GateResult, ReviewReport } from "../types.js";
2
+ import { buildLspGatePrompt } from "./lsp-gate.js";
3
+ import { buildAiReviewPrompt } from "./ai-review-gate.js";
4
+ import { buildTestGatePrompt } from "./test-gate.js";
5
+
6
+ export interface GateRunnerOptions {
7
+ profile: Profile;
8
+ changedFiles: string[];
9
+ testCommand: string | null;
10
+ lspAvailable: boolean;
11
+ }
12
+
13
+ export function getActiveGates(profile: Profile, lspAvailable: boolean): string[] {
14
+ const gates: string[] = [];
15
+ if (profile.gates.lspDiagnostics && lspAvailable) gates.push("lsp-diagnostics");
16
+ if (profile.gates.aiReview.enabled) gates.push("ai-review");
17
+ if (profile.gates.codeQuality) gates.push("code-quality");
18
+ if (profile.gates.testSuite) gates.push("test-suite");
19
+ if (profile.gates.e2e) gates.push("e2e");
20
+ return gates;
21
+ }
22
+
23
+ export function buildReviewPrompt(options: GateRunnerOptions): string {
24
+ const { profile, changedFiles, testCommand, lspAvailable } = options;
25
+ const sections: string[] = [
26
+ "# Code Review",
27
+ "",
28
+ `Profile: ${profile.name}`,
29
+ "",
30
+ "Run the following quality checks and report results for each:",
31
+ "",
32
+ ];
33
+
34
+ if (profile.gates.lspDiagnostics && lspAvailable) {
35
+ sections.push("## 1. LSP Diagnostics", buildLspGatePrompt(changedFiles), "");
36
+ }
37
+
38
+ if (profile.gates.aiReview.enabled) {
39
+ sections.push(
40
+ "## 2. Code Review",
41
+ buildAiReviewPrompt(changedFiles, profile.gates.aiReview.depth),
42
+ ""
43
+ );
44
+ }
45
+
46
+ if (profile.gates.testSuite) {
47
+ sections.push(
48
+ "## 3. Test Suite",
49
+ buildTestGatePrompt(testCommand, false),
50
+ ""
51
+ );
52
+ }
53
+
54
+ return sections.join("\n");
55
+ }
56
+
57
+ export function createReviewReport(
58
+ profile: string,
59
+ gates: GateResult[]
60
+ ): ReviewReport {
61
+ return {
62
+ profile,
63
+ timestamp: new Date().toISOString(),
64
+ gates,
65
+ passed: gates.every((g) => g.passed),
66
+ };
67
+ }
@@ -0,0 +1,24 @@
1
+ import type { GateResult } from "../types.js";
2
+
3
+ export function buildLspGatePrompt(changedFiles: string[]): string {
4
+ return [
5
+ "Run LSP diagnostics on these files and report results:",
6
+ ...changedFiles.map((f) => `- ${f}`),
7
+ "",
8
+ 'Use the lsp tool with action "diagnostics" for each file.',
9
+ "Summarize: total errors, total warnings, and list each issue.",
10
+ ].join("\n");
11
+ }
12
+
13
+ export function createLspGateResult(
14
+ hasErrors: boolean,
15
+ errorCount: number,
16
+ warningCount: number,
17
+ issues: { severity: "error" | "warning" | "info"; message: string; file?: string; line?: number }[]
18
+ ): GateResult {
19
+ return {
20
+ gate: "lsp-diagnostics",
21
+ passed: !hasErrors,
22
+ issues,
23
+ };
24
+ }
@@ -0,0 +1,39 @@
1
+ import type { GateResult } from "../types.js";
2
+
3
+ export function buildTestGatePrompt(
4
+ testCommand: string | null,
5
+ changedOnly: boolean,
6
+ changedFiles?: string[]
7
+ ): string {
8
+ const cmd = testCommand ?? "npm test";
9
+ const scope = changedOnly && changedFiles
10
+ ? `Only run tests related to: ${changedFiles.join(", ")}`
11
+ : "Run the full test suite.";
12
+
13
+ return [
14
+ scope,
15
+ "",
16
+ `Command: ${cmd}`,
17
+ "",
18
+ "Report the results:",
19
+ "- Total tests, passed, failed, skipped",
20
+ "- For each failure: test name, file, error message",
21
+ ].join("\n");
22
+ }
23
+
24
+ export function createTestGateResult(
25
+ passed: boolean,
26
+ totalTests: number,
27
+ failedTests: number,
28
+ failures: { message: string; file?: string }[]
29
+ ): GateResult {
30
+ return {
31
+ gate: "test-suite",
32
+ passed,
33
+ issues: failures.map((f) => ({
34
+ severity: "error" as const,
35
+ message: `Test failed: ${f.message}`,
36
+ file: f.file,
37
+ })),
38
+ };
39
+ }
@@ -0,0 +1,22 @@
1
+ /** Build prompt to analyze commits and suggest version bump */
2
+ export function buildAnalyzerPrompt(lastTag: string | null): string {
3
+ const sinceArg = lastTag ? `${lastTag}..HEAD` : "HEAD~20..HEAD";
4
+ return [
5
+ "# Release Analysis",
6
+ "",
7
+ `Analyze commits since ${lastTag ?? "beginning"}.`,
8
+ "",
9
+ `Run: git log ${sinceArg} --oneline --no-decorate`,
10
+ "",
11
+ "Then determine:",
12
+ "1. Version bump type: major (breaking changes), minor (new features), patch (fixes)",
13
+ "2. Categorize commits: features, fixes, breaking changes, other",
14
+ "3. Suggest the next version number",
15
+ "",
16
+ "Report in this format:",
17
+ "- Current version: <from package.json or last tag>",
18
+ "- Suggested bump: major|minor|patch",
19
+ "- Next version: X.Y.Z",
20
+ "- Changes summary: categorized list",
21
+ ].join("\n");
22
+ }
@@ -0,0 +1,26 @@
1
+ /** Build prompt to generate release notes */
2
+ export function buildNotesPrompt(version: string, lastTag: string | null): string {
3
+ const sinceArg = lastTag ? `${lastTag}..HEAD` : "HEAD~20..HEAD";
4
+ return [
5
+ "# Generate Release Notes",
6
+ "",
7
+ `Version: ${version}`,
8
+ "",
9
+ `Run: git log ${sinceArg} --format="%h %s"`,
10
+ "",
11
+ "Generate release notes in this format:",
12
+ "",
13
+ `## ${version}`,
14
+ "",
15
+ "### Features",
16
+ "- Description (commit hash)",
17
+ "",
18
+ "### Fixes",
19
+ "- Description (commit hash)",
20
+ "",
21
+ "### Breaking Changes",
22
+ "- Description (commit hash)",
23
+ "",
24
+ "Keep descriptions user-facing (not commit-message-level detail).",
25
+ ].join("\n");
26
+ }
@@ -0,0 +1,33 @@
1
+ /** Build prompt to execute release */
2
+ export function buildPublishPrompt(
3
+ version: string,
4
+ pipeline: string | null
5
+ ): string {
6
+ const steps: string[] = [
7
+ "# Publish Release",
8
+ "",
9
+ `Version: ${version}`,
10
+ "",
11
+ "Execute these steps (ask for confirmation before each):",
12
+ "",
13
+ "1. Update version in package.json",
14
+ `2. git add package.json && git commit -m "release: v${version}"`,
15
+ `3. git tag v${version}`,
16
+ ];
17
+
18
+ if (pipeline === "npm") {
19
+ steps.push("4. npm publish");
20
+ } else if (pipeline === "github") {
21
+ steps.push(`4. gh release create v${version} --generate-notes`);
22
+ } else {
23
+ steps.push("4. Ask the user what publish step to run");
24
+ }
25
+
26
+ steps.push(
27
+ "",
28
+ "IMPORTANT: Ask for user confirmation before tagging and publishing.",
29
+ "Show them the version, changelog, and what will be published."
30
+ );
31
+
32
+ return steps.join("\n");
33
+ }