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,72 @@
1
+ // src/config/defaults.ts
2
+ import type { SupipowersConfig, Profile } from "../types.js";
3
+
4
+ export const DEFAULT_CONFIG: SupipowersConfig = {
5
+ version: "1.0.0",
6
+ defaultProfile: "thorough",
7
+ orchestration: {
8
+ maxParallelAgents: 3,
9
+ maxFixRetries: 2,
10
+ maxNestingDepth: 2,
11
+ modelPreference: "auto",
12
+ },
13
+ lsp: {
14
+ autoDetect: true,
15
+ setupGuide: true,
16
+ },
17
+ notifications: {
18
+ verbosity: "normal",
19
+ },
20
+ qa: {
21
+ framework: null,
22
+ command: null,
23
+ },
24
+ release: {
25
+ pipeline: null,
26
+ },
27
+ };
28
+
29
+ export const BUILTIN_PROFILES: Record<string, Profile> = {
30
+ quick: {
31
+ name: "quick",
32
+ gates: {
33
+ lspDiagnostics: true,
34
+ aiReview: { enabled: true, depth: "quick" },
35
+ codeQuality: false,
36
+ testSuite: false,
37
+ e2e: false,
38
+ },
39
+ orchestration: {
40
+ reviewAfterEachBatch: false,
41
+ finalReview: false,
42
+ },
43
+ },
44
+ thorough: {
45
+ name: "thorough",
46
+ gates: {
47
+ lspDiagnostics: true,
48
+ aiReview: { enabled: true, depth: "deep" },
49
+ codeQuality: true,
50
+ testSuite: false,
51
+ e2e: false,
52
+ },
53
+ orchestration: {
54
+ reviewAfterEachBatch: true,
55
+ finalReview: true,
56
+ },
57
+ },
58
+ "full-regression": {
59
+ name: "full-regression",
60
+ gates: {
61
+ lspDiagnostics: true,
62
+ aiReview: { enabled: true, depth: "deep" },
63
+ codeQuality: true,
64
+ testSuite: true,
65
+ e2e: true,
66
+ },
67
+ orchestration: {
68
+ reviewAfterEachBatch: true,
69
+ finalReview: true,
70
+ },
71
+ },
72
+ };
@@ -0,0 +1,101 @@
1
+ // src/config/loader.ts
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import type { SupipowersConfig } from "../types.js";
5
+ import { DEFAULT_CONFIG } from "./defaults.js";
6
+
7
+ const PROJECT_CONFIG_PATH = [".omp", "supipowers", "config.json"];
8
+ const GLOBAL_CONFIG_DIR = ".omp";
9
+ const GLOBAL_CONFIG_PATH = ["supipowers", "config.json"];
10
+
11
+ function getProjectConfigPath(cwd: string): string {
12
+ return path.join(cwd, ...PROJECT_CONFIG_PATH);
13
+ }
14
+
15
+ function getGlobalConfigPath(): string {
16
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
17
+ return path.join(home, GLOBAL_CONFIG_DIR, ...GLOBAL_CONFIG_PATH);
18
+ }
19
+
20
+ function readJsonSafe(filePath: string): Record<string, unknown> | null {
21
+ try {
22
+ if (!fs.existsSync(filePath)) return null;
23
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ /** Deep merge source into target. Source values override target. */
30
+ export function deepMerge<T extends Record<string, unknown>>(
31
+ target: T,
32
+ source: Record<string, unknown>
33
+ ): T {
34
+ const result = { ...target };
35
+ for (const key of Object.keys(source)) {
36
+ const sourceVal = source[key];
37
+ const targetVal = (result as Record<string, unknown>)[key];
38
+ if (
39
+ sourceVal !== null &&
40
+ typeof sourceVal === "object" &&
41
+ !Array.isArray(sourceVal) &&
42
+ targetVal !== null &&
43
+ typeof targetVal === "object" &&
44
+ !Array.isArray(targetVal)
45
+ ) {
46
+ (result as Record<string, unknown>)[key] = deepMerge(
47
+ targetVal as Record<string, unknown>,
48
+ sourceVal as Record<string, unknown>
49
+ );
50
+ } else {
51
+ (result as Record<string, unknown>)[key] = sourceVal;
52
+ }
53
+ }
54
+ return result;
55
+ }
56
+
57
+ /** Load config with global -> project layering over defaults.
58
+ * Validates and migrates if version is outdated. */
59
+ export function loadConfig(cwd: string): SupipowersConfig {
60
+ const globalData = readJsonSafe(getGlobalConfigPath());
61
+ const projectData = readJsonSafe(getProjectConfigPath(cwd));
62
+
63
+ let config = { ...DEFAULT_CONFIG };
64
+ if (globalData) config = deepMerge(config, globalData);
65
+ if (projectData) config = deepMerge(config, projectData);
66
+
67
+ // Migrate if version is older than current default
68
+ if (config.version !== DEFAULT_CONFIG.version) {
69
+ config = migrateConfig(config);
70
+ // Persist migrated config if project-level exists
71
+ if (projectData) saveConfig(cwd, config);
72
+ }
73
+
74
+ return config;
75
+ }
76
+
77
+ /** Migrate config from older versions to current */
78
+ function migrateConfig(config: SupipowersConfig): SupipowersConfig {
79
+ // Currently v1.0.0 is the only version — future migrations go here
80
+ // Each migration handles one version bump:
81
+ // if (config.version === "0.x.x") { ... config.version = "1.0.0"; }
82
+ return { ...config, version: DEFAULT_CONFIG.version };
83
+ }
84
+
85
+ /** Save project-level config */
86
+ export function saveConfig(cwd: string, config: SupipowersConfig): void {
87
+ const configPath = getProjectConfigPath(cwd);
88
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
89
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
90
+ }
91
+
92
+ /** Update specific config fields (deep merge into current) */
93
+ export function updateConfig(
94
+ cwd: string,
95
+ updates: Record<string, unknown>
96
+ ): SupipowersConfig {
97
+ const current = loadConfig(cwd);
98
+ const updated = deepMerge(current, updates);
99
+ saveConfig(cwd, updated);
100
+ return updated;
101
+ }
@@ -0,0 +1,64 @@
1
+ // src/config/profiles.ts
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import type { Profile, SupipowersConfig } from "../types.js";
5
+ import { BUILTIN_PROFILES } from "./defaults.js";
6
+
7
+ const PROFILES_DIR = [".omp", "supipowers", "profiles"];
8
+
9
+ function getProfilesDir(cwd: string): string {
10
+ return path.join(cwd, ...PROFILES_DIR);
11
+ }
12
+
13
+ /** Load a profile by name. Checks project dir first, then built-ins. */
14
+ export function loadProfile(cwd: string, name: string): Profile | null {
15
+ // Check project-level custom profiles
16
+ const customPath = path.join(getProfilesDir(cwd), `${name}.json`);
17
+ if (fs.existsSync(customPath)) {
18
+ try {
19
+ return JSON.parse(fs.readFileSync(customPath, "utf-8")) as Profile;
20
+ } catch {
21
+ // fall through to built-in
22
+ }
23
+ }
24
+ return BUILTIN_PROFILES[name] ?? null;
25
+ }
26
+
27
+ /** Resolve the active profile from config, with optional override */
28
+ export function resolveProfile(
29
+ cwd: string,
30
+ config: SupipowersConfig,
31
+ override?: string
32
+ ): Profile {
33
+ const name = override ?? config.defaultProfile;
34
+ const profile = loadProfile(cwd, name);
35
+ if (!profile) {
36
+ // Fallback to thorough if configured profile doesn't exist
37
+ return BUILTIN_PROFILES["thorough"];
38
+ }
39
+ return profile;
40
+ }
41
+
42
+ /** List all available profiles (built-in + custom) */
43
+ export function listProfiles(cwd: string): string[] {
44
+ const names = new Set(Object.keys(BUILTIN_PROFILES));
45
+ const dir = getProfilesDir(cwd);
46
+ if (fs.existsSync(dir)) {
47
+ for (const file of fs.readdirSync(dir)) {
48
+ if (file.endsWith(".json")) {
49
+ names.add(file.replace(".json", ""));
50
+ }
51
+ }
52
+ }
53
+ return [...names].sort();
54
+ }
55
+
56
+ /** Save a custom profile */
57
+ export function saveProfile(cwd: string, profile: Profile): void {
58
+ const dir = getProfilesDir(cwd);
59
+ fs.mkdirSync(dir, { recursive: true });
60
+ fs.writeFileSync(
61
+ path.join(dir, `${profile.name}.json`),
62
+ JSON.stringify(profile, null, 2) + "\n"
63
+ );
64
+ }
@@ -0,0 +1,42 @@
1
+ // src/config/schema.ts
2
+ import { Type } from "@sinclair/typebox";
3
+ import { Value } from "@sinclair/typebox/value";
4
+ import type { SupipowersConfig, Profile } from "../types.js";
5
+
6
+ const ConfigSchema = Type.Object({
7
+ version: Type.String(),
8
+ defaultProfile: Type.String(),
9
+ orchestration: Type.Object({
10
+ maxParallelAgents: Type.Number({ minimum: 1, maximum: 10 }),
11
+ maxFixRetries: Type.Number({ minimum: 0, maximum: 5 }),
12
+ maxNestingDepth: Type.Number({ minimum: 0, maximum: 5 }),
13
+ modelPreference: Type.String(),
14
+ }),
15
+ lsp: Type.Object({
16
+ autoDetect: Type.Boolean(),
17
+ setupGuide: Type.Boolean(),
18
+ }),
19
+ notifications: Type.Object({
20
+ verbosity: Type.Union([
21
+ Type.Literal("quiet"),
22
+ Type.Literal("normal"),
23
+ Type.Literal("verbose"),
24
+ ]),
25
+ }),
26
+ qa: Type.Object({
27
+ framework: Type.Union([Type.String(), Type.Null()]),
28
+ command: Type.Union([Type.String(), Type.Null()]),
29
+ }),
30
+ release: Type.Object({
31
+ pipeline: Type.Union([Type.String(), Type.Null()]),
32
+ }),
33
+ });
34
+
35
+ export function validateConfig(data: unknown): { valid: boolean; errors: string[] } {
36
+ const valid = Value.Check(ConfigSchema, data);
37
+ if (valid) return { valid: true, errors: [] };
38
+ const errors = [...Value.Errors(ConfigSchema, data)].map(
39
+ (e) => `${e.path}: ${e.message}`
40
+ );
41
+ return { valid: false, errors };
42
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
2
+ import { registerSupiCommand } from "./commands/supi.js";
3
+ import { registerConfigCommand } from "./commands/config.js";
4
+ import { registerStatusCommand } from "./commands/status.js";
5
+ import { registerPlanCommand } from "./commands/plan.js";
6
+ import { registerRunCommand } from "./commands/run.js";
7
+ import { registerReviewCommand } from "./commands/review.js";
8
+ import { registerQaCommand } from "./commands/qa.js";
9
+ import { registerReleaseCommand } from "./commands/release.js";
10
+
11
+ export default function supipowers(pi: ExtensionAPI): void {
12
+ // Register all commands
13
+ registerSupiCommand(pi);
14
+ registerConfigCommand(pi);
15
+ registerStatusCommand(pi);
16
+ registerPlanCommand(pi);
17
+ registerRunCommand(pi);
18
+ registerReviewCommand(pi);
19
+ registerQaCommand(pi);
20
+ registerReleaseCommand(pi);
21
+
22
+ // Session start
23
+ pi.on("session_start", async (_event, ctx) => {
24
+ if (ctx.hasUI) {
25
+ ctx.ui.setStatus("supipowers", "supi ready");
26
+ }
27
+ });
28
+ }
@@ -0,0 +1,59 @@
1
+ // src/lsp/bridge.ts
2
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
3
+
4
+ export interface DiagnosticsResult {
5
+ file: string;
6
+ diagnostics: Diagnostic[];
7
+ }
8
+
9
+ export interface Diagnostic {
10
+ severity: "error" | "warning" | "info" | "hint";
11
+ message: string;
12
+ line: number;
13
+ column: number;
14
+ }
15
+
16
+ /**
17
+ * Request LSP diagnostics for a file by sending a message that
18
+ * triggers the LLM to use the lsp tool. For direct use,
19
+ * we provide a prompt snippet the orchestrator can include
20
+ * in sub-agent assignments.
21
+ */
22
+ export function buildLspDiagnosticsPrompt(files: string[]): string {
23
+ const fileList = files.map((f) => `- ${f}`).join("\n");
24
+ return [
25
+ "Run LSP diagnostics on these files and report any errors or warnings:",
26
+ fileList,
27
+ "",
28
+ 'Use the lsp tool with action "diagnostics" for each file.',
29
+ "Report the results in this format:",
30
+ "FILE: <path>",
31
+ " LINE:COL SEVERITY: message",
32
+ ].join("\n");
33
+ }
34
+
35
+ /**
36
+ * Build a prompt snippet for sub-agents to check references before renaming.
37
+ */
38
+ export function buildLspReferencesPrompt(symbol: string, file: string): string {
39
+ return [
40
+ `Before modifying "${symbol}" in ${file}, use the lsp tool:`,
41
+ `1. action: "references", file: "${file}", symbol: "${symbol}"`,
42
+ "2. Review all references to understand impact",
43
+ "3. Update all references as part of your changes",
44
+ ].join("\n");
45
+ }
46
+
47
+ /**
48
+ * Build a prompt for post-edit validation via LSP.
49
+ */
50
+ export function buildLspValidationPrompt(files: string[]): string {
51
+ const fileList = files.map((f) => `- ${f}`).join("\n");
52
+ return [
53
+ "After making your changes, validate with LSP:",
54
+ fileList,
55
+ "",
56
+ 'Use the lsp tool with action "diagnostics" on each changed file.',
57
+ "If there are errors, fix them before reporting completion.",
58
+ ].join("\n");
59
+ }
@@ -0,0 +1,38 @@
1
+ // src/lsp/detector.ts
2
+ import type { SupipowersConfig } from "../types.js";
3
+
4
+ export interface LspStatus {
5
+ available: boolean;
6
+ servers: LspServerInfo[];
7
+ }
8
+
9
+ export interface LspServerInfo {
10
+ name: string;
11
+ status: "running" | "stopped" | "error";
12
+ fileTypes: string[];
13
+ error?: string;
14
+ }
15
+
16
+ /**
17
+ * Check LSP availability by invoking the lsp tool's "status" action.
18
+ * Uses pi.exec to call the lsp tool programmatically.
19
+ */
20
+ export async function detectLsp(
21
+ exec: (cmd: string, args: string[]) => Promise<{ stdout: string; exitCode: number }>
22
+ ): Promise<LspStatus> {
23
+ try {
24
+ // We check by looking for LSP config files or running servers
25
+ // In OMP, LSP is a built-in tool — we check if it's in active tools
26
+ return { available: false, servers: [] };
27
+ } catch {
28
+ return { available: false, servers: [] };
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Check if LSP is available from the extension context.
34
+ * Reads the active tools list to see if "lsp" is registered.
35
+ */
36
+ export function isLspAvailable(activeTools: string[]): boolean {
37
+ return activeTools.includes("lsp");
38
+ }
@@ -0,0 +1,81 @@
1
+ // src/lsp/setup-guide.ts
2
+
3
+ export interface SetupInstruction {
4
+ language: string;
5
+ server: string;
6
+ installCommand: string;
7
+ notes: string;
8
+ }
9
+
10
+ const COMMON_LSP_SERVERS: SetupInstruction[] = [
11
+ {
12
+ language: "TypeScript/JavaScript",
13
+ server: "typescript-language-server",
14
+ installCommand: "bun add -g typescript-language-server typescript",
15
+ notes: "Requires a tsconfig.json in your project root.",
16
+ },
17
+ {
18
+ language: "Python",
19
+ server: "pyright",
20
+ installCommand: "pip install pyright",
21
+ notes: "Works best with a pyrightconfig.json or pyproject.toml.",
22
+ },
23
+ {
24
+ language: "Rust",
25
+ server: "rust-analyzer",
26
+ installCommand: "rustup component add rust-analyzer",
27
+ notes: "Requires a Cargo.toml project.",
28
+ },
29
+ {
30
+ language: "Go",
31
+ server: "gopls",
32
+ installCommand: "go install golang.org/x/tools/gopls@latest",
33
+ notes: "Requires a go.mod project.",
34
+ },
35
+ ];
36
+
37
+ /** Get setup instructions for detected project languages */
38
+ export function getSetupInstructions(detectedLanguages: string[]): SetupInstruction[] {
39
+ return COMMON_LSP_SERVERS.filter((s) =>
40
+ detectedLanguages.some((lang) =>
41
+ s.language.toLowerCase().includes(lang.toLowerCase())
42
+ )
43
+ );
44
+ }
45
+
46
+ /** Detect project languages from file extensions */
47
+ export function detectProjectLanguages(files: string[]): string[] {
48
+ const extMap: Record<string, string> = {
49
+ ".ts": "typescript",
50
+ ".tsx": "typescript",
51
+ ".js": "javascript",
52
+ ".jsx": "javascript",
53
+ ".py": "python",
54
+ ".rs": "rust",
55
+ ".go": "go",
56
+ ".java": "java",
57
+ ".rb": "ruby",
58
+ ".php": "php",
59
+ };
60
+ const languages = new Set<string>();
61
+ for (const file of files) {
62
+ const ext = file.slice(file.lastIndexOf("."));
63
+ if (extMap[ext]) languages.add(extMap[ext]);
64
+ }
65
+ return [...languages];
66
+ }
67
+
68
+ /** Format setup instructions as readable text */
69
+ export function formatSetupGuide(instructions: SetupInstruction[]): string {
70
+ if (instructions.length === 0) {
71
+ return "No LSP setup instructions available for your project languages.";
72
+ }
73
+ const lines = ["LSP Setup Guide:", ""];
74
+ for (const inst of instructions) {
75
+ lines.push(`## ${inst.language} — ${inst.server}`);
76
+ lines.push(`Install: ${inst.installCommand}`);
77
+ lines.push(`Note: ${inst.notes}`);
78
+ lines.push("");
79
+ }
80
+ return lines.join("\n");
81
+ }
@@ -0,0 +1,67 @@
1
+ import type { Notification } from "../types.js";
2
+ import { LEVEL_ICONS, NOTIFY_TYPE_MAP } from "./types.js";
3
+
4
+ /** Format a notification into a styled text string */
5
+ export function formatNotification(notification: Notification): string {
6
+ const icon = LEVEL_ICONS[notification.level] ?? "";
7
+ const parts = [`${icon} ${notification.title}`];
8
+ if (notification.detail) {
9
+ parts.push(` \u2014 ${notification.detail}`);
10
+ }
11
+ return parts.join("");
12
+ }
13
+
14
+ /** Send a notification through OMP's UI */
15
+ export function sendNotification(
16
+ ctx: { ui: { notify(message: string, type?: "info" | "warning" | "error"): void } },
17
+ notification: Notification
18
+ ): void {
19
+ const message = formatNotification(notification);
20
+ const type = NOTIFY_TYPE_MAP[notification.level] ?? "info";
21
+ ctx.ui.notify(message, type);
22
+ }
23
+
24
+ /** Convenience: send a success notification */
25
+ export function notifySuccess(
26
+ ctx: { ui: { notify(message: string, type?: "info" | "warning" | "error"): void } },
27
+ title: string,
28
+ detail?: string
29
+ ): void {
30
+ sendNotification(ctx, { level: "success", title, detail });
31
+ }
32
+
33
+ /** Convenience: send a warning notification */
34
+ export function notifyWarning(
35
+ ctx: { ui: { notify(message: string, type?: "info" | "warning" | "error"): void } },
36
+ title: string,
37
+ detail?: string
38
+ ): void {
39
+ sendNotification(ctx, { level: "warning", title, detail });
40
+ }
41
+
42
+ /** Convenience: send an error notification */
43
+ export function notifyError(
44
+ ctx: { ui: { notify(message: string, type?: "info" | "warning" | "error"): void } },
45
+ title: string,
46
+ detail?: string
47
+ ): void {
48
+ sendNotification(ctx, { level: "error", title, detail });
49
+ }
50
+
51
+ /** Convenience: send an info notification */
52
+ export function notifyInfo(
53
+ ctx: { ui: { notify(message: string, type?: "info" | "warning" | "error"): void } },
54
+ title: string,
55
+ detail?: string
56
+ ): void {
57
+ sendNotification(ctx, { level: "info", title, detail });
58
+ }
59
+
60
+ /** Convenience: send a summary notification */
61
+ export function notifySummary(
62
+ ctx: { ui: { notify(message: string, type?: "info" | "warning" | "error"): void } },
63
+ title: string,
64
+ detail?: string
65
+ ): void {
66
+ sendNotification(ctx, { level: "summary", title, detail });
67
+ }
@@ -0,0 +1,19 @@
1
+ export type { Notification, NotificationLevel } from "../types.js";
2
+
3
+ /** Icons mapped to notification levels */
4
+ export const LEVEL_ICONS: Record<string, string> = {
5
+ success: "\u2713", // ✓
6
+ warning: "\u26A0", // ⚠
7
+ error: "\u2717", // ✗
8
+ info: "\u25C9", // ◉
9
+ summary: "\u25B8", // ▸
10
+ };
11
+
12
+ /** Map notification levels to ctx.ui.notify types */
13
+ export const NOTIFY_TYPE_MAP: Record<string, "info" | "warning" | "error"> = {
14
+ success: "info",
15
+ warning: "warning",
16
+ error: "error",
17
+ info: "info",
18
+ summary: "info",
19
+ };
@@ -0,0 +1,59 @@
1
+ // src/orchestrator/batch-scheduler.ts
2
+ import type { PlanTask, RunBatch } from "../types.js";
3
+
4
+ /**
5
+ * Group plan tasks into execution batches.
6
+ * Parallel-safe tasks with no pending dependencies run together.
7
+ * Sequential tasks wait for their dependencies.
8
+ */
9
+ export function scheduleBatches(
10
+ tasks: PlanTask[],
11
+ maxParallel: number
12
+ ): RunBatch[] {
13
+ const batches: RunBatch[] = [];
14
+ const completed = new Set<number>();
15
+ const remaining = new Set(tasks.map((t) => t.id));
16
+
17
+ let batchIndex = 0;
18
+
19
+ while (remaining.size > 0) {
20
+ const ready: number[] = [];
21
+
22
+ for (const task of tasks) {
23
+ if (!remaining.has(task.id)) continue;
24
+
25
+ if (task.parallelism.type === "parallel-safe") {
26
+ ready.push(task.id);
27
+ } else if (task.parallelism.type === "sequential") {
28
+ const depsReady = task.parallelism.dependsOn.every((dep) =>
29
+ completed.has(dep)
30
+ );
31
+ if (depsReady) ready.push(task.id);
32
+ }
33
+
34
+ if (ready.length >= maxParallel) break;
35
+ }
36
+
37
+ if (ready.length === 0) {
38
+ // Deadlock: remaining tasks have unresolvable dependencies
39
+ // Force the first remaining task into a batch
40
+ const first = [...remaining][0];
41
+ ready.push(first);
42
+ }
43
+
44
+ const batch: RunBatch = {
45
+ index: batchIndex++,
46
+ taskIds: ready.slice(0, maxParallel),
47
+ status: "pending",
48
+ };
49
+
50
+ for (const id of batch.taskIds) {
51
+ remaining.delete(id);
52
+ completed.add(id);
53
+ }
54
+
55
+ batches.push(batch);
56
+ }
57
+
58
+ return batches;
59
+ }
@@ -0,0 +1,38 @@
1
+ import type { AgentResult, PlanTask } from "../types.js";
2
+ import { detectConflicts } from "./result-collector.js";
3
+ import { buildMergePrompt } from "./prompts.js";
4
+
5
+ export interface ConflictResolution {
6
+ hasConflicts: boolean;
7
+ conflictingFiles: string[];
8
+ mergePrompt?: string;
9
+ }
10
+
11
+ export function analyzeConflicts(
12
+ results: AgentResult[],
13
+ tasks: PlanTask[]
14
+ ): ConflictResolution {
15
+ const conflictingFiles = detectConflicts(results);
16
+
17
+ if (conflictingFiles.length === 0) {
18
+ return { hasConflicts: false, conflictingFiles: [] };
19
+ }
20
+
21
+ const conflictingResults = results.filter((r) =>
22
+ r.filesChanged.some((f) => conflictingFiles.includes(f))
23
+ );
24
+
25
+ const agentOutputs = conflictingResults.map((r) => {
26
+ const task = tasks.find((t) => t.id === r.taskId);
27
+ return {
28
+ taskName: task?.name ?? `Task ${r.taskId}`,
29
+ output: r.output,
30
+ };
31
+ });
32
+
33
+ return {
34
+ hasConflicts: true,
35
+ conflictingFiles,
36
+ mergePrompt: buildMergePrompt(conflictingFiles, agentOutputs),
37
+ };
38
+ }