supipowers 0.4.0 → 0.6.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 (57) hide show
  1. package/package.json +3 -3
  2. package/skills/context-mode/SKILL.md +38 -0
  3. package/skills/qa-strategy/SKILL.md +103 -21
  4. package/src/commands/config.ts +23 -2
  5. package/src/commands/fix-pr.ts +1 -1
  6. package/src/commands/plan.ts +1 -1
  7. package/src/commands/qa.ts +232 -148
  8. package/src/commands/release.ts +1 -1
  9. package/src/commands/review.ts +1 -1
  10. package/src/commands/run.ts +9 -4
  11. package/src/commands/supi.ts +1 -1
  12. package/src/config/defaults.ts +11 -0
  13. package/src/config/schema.ts +11 -0
  14. package/src/context-mode/compressor.ts +200 -0
  15. package/src/context-mode/detector.ts +43 -0
  16. package/src/context-mode/event-extractor.ts +170 -0
  17. package/src/context-mode/event-store.ts +168 -0
  18. package/src/context-mode/hooks.ts +176 -0
  19. package/src/context-mode/installer.ts +71 -0
  20. package/src/context-mode/snapshot-builder.ts +127 -0
  21. package/src/discipline/debugging.ts +7 -7
  22. package/src/discipline/receiving-review.ts +5 -5
  23. package/src/discipline/tdd.ts +2 -2
  24. package/src/discipline/verification.ts +9 -9
  25. package/src/git/base-branch.ts +30 -0
  26. package/src/git/branch-finish.ts +12 -3
  27. package/src/git/sanitize.ts +19 -0
  28. package/src/git/worktree.ts +38 -11
  29. package/src/index.ts +8 -1
  30. package/src/orchestrator/agent-prompts.ts +15 -7
  31. package/src/orchestrator/conflict-resolver.ts +3 -2
  32. package/src/orchestrator/dispatcher.ts +76 -21
  33. package/src/orchestrator/prompts.ts +46 -6
  34. package/src/planning/plan-reviewer.ts +1 -1
  35. package/src/planning/plan-writer-prompt.ts +6 -9
  36. package/src/planning/prompt-builder.ts +17 -16
  37. package/src/planning/spec-reviewer.ts +2 -2
  38. package/src/qa/config.ts +43 -0
  39. package/src/qa/matrix.ts +84 -0
  40. package/src/qa/prompt-builder.ts +212 -0
  41. package/src/qa/scripts/detect-app-type.sh +68 -0
  42. package/src/qa/scripts/discover-routes.sh +143 -0
  43. package/src/qa/scripts/ensure-playwright.sh +38 -0
  44. package/src/qa/scripts/run-e2e-tests.sh +99 -0
  45. package/src/qa/scripts/start-dev-server.sh +46 -0
  46. package/src/qa/scripts/stop-dev-server.sh +36 -0
  47. package/src/qa/session.ts +39 -55
  48. package/src/qa/types.ts +97 -0
  49. package/src/storage/qa-sessions.ts +9 -9
  50. package/src/types.ts +22 -70
  51. package/src/qa/detector.ts +0 -61
  52. package/src/qa/phases/discovery.ts +0 -34
  53. package/src/qa/phases/execution.ts +0 -65
  54. package/src/qa/phases/matrix.ts +0 -41
  55. package/src/qa/phases/reporting.ts +0 -71
  56. package/src/qa/report.ts +0 -22
  57. package/src/qa/runner.ts +0 -46
@@ -0,0 +1,176 @@
1
+ // src/context-mode/hooks.ts
2
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
3
+ import type { SupipowersConfig } from "../types.js";
4
+ import { compressToolResult } from "./compressor.js";
5
+ import { detectContextMode, type ContextModeStatus } from "./detector.js";
6
+ import { EventStore } from "./event-store.js";
7
+ import { extractEvents, extractPromptEvents } from "./event-extractor.js";
8
+ import { buildResumeSnapshot } from "./snapshot-builder.js";
9
+ import { readFileSync, mkdirSync } from "node:fs";
10
+ import { join, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ // Cached detection result
14
+ let cachedStatus: ContextModeStatus | null = null;
15
+
16
+ /** HTTP command patterns for blocking */
17
+ const HTTP_PATTERNS = [
18
+ /^\s*curl\s/,
19
+ /^\s*wget\s/,
20
+ /\bcurl\s+(-[a-zA-Z]*\s+)*https?:\/\//,
21
+ /\bwget\s+(-[a-zA-Z]*\s+)*https?:\/\//,
22
+ ];
23
+
24
+ function isHttpCommand(command: unknown): boolean {
25
+ if (typeof command !== "string") return false;
26
+ return HTTP_PATTERNS.some((p) => p.test(command));
27
+ }
28
+
29
+ function loadRoutingSkill(): string | null {
30
+ try {
31
+ const __dirname = dirname(fileURLToPath(import.meta.url));
32
+ const skillPath = join(__dirname, "..", "..", "skills", "context-mode", "SKILL.md");
33
+ return readFileSync(skillPath, "utf-8");
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /** Register context-mode hooks on the extension API */
40
+ export function registerContextModeHooks(pi: ExtensionAPI, config: SupipowersConfig): void {
41
+ if (!config.contextMode.enabled) return;
42
+
43
+ // Phase 2: Event store initialization
44
+ let eventStore: EventStore | null = null;
45
+ let sessionId = `session-${Date.now()}`;
46
+
47
+ if (config.contextMode.eventTracking) {
48
+ try {
49
+ const dbDir = join(process.cwd(), ".omp", "supipowers", "sessions");
50
+ mkdirSync(dbDir, { recursive: true });
51
+ eventStore = new EventStore(join(dbDir, "events.db"));
52
+ eventStore.init();
53
+ } catch (e) {
54
+ (pi as any).logger?.error?.("context-mode: failed to initialize event store", e);
55
+ }
56
+ }
57
+
58
+ // Update module-level refs for compaction hooks
59
+ _eventStoreRef = eventStore;
60
+ _sessionIdRef = sessionId;
61
+
62
+ // Phase 1: Result compression + Phase 2: Event extraction
63
+ pi.on("tool_result", (event) => {
64
+ // Phase 1: compression
65
+ const compressed = compressToolResult(event, config.contextMode.compressionThreshold);
66
+
67
+ // Phase 2: event extraction (fire-and-forget)
68
+ if (eventStore && config.contextMode.eventTracking) {
69
+ try {
70
+ const events = extractEvents(event, sessionId);
71
+ if (events.length > 0) eventStore.writeEvents(events);
72
+ } catch (e) {
73
+ (pi as any).logger?.warn?.("context-mode: event extraction failed", e);
74
+ }
75
+ }
76
+
77
+ return compressed;
78
+ });
79
+
80
+ // Phase 1: Command blocking
81
+ pi.on("tool_call", (event) => {
82
+ if (!config.contextMode.blockHttpCommands) return;
83
+ if (event.toolName !== "bash") return;
84
+
85
+ const command = event.input?.command;
86
+ if (!isHttpCommand(command)) return;
87
+
88
+ // Only block if context-mode has a replacement tool
89
+ if (!cachedStatus) cachedStatus = detectContextMode(pi.getActiveTools());
90
+ if (!cachedStatus.tools.ctxFetchAndIndex) return;
91
+
92
+ return {
93
+ block: true,
94
+ reason:
95
+ "Use ctx_fetch_and_index instead of curl/wget. " +
96
+ "It fetches the URL, indexes the content, and returns a compressed summary.",
97
+ };
98
+ });
99
+
100
+ // Phase 1: Routing instructions + Phase 2: Prompt event extraction
101
+ pi.on("before_agent_start", (event) => {
102
+ // Phase 2: prompt event extraction (fire-and-forget)
103
+ if (eventStore && config.contextMode.eventTracking) {
104
+ try {
105
+ const prompt = (event as any).prompt as string | undefined;
106
+ if (prompt) {
107
+ const events = extractPromptEvents(prompt, sessionId);
108
+ if (events.length > 0) eventStore.writeEvents(events);
109
+ }
110
+ } catch (e) {
111
+ (pi as any).logger?.warn?.("context-mode: prompt event extraction failed", e);
112
+ }
113
+ }
114
+
115
+ // Phase 1: routing instructions
116
+ if (!config.contextMode.routingInstructions) return;
117
+ if (!cachedStatus) cachedStatus = detectContextMode(pi.getActiveTools());
118
+ if (!cachedStatus.available) return;
119
+
120
+ const skill = loadRoutingSkill();
121
+ if (!skill) return;
122
+
123
+ const systemPrompt = (event as any).systemPrompt as string | undefined;
124
+ if (!systemPrompt) return { systemPrompt: skill };
125
+ return { systemPrompt: systemPrompt + "\n\n" + skill };
126
+ });
127
+
128
+ // Phase 3: Compaction integration
129
+ if (config.contextMode.compaction && eventStore) {
130
+ let pendingSnapshot: string | null = null;
131
+
132
+ pi.on("session_before_compact", () => {
133
+ try {
134
+ pendingSnapshot = buildResumeSnapshot(eventStore!, sessionId);
135
+ } catch (e) {
136
+ (pi as any).logger?.warn?.("context-mode: snapshot build failed", e);
137
+ pendingSnapshot = null;
138
+ }
139
+ return undefined; // don't cancel or replace compaction
140
+ });
141
+
142
+ pi.on("session.compacting", () => {
143
+ if (!pendingSnapshot) return undefined;
144
+ const snapshot = pendingSnapshot;
145
+ pendingSnapshot = null;
146
+ return {
147
+ context: snapshot.split("\n"),
148
+ preserveData: {
149
+ resumeSnapshot: snapshot,
150
+ eventCounts: eventStore!.getEventCounts(sessionId),
151
+ },
152
+ };
153
+ });
154
+ }
155
+ }
156
+
157
+ /** Get the event store instance (for use by compaction hooks) */
158
+ export function getEventStore(): EventStore | null {
159
+ return _eventStoreRef;
160
+ }
161
+
162
+ /** Get the session ID (for use by compaction hooks) */
163
+ export function getSessionId(): string {
164
+ return _sessionIdRef;
165
+ }
166
+
167
+ // Module-level refs updated by registerContextModeHooks
168
+ let _eventStoreRef: EventStore | null = null;
169
+ let _sessionIdRef = "";
170
+
171
+ /** Reset cached state (for testing) */
172
+ export function _resetCache(): void {
173
+ cachedStatus = null;
174
+ _eventStoreRef = null;
175
+ _sessionIdRef = "";
176
+ }
@@ -0,0 +1,71 @@
1
+ // src/context-mode/installer.ts
2
+ import { detectContextMode } from "./detector.js";
3
+
4
+ type ExecFn = (cmd: string, args: string[]) => Promise<{ stdout: string; code: number }>;
5
+
6
+ /** Installation status */
7
+ export interface ContextModeInstallStatus {
8
+ cliInstalled: boolean;
9
+ mcpConfigured: boolean;
10
+ toolsAvailable: boolean;
11
+ version: string | null;
12
+ }
13
+
14
+ /** Check context-mode installation status */
15
+ export async function checkInstallation(
16
+ exec: ExecFn,
17
+ activeTools: string[],
18
+ ): Promise<ContextModeInstallStatus> {
19
+ const status = detectContextMode(activeTools);
20
+
21
+ // Check CLI
22
+ let cliInstalled = false;
23
+ let version: string | null = null;
24
+
25
+ try {
26
+ const whichResult = await exec("which", ["context-mode"]);
27
+ cliInstalled = whichResult.code === 0;
28
+ } catch {
29
+ cliInstalled = false;
30
+ }
31
+
32
+ // Get version
33
+ if (cliInstalled) {
34
+ try {
35
+ const versionResult = await exec("context-mode", ["--version"]);
36
+ if (versionResult.code === 0) {
37
+ version = versionResult.stdout.trim() || null;
38
+ }
39
+ } catch {
40
+ version = null;
41
+ }
42
+ }
43
+
44
+ return {
45
+ cliInstalled,
46
+ mcpConfigured: status.available,
47
+ toolsAvailable: status.available,
48
+ version,
49
+ };
50
+ }
51
+
52
+ /** Install context-mode globally */
53
+ export async function installContextMode(
54
+ exec: ExecFn,
55
+ ): Promise<{ success: boolean; error?: string }> {
56
+ try {
57
+ const result = await exec("npm", ["install", "-g", "context-mode"]);
58
+ if (result.code !== 0) {
59
+ return {
60
+ success: false,
61
+ error: `npm install failed (exit ${result.code}). Check permissions or try: sudo npm install -g context-mode`,
62
+ };
63
+ }
64
+ return { success: true };
65
+ } catch (e) {
66
+ return {
67
+ success: false,
68
+ error: `Installation failed: ${e instanceof Error ? e.message : String(e)}`,
69
+ };
70
+ }
71
+ }
@@ -0,0 +1,127 @@
1
+ // src/context-mode/snapshot-builder.ts
2
+ import type { EventStore, TrackedEvent } from "./event-store.js";
3
+
4
+ const CAPS = {
5
+ tasks: 10,
6
+ decisions: 5,
7
+ files: 20,
8
+ errors: 3,
9
+ git: 5,
10
+ };
11
+
12
+ /** Build a resume snapshot from tracked events for a session */
13
+ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string): string {
14
+ const counts = eventStore.getEventCounts(sessionId);
15
+ const hasAnyEvents = Object.values(counts).some((c) => c > 0);
16
+ if (!hasAnyEvents) return "";
17
+
18
+ const sections: string[] = ["<session_knowledge>"];
19
+
20
+ // Last request
21
+ const prompts = eventStore.getEvents(sessionId, { categories: ["prompt"], limit: 1 });
22
+ if (prompts.length > 0) {
23
+ const data = safeParse(prompts[0].data);
24
+ const prompt = typeof data?.prompt === "string" ? data.prompt.slice(0, 200) : "";
25
+ if (prompt) {
26
+ sections.push(` <last_request>${prompt}</last_request>`);
27
+ }
28
+ }
29
+
30
+ // Pending tasks
31
+ const tasks = eventStore.getEvents(sessionId, { categories: ["task"], limit: CAPS.tasks });
32
+ if (tasks.length > 0) {
33
+ sections.push(" <pending_tasks>");
34
+ for (const t of tasks) {
35
+ const data = safeParse(t.data);
36
+ const content = extractTaskContent(data);
37
+ if (content) sections.push(` - ${content.slice(0, 100)}`);
38
+ }
39
+ sections.push(" </pending_tasks>");
40
+ }
41
+
42
+ // Key decisions
43
+ const decisions = eventStore.getEvents(sessionId, { categories: ["decision"], limit: CAPS.decisions });
44
+ if (decisions.length > 0) {
45
+ sections.push(" <key_decisions>");
46
+ for (const d of decisions) {
47
+ const data = safeParse(d.data);
48
+ const prompt = typeof data?.prompt === "string" ? data.prompt.slice(0, 100) : "";
49
+ if (prompt) sections.push(` - ${prompt}`);
50
+ }
51
+ sections.push(" </key_decisions>");
52
+ }
53
+
54
+ // Files modified (write/edit only, deduplicated)
55
+ const fileEvents = eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 });
56
+ const modifiedPaths = new Set<string>();
57
+ for (const f of fileEvents) {
58
+ const data = safeParse(f.data);
59
+ if (data?.op === "edit" || data?.op === "write") {
60
+ if (typeof data.path === "string") modifiedPaths.add(data.path);
61
+ }
62
+ }
63
+ if (modifiedPaths.size > 0) {
64
+ sections.push(" <files_modified>");
65
+ const paths = [...modifiedPaths].slice(0, CAPS.files);
66
+ for (const p of paths) sections.push(` - ${p}`);
67
+ sections.push(" </files_modified>");
68
+ }
69
+
70
+ // Recent errors
71
+ const errors = eventStore.getEvents(sessionId, { categories: ["error"], limit: CAPS.errors });
72
+ if (errors.length > 0) {
73
+ sections.push(" <recent_errors>");
74
+ for (const e of errors) {
75
+ const data = safeParse(e.data);
76
+ const summary = formatErrorSummary(data);
77
+ if (summary) sections.push(` - ${summary.slice(0, 150)}`);
78
+ }
79
+ sections.push(" </recent_errors>");
80
+ }
81
+
82
+ // Git state
83
+ const gitEvents = eventStore.getEvents(sessionId, { categories: ["git"], limit: CAPS.git });
84
+ if (gitEvents.length > 0) {
85
+ sections.push(" <git_state>");
86
+ for (const g of gitEvents) {
87
+ const data = safeParse(g.data);
88
+ const cmd = typeof data?.command === "string" ? data.command.slice(0, 100) : "";
89
+ if (cmd) sections.push(` - ${cmd}`);
90
+ }
91
+ sections.push(" </git_state>");
92
+ }
93
+
94
+ sections.push("</session_knowledge>");
95
+
96
+ // If only the wrapper tags exist (no inner sections), return empty
97
+ if (sections.length <= 2) return "";
98
+
99
+ return sections.join("\n");
100
+ }
101
+
102
+ function safeParse(json: string): Record<string, unknown> | null {
103
+ try {
104
+ return JSON.parse(json);
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ function extractTaskContent(data: Record<string, unknown> | null): string | null {
111
+ if (!data?.input) return null;
112
+ const input = data.input as Record<string, unknown>;
113
+ if (Array.isArray(input.ops)) {
114
+ const ops = input.ops as Array<{ content?: string; op?: string }>;
115
+ return ops.map((o) => `${o.op ?? "task"}: ${o.content ?? ""}`).join("; ");
116
+ }
117
+ return JSON.stringify(input).slice(0, 100);
118
+ }
119
+
120
+ function formatErrorSummary(data: Record<string, unknown> | null): string | null {
121
+ if (!data) return null;
122
+ const command = typeof data.command === "string" ? data.command : "";
123
+ const toolName = typeof data.toolName === "string" ? data.toolName : "";
124
+ const exitCode = typeof data.exitCode === "number" ? ` (exit ${data.exitCode})` : "";
125
+ const prefix = command || toolName;
126
+ return prefix ? `${prefix}${exitCode}` : null;
127
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Systematic debugging instructions for sub-agent prompts.
3
- * Matches superpowers' systematic-debugging skill depth.
3
+ * Matches supipowers' systematic-debugging skill depth.
4
4
  */
5
5
  export function buildDebuggingInstructions(): string {
6
6
  return [
@@ -46,12 +46,12 @@ export function buildDebuggingInstructions(): string {
46
46
  "",
47
47
  "### Red Flags — STOP and Follow the Process",
48
48
  "",
49
- "- \"Quick fix for now, investigate later\"",
50
- "- \"Just try changing X and see if it works\"",
51
- "- \"Skip the test, I'll manually verify\"",
52
- "- \"It's probably X, let me fix that\"",
53
- "- \"I don't fully understand but this might work\"",
54
- "- \"One more fix attempt\" (when already tried 2+)",
49
+ '- "Quick fix for now, investigate later"',
50
+ '- "Just try changing X and see if it works"',
51
+ '- "Skip the test, I\'ll manually verify"',
52
+ '- "It\'s probably X, let me fix that"',
53
+ '- "I don\'t fully understand but this might work"',
54
+ '- "One more fix attempt" (when already tried 2+)',
55
55
  "- Each fix reveals a new problem in a different place",
56
56
  ].join("\n");
57
57
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Receiving code review instructions for sub-agent prompts.
3
- * Matches superpowers' receiving-code-review skill:
3
+ * Matches supipowers' receiving-code-review skill:
4
4
  * technical rigor and verification, not performative agreement.
5
5
  */
6
6
  export function buildReceivingReviewInstructions(): string {
@@ -22,9 +22,9 @@ export function buildReceivingReviewInstructions(): string {
22
22
  "### Forbidden Responses",
23
23
  "",
24
24
  "Never use performative agreement:",
25
- "- \"You're absolutely right!\"",
26
- "- \"Great point!\"",
27
- "- \"Excellent catch!\"",
25
+ '- "You\'re absolutely right!"',
26
+ '- "Great point!"',
27
+ '- "Excellent catch!"',
28
28
  "",
29
29
  "Instead: restate requirements, ask clarifying questions, take action.",
30
30
  "",
@@ -41,7 +41,7 @@ export function buildReceivingReviewInstructions(): string {
41
41
  "",
42
42
  "### YAGNI Check",
43
43
  "",
44
- "For suggested \"professional features\" — grep the codebase for actual usage.",
44
+ 'For suggested "professional features" — grep the codebase for actual usage.',
45
45
  "If unused, suggest removal instead of implementing.",
46
46
  "",
47
47
  "### Implementation Order",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * TDD enforcement instructions for sub-agent prompts.
3
- * Matches superpowers' test-driven-development skill depth.
3
+ * Matches supipowers' test-driven-development skill depth.
4
4
  */
5
5
  export function buildTddInstructions(): string {
6
6
  return [
@@ -49,7 +49,7 @@ export function buildTddInstructions(): string {
49
49
  "- Test passes immediately",
50
50
  "- Can't explain why test failed",
51
51
  "- Tests added later",
52
- "- Rationalizing \"just this once\"",
52
+ '- Rationalizing "just this once"',
53
53
  "",
54
54
  "### Testing Anti-Patterns",
55
55
  "",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Verification-before-completion instructions for sub-agent prompts.
3
- * Matches superpowers' verification-before-completion skill depth.
3
+ * Matches supipowers' verification-before-completion skill depth.
4
4
  */
5
5
  export function buildVerificationInstructions(): string {
6
6
  return [
@@ -27,31 +27,31 @@ export function buildVerificationInstructions(): string {
27
27
  "",
28
28
  "| Claim | Requires | Not Sufficient |",
29
29
  "|-------|----------|----------------|",
30
- "| Tests pass | Test command output: 0 failures | Previous run, \"should pass\" |",
30
+ '| Tests pass | Test command output: 0 failures | Previous run, "should pass" |',
31
31
  "| Build succeeds | Build command: exit 0 | Linter passing, logs look good |",
32
32
  "| Bug fixed | Test original symptom: passes | Code changed, assumed fixed |",
33
33
  "| Regression test works | Red-green cycle verified | Test passes once |",
34
- "| Agent completed | VCS diff shows changes | Agent reports \"success\" |",
34
+ '| Agent completed | VCS diff shows changes | Agent reports "success" |',
35
35
  "| Requirements met | Line-by-line checklist | Tests passing |",
36
36
  "",
37
37
  "### Red Flags — STOP Before Claiming",
38
38
  "",
39
- "- Using \"should\", \"probably\", \"seems to\"",
40
- "- Expressing satisfaction before verification (\"Great!\", \"Perfect!\", \"Done!\")",
39
+ '- Using "should", "probably", "seems to"',
40
+ '- Expressing satisfaction before verification ("Great!", "Perfect!", "Done!")',
41
41
  "- About to commit/push/PR without verification",
42
42
  "- Trusting agent success reports without checking",
43
43
  "- Relying on partial verification",
44
- "- Thinking \"just this once\"",
44
+ '- Thinking "just this once"',
45
45
  "",
46
46
  "### Verification Patterns",
47
47
  "",
48
48
  "**Tests:**",
49
49
  "- Run test command → see actual pass count → then claim",
50
- "- Never say \"should pass now\" or \"looks correct\"",
50
+ '- Never say "should pass now" or "looks correct"',
51
51
  "",
52
52
  "**Regression tests (TDD red-green):**",
53
53
  "- Write → Run (pass) → Revert fix → Run (MUST FAIL) → Restore → Run (pass)",
54
- "- Never say \"I've written a regression test\" without red-green verification",
54
+ '- Never say "I\'ve written a regression test" without red-green verification',
55
55
  "",
56
56
  "**Build:**",
57
57
  "- Run build → see exit 0 → then claim",
@@ -59,7 +59,7 @@ export function buildVerificationInstructions(): string {
59
59
  "",
60
60
  "**Requirements:**",
61
61
  "- Re-read plan → create checklist → verify each → report gaps or completion",
62
- "- Never say \"tests pass, phase complete\" without checking requirements",
62
+ '- Never say "tests pass, phase complete" without checking requirements',
63
63
  "",
64
64
  "**Agent delegation:**",
65
65
  "- Agent reports success → check VCS diff → verify changes → report actual state",
@@ -0,0 +1,30 @@
1
+ type ExecFn = (cmd: string, args: string[]) => Promise<{ stdout: string; code: number }>;
2
+
3
+ const FALLBACK = "main";
4
+
5
+ /**
6
+ * Detect the repository's default branch.
7
+ * Strategy:
8
+ * 1. git symbolic-ref refs/remotes/origin/HEAD → parse branch name
9
+ * 2. git config init.defaultBranch
10
+ * 3. Falls back to "main"
11
+ */
12
+ export async function detectBaseBranch(exec: ExecFn): Promise<string> {
13
+ try {
14
+ const result = await exec("git", ["symbolic-ref", "refs/remotes/origin/HEAD"]);
15
+ if (result.code === 0 && result.stdout.trim()) {
16
+ const ref = result.stdout.trim();
17
+ const branch = ref.replace(/^refs\/remotes\/origin\//, "");
18
+ if (branch && branch !== ref) return branch;
19
+ }
20
+ } catch { /* continue to next strategy */ }
21
+
22
+ try {
23
+ const result = await exec("git", ["config", "init.defaultBranch"]);
24
+ if (result.code === 0 && result.stdout.trim()) {
25
+ return result.stdout.trim();
26
+ }
27
+ } catch { /* continue to fallback */ }
28
+
29
+ return FALLBACK;
30
+ }
@@ -1,3 +1,5 @@
1
+ import { assertSafeRef, assertSafePath } from "./sanitize.js";
2
+
1
3
  export interface FinishOption {
2
4
  id: "merge" | "pr" | "keep" | "discard";
3
5
  label: string;
@@ -19,15 +21,21 @@ export interface BranchFinishPromptOptions {
19
21
 
20
22
  /**
21
23
  * Build the prompt that guides the agent through finishing a development branch.
22
- * Follows superpowers' finishing-a-development-branch skill:
24
+ * Follows supipowers' finishing-a-development-branch skill:
23
25
  * - Verify tests pass first
24
26
  * - Present exactly 4 options
25
27
  * - Execute chosen option
26
28
  * - Clean up worktree (conditional)
27
29
  */
28
- export function buildBranchFinishPrompt(options: BranchFinishPromptOptions): string {
30
+ export function buildBranchFinishPrompt(
31
+ options: BranchFinishPromptOptions,
32
+ ): string {
29
33
  const { branchName, baseBranch, worktreePath } = options;
30
34
 
35
+ assertSafeRef(branchName, "branchName");
36
+ assertSafeRef(baseBranch, "baseBranch");
37
+ if (worktreePath) assertSafePath(worktreePath, "worktreePath");
38
+
31
39
  const sections: string[] = [
32
40
  "## Finish Development Branch",
33
41
  "",
@@ -77,6 +85,7 @@ export function buildBranchFinishPrompt(options: BranchFinishPromptOptions): str
77
85
  "",
78
86
  "```bash",
79
87
  `git checkout ${baseBranch}`,
88
+ ...(worktreePath ? [`git worktree remove ${worktreePath}`] : []),
80
89
  `git branch -D ${branchName}`,
81
90
  "```",
82
91
  ];
@@ -88,7 +97,7 @@ export function buildBranchFinishPrompt(options: BranchFinishPromptOptions): str
88
97
  "",
89
98
  `Worktree at: \`${worktreePath}\``,
90
99
  "",
91
- "- **Options 1 and 4:** Clean up the worktree:",
100
+ "- **Option 1:** Clean up the worktree:",
92
101
  " ```bash",
93
102
  ` git worktree remove ${worktreePath}`,
94
103
  " ```",
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Validate a git ref name for safe interpolation into shell commands.
3
+ * Rejects names containing shell metacharacters, whitespace, or git-invalid sequences.
4
+ * Follows git-check-ref-format rules plus shell safety.
5
+ */
6
+ const SAFE_REF = /^[a-zA-Z0-9][a-zA-Z0-9._\/-]*$/;
7
+ const BANNED = /\.\.|\/{2}|@\{|[~^:?*\[\\]/;
8
+
9
+ export function assertSafeRef(value: string, label: string): void {
10
+ if (!value || !SAFE_REF.test(value) || BANNED.test(value) || value.endsWith(".lock") || value.endsWith("/") || value.endsWith(".")) {
11
+ throw new Error(`Unsafe ${label}: "${value}" contains characters not allowed in git ref names or shell commands`);
12
+ }
13
+ }
14
+
15
+ export function assertSafePath(value: string, label: string): void {
16
+ if (!value || /[;&|`$(){}!#<>'"\\]/.test(value)) {
17
+ throw new Error(`Unsafe ${label}: "${value}" contains shell metacharacters`);
18
+ }
19
+ }