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.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/bin/install.mjs +220 -0
- package/package.json +38 -0
- package/skills/code-review/SKILL.md +45 -0
- package/skills/debugging/SKILL.md +23 -0
- package/skills/planning/SKILL.md +54 -0
- package/skills/qa-strategy/SKILL.md +32 -0
- package/src/commands/config.ts +70 -0
- package/src/commands/plan.ts +85 -0
- package/src/commands/qa.ts +52 -0
- package/src/commands/release.ts +60 -0
- package/src/commands/review.ts +84 -0
- package/src/commands/run.ts +175 -0
- package/src/commands/status.ts +51 -0
- package/src/commands/supi.ts +42 -0
- package/src/config/defaults.ts +72 -0
- package/src/config/loader.ts +101 -0
- package/src/config/profiles.ts +64 -0
- package/src/config/schema.ts +42 -0
- package/src/index.ts +28 -0
- package/src/lsp/bridge.ts +59 -0
- package/src/lsp/detector.ts +38 -0
- package/src/lsp/setup-guide.ts +81 -0
- package/src/notifications/renderer.ts +67 -0
- package/src/notifications/types.ts +19 -0
- package/src/orchestrator/batch-scheduler.ts +59 -0
- package/src/orchestrator/conflict-resolver.ts +38 -0
- package/src/orchestrator/dispatcher.ts +106 -0
- package/src/orchestrator/prompts.ts +123 -0
- package/src/orchestrator/result-collector.ts +72 -0
- package/src/qa/detector.ts +61 -0
- package/src/qa/report.ts +22 -0
- package/src/qa/runner.ts +46 -0
- package/src/quality/ai-review-gate.ts +43 -0
- package/src/quality/gate-runner.ts +67 -0
- package/src/quality/lsp-gate.ts +24 -0
- package/src/quality/test-gate.ts +39 -0
- package/src/release/analyzer.ts +22 -0
- package/src/release/notes.ts +26 -0
- package/src/release/publisher.ts +33 -0
- package/src/storage/plans.ts +129 -0
- package/src/storage/reports.ts +36 -0
- package/src/storage/runs.ts +124 -0
- package/src/types.ts +142 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { Plan, PlanTask, TaskComplexity, TaskParallelism } from "../types.js";
|
|
4
|
+
|
|
5
|
+
const PLANS_DIR = [".omp", "supipowers", "plans"];
|
|
6
|
+
|
|
7
|
+
function getPlansDir(cwd: string): string {
|
|
8
|
+
return path.join(cwd, ...PLANS_DIR);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** List all saved plans */
|
|
12
|
+
export function listPlans(cwd: string): string[] {
|
|
13
|
+
const dir = getPlansDir(cwd);
|
|
14
|
+
if (!fs.existsSync(dir)) return [];
|
|
15
|
+
return fs
|
|
16
|
+
.readdirSync(dir)
|
|
17
|
+
.filter((f) => f.endsWith(".md"))
|
|
18
|
+
.sort()
|
|
19
|
+
.reverse();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Read a plan file by name */
|
|
23
|
+
export function readPlanFile(cwd: string, name: string): string | null {
|
|
24
|
+
const filePath = path.join(getPlansDir(cwd), name);
|
|
25
|
+
if (!fs.existsSync(filePath)) return null;
|
|
26
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Save a plan markdown file */
|
|
30
|
+
export function savePlan(cwd: string, filename: string, content: string): string {
|
|
31
|
+
const dir = getPlansDir(cwd);
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
const filePath = path.join(dir, filename);
|
|
34
|
+
fs.writeFileSync(filePath, content);
|
|
35
|
+
return filePath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Parse a plan markdown file into a Plan object */
|
|
39
|
+
export function parsePlan(content: string, filePath: string): Plan {
|
|
40
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
41
|
+
const meta: Record<string, string | string[]> = {};
|
|
42
|
+
|
|
43
|
+
if (frontmatterMatch) {
|
|
44
|
+
for (const line of frontmatterMatch[1].split("\n")) {
|
|
45
|
+
const colonIdx = line.indexOf(":");
|
|
46
|
+
if (colonIdx === -1) continue;
|
|
47
|
+
const key = line.slice(0, colonIdx).trim();
|
|
48
|
+
const val = line.slice(colonIdx + 1).trim();
|
|
49
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
50
|
+
meta[key] = val
|
|
51
|
+
.slice(1, -1)
|
|
52
|
+
.split(",")
|
|
53
|
+
.map((s) => s.trim());
|
|
54
|
+
} else {
|
|
55
|
+
meta[key] = val;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tasks = parseTasksFromMarkdown(content);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name: (meta.name as string) ?? path.basename(filePath, ".md"),
|
|
64
|
+
created: (meta.created as string) ?? "",
|
|
65
|
+
tags: (meta.tags as string[]) ?? [],
|
|
66
|
+
context: extractContext(content),
|
|
67
|
+
tasks,
|
|
68
|
+
filePath,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractContext(content: string): string {
|
|
73
|
+
const contextMatch = content.match(/## Context\n\n?([\s\S]*?)(?=\n## |$)/);
|
|
74
|
+
return contextMatch?.[1]?.trim() ?? "";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseTasksFromMarkdown(content: string): PlanTask[] {
|
|
78
|
+
const tasks: PlanTask[] = [];
|
|
79
|
+
const taskRegex = /### (\d+)\. (.+)/g;
|
|
80
|
+
let match: RegExpExecArray | null;
|
|
81
|
+
|
|
82
|
+
while ((match = taskRegex.exec(content)) !== null) {
|
|
83
|
+
const id = parseInt(match[1], 10);
|
|
84
|
+
const headerLine = match[2];
|
|
85
|
+
const startIdx = match.index + match[0].length;
|
|
86
|
+
const nextTaskMatch = /\n### \d+\. /.exec(content.slice(startIdx));
|
|
87
|
+
const endIdx = nextTaskMatch
|
|
88
|
+
? startIdx + nextTaskMatch.index
|
|
89
|
+
: content.length;
|
|
90
|
+
const body = content.slice(startIdx, endIdx);
|
|
91
|
+
|
|
92
|
+
const name = headerLine.replace(/\[.*?\]/g, "").trim();
|
|
93
|
+
const parallelism = parseParallelism(headerLine);
|
|
94
|
+
const files = parseFiles(body);
|
|
95
|
+
const criteria = parseCriteria(body);
|
|
96
|
+
const complexity = parseComplexity(body);
|
|
97
|
+
|
|
98
|
+
tasks.push({ id, name, description: name, files, criteria, complexity, parallelism });
|
|
99
|
+
}
|
|
100
|
+
return tasks;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseParallelism(header: string): TaskParallelism {
|
|
104
|
+
if (header.includes("[parallel-safe]")) return { type: "parallel-safe" };
|
|
105
|
+
const seqMatch = header.match(/\[sequential: depends on (\d[\d, ]*)\]/);
|
|
106
|
+
if (seqMatch) {
|
|
107
|
+
const deps = seqMatch[1].split(",").map((s) => parseInt(s.trim(), 10));
|
|
108
|
+
return { type: "sequential", dependsOn: deps };
|
|
109
|
+
}
|
|
110
|
+
return { type: "sequential", dependsOn: [] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseFiles(body: string): string[] {
|
|
114
|
+
const filesMatch = body.match(/\*\*files?\*\*:\s*(.+)/i);
|
|
115
|
+
if (!filesMatch) return [];
|
|
116
|
+
return filesMatch[1].split(",").map((s) => s.trim());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseCriteria(body: string): string {
|
|
120
|
+
const match = body.match(/\*\*criteria\*\*:\s*(.+)/i);
|
|
121
|
+
return match?.[1]?.trim() ?? "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseComplexity(body: string): TaskComplexity {
|
|
125
|
+
const match = body.match(/\*\*complexity\*\*:\s*(\w+)/i);
|
|
126
|
+
const val = match?.[1]?.toLowerCase();
|
|
127
|
+
if (val === "small" || val === "medium" || val === "large") return val;
|
|
128
|
+
return "medium";
|
|
129
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ReviewReport } from "../types.js";
|
|
4
|
+
|
|
5
|
+
const REPORTS_DIR = [".omp", "supipowers", "reports"];
|
|
6
|
+
|
|
7
|
+
function getReportsDir(cwd: string): string {
|
|
8
|
+
return path.join(cwd, ...REPORTS_DIR);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Save a review report */
|
|
12
|
+
export function saveReviewReport(cwd: string, report: ReviewReport): string {
|
|
13
|
+
const dir = getReportsDir(cwd);
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
const filename = `review-${report.timestamp.slice(0, 10)}.json`;
|
|
16
|
+
const filePath = path.join(dir, filename);
|
|
17
|
+
fs.writeFileSync(filePath, JSON.stringify(report, null, 2) + "\n");
|
|
18
|
+
return filePath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Load the latest review report */
|
|
22
|
+
export function loadLatestReport(cwd: string): ReviewReport | null {
|
|
23
|
+
const dir = getReportsDir(cwd);
|
|
24
|
+
if (!fs.existsSync(dir)) return null;
|
|
25
|
+
const files = fs
|
|
26
|
+
.readdirSync(dir)
|
|
27
|
+
.filter((f) => f.startsWith("review-") && f.endsWith(".json"))
|
|
28
|
+
.sort()
|
|
29
|
+
.reverse();
|
|
30
|
+
if (files.length === 0) return null;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(path.join(dir, files[0]), "utf-8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { RunManifest, AgentResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
const RUNS_DIR = [".omp", "supipowers", "runs"];
|
|
6
|
+
|
|
7
|
+
function getRunsDir(cwd: string): string {
|
|
8
|
+
return path.join(cwd, ...RUNS_DIR);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getRunDir(cwd: string, runId: string): string {
|
|
12
|
+
return path.join(getRunsDir(cwd), runId);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Generate a unique run ID */
|
|
16
|
+
export function generateRunId(): string {
|
|
17
|
+
const now = new Date();
|
|
18
|
+
const date = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
19
|
+
const time = now.toISOString().slice(11, 19).replace(/:/g, "");
|
|
20
|
+
const suffix = Math.random().toString(36).slice(2, 6);
|
|
21
|
+
return `run-${date}-${time}-${suffix}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Create a new run */
|
|
25
|
+
export function createRun(cwd: string, manifest: RunManifest): void {
|
|
26
|
+
const runDir = getRunDir(cwd, manifest.id);
|
|
27
|
+
fs.mkdirSync(path.join(runDir, "agents"), { recursive: true });
|
|
28
|
+
fs.writeFileSync(
|
|
29
|
+
path.join(runDir, "manifest.json"),
|
|
30
|
+
JSON.stringify(manifest, null, 2) + "\n"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Load a run manifest */
|
|
35
|
+
export function loadRun(cwd: string, runId: string): RunManifest | null {
|
|
36
|
+
const filePath = path.join(getRunDir(cwd, runId), "manifest.json");
|
|
37
|
+
if (!fs.existsSync(filePath)) return null;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Update a run manifest */
|
|
46
|
+
export function updateRun(cwd: string, manifest: RunManifest): void {
|
|
47
|
+
const filePath = path.join(getRunDir(cwd, manifest.id), "manifest.json");
|
|
48
|
+
fs.writeFileSync(filePath, JSON.stringify(manifest, null, 2) + "\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Save an agent result */
|
|
52
|
+
export function saveAgentResult(
|
|
53
|
+
cwd: string,
|
|
54
|
+
runId: string,
|
|
55
|
+
result: AgentResult
|
|
56
|
+
): void {
|
|
57
|
+
const filePath = path.join(
|
|
58
|
+
getRunDir(cwd, runId),
|
|
59
|
+
"agents",
|
|
60
|
+
`task-${result.taskId}.json`
|
|
61
|
+
);
|
|
62
|
+
fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Load an agent result */
|
|
66
|
+
export function loadAgentResult(
|
|
67
|
+
cwd: string,
|
|
68
|
+
runId: string,
|
|
69
|
+
taskId: number
|
|
70
|
+
): AgentResult | null {
|
|
71
|
+
const filePath = path.join(
|
|
72
|
+
getRunDir(cwd, runId),
|
|
73
|
+
"agents",
|
|
74
|
+
`task-${taskId}.json`
|
|
75
|
+
);
|
|
76
|
+
if (!fs.existsSync(filePath)) return null;
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Load all agent results for a run */
|
|
85
|
+
export function loadAllAgentResults(
|
|
86
|
+
cwd: string,
|
|
87
|
+
runId: string
|
|
88
|
+
): AgentResult[] {
|
|
89
|
+
const agentsDir = path.join(getRunDir(cwd, runId), "agents");
|
|
90
|
+
if (!fs.existsSync(agentsDir)) return [];
|
|
91
|
+
return fs
|
|
92
|
+
.readdirSync(agentsDir)
|
|
93
|
+
.filter((f) => f.endsWith(".json"))
|
|
94
|
+
.map((f) => {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(fs.readFileSync(path.join(agentsDir, f), "utf-8"));
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.filter((r): r is AgentResult => r !== null);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** List all runs, newest first */
|
|
105
|
+
export function listRuns(cwd: string): string[] {
|
|
106
|
+
const dir = getRunsDir(cwd);
|
|
107
|
+
if (!fs.existsSync(dir)) return [];
|
|
108
|
+
return fs
|
|
109
|
+
.readdirSync(dir)
|
|
110
|
+
.filter((f) => f.startsWith("run-"))
|
|
111
|
+
.sort()
|
|
112
|
+
.reverse();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Find the latest active run (running or paused) */
|
|
116
|
+
export function findActiveRun(cwd: string): RunManifest | null {
|
|
117
|
+
for (const runId of listRuns(cwd)) {
|
|
118
|
+
const manifest = loadRun(cwd, runId);
|
|
119
|
+
if (manifest && (manifest.status === "running" || manifest.status === "paused")) {
|
|
120
|
+
return manifest;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/types.ts — Shared type definitions for supipowers
|
|
2
|
+
|
|
3
|
+
/** Sub-agent execution status */
|
|
4
|
+
export type AgentStatus = "done" | "done_with_concerns" | "blocked";
|
|
5
|
+
|
|
6
|
+
/** Task complexity level */
|
|
7
|
+
export type TaskComplexity = "small" | "medium" | "large";
|
|
8
|
+
|
|
9
|
+
/** Task parallelism annotation */
|
|
10
|
+
export type TaskParallelism =
|
|
11
|
+
| { type: "parallel-safe" }
|
|
12
|
+
| { type: "sequential"; dependsOn: number[] };
|
|
13
|
+
|
|
14
|
+
/** A single task in a plan */
|
|
15
|
+
export interface PlanTask {
|
|
16
|
+
id: number;
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
files: string[];
|
|
20
|
+
criteria: string;
|
|
21
|
+
complexity: TaskComplexity;
|
|
22
|
+
parallelism: TaskParallelism;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A plan document (parsed from markdown) */
|
|
26
|
+
export interface Plan {
|
|
27
|
+
name: string;
|
|
28
|
+
created: string;
|
|
29
|
+
tags: string[];
|
|
30
|
+
context: string;
|
|
31
|
+
tasks: PlanTask[];
|
|
32
|
+
filePath: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Per-agent result stored after execution */
|
|
36
|
+
export interface AgentResult {
|
|
37
|
+
taskId: number;
|
|
38
|
+
status: AgentStatus;
|
|
39
|
+
output: string;
|
|
40
|
+
concerns?: string;
|
|
41
|
+
filesChanged: string[];
|
|
42
|
+
duration: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Batch status in a run */
|
|
46
|
+
export type BatchStatus = "pending" | "running" | "completed" | "failed";
|
|
47
|
+
|
|
48
|
+
/** A batch of tasks in a run */
|
|
49
|
+
export interface RunBatch {
|
|
50
|
+
index: number;
|
|
51
|
+
taskIds: number[];
|
|
52
|
+
status: BatchStatus;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Overall run status */
|
|
56
|
+
export type RunStatus = "running" | "completed" | "paused" | "failed";
|
|
57
|
+
|
|
58
|
+
/** Run manifest stored on disk */
|
|
59
|
+
export interface RunManifest {
|
|
60
|
+
id: string;
|
|
61
|
+
planRef: string;
|
|
62
|
+
profile: string;
|
|
63
|
+
status: RunStatus;
|
|
64
|
+
startedAt: string;
|
|
65
|
+
completedAt?: string;
|
|
66
|
+
batches: RunBatch[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Notification severity level */
|
|
70
|
+
export type NotificationLevel = "success" | "warning" | "error" | "info" | "summary";
|
|
71
|
+
|
|
72
|
+
/** Notification payload */
|
|
73
|
+
export interface Notification {
|
|
74
|
+
level: NotificationLevel;
|
|
75
|
+
title: string;
|
|
76
|
+
detail?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Quality gate result */
|
|
80
|
+
export interface GateResult {
|
|
81
|
+
gate: string;
|
|
82
|
+
passed: boolean;
|
|
83
|
+
issues: GateIssue[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A single issue from a quality gate */
|
|
87
|
+
export interface GateIssue {
|
|
88
|
+
severity: "error" | "warning" | "info";
|
|
89
|
+
message: string;
|
|
90
|
+
file?: string;
|
|
91
|
+
line?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Review report */
|
|
95
|
+
export interface ReviewReport {
|
|
96
|
+
profile: string;
|
|
97
|
+
timestamp: string;
|
|
98
|
+
gates: GateResult[];
|
|
99
|
+
passed: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Config shape */
|
|
103
|
+
export interface SupipowersConfig {
|
|
104
|
+
version: string;
|
|
105
|
+
defaultProfile: string;
|
|
106
|
+
orchestration: {
|
|
107
|
+
maxParallelAgents: number;
|
|
108
|
+
maxFixRetries: number;
|
|
109
|
+
maxNestingDepth: number;
|
|
110
|
+
modelPreference: string;
|
|
111
|
+
};
|
|
112
|
+
lsp: {
|
|
113
|
+
autoDetect: boolean;
|
|
114
|
+
setupGuide: boolean;
|
|
115
|
+
};
|
|
116
|
+
notifications: {
|
|
117
|
+
verbosity: "quiet" | "normal" | "verbose";
|
|
118
|
+
};
|
|
119
|
+
qa: {
|
|
120
|
+
framework: string | null;
|
|
121
|
+
command: string | null;
|
|
122
|
+
};
|
|
123
|
+
release: {
|
|
124
|
+
pipeline: string | null;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Profile shape */
|
|
129
|
+
export interface Profile {
|
|
130
|
+
name: string;
|
|
131
|
+
gates: {
|
|
132
|
+
lspDiagnostics: boolean;
|
|
133
|
+
aiReview: { enabled: boolean; depth: "quick" | "deep" };
|
|
134
|
+
codeQuality: boolean;
|
|
135
|
+
testSuite: boolean;
|
|
136
|
+
e2e: boolean;
|
|
137
|
+
};
|
|
138
|
+
orchestration: {
|
|
139
|
+
reviewAfterEachBatch: boolean;
|
|
140
|
+
finalReview: boolean;
|
|
141
|
+
};
|
|
142
|
+
}
|