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,70 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { loadConfig, updateConfig } from "../config/loader.js";
|
|
3
|
+
import { listProfiles, resolveProfile } from "../config/profiles.js";
|
|
4
|
+
import { notifyInfo, notifySuccess } from "../notifications/renderer.js";
|
|
5
|
+
|
|
6
|
+
export function registerConfigCommand(pi: ExtensionAPI): void {
|
|
7
|
+
pi.registerCommand("supi:config", {
|
|
8
|
+
description: "View and manage Supipowers configuration and profiles",
|
|
9
|
+
async handler(args, ctx) {
|
|
10
|
+
const config = loadConfig(ctx.cwd);
|
|
11
|
+
|
|
12
|
+
if (!args || args.trim() === "") {
|
|
13
|
+
const profiles = listProfiles(ctx.cwd);
|
|
14
|
+
const activeProfile = resolveProfile(ctx.cwd, config);
|
|
15
|
+
|
|
16
|
+
const lines = [
|
|
17
|
+
"# Supipowers Configuration",
|
|
18
|
+
"",
|
|
19
|
+
`Profile: ${config.defaultProfile}`,
|
|
20
|
+
`Max parallel agents: ${config.orchestration.maxParallelAgents}`,
|
|
21
|
+
`Max fix retries: ${config.orchestration.maxFixRetries}`,
|
|
22
|
+
`Max nesting depth: ${config.orchestration.maxNestingDepth}`,
|
|
23
|
+
`Model preference: ${config.orchestration.modelPreference}`,
|
|
24
|
+
`LSP auto-detect: ${config.lsp.autoDetect}`,
|
|
25
|
+
`Notification verbosity: ${config.notifications.verbosity}`,
|
|
26
|
+
`QA framework: ${config.qa.framework ?? "not detected"}`,
|
|
27
|
+
`Release pipeline: ${config.release.pipeline ?? "not configured"}`,
|
|
28
|
+
"",
|
|
29
|
+
`Available profiles: ${profiles.join(", ")}`,
|
|
30
|
+
"",
|
|
31
|
+
"To update: /supi:config set <key> <value>",
|
|
32
|
+
"Example: /supi:config set orchestration.maxParallelAgents 5",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
pi.sendMessage({
|
|
36
|
+
customType: "supi-config",
|
|
37
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
38
|
+
display: "inline",
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const setMatch = args.match(/^set\s+(\S+)\s+(.+)$/);
|
|
44
|
+
if (setMatch) {
|
|
45
|
+
const [, keyPath, rawValue] = setMatch;
|
|
46
|
+
const keys = keyPath.split(".");
|
|
47
|
+
let value: unknown = rawValue;
|
|
48
|
+
|
|
49
|
+
if (rawValue === "true") value = true;
|
|
50
|
+
else if (rawValue === "false") value = false;
|
|
51
|
+
else if (rawValue === "null") value = null;
|
|
52
|
+
else if (!isNaN(Number(rawValue))) value = Number(rawValue);
|
|
53
|
+
|
|
54
|
+
const update: Record<string, unknown> = {};
|
|
55
|
+
let current = update;
|
|
56
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
57
|
+
current[keys[i]] = {};
|
|
58
|
+
current = current[keys[i]] as Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
current[keys[keys.length - 1]] = value;
|
|
61
|
+
|
|
62
|
+
updateConfig(ctx.cwd, update);
|
|
63
|
+
notifySuccess(ctx, "Config updated", `${keyPath} = ${rawValue}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
notifyInfo(ctx, "Usage", "/supi:config or /supi:config set <key> <value>");
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { loadConfig } from "../config/loader.js";
|
|
3
|
+
import { savePlan } from "../storage/plans.js";
|
|
4
|
+
import { notifySuccess, notifyInfo } from "../notifications/renderer.js";
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
|
|
8
|
+
export function registerPlanCommand(pi: ExtensionAPI): void {
|
|
9
|
+
pi.registerCommand("supi:plan", {
|
|
10
|
+
description: "Start collaborative planning for a feature or task",
|
|
11
|
+
async handler(args, ctx) {
|
|
12
|
+
const config = loadConfig(ctx.cwd);
|
|
13
|
+
|
|
14
|
+
const skillPath = findSkillPath("planning");
|
|
15
|
+
let skillContent = "";
|
|
16
|
+
if (skillPath) {
|
|
17
|
+
try {
|
|
18
|
+
skillContent = fs.readFileSync(skillPath, "utf-8");
|
|
19
|
+
} catch {
|
|
20
|
+
// Skill file not found — proceed without it
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isQuick = args?.startsWith("--quick");
|
|
25
|
+
const quickDesc = isQuick ? args.replace("--quick", "").trim() : "";
|
|
26
|
+
|
|
27
|
+
let prompt: string;
|
|
28
|
+
if (isQuick && quickDesc) {
|
|
29
|
+
prompt = [
|
|
30
|
+
"Generate a concise implementation plan for the following task.",
|
|
31
|
+
"Skip brainstorming — go straight to task breakdown.",
|
|
32
|
+
"",
|
|
33
|
+
`Task: ${quickDesc}`,
|
|
34
|
+
"",
|
|
35
|
+
"Format the plan as markdown with YAML frontmatter (name, created, tags).",
|
|
36
|
+
"Each task should have: name, [parallel-safe] or [sequential] annotation,",
|
|
37
|
+
"**files**, **criteria**, and **complexity** (small/medium/large).",
|
|
38
|
+
"",
|
|
39
|
+
skillContent ? "Follow these planning guidelines:\n" + skillContent : "",
|
|
40
|
+
"",
|
|
41
|
+
"After generating the plan, save it and confirm with the user.",
|
|
42
|
+
].join("\n");
|
|
43
|
+
} else {
|
|
44
|
+
prompt = [
|
|
45
|
+
"You are starting a collaborative planning session with the user.",
|
|
46
|
+
"",
|
|
47
|
+
args ? `The user wants to plan: ${args}` : "Ask the user what they want to build or accomplish.",
|
|
48
|
+
"",
|
|
49
|
+
"Process:",
|
|
50
|
+
"1. Understand the goal — ask clarifying questions (one at a time)",
|
|
51
|
+
"2. Propose 2-3 approaches with trade-offs",
|
|
52
|
+
"3. Generate a task breakdown once aligned",
|
|
53
|
+
"",
|
|
54
|
+
"Format the final plan as markdown with YAML frontmatter (name, created, tags).",
|
|
55
|
+
"Each task: name, [parallel-safe] or [sequential] annotation,",
|
|
56
|
+
"**files**, **criteria**, **complexity** (small/medium/large).",
|
|
57
|
+
"",
|
|
58
|
+
skillContent ? "Follow these planning guidelines:\n" + skillContent : "",
|
|
59
|
+
].join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pi.sendMessage(
|
|
63
|
+
{
|
|
64
|
+
customType: "supi-plan-start",
|
|
65
|
+
content: [{ type: "text", text: prompt }],
|
|
66
|
+
display: "none",
|
|
67
|
+
},
|
|
68
|
+
{ deliverAs: "steer" }
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
notifyInfo(ctx, "Planning started", args ? `Topic: ${args}` : "Describe what you want to build");
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function findSkillPath(skillName: string): string | null {
|
|
77
|
+
const candidates = [
|
|
78
|
+
path.join(process.cwd(), "skills", skillName, "SKILL.md"),
|
|
79
|
+
path.join(__dirname, "..", "..", "skills", skillName, "SKILL.md"),
|
|
80
|
+
];
|
|
81
|
+
for (const p of candidates) {
|
|
82
|
+
if (fs.existsSync(p)) return p;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { detectAndCache } from "../qa/detector.js";
|
|
3
|
+
import { buildQaRunPrompt } from "../qa/runner.js";
|
|
4
|
+
import { notifyInfo, notifyError } from "../notifications/renderer.js";
|
|
5
|
+
|
|
6
|
+
export function registerQaCommand(pi: ExtensionAPI): void {
|
|
7
|
+
pi.registerCommand("supi:qa", {
|
|
8
|
+
description: "Run QA pipeline (test suite, E2E)",
|
|
9
|
+
async handler(args, ctx) {
|
|
10
|
+
const framework = detectAndCache(ctx.cwd);
|
|
11
|
+
|
|
12
|
+
if (!framework) {
|
|
13
|
+
notifyError(
|
|
14
|
+
ctx,
|
|
15
|
+
"No test framework detected",
|
|
16
|
+
"Configure manually: /supi:config set qa.framework vitest && /supi:config set qa.command 'npx vitest run'"
|
|
17
|
+
);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let scope: "all" | "changed" | "e2e" = "all";
|
|
22
|
+
let changedFiles: string[] | undefined;
|
|
23
|
+
|
|
24
|
+
if (args?.includes("--changed")) {
|
|
25
|
+
scope = "changed";
|
|
26
|
+
try {
|
|
27
|
+
const result = await pi.exec("git", ["diff", "--name-only", "HEAD"], { cwd: ctx.cwd });
|
|
28
|
+
if (result.exitCode === 0) {
|
|
29
|
+
changedFiles = result.stdout.split("\n").filter((f) => f.trim().length > 0);
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
scope = "all";
|
|
33
|
+
}
|
|
34
|
+
} else if (args?.includes("--e2e")) {
|
|
35
|
+
scope = "e2e";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
notifyInfo(ctx, "QA started", `${framework.name} | scope: ${scope}`);
|
|
39
|
+
|
|
40
|
+
const prompt = buildQaRunPrompt(framework.command, scope, changedFiles);
|
|
41
|
+
|
|
42
|
+
pi.sendMessage(
|
|
43
|
+
{
|
|
44
|
+
customType: "supi-qa",
|
|
45
|
+
content: [{ type: "text", text: prompt }],
|
|
46
|
+
display: "none",
|
|
47
|
+
},
|
|
48
|
+
{ deliverAs: "steer" }
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { loadConfig, updateConfig } from "../config/loader.js";
|
|
3
|
+
import { buildAnalyzerPrompt } from "../release/analyzer.js";
|
|
4
|
+
import { notifyInfo } from "../notifications/renderer.js";
|
|
5
|
+
|
|
6
|
+
export function registerReleaseCommand(pi: ExtensionAPI): void {
|
|
7
|
+
pi.registerCommand("supi:release", {
|
|
8
|
+
description: "Release automation — version bump, notes, publish",
|
|
9
|
+
async handler(_args, ctx) {
|
|
10
|
+
const config = loadConfig(ctx.cwd);
|
|
11
|
+
|
|
12
|
+
let lastTag: string | null = null;
|
|
13
|
+
try {
|
|
14
|
+
const result = await pi.exec("git", ["describe", "--tags", "--abbrev=0"], { cwd: ctx.cwd });
|
|
15
|
+
if (result.exitCode === 0) lastTag = result.stdout.trim();
|
|
16
|
+
} catch {
|
|
17
|
+
// no tags yet
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!config.release.pipeline) {
|
|
21
|
+
const prompt = [
|
|
22
|
+
"# Release Setup",
|
|
23
|
+
"",
|
|
24
|
+
"This is your first release with supipowers. How do you publish?",
|
|
25
|
+
"",
|
|
26
|
+
"1. **npm** — npm publish to registry",
|
|
27
|
+
"2. **github** — GitHub Release with gh CLI",
|
|
28
|
+
"3. **manual** — I'll handle publishing myself",
|
|
29
|
+
"",
|
|
30
|
+
"Tell me which option, and I'll save it for future releases.",
|
|
31
|
+
"",
|
|
32
|
+
"After you answer, I'll analyze commits and prepare the release.",
|
|
33
|
+
].join("\n");
|
|
34
|
+
|
|
35
|
+
pi.sendMessage(
|
|
36
|
+
{
|
|
37
|
+
customType: "supi-release-setup",
|
|
38
|
+
content: [{ type: "text", text: prompt }],
|
|
39
|
+
display: "none",
|
|
40
|
+
},
|
|
41
|
+
{ deliverAs: "steer" }
|
|
42
|
+
);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
notifyInfo(ctx, "Release started", `Pipeline: ${config.release.pipeline}`);
|
|
47
|
+
|
|
48
|
+
const prompt = buildAnalyzerPrompt(lastTag);
|
|
49
|
+
|
|
50
|
+
pi.sendMessage(
|
|
51
|
+
{
|
|
52
|
+
customType: "supi-release",
|
|
53
|
+
content: [{ type: "text", text: prompt }],
|
|
54
|
+
display: "none",
|
|
55
|
+
},
|
|
56
|
+
{ deliverAs: "steer" }
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { loadConfig } from "../config/loader.js";
|
|
3
|
+
import { resolveProfile } from "../config/profiles.js";
|
|
4
|
+
import { buildReviewPrompt } from "../quality/gate-runner.js";
|
|
5
|
+
import { isLspAvailable } from "../lsp/detector.js";
|
|
6
|
+
import { notifyInfo, notifyWarning } from "../notifications/renderer.js";
|
|
7
|
+
|
|
8
|
+
export function registerReviewCommand(pi: ExtensionAPI): void {
|
|
9
|
+
pi.registerCommand("supi:review", {
|
|
10
|
+
description: "Run quality gates at chosen depth (quick/thorough/full-regression)",
|
|
11
|
+
async handler(args, ctx) {
|
|
12
|
+
const config = loadConfig(ctx.cwd);
|
|
13
|
+
|
|
14
|
+
let profileOverride: string | undefined;
|
|
15
|
+
if (args?.includes("--quick")) profileOverride = "quick";
|
|
16
|
+
else if (args?.includes("--thorough")) profileOverride = "thorough";
|
|
17
|
+
else if (args?.includes("--full")) profileOverride = "full-regression";
|
|
18
|
+
else if (args?.includes("--profile")) {
|
|
19
|
+
const match = args.match(/--profile\s+(\S+)/);
|
|
20
|
+
if (match) profileOverride = match[1];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const profile = resolveProfile(ctx.cwd, config, profileOverride);
|
|
24
|
+
const lsp = isLspAvailable(pi.getActiveTools());
|
|
25
|
+
|
|
26
|
+
if (!lsp && profile.gates.lspDiagnostics) {
|
|
27
|
+
notifyWarning(
|
|
28
|
+
ctx,
|
|
29
|
+
"LSP not available",
|
|
30
|
+
"Review will continue without LSP diagnostics. Run /supi:config for setup."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let changedFiles: string[] = [];
|
|
35
|
+
try {
|
|
36
|
+
const result = await pi.exec("git", ["diff", "--name-only", "HEAD"], { cwd: ctx.cwd });
|
|
37
|
+
if (result.exitCode === 0) {
|
|
38
|
+
changedFiles = result.stdout
|
|
39
|
+
.split("\n")
|
|
40
|
+
.map((f) => f.trim())
|
|
41
|
+
.filter((f) => f.length > 0);
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// If git fails, we'll review without file filtering
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (changedFiles.length === 0) {
|
|
48
|
+
try {
|
|
49
|
+
const result = await pi.exec("git", ["diff", "--name-only", "--cached"], { cwd: ctx.cwd });
|
|
50
|
+
if (result.exitCode === 0) {
|
|
51
|
+
changedFiles = result.stdout
|
|
52
|
+
.split("\n")
|
|
53
|
+
.map((f) => f.trim())
|
|
54
|
+
.filter((f) => f.length > 0);
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// continue without
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (changedFiles.length === 0) {
|
|
62
|
+
notifyInfo(ctx, "No changed files detected", "Reviewing all files in scope");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const reviewPrompt = buildReviewPrompt({
|
|
66
|
+
profile,
|
|
67
|
+
changedFiles,
|
|
68
|
+
testCommand: config.qa.command,
|
|
69
|
+
lspAvailable: lsp,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
notifyInfo(ctx, `Review started`, `profile: ${profile.name}`);
|
|
73
|
+
|
|
74
|
+
pi.sendMessage(
|
|
75
|
+
{
|
|
76
|
+
customType: "supi-review",
|
|
77
|
+
content: [{ type: "text", text: reviewPrompt }],
|
|
78
|
+
display: "none",
|
|
79
|
+
},
|
|
80
|
+
{ deliverAs: "steer" }
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { loadConfig } from "../config/loader.js";
|
|
3
|
+
import { resolveProfile } from "../config/profiles.js";
|
|
4
|
+
import { listPlans, readPlanFile, parsePlan } from "../storage/plans.js";
|
|
5
|
+
import {
|
|
6
|
+
generateRunId,
|
|
7
|
+
createRun,
|
|
8
|
+
updateRun,
|
|
9
|
+
findActiveRun,
|
|
10
|
+
saveAgentResult,
|
|
11
|
+
loadAllAgentResults,
|
|
12
|
+
} from "../storage/runs.js";
|
|
13
|
+
import { scheduleBatches } from "../orchestrator/batch-scheduler.js";
|
|
14
|
+
import { dispatchAgent, dispatchFixAgent } from "../orchestrator/dispatcher.js";
|
|
15
|
+
import { summarizeBatch, buildRunSummary } from "../orchestrator/result-collector.js";
|
|
16
|
+
import { analyzeConflicts } from "../orchestrator/conflict-resolver.js";
|
|
17
|
+
import { isLspAvailable } from "../lsp/detector.js";
|
|
18
|
+
import {
|
|
19
|
+
notifyInfo,
|
|
20
|
+
notifySuccess,
|
|
21
|
+
notifyWarning,
|
|
22
|
+
notifyError,
|
|
23
|
+
notifySummary,
|
|
24
|
+
} from "../notifications/renderer.js";
|
|
25
|
+
import type { RunManifest, AgentResult } from "../types.js";
|
|
26
|
+
|
|
27
|
+
export function registerRunCommand(pi: ExtensionAPI): void {
|
|
28
|
+
pi.registerCommand("supi:run", {
|
|
29
|
+
description: "Execute a plan with sub-agent orchestration",
|
|
30
|
+
async handler(args, ctx) {
|
|
31
|
+
const config = loadConfig(ctx.cwd);
|
|
32
|
+
const profile = resolveProfile(ctx.cwd, config, args?.replace("--profile ", "") || undefined);
|
|
33
|
+
|
|
34
|
+
let manifest = findActiveRun(ctx.cwd);
|
|
35
|
+
|
|
36
|
+
if (!manifest) {
|
|
37
|
+
const plans = listPlans(ctx.cwd);
|
|
38
|
+
if (plans.length === 0) {
|
|
39
|
+
notifyError(ctx, "No plans found", "Run /supi:plan first to create a plan");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const planName = args?.trim() || plans[0];
|
|
44
|
+
const planContent = readPlanFile(ctx.cwd, planName);
|
|
45
|
+
if (!planContent) {
|
|
46
|
+
notifyError(ctx, "Plan not found", planName);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const plan = parsePlan(planContent, planName);
|
|
51
|
+
const batches = scheduleBatches(plan.tasks, config.orchestration.maxParallelAgents);
|
|
52
|
+
|
|
53
|
+
manifest = {
|
|
54
|
+
id: generateRunId(),
|
|
55
|
+
planRef: planName,
|
|
56
|
+
profile: profile.name,
|
|
57
|
+
status: "running",
|
|
58
|
+
startedAt: new Date().toISOString(),
|
|
59
|
+
batches,
|
|
60
|
+
};
|
|
61
|
+
createRun(ctx.cwd, manifest);
|
|
62
|
+
notifyInfo(ctx, `Run started: ${manifest.id}`, `${plan.tasks.length} tasks in ${batches.length} batches`);
|
|
63
|
+
} else {
|
|
64
|
+
notifyInfo(ctx, `Resuming run: ${manifest.id}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const planContent = readPlanFile(ctx.cwd, manifest.planRef);
|
|
68
|
+
if (!planContent) {
|
|
69
|
+
notifyError(ctx, "Plan file missing", manifest.planRef);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const plan = parsePlan(planContent, manifest.planRef);
|
|
73
|
+
const lsp = isLspAvailable(pi.getActiveTools());
|
|
74
|
+
|
|
75
|
+
for (const batch of manifest.batches) {
|
|
76
|
+
if (batch.status === "completed") continue;
|
|
77
|
+
|
|
78
|
+
batch.status = "running";
|
|
79
|
+
updateRun(ctx.cwd, manifest);
|
|
80
|
+
|
|
81
|
+
notifyInfo(
|
|
82
|
+
ctx,
|
|
83
|
+
`Batch ${batch.index + 1}/${manifest.batches.length}`,
|
|
84
|
+
`${batch.taskIds.length} tasks`
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const batchResults: AgentResult[] = [];
|
|
88
|
+
const agentPromises = batch.taskIds.map((taskId) => {
|
|
89
|
+
const task = plan.tasks.find((t) => t.id === taskId);
|
|
90
|
+
if (!task) return Promise.resolve(null);
|
|
91
|
+
|
|
92
|
+
return dispatchAgent({
|
|
93
|
+
pi,
|
|
94
|
+
ctx,
|
|
95
|
+
task,
|
|
96
|
+
planContext: plan.context,
|
|
97
|
+
config,
|
|
98
|
+
lspAvailable: lsp,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const results = await Promise.all(agentPromises);
|
|
103
|
+
for (const result of results) {
|
|
104
|
+
if (result) {
|
|
105
|
+
batchResults.push(result);
|
|
106
|
+
saveAgentResult(ctx.cwd, manifest.id, result);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const conflicts = analyzeConflicts(batchResults, plan.tasks);
|
|
111
|
+
if (conflicts.hasConflicts) {
|
|
112
|
+
notifyWarning(
|
|
113
|
+
ctx,
|
|
114
|
+
"File conflicts detected",
|
|
115
|
+
conflicts.conflictingFiles.join(", ")
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const failedResults = batchResults.filter((r) => r.status === "blocked");
|
|
120
|
+
for (const failed of failedResults) {
|
|
121
|
+
if (config.orchestration.maxFixRetries > 0) {
|
|
122
|
+
const task = plan.tasks.find((t) => t.id === failed.taskId);
|
|
123
|
+
if (!task) continue;
|
|
124
|
+
|
|
125
|
+
for (let retry = 0; retry < config.orchestration.maxFixRetries; retry++) {
|
|
126
|
+
notifyInfo(ctx, `Retrying task ${failed.taskId}`, `attempt ${retry + 1}`);
|
|
127
|
+
const fixResult = await dispatchFixAgent({
|
|
128
|
+
pi,
|
|
129
|
+
ctx,
|
|
130
|
+
task,
|
|
131
|
+
planContext: plan.context,
|
|
132
|
+
config,
|
|
133
|
+
lspAvailable: lsp,
|
|
134
|
+
previousOutput: failed.output,
|
|
135
|
+
failureReason: failed.output,
|
|
136
|
+
});
|
|
137
|
+
saveAgentResult(ctx.cwd, manifest.id, fixResult);
|
|
138
|
+
if (fixResult.status !== "blocked") break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const allResults = loadAllAgentResults(ctx.cwd, manifest.id);
|
|
144
|
+
const summary = summarizeBatch(batch, allResults);
|
|
145
|
+
|
|
146
|
+
batch.status = summary.allPassed ? "completed" : "failed";
|
|
147
|
+
updateRun(ctx.cwd, manifest);
|
|
148
|
+
|
|
149
|
+
if (!summary.allPassed) {
|
|
150
|
+
notifyWarning(
|
|
151
|
+
ctx,
|
|
152
|
+
`Batch ${batch.index + 1} had issues`,
|
|
153
|
+
`${summary.blocked} blocked, ${summary.doneWithConcerns} with concerns`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const allResults = loadAllAgentResults(ctx.cwd, manifest.id);
|
|
159
|
+
const runSummary = buildRunSummary(allResults);
|
|
160
|
+
|
|
161
|
+
manifest.status = runSummary.blocked > 0 ? "failed" : "completed";
|
|
162
|
+
manifest.completedAt = new Date().toISOString();
|
|
163
|
+
updateRun(ctx.cwd, manifest);
|
|
164
|
+
|
|
165
|
+
const durationSec = Math.round(runSummary.totalDuration / 1000);
|
|
166
|
+
notifySummary(
|
|
167
|
+
ctx,
|
|
168
|
+
"Run complete",
|
|
169
|
+
`${runSummary.done + runSummary.doneWithConcerns}/${runSummary.totalTasks} tasks done ` +
|
|
170
|
+
`(${runSummary.done} clean, ${runSummary.doneWithConcerns} with concerns, ` +
|
|
171
|
+
`${runSummary.blocked} blocked) | ${runSummary.totalFilesChanged} files | ${durationSec}s`
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { findActiveRun, loadAllAgentResults } from "../storage/runs.js";
|
|
3
|
+
import { notifyInfo } from "../notifications/renderer.js";
|
|
4
|
+
|
|
5
|
+
export function registerStatusCommand(pi: ExtensionAPI): void {
|
|
6
|
+
pi.registerCommand("supi:status", {
|
|
7
|
+
description: "Check on running sub-agents and task progress",
|
|
8
|
+
async handler(_args, ctx) {
|
|
9
|
+
const activeRun = findActiveRun(ctx.cwd);
|
|
10
|
+
|
|
11
|
+
if (!activeRun) {
|
|
12
|
+
notifyInfo(ctx, "No active runs", "Use /supi:run to execute a plan");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const results = loadAllAgentResults(ctx.cwd, activeRun.id);
|
|
17
|
+
const completedIds = new Set(results.map((r) => r.taskId));
|
|
18
|
+
const totalTasks = activeRun.batches.reduce(
|
|
19
|
+
(sum, b) => sum + b.taskIds.length,
|
|
20
|
+
0
|
|
21
|
+
);
|
|
22
|
+
const completedCount = results.length;
|
|
23
|
+
const doneCount = results.filter((r) => r.status === "done").length;
|
|
24
|
+
const concernCount = results.filter((r) => r.status === "done_with_concerns").length;
|
|
25
|
+
const blockedCount = results.filter((r) => r.status === "blocked").length;
|
|
26
|
+
|
|
27
|
+
const currentBatch = activeRun.batches.find((b) => b.status !== "completed");
|
|
28
|
+
|
|
29
|
+
const lines = [
|
|
30
|
+
`# Run: ${activeRun.id}`,
|
|
31
|
+
"",
|
|
32
|
+
`Status: ${activeRun.status}`,
|
|
33
|
+
`Plan: ${activeRun.planRef}`,
|
|
34
|
+
`Profile: ${activeRun.profile}`,
|
|
35
|
+
`Progress: ${completedCount}/${totalTasks} tasks`,
|
|
36
|
+
"",
|
|
37
|
+
` Done: ${doneCount}`,
|
|
38
|
+
` With concerns: ${concernCount}`,
|
|
39
|
+
` Blocked: ${blockedCount}`,
|
|
40
|
+
"",
|
|
41
|
+
`Current batch: ${currentBatch ? `#${currentBatch.index} (${currentBatch.status})` : "none"}`,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
pi.sendMessage({
|
|
45
|
+
customType: "supi-status",
|
|
46
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
47
|
+
display: "inline",
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { loadConfig } from "../config/loader.js";
|
|
3
|
+
import { findActiveRun } from "../storage/runs.js";
|
|
4
|
+
import { loadLatestReport } from "../storage/reports.js";
|
|
5
|
+
import { listPlans } from "../storage/plans.js";
|
|
6
|
+
|
|
7
|
+
export function registerSupiCommand(pi: ExtensionAPI): void {
|
|
8
|
+
pi.registerCommand("supi", {
|
|
9
|
+
description: "Supipowers overview — show available commands and project status",
|
|
10
|
+
async handler(_args, ctx) {
|
|
11
|
+
const config = loadConfig(ctx.cwd);
|
|
12
|
+
const activeRun = findActiveRun(ctx.cwd);
|
|
13
|
+
const latestReport = loadLatestReport(ctx.cwd);
|
|
14
|
+
const plans = listPlans(ctx.cwd);
|
|
15
|
+
|
|
16
|
+
const lines: string[] = [
|
|
17
|
+
"# Supipowers",
|
|
18
|
+
"",
|
|
19
|
+
"## Commands",
|
|
20
|
+
" /supi:plan — Start collaborative planning",
|
|
21
|
+
" /supi:run — Execute a plan with sub-agents",
|
|
22
|
+
" /supi:review — Run quality gates",
|
|
23
|
+
" /supi:qa — Run QA pipeline",
|
|
24
|
+
" /supi:release — Release automation",
|
|
25
|
+
" /supi:config — Manage configuration",
|
|
26
|
+
" /supi:status — Check running tasks",
|
|
27
|
+
"",
|
|
28
|
+
"## Project Status",
|
|
29
|
+
` Profile: ${config.defaultProfile}`,
|
|
30
|
+
` Plans: ${plans.length}`,
|
|
31
|
+
` Active run: ${activeRun ? activeRun.id : "none"}`,
|
|
32
|
+
` Last review: ${latestReport ? `${latestReport.timestamp.slice(0, 10)} (${latestReport.passed ? "passed" : "failed"})` : "none"}`,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
pi.sendMessage({
|
|
36
|
+
customType: "supi-overview",
|
|
37
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
38
|
+
display: "inline",
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|