@demon-utils/playwright 0.1.6 → 0.1.7

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 CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@demon-utils/playwright",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
+ "scripts": {
6
+ "build": "./build.sh",
7
+ "test:e2e": "playwright test -c e2e/playwright.config.ts"
8
+ },
5
9
  "module": "dist/index.js",
6
10
  "types": "src/index.ts",
7
11
  "exports": {
@@ -15,9 +19,14 @@
15
19
  "src"
16
20
  ],
17
21
  "bin": {
18
- "demon-demo-review": "dist/bin/demon-demo-review.js"
22
+ "demon-demo-review": "dist/bin/demon-demo-review.js",
23
+ "demon-demo-init": "dist/bin/demon-demo-init.js",
24
+ "demoon": "dist/bin/demoon.js"
19
25
  },
20
26
  "type": "module",
27
+ "dependencies": {
28
+ "@octokit/graphql": "^8.2.1"
29
+ },
21
30
  "peerDependencies": {
22
31
  "@playwright/test": ">=1.40.0"
23
32
  },
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, writeFileSync } from "node:fs";
3
+ import { resolve, join, dirname } from "node:path";
4
+
5
+ const DEMO_CONFIG_FILENAME = "playwright.demo.config.ts";
6
+
7
+ const EXAMPLE_DEMO_TEMPLATE = `import { test } from "@playwright/test";
8
+ import { DemoRecorder } from "@demon-utils/playwright";
9
+ // You can also import hideCommentary to manually hide tooltips:
10
+ // import { hideCommentary } from "@demon-utils/playwright";
11
+
12
+ test("example demo", async ({ page }, testInfo) => {
13
+ const demo = new DemoRecorder({ testStep: test.step });
14
+
15
+ // Navigate to the starting page
16
+ await demo.step(page, "Navigate to the application", { selector: "body" });
17
+ await page.goto("/");
18
+ await page.waitForTimeout(1000);
19
+
20
+ // Demonstrate an interaction
21
+ await demo.step(page, "Click a button or link", { selector: "body" });
22
+ // await page.click("#my-button");
23
+ await page.waitForTimeout(1000);
24
+
25
+ // Save the demo steps metadata
26
+ await demo.save(testInfo.outputDir);
27
+ });
28
+ `;
29
+
30
+ function findConfigDir(startDir: string): string | null {
31
+ let current = resolve(startDir);
32
+ const root = resolve("/");
33
+
34
+ while (current !== root) {
35
+ const configPath = join(current, DEMO_CONFIG_FILENAME);
36
+ if (existsSync(configPath)) {
37
+ return current;
38
+ }
39
+ const parent = dirname(current);
40
+ if (parent === current) break;
41
+ current = parent;
42
+ }
43
+
44
+ return null;
45
+ }
46
+
47
+ const startPath = process.argv[2] ?? process.cwd();
48
+ const resolved = resolve(startPath);
49
+
50
+ const configDir = findConfigDir(resolved);
51
+
52
+ if (!configDir) {
53
+ console.error(`Error: Could not find ${DEMO_CONFIG_FILENAME} in any parent directory of "${resolved}".`);
54
+ process.exit(1);
55
+ }
56
+
57
+ const examplePath = join(configDir, "example.demo.ts");
58
+ writeFileSync(examplePath, EXAMPLE_DEMO_TEMPLATE);
59
+ console.log(examplePath);
@@ -1,31 +1,28 @@
1
1
  #!/usr/bin/env bun
2
- import { existsSync, statSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { resolve, join, basename, dirname } from "node:path";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
4
 
5
- import {
6
- buildReviewPrompt,
7
- invokeClaude,
8
- parseLlmResponse,
9
- } from "../review.ts";
10
- import { generateReviewHtml } from "../html-generator.ts";
11
- import { getRepoContext } from "../git-context.ts";
12
- import type { ReviewMetadata } from "../review-types.ts";
5
+ import { generateReview, discoverDemoFiles } from "../review-generator.ts";
13
6
 
14
7
  let dir: string | undefined;
15
8
  let agent: string | undefined;
9
+ let diffBase: string | undefined;
16
10
 
17
11
  const args = process.argv.slice(2);
18
12
  for (let i = 0; i < args.length; i++) {
19
13
  if (args[i] === "--agent") {
20
14
  agent = args[++i];
15
+ } else if (args[i] === "--base") {
16
+ diffBase = args[++i];
21
17
  } else if (!dir) {
22
18
  dir = args[i];
23
19
  }
24
20
  }
25
21
 
26
22
  if (!dir) {
27
- console.error("Usage: demon-demo-review [--agent <path>] <directory>");
28
- console.error(" Discovers .webm video files in the given directory.");
23
+ console.error("Usage: demon-demo-review [--agent <path>] [--base <ref>] <directory>");
24
+ console.error(" Discovers .webm and .jsonl demo files in the given directory.");
25
+ console.error(" --base <ref> Base commit/branch for diff (auto-detects main/master if on feature branch)");
29
26
  process.exit(1);
30
27
  }
31
28
 
@@ -36,102 +33,27 @@ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
36
33
  process.exit(1);
37
34
  }
38
35
 
39
- // Discover .webm files search top-level first, then one level deep
40
- // (Playwright creates per-test subdirectories under outputDir)
41
- let webmFiles = readdirSync(resolved)
42
- .filter((f) => f.endsWith(".webm"))
43
- .map((f) => join(resolved, f));
36
+ // Discover and print demo files
37
+ const demoFiles = discoverDemoFiles(resolved);
44
38
 
45
- if (webmFiles.length === 0) {
46
- for (const entry of readdirSync(resolved, { withFileTypes: true })) {
47
- if (!entry.isDirectory()) continue;
48
- const subdir = join(resolved, entry.name);
49
- for (const f of readdirSync(subdir)) {
50
- if (f.endsWith(".webm")) {
51
- webmFiles.push(join(subdir, f));
52
- }
53
- }
54
- }
55
- }
56
-
57
- webmFiles.sort();
58
-
59
- if (webmFiles.length === 0) {
60
- console.error(`Error: No .webm files found in "${resolved}" or its subdirectories.`);
39
+ if (demoFiles.length === 0) {
40
+ console.error(`Error: No .webm or .jsonl files found in "${resolved}" or its subdirectories.`);
61
41
  process.exit(1);
62
42
  }
63
43
 
64
- for (const file of webmFiles) {
65
- console.log(file);
44
+ for (const file of demoFiles) {
45
+ console.log(file.path);
66
46
  }
67
47
 
68
- // Collect demo-steps.json from the directory of each .webm file
69
- const stepsMap: Record<string, Array<{ text: string; timestampSeconds: number }>> = {};
70
- for (const webmFile of webmFiles) {
71
- const stepsPath = join(dirname(webmFile), "demo-steps.json");
72
- if (!existsSync(stepsPath)) continue;
73
- try {
74
- const raw = readFileSync(stepsPath, "utf-8");
75
- const parsed = JSON.parse(raw);
76
- if (Array.isArray(parsed)) {
77
- stepsMap[basename(webmFile)] = parsed;
78
- }
79
- } catch {
80
- // skip malformed steps files
81
- }
82
- }
83
-
84
- if (Object.keys(stepsMap).length === 0) {
85
- console.error("Error: No demo-steps.json found alongside any .webm files.");
86
- console.error("Use DemoRecorder in your demo tests to generate step data.");
87
- process.exit(1);
88
- }
89
-
90
- // Gather repo context (git diff + guidelines)
91
- let gitDiff: string | undefined;
92
- let guidelines: string[] | undefined;
93
48
  try {
94
- const repoContext = await getRepoContext(resolved);
95
- gitDiff = repoContext.gitDiff;
96
- guidelines = repoContext.guidelines;
97
- } catch (err) {
98
- console.warn(
99
- "Warning: Could not gather repo context:",
100
- err instanceof Error ? err.message : err,
101
- );
102
- }
103
-
104
- try {
105
- const basenames = webmFiles.map((f) => basename(f));
106
-
107
- const prompt = buildReviewPrompt({ filenames: basenames, stepsMap, gitDiff, guidelines });
108
-
109
49
  console.log("Invoking claude to generate review metadata...");
110
- const rawOutput = await invokeClaude(prompt, { agent });
111
-
112
- const llmResponse = parseLlmResponse(rawOutput);
113
-
114
- // Construct final metadata by merging LLM summaries with steps
115
- const metadata: ReviewMetadata = {
116
- demos: llmResponse.demos.map((demo) => ({
117
- file: demo.file,
118
- summary: demo.summary,
119
- steps: stepsMap[demo.file] ?? [],
120
- })),
121
- review: llmResponse.review,
122
- };
123
-
124
- const outputPath = join(resolved, "review-metadata.json");
125
- writeFileSync(outputPath, JSON.stringify(metadata, null, 2) + "\n");
126
- console.log(`Review metadata written to ${outputPath}`);
50
+ const result = await generateReview({ directory: resolved, agent, diffBase });
127
51
 
128
- const html = generateReviewHtml({ metadata });
129
- const htmlPath = join(resolved, "review.html");
130
- writeFileSync(htmlPath, html);
131
- console.log(resolve(htmlPath));
52
+ console.log(`Review metadata written to ${result.metadataPath}`);
53
+ console.log(resolve(result.htmlPath));
132
54
  } catch (err) {
133
55
  console.error(
134
- "Error generating review metadata:",
56
+ "Error generating review:",
135
57
  err instanceof Error ? err.message : err,
136
58
  );
137
59
  process.exit(1);
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bun
2
+ import { resolve } from "node:path";
3
+ import { readFileSync } from "node:fs";
4
+
5
+ import { runReviewOrchestration } from "../orchestrator.ts";
6
+ import { startFeedbackServer, type FeedbackPayload } from "../feedback-server.ts";
7
+ import type { GitHubIssue } from "../github-issue.ts";
8
+
9
+ function printUsage(): void {
10
+ console.error("Usage: demoon <command> [options]");
11
+ console.error("");
12
+ console.error("Commands:");
13
+ console.error(" review Generate a review from a GitHub issue");
14
+ console.error("");
15
+ console.error("Review options:");
16
+ console.error(" --github-issue-id <id> GitHub issue number (required unless --issue-file is provided)");
17
+ console.error(" --issue-file <path> Path to JSON file with issue data (for testing, skips GitHub API)");
18
+ console.error(" --base <ref> Base commit/branch for diff (auto-detects main/master if on feature branch)");
19
+ console.error(" --agent <path> Path to Claude agent binary");
20
+ console.error(" --port <number> Port for feedback server (default: random available port)");
21
+ console.error("");
22
+ console.error("Environment variables:");
23
+ console.error(" GITHUB_TOKEN or GH_TOKEN GitHub personal access token (required for API access)");
24
+ }
25
+
26
+ async function main(): Promise<void> {
27
+ const args = process.argv.slice(2);
28
+
29
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
30
+ printUsage();
31
+ process.exit(args.length === 0 ? 1 : 0);
32
+ }
33
+
34
+ const command = args[0];
35
+
36
+ if (command !== "review") {
37
+ console.error(`Unknown command: ${command}`);
38
+ console.error("");
39
+ printUsage();
40
+ process.exit(1);
41
+ }
42
+
43
+ // Parse review command options
44
+ let issueId: string | undefined;
45
+ let issueFile: string | undefined;
46
+ let diffBase: string | undefined;
47
+ let agent: string | undefined;
48
+ let port = 0;
49
+
50
+ for (let i = 1; i < args.length; i++) {
51
+ const arg = args[i];
52
+ if (arg === "--github-issue-id" || arg === "--issue") {
53
+ issueId = args[++i];
54
+ } else if (arg === "--issue-file") {
55
+ issueFile = args[++i];
56
+ } else if (arg === "--base") {
57
+ diffBase = args[++i];
58
+ } else if (arg === "--agent") {
59
+ agent = args[++i];
60
+ } else if (arg === "--port") {
61
+ port = parseInt(args[++i] ?? "0", 10);
62
+ } else if (!arg?.startsWith("-")) {
63
+ // Allow positional issue ID for convenience
64
+ if (!issueId) {
65
+ issueId = arg;
66
+ }
67
+ }
68
+ }
69
+
70
+ // Load issue from file if provided
71
+ let issue: GitHubIssue | undefined;
72
+ if (issueFile) {
73
+ const content = readFileSync(issueFile, "utf-8");
74
+ issue = JSON.parse(content) as GitHubIssue;
75
+ issueId = String(issue.number);
76
+ }
77
+
78
+ if (!issueId && !issue) {
79
+ console.error("Error: --github-issue-id or --issue-file is required");
80
+ console.error("");
81
+ printUsage();
82
+ process.exit(1);
83
+ }
84
+
85
+ // Start feedback server
86
+ const feedbackServer = startFeedbackServer(port);
87
+
88
+ try {
89
+ if (issue) {
90
+ console.log(`Using issue from file: #${issue.number} - ${issue.title}`);
91
+ } else {
92
+ console.log(`Fetching GitHub issue #${issueId}...`);
93
+ }
94
+
95
+ const result = await runReviewOrchestration({
96
+ issueId: issueId!,
97
+ issue,
98
+ diffBase,
99
+ agent,
100
+ feedbackEndpoint: feedbackServer.feedbackEndpoint,
101
+ });
102
+
103
+ console.log("");
104
+ console.log(`Review generated for issue #${result.issue.number}: ${result.issue.title}`);
105
+ console.log("");
106
+ console.log(`Verdict: ${result.metadata.review?.verdict ?? "unknown"}`);
107
+ if (result.metadata.review?.verdictReason) {
108
+ console.log(`Reason: ${result.metadata.review.verdictReason}`);
109
+ }
110
+ console.log("");
111
+ console.log(`Review folder: ${result.reviewFolder}`);
112
+ console.log(`Review HTML: ${resolve(result.htmlPath)}`);
113
+ console.log("");
114
+ console.log(`Feedback server running at: http://localhost:${feedbackServer.port}`);
115
+ console.log(`Open the review HTML and approve/request changes to complete.`);
116
+ console.log("");
117
+
118
+ // Wait for user feedback
119
+ const feedback: FeedbackPayload = await feedbackServer.waitForFeedback();
120
+
121
+ console.log("");
122
+ console.log("=".repeat(60));
123
+ console.log(`User verdict: ${feedback.verdict}`);
124
+ if (feedback.feedback) {
125
+ console.log("");
126
+ console.log("Feedback:");
127
+ console.log(feedback.feedback);
128
+ }
129
+ console.log("=".repeat(60));
130
+
131
+ // Exit with appropriate code
132
+ process.exit(feedback.verdict === "approve" ? 0 : 1);
133
+ } catch (err) {
134
+ console.error("Error:", err instanceof Error ? err.message : err);
135
+ feedbackServer.stop();
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ main();
@@ -0,0 +1,138 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ export type ReviewVerdict = "approve" | "request_changes";
4
+
5
+ export interface FeedbackPayload {
6
+ verdict: ReviewVerdict;
7
+ feedback?: string;
8
+ }
9
+
10
+ interface PendingReview {
11
+ resolve: (payload: FeedbackPayload) => void;
12
+ reject: (error: Error) => void;
13
+ }
14
+
15
+ const pendingReviews = new Map<string, PendingReview>();
16
+
17
+ const CORS_HEADERS = {
18
+ "Access-Control-Allow-Origin": "*",
19
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
20
+ "Access-Control-Allow-Headers": "Content-Type",
21
+ };
22
+
23
+ function isValidPayload(body: unknown): body is FeedbackPayload {
24
+ if (typeof body !== "object" || body === null) return false;
25
+ const obj = body as Record<string, unknown>;
26
+ if (obj["verdict"] !== "approve" && obj["verdict"] !== "request_changes") return false;
27
+ if (obj["feedback"] !== undefined && typeof obj["feedback"] !== "string") return false;
28
+ return true;
29
+ }
30
+
31
+ export interface FeedbackServerResult {
32
+ server: ReturnType<typeof Bun.serve>;
33
+ port: number;
34
+ reviewId: string;
35
+ feedbackEndpoint: string;
36
+ waitForFeedback: () => Promise<FeedbackPayload>;
37
+ stop: () => void;
38
+ }
39
+
40
+ export function startFeedbackServer(preferredPort = 0): FeedbackServerResult {
41
+ const reviewId = randomUUID();
42
+
43
+ const server = Bun.serve({
44
+ port: preferredPort,
45
+ async fetch(req) {
46
+ const url = new URL(req.url);
47
+
48
+ // Handle CORS preflight
49
+ if (req.method === "OPTIONS") {
50
+ return new Response(null, {
51
+ status: 204,
52
+ headers: CORS_HEADERS,
53
+ });
54
+ }
55
+
56
+ // Handle feedback endpoint
57
+ if (url.pathname === "/feedback" && req.method === "POST") {
58
+ const requestReviewId = url.searchParams.get("reviewId");
59
+ const headers = {
60
+ "Content-Type": "application/json",
61
+ ...CORS_HEADERS,
62
+ };
63
+
64
+ if (!requestReviewId) {
65
+ return new Response(
66
+ JSON.stringify({ error: "Missing reviewId query parameter" }),
67
+ { status: 400, headers }
68
+ );
69
+ }
70
+
71
+ const pending = pendingReviews.get(requestReviewId);
72
+ if (!pending) {
73
+ return new Response(
74
+ JSON.stringify({ error: "Review not found or already completed" }),
75
+ { status: 404, headers }
76
+ );
77
+ }
78
+
79
+ let body: unknown;
80
+ try {
81
+ body = await req.json();
82
+ } catch {
83
+ return new Response(
84
+ JSON.stringify({ error: "Invalid JSON" }),
85
+ { status: 400, headers }
86
+ );
87
+ }
88
+
89
+ if (!isValidPayload(body)) {
90
+ return new Response(
91
+ JSON.stringify({ error: "Invalid payload" }),
92
+ { status: 400, headers }
93
+ );
94
+ }
95
+
96
+ pending.resolve(body);
97
+ pendingReviews.delete(requestReviewId);
98
+
99
+ return new Response(
100
+ JSON.stringify({ success: true, verdict: body.verdict }),
101
+ { status: 200, headers }
102
+ );
103
+ }
104
+
105
+ // Health check
106
+ if (url.pathname === "/health") {
107
+ return new Response(JSON.stringify({ status: "ok" }), {
108
+ headers: { "Content-Type": "application/json" },
109
+ });
110
+ }
111
+
112
+ return new Response("Not Found", { status: 404 });
113
+ },
114
+ });
115
+
116
+ const port = server.port ?? 3000;
117
+ const feedbackEndpoint = `http://localhost:${port}/feedback?reviewId=${reviewId}`;
118
+
119
+ const feedbackPromise = new Promise<FeedbackPayload>((resolve, reject) => {
120
+ pendingReviews.set(reviewId, { resolve, reject });
121
+ });
122
+
123
+ return {
124
+ server,
125
+ port,
126
+ reviewId,
127
+ feedbackEndpoint,
128
+ waitForFeedback: () => feedbackPromise,
129
+ stop: () => {
130
+ const pending = pendingReviews.get(reviewId);
131
+ if (pending) {
132
+ pending.reject(new Error("Server stopped"));
133
+ pendingReviews.delete(reviewId);
134
+ }
135
+ server.stop();
136
+ },
137
+ };
138
+ }
@@ -23,9 +23,10 @@ function mockReadFile(files: Record<string, string>): ReadFileFn {
23
23
  }
24
24
 
25
25
  describe("getRepoContext", () => {
26
- test("returns diff from working tree when dirty", async () => {
26
+ test("returns diff from working tree when dirty and on main", async () => {
27
27
  const exec = mockExec({
28
28
  "git rev-parse --show-toplevel": "/repo\n",
29
+ "git rev-parse --abbrev-ref HEAD": "main\n",
29
30
  "git diff HEAD": "diff --git a/file.ts\n+added line\n",
30
31
  "git ls-files": "src/index.ts\n",
31
32
  });
@@ -35,9 +36,10 @@ describe("getRepoContext", () => {
35
36
  expect(ctx.gitDiff).toBe("diff --git a/file.ts\n+added line");
36
37
  });
37
38
 
38
- test("falls back to HEAD~1..HEAD when working tree is clean", async () => {
39
+ test("falls back to HEAD~1..HEAD when working tree is clean and on main", async () => {
39
40
  const exec = mockExec({
40
41
  "git rev-parse --show-toplevel": "/repo\n",
42
+ "git rev-parse --abbrev-ref HEAD": "main\n",
41
43
  "git diff HEAD": "",
42
44
  "git diff HEAD~1..HEAD": "diff --git a/committed.ts\n+committed line\n",
43
45
  "git ls-files": "",
@@ -48,9 +50,72 @@ describe("getRepoContext", () => {
48
50
  expect(ctx.gitDiff).toBe("diff --git a/committed.ts\n+committed line");
49
51
  });
50
52
 
53
+ test("auto-detects main as base when on feature branch", async () => {
54
+ const exec = mockExec({
55
+ "git rev-parse --show-toplevel": "/repo\n",
56
+ "git rev-parse --abbrev-ref HEAD": "feature-branch\n",
57
+ "git rev-parse --verify main": "abc123\n",
58
+ "git diff main...HEAD": "diff --git a/feature.ts\n+feature line\n",
59
+ "git ls-files": "",
60
+ });
61
+ const readFile = mockReadFile({});
62
+
63
+ const ctx = await getRepoContext("/repo/demos", { exec, readFile });
64
+ expect(ctx.gitDiff).toBe("diff --git a/feature.ts\n+feature line");
65
+ });
66
+
67
+ test("auto-detects master as base when main does not exist", async () => {
68
+ const exec: ExecFn = async (cmd: string[], _cwd: string) => {
69
+ const key = cmd.join(" ");
70
+ const responses: Record<string, string> = {
71
+ "git rev-parse --show-toplevel": "/repo\n",
72
+ "git rev-parse --abbrev-ref HEAD": "feature-branch\n",
73
+ "git rev-parse --verify master": "abc123\n",
74
+ "git diff master...HEAD": "diff --git a/feature.ts\n+feature line\n",
75
+ "git ls-files": "",
76
+ };
77
+ if (key === "git rev-parse --verify main") {
78
+ throw new Error("fatal: Needed a single revision");
79
+ }
80
+ if (key in responses) {
81
+ return responses[key]!;
82
+ }
83
+ throw new Error(`Unexpected command: ${key}`);
84
+ };
85
+ const readFile = mockReadFile({});
86
+
87
+ const ctx = await getRepoContext("/repo/demos", { exec, readFile });
88
+ expect(ctx.gitDiff).toBe("diff --git a/feature.ts\n+feature line");
89
+ });
90
+
91
+ test("uses explicit diffBase when provided", async () => {
92
+ const exec = mockExec({
93
+ "git rev-parse --show-toplevel": "/repo\n",
94
+ "git diff develop...HEAD": "diff --git a/feature.ts\n+feature line\n",
95
+ "git ls-files": "",
96
+ });
97
+ const readFile = mockReadFile({});
98
+
99
+ const ctx = await getRepoContext("/repo/demos", { exec, readFile, diffBase: "develop" });
100
+ expect(ctx.gitDiff).toBe("diff --git a/feature.ts\n+feature line");
101
+ });
102
+
103
+ test("uses explicit diffBase with commit hash", async () => {
104
+ const exec = mockExec({
105
+ "git rev-parse --show-toplevel": "/repo\n",
106
+ "git diff abc123...HEAD": "diff --git a/commit.ts\n+commit changes\n",
107
+ "git ls-files": "",
108
+ });
109
+ const readFile = mockReadFile({});
110
+
111
+ const ctx = await getRepoContext("/repo/demos", { exec, readFile, diffBase: "abc123" });
112
+ expect(ctx.gitDiff).toBe("diff --git a/commit.ts\n+commit changes");
113
+ });
114
+
51
115
  test("discovers CLAUDE.md and SKILL.md files", async () => {
52
116
  const exec = mockExec({
53
117
  "git rev-parse --show-toplevel": "/repo\n",
118
+ "git rev-parse --abbrev-ref HEAD": "main\n",
54
119
  "git diff HEAD": "some diff\n",
55
120
  "git ls-files": "CLAUDE.md\nplugins/demo/SKILL.md\nsrc/index.ts\n",
56
121
  });
@@ -68,6 +133,7 @@ describe("getRepoContext", () => {
68
133
  test("returns empty guidelines when no CLAUDE.md or SKILL.md exist", async () => {
69
134
  const exec = mockExec({
70
135
  "git rev-parse --show-toplevel": "/repo\n",
136
+ "git rev-parse --abbrev-ref HEAD": "main\n",
71
137
  "git diff HEAD": "some diff\n",
72
138
  "git ls-files": "src/index.ts\npackage.json\n",
73
139
  });