santree 0.1.1 → 0.1.3
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/dist/commands/helpers/template.d.ts +14 -0
- package/dist/commands/helpers/template.js +129 -0
- package/dist/commands/pr/create.js +13 -14
- package/dist/commands/pr/fix.js +28 -18
- package/dist/commands/pr/review.js +22 -12
- package/dist/commands/worktree/work.js +17 -11
- package/dist/lib/ai.d.ts +26 -3
- package/dist/lib/ai.js +124 -8
- package/dist/lib/exec.d.ts +7 -0
- package/dist/lib/exec.js +15 -1
- package/dist/lib/github.d.ts +93 -0
- package/dist/lib/github.js +291 -1
- package/dist/lib/prompts.d.ts +25 -0
- package/dist/lib/prompts.js +12 -0
- package/package.json +1 -1
- package/prompts/diff.njk +18 -0
- package/prompts/fill-pr.njk +1 -10
- package/prompts/fix-pr.njk +6 -6
- package/prompts/pr.njk +68 -0
- package/prompts/review.njk +3 -4
- package/prompts/{implement.njk → work.njk} +8 -2
- package/prompts/plan.njk +0 -19
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
export declare const description = "Render a template to stdout";
|
|
3
|
+
export declare const args: z.ZodTuple<[z.ZodEnum<{
|
|
4
|
+
linear: "linear";
|
|
5
|
+
"git-changes": "git-changes";
|
|
6
|
+
pr: "pr";
|
|
7
|
+
"fix-pr": "fix-pr";
|
|
8
|
+
review: "review";
|
|
9
|
+
}>], null>;
|
|
10
|
+
type Props = {
|
|
11
|
+
args: z.infer<typeof args>;
|
|
12
|
+
};
|
|
13
|
+
export default function Template({ args }: Props): import("react/jsx-runtime").JSX.Element | null;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState, useRef } from "react";
|
|
3
|
+
import { Text, Box, useApp } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { argument } from "pastel";
|
|
6
|
+
import { z } from "zod/v4";
|
|
7
|
+
import { findRepoRoot, findMainRepoRoot, getCurrentBranch, extractTicketId, } from "../../lib/git.js";
|
|
8
|
+
import { renderTicket } from "../../lib/prompts.js";
|
|
9
|
+
import { getTicketContent } from "../../lib/linear.js";
|
|
10
|
+
import { resolveAIContext, renderAIPrompt, fetchAndRenderPR, fetchAndRenderDiff, } from "../../lib/ai.js";
|
|
11
|
+
export const description = "Render a template to stdout";
|
|
12
|
+
export const args = z.tuple([
|
|
13
|
+
z.enum(["linear", "git-changes", "pr", "fix-pr", "review"]).describe(argument({
|
|
14
|
+
name: "type",
|
|
15
|
+
description: "Template type (linear, git-changes, pr, fix-pr, or review)",
|
|
16
|
+
})),
|
|
17
|
+
]);
|
|
18
|
+
export default function Template({ args }) {
|
|
19
|
+
const [type] = args;
|
|
20
|
+
const { exit } = useApp();
|
|
21
|
+
const [status, setStatus] = useState("loading");
|
|
22
|
+
const [message, setMessage] = useState("");
|
|
23
|
+
const hasRun = useRef(false);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (hasRun.current)
|
|
26
|
+
return;
|
|
27
|
+
hasRun.current = true;
|
|
28
|
+
async function run() {
|
|
29
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
30
|
+
const repoRoot = findRepoRoot();
|
|
31
|
+
if (!repoRoot) {
|
|
32
|
+
setStatus("error");
|
|
33
|
+
setMessage("Not inside a git repository");
|
|
34
|
+
setTimeout(() => exit(), 100);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const branch = getCurrentBranch();
|
|
38
|
+
if (!branch) {
|
|
39
|
+
setStatus("error");
|
|
40
|
+
setMessage("Could not determine current branch");
|
|
41
|
+
setTimeout(() => exit(), 100);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (type === "git-changes") {
|
|
45
|
+
const output = await fetchAndRenderDiff(branch);
|
|
46
|
+
process.stdout.write(output);
|
|
47
|
+
setStatus("done");
|
|
48
|
+
setTimeout(() => exit(), 100);
|
|
49
|
+
}
|
|
50
|
+
else if (type === "pr") {
|
|
51
|
+
const output = await fetchAndRenderPR(branch);
|
|
52
|
+
if (!output) {
|
|
53
|
+
setStatus("error");
|
|
54
|
+
setMessage(`No pull request found for branch '${branch}'`);
|
|
55
|
+
setTimeout(() => exit(), 100);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
process.stdout.write(output);
|
|
59
|
+
setStatus("done");
|
|
60
|
+
setTimeout(() => exit(), 100);
|
|
61
|
+
}
|
|
62
|
+
else if (type === "fix-pr" || type === "review") {
|
|
63
|
+
const result = await resolveAIContext();
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
setStatus("error");
|
|
66
|
+
setMessage(result.error);
|
|
67
|
+
setTimeout(() => exit(), 100);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const ctx = result.context;
|
|
71
|
+
const diffContent = await fetchAndRenderDiff(branch);
|
|
72
|
+
if (type === "fix-pr") {
|
|
73
|
+
const prFeedback = await fetchAndRenderPR(branch);
|
|
74
|
+
if (!prFeedback) {
|
|
75
|
+
setStatus("error");
|
|
76
|
+
setMessage(`No pull request found for branch '${branch}'`);
|
|
77
|
+
setTimeout(() => exit(), 100);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const output = renderAIPrompt("fix-pr", ctx, {
|
|
81
|
+
pr_feedback: prFeedback,
|
|
82
|
+
diff_content: diffContent,
|
|
83
|
+
});
|
|
84
|
+
process.stdout.write(output);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const output = renderAIPrompt("review", ctx, {
|
|
88
|
+
diff_content: diffContent,
|
|
89
|
+
});
|
|
90
|
+
process.stdout.write(output);
|
|
91
|
+
}
|
|
92
|
+
setStatus("done");
|
|
93
|
+
setTimeout(() => exit(), 100);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const ticketId = extractTicketId(branch);
|
|
97
|
+
if (!ticketId) {
|
|
98
|
+
setStatus("error");
|
|
99
|
+
setMessage("Could not extract ticket ID from branch name. Expected format: user/TEAM-123-description");
|
|
100
|
+
setTimeout(() => exit(), 100);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const mainRoot = findMainRepoRoot() ?? repoRoot;
|
|
104
|
+
const ticket = await getTicketContent(ticketId, mainRoot);
|
|
105
|
+
if (!ticket) {
|
|
106
|
+
setStatus("error");
|
|
107
|
+
setMessage(`Could not fetch Linear ticket ${ticketId}. Run 'santree linear auth' to authenticate.`);
|
|
108
|
+
setTimeout(() => exit(), 100);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
process.stdout.write(renderTicket(ticket));
|
|
112
|
+
setStatus("done");
|
|
113
|
+
setTimeout(() => exit(), 100);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
run();
|
|
117
|
+
}, [type]);
|
|
118
|
+
if (status === "done")
|
|
119
|
+
return null;
|
|
120
|
+
const spinnerTexts = {
|
|
121
|
+
linear: "Fetching Linear ticket...",
|
|
122
|
+
"git-changes": "Gathering changes...",
|
|
123
|
+
pr: "Fetching PR feedback...",
|
|
124
|
+
"fix-pr": "Building fix-pr prompt...",
|
|
125
|
+
review: "Building review prompt...",
|
|
126
|
+
};
|
|
127
|
+
const spinnerText = spinnerTexts[type] ?? "Loading...";
|
|
128
|
+
return (_jsxs(Box, { children: [status === "loading" && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", spinnerText] })] })), status === "error" && (_jsx(Text, { color: "red", bold: true, children: message }))] }));
|
|
129
|
+
}
|
|
@@ -3,14 +3,15 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { Text, Box, useInput, useApp } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { exec
|
|
6
|
+
import { exec } from "child_process";
|
|
7
7
|
import { promisify } from "util";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { writeFileSync } from "fs";
|
|
10
10
|
import { tmpdir } from "os";
|
|
11
11
|
import { findMainRepoRoot, findRepoRoot, getCurrentBranch, getBaseBranch, hasUncommittedChanges, getCommitsAhead, remoteBranchExists, getUnpushedCommits, extractTicketId, isInWorktree, getFirstCommitMessage, getCommitLog, getDiffStat, getDiffContent, } from "../../lib/git.js";
|
|
12
12
|
import { ghCliAvailable, getPRInfoAsync, pushBranch, createPR, getPRTemplate, } from "../../lib/github.js";
|
|
13
|
-
import { renderPrompt } from "../../lib/prompts.js";
|
|
13
|
+
import { renderPrompt, renderDiff } from "../../lib/prompts.js";
|
|
14
|
+
import { runAgent } from "../../lib/ai.js";
|
|
14
15
|
const execAsync = promisify(exec);
|
|
15
16
|
export const description = "Create a GitHub pull request";
|
|
16
17
|
export const options = z.object({
|
|
@@ -59,29 +60,27 @@ export default function PR({ options }) {
|
|
|
59
60
|
setTimeout(() => exit(), 100);
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
|
-
const commitLog = getCommitLog(baseBranch) ?? "";
|
|
63
|
-
const diffStat = getDiffStat(baseBranch) ?? "";
|
|
64
|
-
const diff = getDiffContent(baseBranch) ?? "";
|
|
65
63
|
const ticketId = extractTicketId(branch);
|
|
64
|
+
const diffContent = renderDiff({
|
|
65
|
+
base_branch: baseBranch,
|
|
66
|
+
commit_log: getCommitLog(baseBranch),
|
|
67
|
+
diff_stat: getDiffStat(baseBranch),
|
|
68
|
+
diff: getDiffContent(baseBranch),
|
|
69
|
+
});
|
|
66
70
|
const prompt = renderPrompt("fill-pr", {
|
|
67
71
|
pr_template: prTemplate,
|
|
68
|
-
|
|
69
|
-
diff_stat: diffStat,
|
|
70
|
-
diff,
|
|
72
|
+
diff_content: diffContent,
|
|
71
73
|
ticket_id: ticketId ?? "",
|
|
72
74
|
branch_name: branch,
|
|
73
75
|
});
|
|
74
|
-
const result =
|
|
75
|
-
|
|
76
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
77
|
-
});
|
|
78
|
-
if (result.status !== 0) {
|
|
76
|
+
const result = runAgent(prompt);
|
|
77
|
+
if (!result.success) {
|
|
79
78
|
setStatus("error");
|
|
80
79
|
setMessage("Failed to generate PR body with Claude");
|
|
81
80
|
setTimeout(() => exit(), 100);
|
|
82
81
|
return;
|
|
83
82
|
}
|
|
84
|
-
const body = result.
|
|
83
|
+
const body = result.output;
|
|
85
84
|
bodyFile = join(tmpdir(), `santree-pr-${Date.now()}.md`);
|
|
86
85
|
writeFileSync(bodyFile, body);
|
|
87
86
|
}
|
package/dist/commands/pr/fix.js
CHANGED
|
@@ -2,8 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Text, Box } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
|
-
import { resolveAIContext, renderAIPrompt,
|
|
6
|
-
import { getPRInfo, getPRComments } from "../../lib/github.js";
|
|
5
|
+
import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, fetchAndRenderPR, fetchAndRenderDiff, } from "../../lib/ai.js";
|
|
7
6
|
export const description = "Fix PR review comments";
|
|
8
7
|
export default function Fix() {
|
|
9
8
|
const [status, setStatus] = useState("loading");
|
|
@@ -23,26 +22,37 @@ export default function Fix() {
|
|
|
23
22
|
const ctx = result.context;
|
|
24
23
|
setBranch(ctx.branch);
|
|
25
24
|
setTicketId(ctx.ticketId);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
const prFeedback = await fetchAndRenderPR(ctx.branch);
|
|
26
|
+
if (!prFeedback) {
|
|
27
|
+
setStatus("error");
|
|
28
|
+
setError(`No pull request found for branch '${ctx.branch}'`);
|
|
29
|
+
return;
|
|
31
30
|
}
|
|
31
|
+
const diffContent = await fetchAndRenderDiff(ctx.branch);
|
|
32
32
|
setStatus("launching");
|
|
33
|
-
const prompt = renderAIPrompt("fix-pr", ctx, {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
setStatus("error");
|
|
37
|
-
setError(`Failed to launch happy: ${err.message}`);
|
|
38
|
-
});
|
|
39
|
-
child.on("close", () => {
|
|
40
|
-
if (ctx.ticketId)
|
|
41
|
-
cleanupImages(ctx.ticketId);
|
|
42
|
-
process.exit(0);
|
|
33
|
+
const prompt = renderAIPrompt("fix-pr", ctx, {
|
|
34
|
+
pr_feedback: prFeedback,
|
|
35
|
+
diff_content: diffContent,
|
|
43
36
|
});
|
|
37
|
+
try {
|
|
38
|
+
const child = launchAgent(prompt);
|
|
39
|
+
child.on("error", (err) => {
|
|
40
|
+
setStatus("error");
|
|
41
|
+
setError(`Failed to launch agent: ${err.message}`);
|
|
42
|
+
});
|
|
43
|
+
child.on("close", () => {
|
|
44
|
+
if (ctx.ticketId)
|
|
45
|
+
cleanupImages(ctx.ticketId);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
setStatus("error");
|
|
51
|
+
setError(err instanceof Error ? err.message : "Failed to launch agent");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
44
54
|
}
|
|
45
55
|
init();
|
|
46
56
|
}, []);
|
|
47
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Fix PR" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "magenta", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", bold: true, children: " fix PR " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket and PR
|
|
57
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Fix PR" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "magenta", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", bold: true, children: " fix PR " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude (through Happy)..." }), _jsxs(Text, { dimColor: true, children: [" happy ", `"<fix-pr prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
|
|
48
58
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Text, Box } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
|
-
import { resolveAIContext, renderAIPrompt,
|
|
5
|
+
import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, fetchAndRenderDiff, } from "../../lib/ai.js";
|
|
6
6
|
export const description = "Review changes against ticket requirements";
|
|
7
7
|
export default function Review() {
|
|
8
8
|
const [status, setStatus] = useState("loading");
|
|
@@ -22,20 +22,30 @@ export default function Review() {
|
|
|
22
22
|
const ctx = result.context;
|
|
23
23
|
setBranch(ctx.branch);
|
|
24
24
|
setTicketId(ctx.ticketId);
|
|
25
|
+
const diffContent = await fetchAndRenderDiff(ctx.branch);
|
|
25
26
|
setStatus("launching");
|
|
26
|
-
const prompt = renderAIPrompt("review", ctx
|
|
27
|
-
|
|
28
|
-
child.on("error", (err) => {
|
|
29
|
-
setStatus("error");
|
|
30
|
-
setError(`Failed to launch happy: ${err.message}`);
|
|
31
|
-
});
|
|
32
|
-
child.on("close", () => {
|
|
33
|
-
if (ctx.ticketId)
|
|
34
|
-
cleanupImages(ctx.ticketId);
|
|
35
|
-
process.exit(0);
|
|
27
|
+
const prompt = renderAIPrompt("review", ctx, {
|
|
28
|
+
diff_content: diffContent,
|
|
36
29
|
});
|
|
30
|
+
try {
|
|
31
|
+
const child = launchAgent(prompt);
|
|
32
|
+
child.on("error", (err) => {
|
|
33
|
+
setStatus("error");
|
|
34
|
+
setError(`Failed to launch agent: ${err.message}`);
|
|
35
|
+
});
|
|
36
|
+
child.on("close", () => {
|
|
37
|
+
if (ctx.ticketId)
|
|
38
|
+
cleanupImages(ctx.ticketId);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
setStatus("error");
|
|
44
|
+
setError(err instanceof Error ? err.message : "Failed to launch agent");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
37
47
|
}
|
|
38
48
|
init();
|
|
39
49
|
}, []);
|
|
40
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Review" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "yellow", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "yellow", color: "white", bold: true, children: " review " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket
|
|
50
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Review" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "yellow", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "yellow", color: "white", bold: true, children: " review " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket, diff, and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude (through Happy)..." }), _jsxs(Text, { dimColor: true, children: [" happy ", `"<review prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
|
|
41
51
|
}
|
|
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { Text, Box } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { resolveAIContext, renderAIPrompt,
|
|
6
|
+
import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, } from "../../lib/ai.js";
|
|
7
7
|
export const description = "Launch Claude to work on current ticket";
|
|
8
8
|
export const options = z.object({
|
|
9
9
|
plan: z.boolean().optional().describe("Only create implementation plan"),
|
|
@@ -44,17 +44,23 @@ export default function Work({ options }) {
|
|
|
44
44
|
if (status !== "ready" || !aiContext)
|
|
45
45
|
return;
|
|
46
46
|
setStatus("launching");
|
|
47
|
-
const prompt = renderAIPrompt(
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const prompt = renderAIPrompt("work", aiContext, { mode });
|
|
48
|
+
try {
|
|
49
|
+
const child = launchAgent(prompt, { planMode: mode === "plan" });
|
|
50
|
+
child.on("error", (err) => {
|
|
51
|
+
setStatus("error");
|
|
52
|
+
setError(`Failed to launch agent: ${err.message}`);
|
|
53
|
+
});
|
|
54
|
+
child.on("close", () => {
|
|
55
|
+
if (aiContext.ticketId)
|
|
56
|
+
cleanupImages(aiContext.ticketId);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
50
61
|
setStatus("error");
|
|
51
|
-
setError(
|
|
52
|
-
}
|
|
53
|
-
child.on("close", () => {
|
|
54
|
-
if (aiContext.ticketId)
|
|
55
|
-
cleanupImages(aiContext.ticketId);
|
|
56
|
-
process.exit(0);
|
|
57
|
-
});
|
|
62
|
+
setError(err instanceof Error ? err.message : "Failed to launch agent");
|
|
63
|
+
}
|
|
58
64
|
}, [status, aiContext, mode]);
|
|
59
65
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Work" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : getModeColor(mode), paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: getModeColor(mode), color: "white", bold: true, children: ` ${getModeLabel(mode)} ` })] })] }), _jsxs(Box, { marginTop: 1, children: [status === "loading" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] })), status === "fetching" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Fetching ticket from Linear..." })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude (through Happy)..." }), _jsxs(Text, { dimColor: true, children: [" ", "happy", mode === "plan" ? " --permission-mode plan" : "", " ", `"<${getModeLabel(mode)} prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
|
|
60
66
|
}
|
package/dist/lib/ai.d.ts
CHANGED
|
@@ -27,12 +27,35 @@ export declare function buildPromptContext(ctx: AIContext, extra?: Record<string
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function renderAIPrompt(template: string, ctx: AIContext, extra?: Record<string, string | undefined>): string;
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* Returns
|
|
30
|
+
* Fetch and render PR feedback for a branch (async, non-blocking).
|
|
31
|
+
* Returns rendered markdown or null if no PR exists.
|
|
32
32
|
*/
|
|
33
|
-
export declare function
|
|
33
|
+
export declare function fetchAndRenderPR(branch: string): Promise<string | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Fetch and render diff for a branch against its base branch (async, non-blocking).
|
|
36
|
+
* Returns rendered markdown.
|
|
37
|
+
*/
|
|
38
|
+
export declare function fetchAndRenderDiff(branch: string): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Launch an interactive agent session with a prompt.
|
|
41
|
+
* Resolves the agent binary (happy > claude), passes prompt directly
|
|
42
|
+
* or via temp file if too large for OS arg limit.
|
|
43
|
+
* Throws if no agent binary is found.
|
|
44
|
+
*/
|
|
45
|
+
export declare function launchAgent(prompt: string, opts?: {
|
|
34
46
|
planMode?: boolean;
|
|
35
47
|
}): ChildProcess;
|
|
48
|
+
export interface RunAgentResult {
|
|
49
|
+
success: boolean;
|
|
50
|
+
output: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Run an agent in non-interactive print mode and capture output.
|
|
54
|
+
* Resolves the agent binary (happy > claude), passes prompt directly
|
|
55
|
+
* or via temp file if too large for OS arg limit.
|
|
56
|
+
* Throws if no agent binary is found.
|
|
57
|
+
*/
|
|
58
|
+
export declare function runAgent(prompt: string): RunAgentResult;
|
|
36
59
|
/**
|
|
37
60
|
* Cleanup images downloaded for a ticket.
|
|
38
61
|
*/
|
package/dist/lib/ai.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { execSync, spawn, spawnSync } from "child_process";
|
|
2
|
+
import { writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { getCurrentBranch, extractTicketId, findRepoRoot, findMainRepoRoot, getBaseBranch, } from "./git.js";
|
|
6
|
+
import { renderPrompt, renderTicket, renderDiff, renderPR } from "./prompts.js";
|
|
4
7
|
import { getTicketContent, cleanupImages } from "./linear.js";
|
|
8
|
+
import { getPRInfoAsync, getPRChecksAsync, getPRReviewsAsync, getPRReviewCommentsAsync, getPRConversationCommentsAsync, getFailedCheckDetailsAsync, } from "./github.js";
|
|
9
|
+
import { runAsync } from "./exec.js";
|
|
5
10
|
/**
|
|
6
11
|
* Resolves repo, branch, ticket ID, and fetches the Linear ticket.
|
|
7
12
|
* Returns an error string if any required context is missing.
|
|
@@ -45,17 +50,128 @@ export function buildPromptContext(ctx, extra) {
|
|
|
45
50
|
export function renderAIPrompt(template, ctx, extra) {
|
|
46
51
|
return renderPrompt(template, buildPromptContext(ctx, extra));
|
|
47
52
|
}
|
|
53
|
+
const BOT_AUTHORS = new Set([
|
|
54
|
+
"linear",
|
|
55
|
+
"github-actions",
|
|
56
|
+
"codecov",
|
|
57
|
+
"dependabot",
|
|
58
|
+
"renovate",
|
|
59
|
+
"netlify",
|
|
60
|
+
"vercel",
|
|
61
|
+
]);
|
|
48
62
|
/**
|
|
49
|
-
*
|
|
50
|
-
* Returns
|
|
63
|
+
* Fetch and render PR feedback for a branch (async, non-blocking).
|
|
64
|
+
* Returns rendered markdown or null if no PR exists.
|
|
51
65
|
*/
|
|
52
|
-
export function
|
|
66
|
+
export async function fetchAndRenderPR(branch) {
|
|
67
|
+
const prInfo = await getPRInfoAsync(branch);
|
|
68
|
+
if (!prInfo)
|
|
69
|
+
return null;
|
|
70
|
+
const [checks, reviews, reviewComments, allComments] = await Promise.all([
|
|
71
|
+
getPRChecksAsync(prInfo.number),
|
|
72
|
+
getPRReviewsAsync(prInfo.number),
|
|
73
|
+
getPRReviewCommentsAsync(prInfo.number),
|
|
74
|
+
getPRConversationCommentsAsync(prInfo.number),
|
|
75
|
+
]);
|
|
76
|
+
const failedChecks = await Promise.all((checks ?? []).filter((c) => c.bucket === "fail").map((c) => getFailedCheckDetailsAsync(c)));
|
|
77
|
+
const conversationComments = (allComments ?? []).filter((c) => !BOT_AUTHORS.has(c.author) && !c.author.endsWith("[bot]"));
|
|
78
|
+
return renderPR({
|
|
79
|
+
pr_number: prInfo.number,
|
|
80
|
+
pr_url: prInfo.url ?? "",
|
|
81
|
+
branch,
|
|
82
|
+
checks,
|
|
83
|
+
failed_checks: failedChecks,
|
|
84
|
+
reviews,
|
|
85
|
+
review_comments: reviewComments,
|
|
86
|
+
conversation_comments: conversationComments,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Fetch and render diff for a branch against its base branch (async, non-blocking).
|
|
91
|
+
* Returns rendered markdown.
|
|
92
|
+
*/
|
|
93
|
+
export async function fetchAndRenderDiff(branch) {
|
|
94
|
+
const baseBranch = getBaseBranch(branch);
|
|
95
|
+
const [commitLog, diffStat, diff] = await Promise.all([
|
|
96
|
+
runAsync(`git log ${baseBranch}..HEAD --format="- %s"`).then((v) => v || null),
|
|
97
|
+
runAsync(`git diff ${baseBranch}..HEAD --stat`).then((v) => v || null),
|
|
98
|
+
runAsync(`git diff ${baseBranch}..HEAD`, { maxBuffer: 10 * 1024 * 1024 }).then((v) => v || null),
|
|
99
|
+
]);
|
|
100
|
+
return renderDiff({
|
|
101
|
+
base_branch: baseBranch,
|
|
102
|
+
commit_log: commitLog,
|
|
103
|
+
diff_stat: diffStat,
|
|
104
|
+
diff,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolve which agent binary to use (happy or claude).
|
|
109
|
+
* Returns the binary name, or null if neither is installed.
|
|
110
|
+
*/
|
|
111
|
+
function resolveAgentBinary() {
|
|
112
|
+
for (const bin of ["happy", "claude"]) {
|
|
113
|
+
try {
|
|
114
|
+
execSync(`which ${bin}`, { stdio: "ignore" });
|
|
115
|
+
return bin;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
// Conservative limit: 200KB leaves room for env vars within macOS 256KB ARG_MAX
|
|
124
|
+
const ARG_MAX_SAFE = 200 * 1024;
|
|
125
|
+
/**
|
|
126
|
+
* Build the prompt argument for the agent.
|
|
127
|
+
* If the prompt fits in ARG_MAX, returns it directly.
|
|
128
|
+
* Otherwise, writes to a temp file and returns a short instruction to read it.
|
|
129
|
+
*/
|
|
130
|
+
function promptArg(prompt) {
|
|
131
|
+
if (Buffer.byteLength(prompt) <= ARG_MAX_SAFE) {
|
|
132
|
+
return prompt;
|
|
133
|
+
}
|
|
134
|
+
const filePath = join(tmpdir(), `santree-prompt-${Date.now()}.md`);
|
|
135
|
+
writeFileSync(filePath, prompt);
|
|
136
|
+
return `Read ${filePath} and follow the instructions inside.`;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Launch an interactive agent session with a prompt.
|
|
140
|
+
* Resolves the agent binary (happy > claude), passes prompt directly
|
|
141
|
+
* or via temp file if too large for OS arg limit.
|
|
142
|
+
* Throws if no agent binary is found.
|
|
143
|
+
*/
|
|
144
|
+
export function launchAgent(prompt, opts) {
|
|
145
|
+
const bin = resolveAgentBinary();
|
|
146
|
+
if (!bin) {
|
|
147
|
+
throw new Error("No agent found. Install happy (npm i -g happy-coder) or claude (npm i -g @anthropic-ai/claude-code).");
|
|
148
|
+
}
|
|
53
149
|
const args = [];
|
|
54
150
|
if (opts?.planMode) {
|
|
55
151
|
args.push("--permission-mode", "plan");
|
|
56
152
|
}
|
|
57
|
-
args.push(prompt);
|
|
58
|
-
return spawn(
|
|
153
|
+
args.push("--", promptArg(prompt));
|
|
154
|
+
return spawn(bin, args, { stdio: "inherit" });
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Run an agent in non-interactive print mode and capture output.
|
|
158
|
+
* Resolves the agent binary (happy > claude), passes prompt directly
|
|
159
|
+
* or via temp file if too large for OS arg limit.
|
|
160
|
+
* Throws if no agent binary is found.
|
|
161
|
+
*/
|
|
162
|
+
export function runAgent(prompt) {
|
|
163
|
+
const bin = resolveAgentBinary();
|
|
164
|
+
if (!bin) {
|
|
165
|
+
throw new Error("No agent found. Install happy (npm i -g happy-coder) or claude (npm i -g @anthropic-ai/claude-code).");
|
|
166
|
+
}
|
|
167
|
+
const result = spawnSync(bin, ["-p", "--output-format", "text", "--", promptArg(prompt)], {
|
|
168
|
+
encoding: "utf-8",
|
|
169
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
success: result.status === 0,
|
|
173
|
+
output: result.stdout?.trim() ?? "",
|
|
174
|
+
};
|
|
59
175
|
}
|
|
60
176
|
/**
|
|
61
177
|
* Cleanup images downloaded for a ticket.
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ export declare function run(command: string, options?: {
|
|
|
5
5
|
cwd?: string;
|
|
6
6
|
maxBuffer?: number;
|
|
7
7
|
}): string | null;
|
|
8
|
+
/**
|
|
9
|
+
* Run a shell command asynchronously and return trimmed stdout, or null on failure.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runAsync(command: string, options?: {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
maxBuffer?: number;
|
|
14
|
+
}): Promise<string | null>;
|
|
8
15
|
/**
|
|
9
16
|
* Spawn a command asynchronously and capture its output.
|
|
10
17
|
* Returns the exit code and combined stdout/stderr.
|
package/dist/lib/exec.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { execSync, spawn } from "child_process";
|
|
1
|
+
import { execSync, exec, spawn } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
const execPromise = promisify(exec);
|
|
2
4
|
/**
|
|
3
5
|
* Run a shell command and return trimmed stdout, or null on failure.
|
|
4
6
|
*/
|
|
@@ -10,6 +12,18 @@ export function run(command, options) {
|
|
|
10
12
|
return null;
|
|
11
13
|
}
|
|
12
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Run a shell command asynchronously and return trimmed stdout, or null on failure.
|
|
17
|
+
*/
|
|
18
|
+
export async function runAsync(command, options) {
|
|
19
|
+
try {
|
|
20
|
+
const { stdout } = await execPromise(command, { encoding: "utf-8", ...options });
|
|
21
|
+
return stdout.trim();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
13
27
|
/**
|
|
14
28
|
* Spawn a command asynchronously and capture its output.
|
|
15
29
|
* Returns the exit code and combined stdout/stderr.
|
package/dist/lib/github.d.ts
CHANGED
|
@@ -47,3 +47,96 @@ export declare function getPRTemplate(): string | null;
|
|
|
47
47
|
* Returns empty string if the PR has no comments or on failure.
|
|
48
48
|
*/
|
|
49
49
|
export declare function getPRComments(prNumber: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Async version of getPRChecks.
|
|
52
|
+
*/
|
|
53
|
+
export declare function getPRChecksAsync(prNumber: string): Promise<PRCheck[] | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Async version of getPRReviews.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getPRReviewsAsync(prNumber: string): Promise<PRReview[] | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Async version of getPRReviewComments.
|
|
60
|
+
*/
|
|
61
|
+
export declare function getPRReviewCommentsAsync(prNumber: string): Promise<PRReviewComment[] | null>;
|
|
62
|
+
/**
|
|
63
|
+
* Async version of getPRConversationComments.
|
|
64
|
+
*/
|
|
65
|
+
export declare function getPRConversationCommentsAsync(prNumber: string): Promise<PRConversationComment[] | null>;
|
|
66
|
+
/**
|
|
67
|
+
* Async version of getFailedCheckDetails.
|
|
68
|
+
*/
|
|
69
|
+
export declare function getFailedCheckDetailsAsync(check: PRCheck): Promise<FailedCheckDetail>;
|
|
70
|
+
export interface PRConversationComment {
|
|
71
|
+
author: string;
|
|
72
|
+
body: string;
|
|
73
|
+
createdAt: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Fetch structured conversation comments on a pull request.
|
|
77
|
+
* Runs: `gh pr view <prNumber> --json comments`
|
|
78
|
+
* Returns null if gh CLI fails.
|
|
79
|
+
*/
|
|
80
|
+
export declare function getPRConversationComments(prNumber: string): PRConversationComment[] | null;
|
|
81
|
+
export interface PRCheck {
|
|
82
|
+
name: string;
|
|
83
|
+
state: string;
|
|
84
|
+
bucket: string;
|
|
85
|
+
link: string;
|
|
86
|
+
description: string;
|
|
87
|
+
workflow: string;
|
|
88
|
+
}
|
|
89
|
+
export interface FailedCheckDetail {
|
|
90
|
+
name: string;
|
|
91
|
+
workflow: string;
|
|
92
|
+
description: string;
|
|
93
|
+
link: string;
|
|
94
|
+
failed_step: string | null;
|
|
95
|
+
log: string | null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Fetch details for a failed CI check: which step failed and the failed step's log.
|
|
99
|
+
* Extracts job ID from the check link, fetches job details for the step name,
|
|
100
|
+
* then fetches the job log via the GitHub API and extracts the failed step's output.
|
|
101
|
+
* Returns enriched detail; gracefully degrades if API calls fail.
|
|
102
|
+
*/
|
|
103
|
+
export declare function getFailedCheckDetails(check: PRCheck): FailedCheckDetail;
|
|
104
|
+
export interface PRReview {
|
|
105
|
+
author: {
|
|
106
|
+
login: string;
|
|
107
|
+
};
|
|
108
|
+
state: string;
|
|
109
|
+
body: string;
|
|
110
|
+
submittedAt: string;
|
|
111
|
+
}
|
|
112
|
+
export interface PRReviewComment {
|
|
113
|
+
user: {
|
|
114
|
+
login: string;
|
|
115
|
+
};
|
|
116
|
+
body: string;
|
|
117
|
+
path: string;
|
|
118
|
+
line: number | null;
|
|
119
|
+
original_line: number | null;
|
|
120
|
+
diff_hunk: string;
|
|
121
|
+
created_at: string;
|
|
122
|
+
in_reply_to_id?: number;
|
|
123
|
+
id: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Fetch CI check results for a pull request.
|
|
127
|
+
* Runs: `gh pr checks <prNumber> --json name,state,bucket,link,description,workflow`
|
|
128
|
+
* Returns null if gh CLI fails.
|
|
129
|
+
*/
|
|
130
|
+
export declare function getPRChecks(prNumber: string): PRCheck[] | null;
|
|
131
|
+
/**
|
|
132
|
+
* Fetch reviews for a pull request.
|
|
133
|
+
* Runs: `gh pr view <prNumber> --json reviews`
|
|
134
|
+
* Returns null if gh CLI fails.
|
|
135
|
+
*/
|
|
136
|
+
export declare function getPRReviews(prNumber: string): PRReview[] | null;
|
|
137
|
+
/**
|
|
138
|
+
* Fetch inline review comments for a pull request via the GitHub API.
|
|
139
|
+
* Runs: `gh api repos/{owner}/{repo}/pulls/<prNumber>/comments --paginate`
|
|
140
|
+
* Returns null if gh CLI fails.
|
|
141
|
+
*/
|
|
142
|
+
export declare function getPRReviewComments(prNumber: string): PRReviewComment[] | null;
|
package/dist/lib/github.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execSync, exec } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
|
-
import { run } from "./exec.js";
|
|
3
|
+
import { run, runAsync } from "./exec.js";
|
|
4
4
|
const execAsync = promisify(exec);
|
|
5
5
|
/**
|
|
6
6
|
* Get PR info (number, state, url) for a branch using the GitHub CLI.
|
|
@@ -109,3 +109,293 @@ export function getPRTemplate() {
|
|
|
109
109
|
export function getPRComments(prNumber) {
|
|
110
110
|
return (run(`gh pr view ${prNumber} --json comments --jq '.comments[] | "- \\(.author.login): \\(.body)"'`) ?? "");
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Async version of getPRChecks.
|
|
114
|
+
*/
|
|
115
|
+
export async function getPRChecksAsync(prNumber) {
|
|
116
|
+
const output = await runAsync(`gh pr checks ${prNumber} --json name,state,bucket,link,description,workflow`);
|
|
117
|
+
if (!output)
|
|
118
|
+
return null;
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(output);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Async version of getPRReviews.
|
|
128
|
+
*/
|
|
129
|
+
export async function getPRReviewsAsync(prNumber) {
|
|
130
|
+
const output = await runAsync(`gh pr view ${prNumber} --json reviews`);
|
|
131
|
+
if (!output)
|
|
132
|
+
return null;
|
|
133
|
+
try {
|
|
134
|
+
const data = JSON.parse(output);
|
|
135
|
+
return data.reviews ?? null;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Async version of getPRReviewComments.
|
|
143
|
+
*/
|
|
144
|
+
export async function getPRReviewCommentsAsync(prNumber) {
|
|
145
|
+
const output = await runAsync(`gh api repos/{owner}/{repo}/pulls/${prNumber}/comments --paginate`);
|
|
146
|
+
if (!output)
|
|
147
|
+
return null;
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(output);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Async version of getPRConversationComments.
|
|
157
|
+
*/
|
|
158
|
+
export async function getPRConversationCommentsAsync(prNumber) {
|
|
159
|
+
const output = await runAsync(`gh pr view ${prNumber} --json comments`);
|
|
160
|
+
if (!output)
|
|
161
|
+
return null;
|
|
162
|
+
try {
|
|
163
|
+
const data = JSON.parse(output);
|
|
164
|
+
return (data.comments ?? []).map((c) => ({
|
|
165
|
+
author: c.author?.login ?? "unknown",
|
|
166
|
+
body: c.body ?? "",
|
|
167
|
+
createdAt: c.createdAt ?? "",
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Async version of getFailedCheckDetails.
|
|
176
|
+
*/
|
|
177
|
+
export async function getFailedCheckDetailsAsync(check) {
|
|
178
|
+
const detail = {
|
|
179
|
+
name: check.name,
|
|
180
|
+
workflow: check.workflow,
|
|
181
|
+
description: check.description,
|
|
182
|
+
link: check.link,
|
|
183
|
+
failed_step: null,
|
|
184
|
+
log: null,
|
|
185
|
+
};
|
|
186
|
+
const urlMatch = check.link?.match(/job\/(\d+)/);
|
|
187
|
+
if (!urlMatch)
|
|
188
|
+
return detail;
|
|
189
|
+
const jobId = urlMatch[1];
|
|
190
|
+
let stepStartMs = 0;
|
|
191
|
+
let stepEndMs = 0;
|
|
192
|
+
const jobOutput = await runAsync(`gh api repos/{owner}/{repo}/actions/jobs/${jobId}`);
|
|
193
|
+
if (jobOutput) {
|
|
194
|
+
try {
|
|
195
|
+
const job = JSON.parse(jobOutput);
|
|
196
|
+
const failedStep = job.steps?.find((s) => s.conclusion === "failure");
|
|
197
|
+
if (failedStep) {
|
|
198
|
+
detail.failed_step = failedStep.name;
|
|
199
|
+
stepStartMs = new Date(failedStep.started_at).getTime();
|
|
200
|
+
stepEndMs = new Date(failedStep.completed_at).getTime() + 999;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch { }
|
|
204
|
+
}
|
|
205
|
+
if (!stepStartMs)
|
|
206
|
+
return detail;
|
|
207
|
+
const logOutput = await runAsync(`gh api repos/{owner}/{repo}/actions/jobs/${jobId}/logs 2>/dev/null`);
|
|
208
|
+
if (logOutput) {
|
|
209
|
+
const lines = logOutput.split("\n");
|
|
210
|
+
const stepLines = lines.filter((line) => {
|
|
211
|
+
const m = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z)/);
|
|
212
|
+
if (!m)
|
|
213
|
+
return false;
|
|
214
|
+
const ms = new Date(m[1]).getTime();
|
|
215
|
+
return ms >= stepStartMs && ms <= stepEndMs;
|
|
216
|
+
});
|
|
217
|
+
const errorIdx = stepLines.findIndex((l) => l.includes("##[error]"));
|
|
218
|
+
const bounded = errorIdx !== -1 ? stepLines.slice(0, errorIdx) : stepLines;
|
|
219
|
+
const segments = [];
|
|
220
|
+
let current = [];
|
|
221
|
+
let inGroup = false;
|
|
222
|
+
for (const raw of bounded) {
|
|
223
|
+
const line = raw.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*/, "");
|
|
224
|
+
if (line.startsWith("##[group]")) {
|
|
225
|
+
if (current.length) {
|
|
226
|
+
segments.push(current);
|
|
227
|
+
current = [];
|
|
228
|
+
}
|
|
229
|
+
inGroup = true;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (line.startsWith("##[endgroup]")) {
|
|
233
|
+
inGroup = false;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (line.startsWith("##["))
|
|
237
|
+
continue;
|
|
238
|
+
if (!inGroup)
|
|
239
|
+
current.push(line);
|
|
240
|
+
}
|
|
241
|
+
if (current.length)
|
|
242
|
+
segments.push(current);
|
|
243
|
+
if (segments.length)
|
|
244
|
+
detail.log = segments[segments.length - 1].join("\n");
|
|
245
|
+
}
|
|
246
|
+
return detail;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Fetch structured conversation comments on a pull request.
|
|
250
|
+
* Runs: `gh pr view <prNumber> --json comments`
|
|
251
|
+
* Returns null if gh CLI fails.
|
|
252
|
+
*/
|
|
253
|
+
export function getPRConversationComments(prNumber) {
|
|
254
|
+
const output = run(`gh pr view ${prNumber} --json comments`);
|
|
255
|
+
if (!output)
|
|
256
|
+
return null;
|
|
257
|
+
try {
|
|
258
|
+
const data = JSON.parse(output);
|
|
259
|
+
return (data.comments ?? []).map((c) => ({
|
|
260
|
+
author: c.author?.login ?? "unknown",
|
|
261
|
+
body: c.body ?? "",
|
|
262
|
+
createdAt: c.createdAt ?? "",
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Fetch details for a failed CI check: which step failed and the failed step's log.
|
|
271
|
+
* Extracts job ID from the check link, fetches job details for the step name,
|
|
272
|
+
* then fetches the job log via the GitHub API and extracts the failed step's output.
|
|
273
|
+
* Returns enriched detail; gracefully degrades if API calls fail.
|
|
274
|
+
*/
|
|
275
|
+
export function getFailedCheckDetails(check) {
|
|
276
|
+
const detail = {
|
|
277
|
+
name: check.name,
|
|
278
|
+
workflow: check.workflow,
|
|
279
|
+
description: check.description,
|
|
280
|
+
link: check.link,
|
|
281
|
+
failed_step: null,
|
|
282
|
+
log: null,
|
|
283
|
+
};
|
|
284
|
+
const urlMatch = check.link?.match(/job\/(\d+)/);
|
|
285
|
+
if (!urlMatch)
|
|
286
|
+
return detail;
|
|
287
|
+
const jobId = urlMatch[1];
|
|
288
|
+
let stepStartMs = 0;
|
|
289
|
+
let stepEndMs = 0;
|
|
290
|
+
const jobOutput = run(`gh api repos/{owner}/{repo}/actions/jobs/${jobId}`);
|
|
291
|
+
if (jobOutput) {
|
|
292
|
+
try {
|
|
293
|
+
const job = JSON.parse(jobOutput);
|
|
294
|
+
const failedStep = job.steps?.find((s) => s.conclusion === "failure");
|
|
295
|
+
if (failedStep) {
|
|
296
|
+
detail.failed_step = failedStep.name;
|
|
297
|
+
stepStartMs = new Date(failedStep.started_at).getTime();
|
|
298
|
+
// Add 1s buffer — step API uses second precision but log has sub-second timestamps
|
|
299
|
+
stepEndMs = new Date(failedStep.completed_at).getTime() + 999;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch { }
|
|
303
|
+
}
|
|
304
|
+
if (!stepStartMs)
|
|
305
|
+
return detail;
|
|
306
|
+
// Fetch job log via API (works even while run is still in progress)
|
|
307
|
+
const logOutput = run(`gh api repos/{owner}/{repo}/actions/jobs/${jobId}/logs 2>/dev/null`);
|
|
308
|
+
if (logOutput) {
|
|
309
|
+
const lines = logOutput.split("\n");
|
|
310
|
+
// Filter to lines within the failed step's time range
|
|
311
|
+
const stepLines = lines.filter((line) => {
|
|
312
|
+
const m = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z)/);
|
|
313
|
+
if (!m)
|
|
314
|
+
return false;
|
|
315
|
+
const ms = new Date(m[1]).getTime();
|
|
316
|
+
return ms >= stepStartMs && ms <= stepEndMs;
|
|
317
|
+
});
|
|
318
|
+
// Truncate at ##[error] — everything after is post-run cleanup noise
|
|
319
|
+
const errorIdx = stepLines.findIndex((l) => l.includes("##[error]"));
|
|
320
|
+
const bounded = errorIdx !== -1 ? stepLines.slice(0, errorIdx) : stepLines;
|
|
321
|
+
// Split non-group output into segments separated by ##[group]..##[endgroup] blocks.
|
|
322
|
+
// The last segment is the actual command output, earlier segments are
|
|
323
|
+
// setup noise (checkout, env vars, etc.).
|
|
324
|
+
const segments = [];
|
|
325
|
+
let current = [];
|
|
326
|
+
let inGroup = false;
|
|
327
|
+
for (const raw of bounded) {
|
|
328
|
+
const line = raw.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*/, "");
|
|
329
|
+
if (line.startsWith("##[group]")) {
|
|
330
|
+
if (current.length) {
|
|
331
|
+
segments.push(current);
|
|
332
|
+
current = [];
|
|
333
|
+
}
|
|
334
|
+
inGroup = true;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (line.startsWith("##[endgroup]")) {
|
|
338
|
+
inGroup = false;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (line.startsWith("##["))
|
|
342
|
+
continue;
|
|
343
|
+
if (!inGroup)
|
|
344
|
+
current.push(line);
|
|
345
|
+
}
|
|
346
|
+
if (current.length)
|
|
347
|
+
segments.push(current);
|
|
348
|
+
if (segments.length)
|
|
349
|
+
detail.log = segments[segments.length - 1].join("\n");
|
|
350
|
+
}
|
|
351
|
+
return detail;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Fetch CI check results for a pull request.
|
|
355
|
+
* Runs: `gh pr checks <prNumber> --json name,state,bucket,link,description,workflow`
|
|
356
|
+
* Returns null if gh CLI fails.
|
|
357
|
+
*/
|
|
358
|
+
export function getPRChecks(prNumber) {
|
|
359
|
+
const output = run(`gh pr checks ${prNumber} --json name,state,bucket,link,description,workflow`);
|
|
360
|
+
if (!output)
|
|
361
|
+
return null;
|
|
362
|
+
try {
|
|
363
|
+
return JSON.parse(output);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Fetch reviews for a pull request.
|
|
371
|
+
* Runs: `gh pr view <prNumber> --json reviews`
|
|
372
|
+
* Returns null if gh CLI fails.
|
|
373
|
+
*/
|
|
374
|
+
export function getPRReviews(prNumber) {
|
|
375
|
+
const output = run(`gh pr view ${prNumber} --json reviews`);
|
|
376
|
+
if (!output)
|
|
377
|
+
return null;
|
|
378
|
+
try {
|
|
379
|
+
const data = JSON.parse(output);
|
|
380
|
+
return data.reviews ?? null;
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Fetch inline review comments for a pull request via the GitHub API.
|
|
388
|
+
* Runs: `gh api repos/{owner}/{repo}/pulls/<prNumber>/comments --paginate`
|
|
389
|
+
* Returns null if gh CLI fails.
|
|
390
|
+
*/
|
|
391
|
+
export function getPRReviewComments(prNumber) {
|
|
392
|
+
const output = run(`gh api repos/{owner}/{repo}/pulls/${prNumber}/comments --paginate`);
|
|
393
|
+
if (!output)
|
|
394
|
+
return null;
|
|
395
|
+
try {
|
|
396
|
+
return JSON.parse(output);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
}
|
package/dist/lib/prompts.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LinearIssue } from "./linear.js";
|
|
2
|
+
import type { PRCheck, PRReview, PRReviewComment, FailedCheckDetail, PRConversationComment } from "./github.js";
|
|
2
3
|
/**
|
|
3
4
|
* Render a nunjucks template from the prompts/ directory.
|
|
4
5
|
* @param template - Template name without extension (e.g. "fill-pr")
|
|
@@ -9,3 +10,27 @@ export declare function renderPrompt(template: string, context: Record<string, s
|
|
|
9
10
|
* Render a LinearIssue into formatted markdown using the ticket template.
|
|
10
11
|
*/
|
|
11
12
|
export declare function renderTicket(issue: LinearIssue): string;
|
|
13
|
+
export interface DiffData {
|
|
14
|
+
base_branch: string;
|
|
15
|
+
commit_log: string | null;
|
|
16
|
+
diff_stat: string | null;
|
|
17
|
+
diff: string | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Render diff data into formatted markdown using the diff template.
|
|
21
|
+
*/
|
|
22
|
+
export declare function renderDiff(data: DiffData): string;
|
|
23
|
+
export interface PRData {
|
|
24
|
+
pr_number: string;
|
|
25
|
+
pr_url: string;
|
|
26
|
+
branch: string;
|
|
27
|
+
checks: PRCheck[] | null;
|
|
28
|
+
failed_checks: FailedCheckDetail[];
|
|
29
|
+
reviews: PRReview[] | null;
|
|
30
|
+
review_comments: PRReviewComment[] | null;
|
|
31
|
+
conversation_comments: PRConversationComment[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Render PR feedback data into formatted markdown using the pr template.
|
|
35
|
+
*/
|
|
36
|
+
export declare function renderPR(data: PRData): string;
|
package/dist/lib/prompts.js
CHANGED
|
@@ -38,3 +38,15 @@ export function renderPrompt(template, context) {
|
|
|
38
38
|
export function renderTicket(issue) {
|
|
39
39
|
return promptsEnv.render("ticket.njk", issue);
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Render diff data into formatted markdown using the diff template.
|
|
43
|
+
*/
|
|
44
|
+
export function renderDiff(data) {
|
|
45
|
+
return promptsEnv.render("diff.njk", data);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Render PR feedback data into formatted markdown using the pr template.
|
|
49
|
+
*/
|
|
50
|
+
export function renderPR(data) {
|
|
51
|
+
return promptsEnv.render("pr.njk", data);
|
|
52
|
+
}
|
package/package.json
CHANGED
package/prompts/diff.njk
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Changes ({{ base_branch }}..HEAD)
|
|
2
|
+
{%- if commit_log %}
|
|
3
|
+
|
|
4
|
+
### Commits
|
|
5
|
+
{{ commit_log }}
|
|
6
|
+
{%- endif %}
|
|
7
|
+
{%- if diff_stat %}
|
|
8
|
+
|
|
9
|
+
### Files Changed
|
|
10
|
+
{{ diff_stat }}
|
|
11
|
+
{%- endif %}
|
|
12
|
+
{%- if diff %}
|
|
13
|
+
|
|
14
|
+
### Diff
|
|
15
|
+
```
|
|
16
|
+
{{ diff }}
|
|
17
|
+
```
|
|
18
|
+
{%- endif %}
|
package/prompts/fill-pr.njk
CHANGED
package/prompts/fix-pr.njk
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
{% else %}
|
|
4
4
|
Note: Could not fetch Linear ticket {{ ticket_id }}. Proceed based on branch name context.
|
|
5
5
|
{% endif %}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{% if pr_comments %}
|
|
9
|
-
## PR Comments to Address
|
|
10
|
-
|
|
11
|
-
{{ pr_comments }}
|
|
6
|
+
{% if pr_feedback %}
|
|
7
|
+
{{ pr_feedback }}
|
|
12
8
|
{% endif %}
|
|
9
|
+
{% if diff_content %}
|
|
10
|
+
{{ diff_content }}
|
|
11
|
+
{% endif %}
|
|
12
|
+
|
|
13
13
|
## Task
|
|
14
14
|
|
|
15
15
|
1. Review each PR comment and understand what changes are requested
|
package/prompts/pr.njk
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
## Pull Request #{{ pr_number }}
|
|
2
|
+
**Branch:** {{ branch }}
|
|
3
|
+
[View on GitHub]({{ pr_url }})
|
|
4
|
+
|
|
5
|
+
{%- if checks %}
|
|
6
|
+
|
|
7
|
+
### CI Checks
|
|
8
|
+
{% for check in checks %}
|
|
9
|
+
- {{ "PASS" if check.bucket == "pass" else ("FAIL" if check.bucket == "fail" else "PENDING") }} — {{ check.name }}{% if check.description %}: {{ check.description }}{% endif %}
|
|
10
|
+
{%- endfor %}
|
|
11
|
+
{%- endif %}
|
|
12
|
+
{%- if failed_checks | length %}
|
|
13
|
+
|
|
14
|
+
### Failed Checks
|
|
15
|
+
{% for check in failed_checks %}
|
|
16
|
+
**{{ check.name }}**{% if check.workflow %} ({{ check.workflow }}){% endif %}
|
|
17
|
+
{% if check.failed_step %}- Failed step: `{{ check.failed_step }}`{% endif %}
|
|
18
|
+
{% if check.link %}- [View logs]({{ check.link }}){% endif %}
|
|
19
|
+
{% if check.log %}
|
|
20
|
+
```
|
|
21
|
+
{{ check.log }}
|
|
22
|
+
```
|
|
23
|
+
{% endif %}
|
|
24
|
+
{% endfor %}
|
|
25
|
+
{%- endif %}
|
|
26
|
+
{%- if reviews | length %}
|
|
27
|
+
|
|
28
|
+
### Reviews
|
|
29
|
+
{% for review in reviews %}
|
|
30
|
+
**{{ review.author.login }}** — {{ review.state }}{% if review.submittedAt %} — {{ review.submittedAt | date }}{% endif %}
|
|
31
|
+
{% if review.body %}{{ review.body }}{% endif %}
|
|
32
|
+
{% if not loop.last %}
|
|
33
|
+
---
|
|
34
|
+
{% endif %}
|
|
35
|
+
{%- endfor %}
|
|
36
|
+
{%- endif %}
|
|
37
|
+
{%- if review_comments | length %}
|
|
38
|
+
|
|
39
|
+
### Inline Review Comments
|
|
40
|
+
{% for comment in review_comments %}
|
|
41
|
+
{% if not comment.in_reply_to_id %}
|
|
42
|
+
**{{ comment.user.login }}** on `{{ comment.path }}{% if comment.line %}:{{ comment.line }}{% endif %}` — {{ comment.created_at | date }}
|
|
43
|
+
```
|
|
44
|
+
{{ comment.diff_hunk }}
|
|
45
|
+
```
|
|
46
|
+
{{ comment.body }}
|
|
47
|
+
{% for reply in review_comments %}
|
|
48
|
+
{% if reply.in_reply_to_id == comment.id %}
|
|
49
|
+
> **{{ reply.user.login }}** — {{ reply.created_at | date }}
|
|
50
|
+
> {{ reply.body | indent(2) }}
|
|
51
|
+
|
|
52
|
+
{% endif %}
|
|
53
|
+
{%- endfor %}
|
|
54
|
+
---
|
|
55
|
+
{% endif %}
|
|
56
|
+
{%- endfor %}
|
|
57
|
+
{%- endif %}
|
|
58
|
+
{%- if conversation_comments | length %}
|
|
59
|
+
|
|
60
|
+
### Conversation Comments
|
|
61
|
+
{% for comment in conversation_comments %}
|
|
62
|
+
**{{ comment.author }}** — {{ comment.createdAt | date }}
|
|
63
|
+
{{ comment.body }}
|
|
64
|
+
{% if not loop.last %}
|
|
65
|
+
---
|
|
66
|
+
{% endif %}
|
|
67
|
+
{%- endfor %}
|
|
68
|
+
{%- endif %}
|
package/prompts/review.njk
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
{% else %}
|
|
4
4
|
Note: Could not fetch Linear ticket {{ ticket_id }}. Proceed based on branch name context.
|
|
5
5
|
{% endif %}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Review the current changes by running `git diff` against the base branch.
|
|
6
|
+
{% if diff_content %}
|
|
7
|
+
{{ diff_content }}
|
|
8
|
+
{% endif %}
|
|
10
9
|
|
|
11
10
|
Analyze:
|
|
12
11
|
- Do the changes fully address the ticket requirements?
|
|
@@ -4,12 +4,18 @@
|
|
|
4
4
|
Note: Could not fetch Linear ticket {{ ticket_id }}. Proceed based on branch name context.
|
|
5
5
|
{% endif %}
|
|
6
6
|
|
|
7
|
-
If a PR URL is linked in the ticket, use `gh` CLI to fetch PR details, comments, and review feedback.
|
|
8
|
-
|
|
9
7
|
Review the codebase to understand the relevant areas and existing patterns.
|
|
8
|
+
{% if mode == "plan" %}
|
|
9
|
+
Create a detailed implementation plan with:
|
|
10
|
+
- Step-by-step approach
|
|
11
|
+
- Files to modify
|
|
12
|
+
- Potential risks or edge cases
|
|
10
13
|
|
|
14
|
+
Do NOT implement yet - just plan. Wait for approval before making changes.
|
|
15
|
+
{% else %}
|
|
11
16
|
Create an implementation plan, then implement the changes.
|
|
12
17
|
|
|
13
18
|
After implementation:
|
|
14
19
|
- Run tests if applicable
|
|
15
20
|
- Ensure code follows existing patterns
|
|
21
|
+
{% endif %}
|
package/prompts/plan.njk
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{% if ticket_content %}
|
|
2
|
-
{{ ticket_content }}
|
|
3
|
-
{% else %}
|
|
4
|
-
Note: Could not fetch Linear ticket {{ ticket_id }}. Proceed based on branch name context.
|
|
5
|
-
{% endif %}
|
|
6
|
-
|
|
7
|
-
If a PR URL is linked in the ticket, use `gh` CLI to fetch PR details, comments, and review feedback for additional context.
|
|
8
|
-
|
|
9
|
-
Review the codebase to understand:
|
|
10
|
-
- Relevant files and modules
|
|
11
|
-
- Existing patterns and conventions
|
|
12
|
-
- Dependencies and potential impact areas
|
|
13
|
-
|
|
14
|
-
Create a detailed implementation plan with:
|
|
15
|
-
- Step-by-step approach
|
|
16
|
-
- Files to modify
|
|
17
|
-
- Potential risks or edge cases
|
|
18
|
-
|
|
19
|
-
Do NOT implement yet - just plan. Wait for approval before making changes.
|