whitesmith 0.0.0 → 0.0.2
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/README.md +228 -0
- package/dist/auto-work.d.ts +11 -0
- package/dist/auto-work.d.ts.map +1 -0
- package/dist/auto-work.js +22 -0
- package/dist/auto-work.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +108 -1
- package/dist/cli.js.map +1 -1
- package/dist/comment.d.ts +29 -0
- package/dist/comment.d.ts.map +1 -0
- package/dist/comment.js +390 -0
- package/dist/comment.js.map +1 -0
- package/dist/git.d.ts +12 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +57 -14
- package/dist/git.js.map +1 -1
- package/dist/harnesses/agent-harness.d.ts +13 -0
- package/dist/harnesses/agent-harness.d.ts.map +1 -1
- package/dist/harnesses/index.d.ts +1 -1
- package/dist/harnesses/index.d.ts.map +1 -1
- package/dist/harnesses/pi.d.ts +7 -5
- package/dist/harnesses/pi.d.ts.map +1 -1
- package/dist/harnesses/pi.js +122 -9
- package/dist/harnesses/pi.js.map +1 -1
- package/dist/install-ci.d.ts +7 -0
- package/dist/install-ci.d.ts.map +1 -0
- package/dist/install-ci.js +760 -0
- package/dist/install-ci.js.map +1 -0
- package/dist/orchestrator.d.ts +24 -4
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +254 -63
- package/dist/orchestrator.js.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +1 -0
- package/dist/prompts.js.map +1 -1
- package/dist/providers/github-ci.d.ts +16 -0
- package/dist/providers/github-ci.d.ts.map +1 -0
- package/dist/providers/github-ci.js +733 -0
- package/dist/providers/github-ci.js.map +1 -0
- package/dist/providers/github.d.ts +21 -0
- package/dist/providers/github.d.ts.map +1 -1
- package/dist/providers/github.js +88 -3
- package/dist/providers/github.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/issue-provider.d.ts +26 -0
- package/dist/providers/issue-provider.d.ts.map +1 -1
- package/dist/task-manager.d.ts +4 -0
- package/dist/task-manager.d.ts.map +1 -1
- package/dist/task-manager.js +6 -0
- package/dist/task-manager.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -1
- package/src/auto-work.ts +26 -0
- package/src/cli.ts +123 -1
- package/src/comment.ts +531 -0
- package/src/git.ts +58 -12
- package/src/harnesses/agent-harness.ts +15 -0
- package/src/harnesses/index.ts +1 -1
- package/src/harnesses/pi.ts +146 -10
- package/src/orchestrator.ts +290 -72
- package/src/prompts.ts +1 -0
- package/src/providers/github-ci.ts +840 -0
- package/src/providers/github.ts +118 -5
- package/src/providers/index.ts +1 -0
- package/src/providers/issue-provider.ts +25 -1
- package/src/task-manager.ts +7 -0
- package/src/types.ts +11 -0
package/src/comment.ts
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import type {AgentHarness} from './harnesses/agent-harness.js';
|
|
4
|
+
import type {IssueProvider} from './providers/issue-provider.js';
|
|
5
|
+
import {GitManager} from './git.js';
|
|
6
|
+
import {TaskManager} from './task-manager.js';
|
|
7
|
+
import {LABELS} from './types.js';
|
|
8
|
+
|
|
9
|
+
export interface CommentConfig {
|
|
10
|
+
/** Issue or PR number */
|
|
11
|
+
number: number;
|
|
12
|
+
/** The comment body text */
|
|
13
|
+
commentBody: string;
|
|
14
|
+
/** Working directory (the repo) */
|
|
15
|
+
workDir: string;
|
|
16
|
+
/** GitHub repo in "owner/repo" format (auto-detected if not set) */
|
|
17
|
+
repo?: string;
|
|
18
|
+
/** Log file path */
|
|
19
|
+
logFile?: string;
|
|
20
|
+
/** Whether to post the response as a GitHub comment (issue-only) */
|
|
21
|
+
post: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A related PR with its metadata */
|
|
25
|
+
interface RelatedPR {
|
|
26
|
+
branch: string;
|
|
27
|
+
number: number;
|
|
28
|
+
title: string;
|
|
29
|
+
state: string;
|
|
30
|
+
url: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whitesmith context gathered for prompts */
|
|
34
|
+
interface WhitesmithContext {
|
|
35
|
+
/** The issue this comment relates to (for PR comments, the parent issue) */
|
|
36
|
+
parentIssue?: {number: number; title: string; body: string; url: string; labels: string[]};
|
|
37
|
+
/** Task proposal PR (investigate/<N>) */
|
|
38
|
+
taskPR?: RelatedPR;
|
|
39
|
+
/** Implementation PRs (task/<N>-*) */
|
|
40
|
+
implementationPRs: RelatedPR[];
|
|
41
|
+
/** Task files on the current branch (if tasks-accepted) */
|
|
42
|
+
tasks: Array<{id: string; title: string; filePath: string}>;
|
|
43
|
+
/** Whitesmith state label, if any */
|
|
44
|
+
stateLabel?: string;
|
|
45
|
+
/** The issue/PR number (used to tell the agent how to fetch comments) */
|
|
46
|
+
number: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Handle a comment on a PR.
|
|
51
|
+
*/
|
|
52
|
+
export async function handlePRComment(
|
|
53
|
+
config: CommentConfig,
|
|
54
|
+
issues: IssueProvider,
|
|
55
|
+
agent: AgentHarness,
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
const git = new GitManager(config.workDir);
|
|
58
|
+
|
|
59
|
+
// Get PR details
|
|
60
|
+
const pr = await issues.getPR(config.number);
|
|
61
|
+
if (!pr) {
|
|
62
|
+
throw new Error(`Could not find PR #${config.number}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`PR #${config.number}: ${pr.title}`);
|
|
66
|
+
console.log(`Branch: ${pr.branch}`);
|
|
67
|
+
|
|
68
|
+
// Clean up temp files before checkout to avoid conflicts
|
|
69
|
+
git.cleanupTempFiles();
|
|
70
|
+
|
|
71
|
+
// Checkout PR branch
|
|
72
|
+
await git.fetch();
|
|
73
|
+
await git.checkout(pr.branch);
|
|
74
|
+
|
|
75
|
+
// Gather whitesmith context based on branch naming
|
|
76
|
+
const context = await gatherContextForPR(pr.branch, config.workDir, issues, config.number);
|
|
77
|
+
|
|
78
|
+
const responseFile = '.whitesmith-response.md';
|
|
79
|
+
|
|
80
|
+
const prompt = buildPRCommentPrompt({
|
|
81
|
+
title: pr.title,
|
|
82
|
+
url: pr.url,
|
|
83
|
+
number: config.number,
|
|
84
|
+
body: pr.body,
|
|
85
|
+
branch: pr.branch,
|
|
86
|
+
commentBody: config.commentBody,
|
|
87
|
+
context,
|
|
88
|
+
responseFile,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const {exitCode} = await agent.run({
|
|
92
|
+
prompt,
|
|
93
|
+
workDir: config.workDir,
|
|
94
|
+
logFile: config.logFile,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (exitCode !== 0) {
|
|
98
|
+
throw new Error(`Agent failed with exit code ${exitCode}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Read the response file before committing (so it doesn't get committed)
|
|
102
|
+
const responsePath = path.join(config.workDir, responseFile);
|
|
103
|
+
let response: string | null = null;
|
|
104
|
+
if (fs.existsSync(responsePath)) {
|
|
105
|
+
response = fs.readFileSync(responsePath, 'utf-8');
|
|
106
|
+
try {
|
|
107
|
+
fs.unlinkSync(responsePath);
|
|
108
|
+
} catch {
|
|
109
|
+
// ignore
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Commit and push any changes
|
|
114
|
+
const committed = await git.commitAll(`fix(#${config.number}): address review comment`);
|
|
115
|
+
if (committed) {
|
|
116
|
+
await git.forcePush(pr.branch);
|
|
117
|
+
console.log(`Changes pushed to ${pr.branch}`);
|
|
118
|
+
} else {
|
|
119
|
+
console.log('No changes to commit.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Post the response as a PR comment
|
|
123
|
+
if (response) {
|
|
124
|
+
if (config.post) {
|
|
125
|
+
await issues.comment(config.number, response);
|
|
126
|
+
console.log(`Response posted as comment on PR #${config.number}`);
|
|
127
|
+
} else {
|
|
128
|
+
process.stdout.write(response);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
console.log('Agent did not produce a response file.');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handle a comment on an issue (not a PR).
|
|
137
|
+
*/
|
|
138
|
+
export async function handleIssueComment(
|
|
139
|
+
config: CommentConfig,
|
|
140
|
+
issues: IssueProvider,
|
|
141
|
+
agent: AgentHarness,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
const git = new GitManager(config.workDir);
|
|
144
|
+
const issue = await issues.getIssue(config.number);
|
|
145
|
+
console.log(`Issue #${config.number}: ${issue.title}`);
|
|
146
|
+
|
|
147
|
+
// Fetch so the agent can checkout related PR branches if needed
|
|
148
|
+
await git.fetch();
|
|
149
|
+
|
|
150
|
+
// Gather whitesmith context for this issue
|
|
151
|
+
const context = await gatherContextForIssue(config.number, config.workDir, issues);
|
|
152
|
+
|
|
153
|
+
const responseFile = '.whitesmith-response.md';
|
|
154
|
+
const prompt = buildIssueCommentPrompt({
|
|
155
|
+
title: issue.title,
|
|
156
|
+
url: issue.url,
|
|
157
|
+
number: config.number,
|
|
158
|
+
body: issue.body,
|
|
159
|
+
commentBody: config.commentBody,
|
|
160
|
+
responseFile,
|
|
161
|
+
context,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const {exitCode} = await agent.run({
|
|
165
|
+
prompt,
|
|
166
|
+
workDir: config.workDir,
|
|
167
|
+
logFile: config.logFile,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (exitCode !== 0) {
|
|
171
|
+
throw new Error(`Agent failed with exit code ${exitCode}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Read the response file
|
|
175
|
+
const responsePath = path.join(config.workDir, responseFile);
|
|
176
|
+
|
|
177
|
+
if (!fs.existsSync(responsePath)) {
|
|
178
|
+
console.error('Agent did not produce a response file.');
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const response = fs.readFileSync(responsePath, 'utf-8');
|
|
184
|
+
|
|
185
|
+
// Clean up the response file
|
|
186
|
+
try {
|
|
187
|
+
fs.unlinkSync(responsePath);
|
|
188
|
+
} catch {
|
|
189
|
+
// ignore
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (config.post) {
|
|
193
|
+
await issues.comment(config.number, response);
|
|
194
|
+
console.log(`Response posted as comment on issue #${config.number}`);
|
|
195
|
+
} else {
|
|
196
|
+
// Print to stdout
|
|
197
|
+
process.stdout.write(response);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Detect whether a given number is a PR or an issue.
|
|
203
|
+
*/
|
|
204
|
+
export async function isPullRequest(issues: IssueProvider, number: number): Promise<boolean> {
|
|
205
|
+
const pr = await issues.getPR(number);
|
|
206
|
+
return pr !== null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- Context gathering ---
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parse a whitesmith branch name to extract the issue number.
|
|
213
|
+
*
|
|
214
|
+
* - `investigate/<N>` → issue N (task proposal PR)
|
|
215
|
+
* - `task/<N>-<seq>` → issue N (implementation PR)
|
|
216
|
+
*/
|
|
217
|
+
function parseWhitesmithBranch(
|
|
218
|
+
branch: string,
|
|
219
|
+
): {type: 'investigate' | 'task'; issueNumber: number; taskId?: string} | null {
|
|
220
|
+
const investigateMatch = branch.match(/^investigate\/(\d+)$/);
|
|
221
|
+
if (investigateMatch) {
|
|
222
|
+
return {type: 'investigate', issueNumber: parseInt(investigateMatch[1], 10)};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const taskMatch = branch.match(/^task\/(\d+)-(\d+.*)$/);
|
|
226
|
+
if (taskMatch) {
|
|
227
|
+
return {
|
|
228
|
+
type: 'task',
|
|
229
|
+
issueNumber: parseInt(taskMatch[1], 10),
|
|
230
|
+
taskId: `${taskMatch[1]}-${taskMatch[2]}`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Gather whitesmith context for a PR comment based on its branch name.
|
|
239
|
+
*/
|
|
240
|
+
async function gatherContextForPR(
|
|
241
|
+
branch: string,
|
|
242
|
+
workDir: string,
|
|
243
|
+
issues: IssueProvider,
|
|
244
|
+
commentNumber: number,
|
|
245
|
+
): Promise<WhitesmithContext> {
|
|
246
|
+
const context: WhitesmithContext = {implementationPRs: [], tasks: [], number: commentNumber};
|
|
247
|
+
|
|
248
|
+
const parsed = parseWhitesmithBranch(branch);
|
|
249
|
+
if (!parsed) return context;
|
|
250
|
+
|
|
251
|
+
// Fetch the parent issue
|
|
252
|
+
try {
|
|
253
|
+
const issue = await issues.getIssue(parsed.issueNumber);
|
|
254
|
+
context.parentIssue = {
|
|
255
|
+
number: issue.number,
|
|
256
|
+
title: issue.title,
|
|
257
|
+
body: issue.body,
|
|
258
|
+
url: issue.url,
|
|
259
|
+
labels: issue.labels,
|
|
260
|
+
};
|
|
261
|
+
context.stateLabel = issue.labels.find((l) => l.startsWith('whitesmith:'));
|
|
262
|
+
} catch {
|
|
263
|
+
// Issue might not exist
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Find related PRs for this issue
|
|
267
|
+
await gatherRelatedPRs(parsed.issueNumber, issues, context);
|
|
268
|
+
|
|
269
|
+
// If tasks are on main, list them
|
|
270
|
+
const taskManager = new TaskManager(workDir);
|
|
271
|
+
context.tasks = taskManager.listTasks(parsed.issueNumber).map((t) => ({
|
|
272
|
+
id: t.id,
|
|
273
|
+
title: t.title,
|
|
274
|
+
filePath: t.filePath,
|
|
275
|
+
}));
|
|
276
|
+
|
|
277
|
+
return context;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Gather whitesmith context for an issue comment.
|
|
282
|
+
*/
|
|
283
|
+
async function gatherContextForIssue(
|
|
284
|
+
issueNumber: number,
|
|
285
|
+
workDir: string,
|
|
286
|
+
issues: IssueProvider,
|
|
287
|
+
): Promise<WhitesmithContext> {
|
|
288
|
+
const context: WhitesmithContext = {implementationPRs: [], tasks: [], number: issueNumber};
|
|
289
|
+
|
|
290
|
+
// Get the issue's labels for state
|
|
291
|
+
try {
|
|
292
|
+
const issue = await issues.getIssue(issueNumber);
|
|
293
|
+
context.stateLabel = issue.labels.find((l) => l.startsWith('whitesmith:'));
|
|
294
|
+
} catch {
|
|
295
|
+
// ignore
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Find related PRs
|
|
299
|
+
await gatherRelatedPRs(issueNumber, issues, context);
|
|
300
|
+
|
|
301
|
+
// List tasks on main
|
|
302
|
+
const taskManager = new TaskManager(workDir);
|
|
303
|
+
context.tasks = taskManager.listTasks(issueNumber).map((t) => ({
|
|
304
|
+
id: t.id,
|
|
305
|
+
title: t.title,
|
|
306
|
+
filePath: t.filePath,
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
return context;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Find task proposal and implementation PRs related to an issue.
|
|
314
|
+
*/
|
|
315
|
+
async function gatherRelatedPRs(
|
|
316
|
+
issueNumber: number,
|
|
317
|
+
issues: IssueProvider,
|
|
318
|
+
context: WhitesmithContext,
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
// Task proposal PR
|
|
321
|
+
const taskPR = await issues.getPRForBranch(`investigate/${issueNumber}`);
|
|
322
|
+
if (taskPR) {
|
|
323
|
+
context.taskPR = {
|
|
324
|
+
branch: `investigate/${issueNumber}`,
|
|
325
|
+
number: taskPR.number,
|
|
326
|
+
title: '',
|
|
327
|
+
state: taskPR.state,
|
|
328
|
+
url: taskPR.url,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Implementation PRs
|
|
333
|
+
const implPRs = await issues.listPRsByBranchPrefix(`task/${issueNumber}-`);
|
|
334
|
+
context.implementationPRs = implPRs;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// --- Prompt builders ---
|
|
338
|
+
|
|
339
|
+
function formatWhitesmithContext(context: WhitesmithContext): string {
|
|
340
|
+
const sections: string[] = [];
|
|
341
|
+
|
|
342
|
+
if (context.stateLabel) {
|
|
343
|
+
const stateDescriptions: Record<string, string> = {
|
|
344
|
+
[LABELS.INVESTIGATING]:
|
|
345
|
+
'The agent is currently investigating this issue and generating tasks.',
|
|
346
|
+
[LABELS.TASKS_PROPOSED]: 'Tasks have been proposed in a PR and are awaiting review.',
|
|
347
|
+
[LABELS.TASKS_ACCEPTED]: 'Tasks have been accepted and are being implemented.',
|
|
348
|
+
[LABELS.COMPLETED]: 'All tasks for this issue have been completed.',
|
|
349
|
+
};
|
|
350
|
+
const desc = stateDescriptions[context.stateLabel] || context.stateLabel;
|
|
351
|
+
sections.push(`### Whitesmith State\n\n**${context.stateLabel}**: ${desc}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (context.parentIssue) {
|
|
355
|
+
sections.push(
|
|
356
|
+
`### Parent Issue\n\n` +
|
|
357
|
+
`- **Title:** ${context.parentIssue.title}\n` +
|
|
358
|
+
`- **URL:** ${context.parentIssue.url}\n` +
|
|
359
|
+
`- **Number:** #${context.parentIssue.number}\n` +
|
|
360
|
+
`- **Labels:** ${context.parentIssue.labels.join(', ') || 'none'}\n\n` +
|
|
361
|
+
`#### Issue Description\n\n${context.parentIssue.body}`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (context.taskPR) {
|
|
366
|
+
sections.push(
|
|
367
|
+
`### Task Proposal PR\n\n` +
|
|
368
|
+
`- **Branch:** \`${context.taskPR.branch}\`\n` +
|
|
369
|
+
`- **State:** ${context.taskPR.state}\n` +
|
|
370
|
+
`- **URL:** ${context.taskPR.url}`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (context.implementationPRs.length > 0) {
|
|
375
|
+
const prList = context.implementationPRs
|
|
376
|
+
.map((pr) => `- **#${pr.number}** ${pr.title} — \`${pr.branch}\` (${pr.state}) — ${pr.url}`)
|
|
377
|
+
.join('\n');
|
|
378
|
+
sections.push(`### Implementation PRs\n\n${prList}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (context.tasks.length > 0) {
|
|
382
|
+
const taskList = context.tasks
|
|
383
|
+
.map((t) => `- **${t.id}**: ${t.title} (\`${t.filePath}\`)`)
|
|
384
|
+
.join('\n');
|
|
385
|
+
sections.push(`### Pending Tasks\n\n${taskList}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
sections.push(
|
|
389
|
+
`### Conversation History\n\n` +
|
|
390
|
+
`Previous comments are **not** included here to save context space. ` +
|
|
391
|
+
`If you need to read the conversation history, run:\n\n` +
|
|
392
|
+
`\`\`\`bash\ngh issue view ${context.number} --comments\n\`\`\``,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
if (sections.length === 0) {
|
|
396
|
+
return '';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
`\n## Whitesmith Context\n\n` +
|
|
401
|
+
`The following is the current whitesmith pipeline state and conversation history related to this issue/PR. ` +
|
|
402
|
+
`Use this context to provide informed responses.\n\n` +
|
|
403
|
+
sections.join('\n\n') +
|
|
404
|
+
'\n'
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
interface PRCommentPromptArgs {
|
|
409
|
+
title: string;
|
|
410
|
+
url: string;
|
|
411
|
+
number: number;
|
|
412
|
+
body: string;
|
|
413
|
+
branch: string;
|
|
414
|
+
commentBody: string;
|
|
415
|
+
context: WhitesmithContext;
|
|
416
|
+
responseFile: string;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function buildPRCommentPrompt(args: PRCommentPromptArgs): string {
|
|
420
|
+
return `# Agent Task from PR Comment
|
|
421
|
+
|
|
422
|
+
## Pull Request
|
|
423
|
+
|
|
424
|
+
- **Title:** ${args.title}
|
|
425
|
+
- **URL:** ${args.url}
|
|
426
|
+
- **PR Number:** #${args.number}
|
|
427
|
+
- **Branch:** ${args.branch}
|
|
428
|
+
|
|
429
|
+
### PR Description
|
|
430
|
+
|
|
431
|
+
${args.body}
|
|
432
|
+
${formatWhitesmithContext(args.context)}
|
|
433
|
+
## Triggering Comment
|
|
434
|
+
|
|
435
|
+
${args.commentBody}
|
|
436
|
+
|
|
437
|
+
## Instructions
|
|
438
|
+
|
|
439
|
+
You are responding to a comment on a pull request. The comment may be a question, feedback,
|
|
440
|
+
a request for changes, or a general discussion point.
|
|
441
|
+
|
|
442
|
+
1. You are already on the PR branch: \`${args.branch}\`
|
|
443
|
+
2. Read and understand the comment.
|
|
444
|
+
3. Review the whitesmith context above to understand the pipeline state.
|
|
445
|
+
4. You have full access to the repository code — read files, explore the codebase as needed.
|
|
446
|
+
5. If the comment requests code changes, make them. Do NOT commit — the caller will handle committing and pushing.
|
|
447
|
+
6. **Always** write a response in Markdown to the file \`${args.responseFile}\`. This will be posted as a reply comment on the PR.
|
|
448
|
+
- If you made changes, summarize what you changed and why.
|
|
449
|
+
- If the comment is a question or discussion, provide a thoughtful answer.
|
|
450
|
+
- Be thorough but concise.
|
|
451
|
+
|
|
452
|
+
Do NOT push. Do NOT create a new PR. Do NOT commit. The caller handles all of that.
|
|
453
|
+
`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
interface IssueCommentPromptArgs {
|
|
457
|
+
title: string;
|
|
458
|
+
url: string;
|
|
459
|
+
number: number;
|
|
460
|
+
body: string;
|
|
461
|
+
commentBody: string;
|
|
462
|
+
responseFile: string;
|
|
463
|
+
context: WhitesmithContext;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function buildIssueCommentPrompt(args: IssueCommentPromptArgs): string {
|
|
467
|
+
// Build the list of related PR branches the agent can work on
|
|
468
|
+
const relatedBranches: string[] = [];
|
|
469
|
+
if (args.context.taskPR && args.context.taskPR.state === 'open') {
|
|
470
|
+
relatedBranches.push(args.context.taskPR.branch);
|
|
471
|
+
}
|
|
472
|
+
for (const pr of args.context.implementationPRs) {
|
|
473
|
+
if (pr.state === 'open') {
|
|
474
|
+
relatedBranches.push(pr.branch);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let workOnPRInstructions = '';
|
|
479
|
+
if (relatedBranches.length > 0) {
|
|
480
|
+
const branchList = relatedBranches.map((b) => ` - \`${b}\``).join('\n');
|
|
481
|
+
workOnPRInstructions = `
|
|
482
|
+
|
|
483
|
+
### Working on related PRs
|
|
484
|
+
|
|
485
|
+
If the comment asks you to make changes to a related PR (e.g. update the task plan,
|
|
486
|
+
fix something in an implementation), you **can and should** do so. The related open PR branches are:
|
|
487
|
+
|
|
488
|
+
${branchList}
|
|
489
|
+
|
|
490
|
+
To work on a PR branch:
|
|
491
|
+
|
|
492
|
+
1. \`git checkout <branch>\` (branches have been fetched already)
|
|
493
|
+
2. Make your changes.
|
|
494
|
+
3. Commit with a descriptive message.
|
|
495
|
+
4. \`git push origin <branch>\`
|
|
496
|
+
5. Still write \`${args.responseFile}\` summarizing what you did.
|
|
497
|
+
|
|
498
|
+
When done, \`git checkout main\` to return to the default branch.`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return `# Agent Task from Issue Comment
|
|
502
|
+
|
|
503
|
+
## Issue
|
|
504
|
+
|
|
505
|
+
- **Title:** ${args.title}
|
|
506
|
+
- **URL:** ${args.url}
|
|
507
|
+
- **Issue Number:** #${args.number}
|
|
508
|
+
|
|
509
|
+
### Issue Description
|
|
510
|
+
|
|
511
|
+
${args.body}
|
|
512
|
+
${formatWhitesmithContext(args.context)}
|
|
513
|
+
## Triggering Comment
|
|
514
|
+
|
|
515
|
+
${args.commentBody}
|
|
516
|
+
|
|
517
|
+
## Instructions
|
|
518
|
+
|
|
519
|
+
You are responding to a comment on an issue.
|
|
520
|
+
|
|
521
|
+
1. Read and understand the issue description and the triggering comment.
|
|
522
|
+
2. Review the whitesmith context above to understand what work is already in progress.
|
|
523
|
+
3. You have full access to the repository code — read files, explore the codebase as needed.
|
|
524
|
+
4. Analyze the request and formulate a helpful response.
|
|
525
|
+
5. Write your response in Markdown to the file \`${args.responseFile}\`.
|
|
526
|
+
|
|
527
|
+
Your response will be posted as a comment on the issue.
|
|
528
|
+
Be thorough but concise. Include code snippets, file references, or suggestions as appropriate.
|
|
529
|
+
If there are pending PRs or tasks, reference them in your response when relevant.${workOnPRInstructions}
|
|
530
|
+
`;
|
|
531
|
+
}
|
package/src/git.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {exec} from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
2
4
|
import {promisify} from 'node:util';
|
|
3
5
|
|
|
4
6
|
const execAsync = promisify(exec);
|
|
@@ -25,6 +27,21 @@ export class GitManager {
|
|
|
25
27
|
return this.git('rev-parse --abbrev-ref HEAD');
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Remove all .whitesmith-* temp files from the working directory.
|
|
32
|
+
*/
|
|
33
|
+
cleanupTempFiles(): void {
|
|
34
|
+
for (const entry of fs.readdirSync(this.workDir)) {
|
|
35
|
+
if (entry.startsWith('.whitesmith-')) {
|
|
36
|
+
try {
|
|
37
|
+
fs.unlinkSync(path.join(this.workDir, entry));
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
/**
|
|
29
46
|
* Fetch latest from origin
|
|
30
47
|
*/
|
|
@@ -56,25 +73,30 @@ export class GitManager {
|
|
|
56
73
|
* Stage all changes and commit
|
|
57
74
|
*/
|
|
58
75
|
async commitAll(message: string, exclude?: string[]): Promise<boolean> {
|
|
76
|
+
// Always exclude whitesmith temp files
|
|
77
|
+
const allExclude = ['.whitesmith-*', ...(exclude || [])];
|
|
78
|
+
|
|
79
|
+
// Remove any whitesmith temp files from the working tree
|
|
80
|
+
this.cleanupTempFiles();
|
|
81
|
+
|
|
59
82
|
// Check if there are changes
|
|
60
83
|
const status = await this.git('status --porcelain');
|
|
61
84
|
if (!status) return false;
|
|
62
85
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} catch {
|
|
71
|
-
// File might not be staged
|
|
72
|
-
}
|
|
86
|
+
// Add all then unstage excluded patterns
|
|
87
|
+
await this.git('add -A');
|
|
88
|
+
for (const pattern of allExclude) {
|
|
89
|
+
try {
|
|
90
|
+
await this.git(`reset HEAD -- ${pattern}`);
|
|
91
|
+
} catch {
|
|
92
|
+
// File might not be staged
|
|
73
93
|
}
|
|
74
|
-
} else {
|
|
75
|
-
await this.git('add -A');
|
|
76
94
|
}
|
|
77
95
|
|
|
96
|
+
// Check if anything is still staged after exclusions
|
|
97
|
+
const staged = await this.git('diff --cached --name-only');
|
|
98
|
+
if (!staged) return false;
|
|
99
|
+
|
|
78
100
|
await this.git(`commit -m "${message.replace(/"/g, '\\"')}"`);
|
|
79
101
|
return true;
|
|
80
102
|
}
|
|
@@ -136,6 +158,30 @@ export class GitManager {
|
|
|
136
158
|
}
|
|
137
159
|
}
|
|
138
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Check if a file exists on a remote ref (without checking out)
|
|
163
|
+
*/
|
|
164
|
+
async remoteFileExists(ref: string, filePath: string): Promise<boolean> {
|
|
165
|
+
try {
|
|
166
|
+
await this.git(`show ${ref}:${filePath}`);
|
|
167
|
+
return true;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if a path has any files on a remote ref (without checking out)
|
|
175
|
+
*/
|
|
176
|
+
async remotePathHasFiles(ref: string, dirPath: string): Promise<boolean> {
|
|
177
|
+
try {
|
|
178
|
+
const result = await this.git(`ls-tree ${ref} -- ${dirPath}`);
|
|
179
|
+
return result.length > 0;
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
139
185
|
/**
|
|
140
186
|
* Verify we're on the expected branch
|
|
141
187
|
*/
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
* Implementations wrap specific tools (pi, claude CLI, aider, etc.)
|
|
4
4
|
*/
|
|
5
5
|
export interface AgentHarness {
|
|
6
|
+
/**
|
|
7
|
+
* Validate that the agent is available and properly configured.
|
|
8
|
+
* Throws an error with a descriptive message if validation fails.
|
|
9
|
+
*/
|
|
10
|
+
validate(): Promise<void>;
|
|
11
|
+
|
|
6
12
|
/**
|
|
7
13
|
* Run the agent with a prompt and return its output.
|
|
8
14
|
* The agent is expected to execute in the given working directory.
|
|
@@ -13,3 +19,12 @@ export interface AgentHarness {
|
|
|
13
19
|
logFile?: string;
|
|
14
20
|
}): Promise<{output: string; exitCode: number}>;
|
|
15
21
|
}
|
|
22
|
+
|
|
23
|
+
export interface AgentHarnessConfig {
|
|
24
|
+
/** Command to invoke the agent (e.g. 'pi') */
|
|
25
|
+
cmd: string;
|
|
26
|
+
/** AI provider name (e.g. 'anthropic', 'openai') */
|
|
27
|
+
provider: string;
|
|
28
|
+
/** AI model ID (e.g. 'claude-opus-4-6') */
|
|
29
|
+
model: string;
|
|
30
|
+
}
|
package/src/harnesses/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type {AgentHarness} from './agent-harness.js';
|
|
1
|
+
export type {AgentHarness, AgentHarnessConfig} from './agent-harness.js';
|
|
2
2
|
export {PiHarness} from './pi.js';
|