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.
- package/package.json +3 -3
- package/skills/context-mode/SKILL.md +38 -0
- package/skills/qa-strategy/SKILL.md +103 -21
- package/src/commands/config.ts +23 -2
- package/src/commands/fix-pr.ts +1 -1
- package/src/commands/plan.ts +1 -1
- package/src/commands/qa.ts +232 -148
- package/src/commands/release.ts +1 -1
- package/src/commands/review.ts +1 -1
- package/src/commands/run.ts +9 -4
- package/src/commands/supi.ts +1 -1
- package/src/config/defaults.ts +11 -0
- package/src/config/schema.ts +11 -0
- package/src/context-mode/compressor.ts +200 -0
- package/src/context-mode/detector.ts +43 -0
- package/src/context-mode/event-extractor.ts +170 -0
- package/src/context-mode/event-store.ts +168 -0
- package/src/context-mode/hooks.ts +176 -0
- package/src/context-mode/installer.ts +71 -0
- package/src/context-mode/snapshot-builder.ts +127 -0
- package/src/discipline/debugging.ts +7 -7
- package/src/discipline/receiving-review.ts +5 -5
- package/src/discipline/tdd.ts +2 -2
- package/src/discipline/verification.ts +9 -9
- package/src/git/base-branch.ts +30 -0
- package/src/git/branch-finish.ts +12 -3
- package/src/git/sanitize.ts +19 -0
- package/src/git/worktree.ts +38 -11
- package/src/index.ts +8 -1
- package/src/orchestrator/agent-prompts.ts +15 -7
- package/src/orchestrator/conflict-resolver.ts +3 -2
- package/src/orchestrator/dispatcher.ts +76 -21
- package/src/orchestrator/prompts.ts +46 -6
- package/src/planning/plan-reviewer.ts +1 -1
- package/src/planning/plan-writer-prompt.ts +6 -9
- package/src/planning/prompt-builder.ts +17 -16
- package/src/planning/spec-reviewer.ts +2 -2
- package/src/qa/config.ts +43 -0
- package/src/qa/matrix.ts +84 -0
- package/src/qa/prompt-builder.ts +212 -0
- package/src/qa/scripts/detect-app-type.sh +68 -0
- package/src/qa/scripts/discover-routes.sh +143 -0
- package/src/qa/scripts/ensure-playwright.sh +38 -0
- package/src/qa/scripts/run-e2e-tests.sh +99 -0
- package/src/qa/scripts/start-dev-server.sh +46 -0
- package/src/qa/scripts/stop-dev-server.sh +36 -0
- package/src/qa/session.ts +39 -55
- package/src/qa/types.ts +97 -0
- package/src/storage/qa-sessions.ts +9 -9
- package/src/types.ts +22 -70
- package/src/qa/detector.ts +0 -61
- package/src/qa/phases/discovery.ts +0 -34
- package/src/qa/phases/execution.ts +0 -65
- package/src/qa/phases/matrix.ts +0 -41
- package/src/qa/phases/reporting.ts +0 -71
- package/src/qa/report.ts +0 -22
- 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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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",
|
package/src/discipline/tdd.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TDD enforcement instructions for sub-agent prompts.
|
|
3
|
-
* Matches
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/git/branch-finish.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
"- **
|
|
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
|
+
}
|