whitesmith 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/__tests__/task-manager.test.d.ts +2 -0
  2. package/dist/__tests__/task-manager.test.d.ts.map +1 -0
  3. package/dist/__tests__/task-manager.test.js +95 -0
  4. package/dist/__tests__/task-manager.test.js.map +1 -0
  5. package/dist/cli.d.ts +5 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +147 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/git.d.ts +60 -0
  10. package/dist/git.d.ts.map +1 -0
  11. package/dist/git.js +138 -0
  12. package/dist/git.js.map +1 -0
  13. package/dist/harnesses/agent-harness.d.ts +19 -0
  14. package/dist/harnesses/agent-harness.d.ts.map +1 -0
  15. package/dist/harnesses/agent-harness.js +2 -0
  16. package/dist/harnesses/agent-harness.js.map +1 -0
  17. package/dist/harnesses/index.d.ts +3 -0
  18. package/dist/harnesses/index.d.ts.map +1 -0
  19. package/dist/harnesses/index.js +2 -0
  20. package/dist/harnesses/index.js.map +1 -0
  21. package/dist/harnesses/pi.d.ts +23 -0
  22. package/dist/harnesses/pi.d.ts.map +1 -0
  23. package/dist/harnesses/pi.js +63 -0
  24. package/dist/harnesses/pi.js.map +1 -0
  25. package/dist/index.d.ts +11 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +8 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/orchestrator.d.ts +44 -0
  30. package/dist/orchestrator.d.ts.map +1 -0
  31. package/dist/orchestrator.js +241 -0
  32. package/dist/orchestrator.js.map +1 -0
  33. package/dist/prompts.d.ts +12 -0
  34. package/dist/prompts.d.ts.map +1 -0
  35. package/dist/prompts.js +112 -0
  36. package/dist/prompts.js.map +1 -0
  37. package/dist/providers/github.d.ts +34 -0
  38. package/dist/providers/github.d.ts.map +1 -0
  39. package/dist/providers/github.js +135 -0
  40. package/dist/providers/github.js.map +1 -0
  41. package/dist/providers/index.d.ts +3 -0
  42. package/dist/providers/index.d.ts.map +1 -0
  43. package/dist/providers/index.js +2 -0
  44. package/dist/providers/index.js.map +1 -0
  45. package/dist/providers/issue-provider.d.ts +59 -0
  46. package/dist/providers/issue-provider.d.ts.map +1 -0
  47. package/dist/providers/issue-provider.js +2 -0
  48. package/dist/providers/issue-provider.js.map +1 -0
  49. package/dist/task-manager.d.ts +57 -0
  50. package/dist/task-manager.d.ts.map +1 -0
  51. package/dist/task-manager.js +158 -0
  52. package/dist/task-manager.js.map +1 -0
  53. package/dist/types.d.ts +92 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +14 -0
  56. package/dist/types.js.map +1 -0
  57. package/package.json +46 -0
  58. package/src/cli.ts +172 -0
  59. package/src/git.ts +148 -0
  60. package/src/harnesses/agent-harness.ts +15 -0
  61. package/src/harnesses/index.ts +2 -0
  62. package/src/harnesses/pi.ts +82 -0
  63. package/src/index.ts +13 -0
  64. package/src/orchestrator.ts +294 -0
  65. package/src/prompts.ts +114 -0
  66. package/src/providers/github.ts +180 -0
  67. package/src/providers/index.ts +2 -0
  68. package/src/providers/issue-provider.ts +59 -0
  69. package/src/task-manager.ts +190 -0
  70. package/src/types.ts +88 -0
package/src/git.ts ADDED
@@ -0,0 +1,148 @@
1
+ import {exec} from 'node:child_process';
2
+ import {promisify} from 'node:util';
3
+
4
+ const execAsync = promisify(exec);
5
+
6
+ /**
7
+ * Git operations for whitesmith.
8
+ */
9
+ export class GitManager {
10
+ private workDir: string;
11
+
12
+ constructor(workDir: string) {
13
+ this.workDir = workDir;
14
+ }
15
+
16
+ private async git(args: string): Promise<string> {
17
+ const {stdout} = await execAsync(`git ${args}`, {cwd: this.workDir});
18
+ return stdout.trim();
19
+ }
20
+
21
+ /**
22
+ * Get the current branch name
23
+ */
24
+ async getCurrentBranch(): Promise<string> {
25
+ return this.git('rev-parse --abbrev-ref HEAD');
26
+ }
27
+
28
+ /**
29
+ * Fetch latest from origin
30
+ */
31
+ async fetch(): Promise<void> {
32
+ await this.git('fetch origin');
33
+ }
34
+
35
+ /**
36
+ * Checkout a branch (create if it doesn't exist)
37
+ */
38
+ async checkout(branch: string, options?: {create?: boolean; startPoint?: string}): Promise<void> {
39
+ if (options?.create) {
40
+ const startPoint = options.startPoint || 'origin/main';
41
+ await this.git(`checkout -b ${branch} ${startPoint}`);
42
+ } else {
43
+ await this.git(`checkout ${branch}`);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Checkout main and pull latest
49
+ */
50
+ async checkoutMain(): Promise<void> {
51
+ await this.git('checkout main');
52
+ await this.git('pull origin main');
53
+ }
54
+
55
+ /**
56
+ * Stage all changes and commit
57
+ */
58
+ async commitAll(message: string, exclude?: string[]): Promise<boolean> {
59
+ // Check if there are changes
60
+ const status = await this.git('status --porcelain');
61
+ if (!status) return false;
62
+
63
+ let addCmd = 'git add -A';
64
+ if (exclude && exclude.length > 0) {
65
+ // Add all then unstage excluded
66
+ await this.git('add -A');
67
+ for (const pattern of exclude) {
68
+ try {
69
+ await this.git(`reset HEAD -- ${pattern}`);
70
+ } catch {
71
+ // File might not be staged
72
+ }
73
+ }
74
+ } else {
75
+ await this.git('add -A');
76
+ }
77
+
78
+ await this.git(`commit -m "${message.replace(/"/g, '\\"')}"`);
79
+ return true;
80
+ }
81
+
82
+ /**
83
+ * Push branch to origin
84
+ */
85
+ async push(branch: string): Promise<void> {
86
+ await this.git(`push origin ${branch}`);
87
+ }
88
+
89
+ /**
90
+ * Force push branch to origin
91
+ */
92
+ async forcePush(branch: string): Promise<void> {
93
+ await this.git(`push --force-with-lease origin ${branch}`);
94
+ }
95
+
96
+ /**
97
+ * Check if there are uncommitted changes
98
+ */
99
+ async hasChanges(): Promise<boolean> {
100
+ const status = await this.git('status --porcelain');
101
+ return status.length > 0;
102
+ }
103
+
104
+ /**
105
+ * Check if a local branch exists
106
+ */
107
+ async localBranchExists(branch: string): Promise<boolean> {
108
+ try {
109
+ await this.git(`rev-parse --verify ${branch}`);
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Delete a local branch
118
+ */
119
+ async deleteLocalBranch(branch: string): Promise<void> {
120
+ try {
121
+ await this.git(`branch -D ${branch}`);
122
+ } catch {
123
+ // Branch might not exist
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Get the default branch (usually main)
129
+ */
130
+ async getDefaultBranch(): Promise<string> {
131
+ try {
132
+ const ref = await this.git('symbolic-ref refs/remotes/origin/HEAD');
133
+ return ref.replace('refs/remotes/origin/', '');
134
+ } catch {
135
+ return 'main';
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Verify we're on the expected branch
141
+ */
142
+ async verifyBranch(expected: string): Promise<void> {
143
+ const current = await this.getCurrentBranch();
144
+ if (current !== expected) {
145
+ throw new Error(`Expected to be on branch '${expected}' but on '${current}'`);
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Interface for AI agent harnesses.
3
+ * Implementations wrap specific tools (pi, claude CLI, aider, etc.)
4
+ */
5
+ export interface AgentHarness {
6
+ /**
7
+ * Run the agent with a prompt and return its output.
8
+ * The agent is expected to execute in the given working directory.
9
+ */
10
+ run(options: {
11
+ prompt: string;
12
+ workDir: string;
13
+ logFile?: string;
14
+ }): Promise<{output: string; exitCode: number}>;
15
+ }
@@ -0,0 +1,2 @@
1
+ export type {AgentHarness} from './agent-harness.js';
2
+ export {PiHarness} from './pi.js';
@@ -0,0 +1,82 @@
1
+ import {exec} from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import type {AgentHarness} from './agent-harness.js';
5
+
6
+ /**
7
+ * Agent harness for @mariozechner/pi-coding-agent.
8
+ *
9
+ * Runs `pi` with a prompt passed via a temp file, captures output.
10
+ */
11
+ export class PiHarness implements AgentHarness {
12
+ private cmd: string;
13
+
14
+ /**
15
+ * @param cmd - Command to invoke pi (default: "pi")
16
+ */
17
+ constructor(cmd: string = 'pi') {
18
+ this.cmd = cmd;
19
+ }
20
+
21
+ async run(options: {
22
+ prompt: string;
23
+ workDir: string;
24
+ logFile?: string;
25
+ }): Promise<{output: string; exitCode: number}> {
26
+ // Write prompt to a temp file to avoid shell escaping issues
27
+ const promptFile = path.join(options.workDir, '.whitesmith-prompt.md');
28
+ fs.writeFileSync(promptFile, options.prompt, 'utf-8');
29
+
30
+ try {
31
+ const result = await this.exec(
32
+ `${this.cmd} --prompt-file "${promptFile}" --yes`,
33
+ options.workDir,
34
+ options.logFile,
35
+ );
36
+ return result;
37
+ } finally {
38
+ // Clean up prompt file
39
+ try {
40
+ fs.unlinkSync(promptFile);
41
+ } catch {
42
+ // Ignore
43
+ }
44
+ }
45
+ }
46
+
47
+ private exec(
48
+ cmd: string,
49
+ workDir: string,
50
+ logFile?: string,
51
+ ): Promise<{output: string; exitCode: number}> {
52
+ return new Promise((resolve) => {
53
+ const child = exec(cmd, {
54
+ cwd: workDir,
55
+ maxBuffer: 50 * 1024 * 1024,
56
+ timeout: 30 * 60 * 1000, // 30 minute timeout
57
+ });
58
+
59
+ let output = '';
60
+ const logStream = logFile
61
+ ? fs.createWriteStream(path.resolve(workDir, logFile), {flags: 'a'})
62
+ : null;
63
+
64
+ child.stdout?.on('data', (data: string) => {
65
+ output += data;
66
+ process.stdout.write(data);
67
+ logStream?.write(data);
68
+ });
69
+
70
+ child.stderr?.on('data', (data: string) => {
71
+ output += data;
72
+ process.stderr.write(data);
73
+ logStream?.write(data);
74
+ });
75
+
76
+ child.on('close', (code) => {
77
+ logStream?.end();
78
+ resolve({output, exitCode: code ?? 1});
79
+ });
80
+ });
81
+ }
82
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export {Orchestrator} from './orchestrator.js';
2
+ export {TaskManager} from './task-manager.js';
3
+ export {GitManager} from './git.js';
4
+ export {buildInvestigatePrompt, buildImplementPrompt} from './prompts.js';
5
+
6
+ export type {IssueProvider} from './providers/issue-provider.js';
7
+ export {GitHubProvider} from './providers/github.js';
8
+
9
+ export type {AgentHarness} from './harnesses/agent-harness.js';
10
+ export {PiHarness} from './harnesses/pi.js';
11
+
12
+ export type {Issue, Task, TaskFrontmatter, DevPulseConfig, Action} from './types.js';
13
+ export {LABELS} from './types.js';
@@ -0,0 +1,294 @@
1
+ import type {DevPulseConfig, Issue, Task, Action} from './types.js';
2
+ import {LABELS} from './types.js';
3
+ import type {IssueProvider} from './providers/issue-provider.js';
4
+ import type {AgentHarness} from './harnesses/agent-harness.js';
5
+ import {TaskManager} from './task-manager.js';
6
+ import {GitManager} from './git.js';
7
+ import {buildInvestigatePrompt, buildImplementPrompt} from './prompts.js';
8
+
9
+ /**
10
+ * Main orchestrator for whitesmith.
11
+ *
12
+ * The loop:
13
+ * 1. Reconcile — check if any issues with tasks-accepted have all tasks done
14
+ * 2. Investigate — pick an unlabeled issue, generate tasks
15
+ * 3. Implement — pick an available task, implement it
16
+ */
17
+ export class Orchestrator {
18
+ private config: DevPulseConfig;
19
+ private issues: IssueProvider;
20
+ private agent: AgentHarness;
21
+ private tasks: TaskManager;
22
+ private git: GitManager;
23
+
24
+ constructor(config: DevPulseConfig, issues: IssueProvider, agent: AgentHarness) {
25
+ this.config = config;
26
+ this.issues = issues;
27
+ this.agent = agent;
28
+ this.tasks = new TaskManager(config.workDir);
29
+ this.git = new GitManager(config.workDir);
30
+ }
31
+
32
+ /**
33
+ * Run the main loop
34
+ */
35
+ async run(): Promise<void> {
36
+ console.log('=== whitesmith ===');
37
+ console.log(`Working directory: ${this.config.workDir}`);
38
+ console.log(`Max iterations: ${this.config.maxIterations}`);
39
+ console.log(`Agent command: ${this.config.agentCmd}`);
40
+ console.log('');
41
+
42
+ // Ensure labels exist
43
+ await this.issues.ensureLabels(Object.values(LABELS));
44
+
45
+ for (let i = 1; i <= this.config.maxIterations; i++) {
46
+ console.log('');
47
+ console.log(`=== Iteration ${i}/${this.config.maxIterations} ===`);
48
+
49
+ // Make sure we're on main with latest
50
+ await this.git.fetch();
51
+ await this.git.checkoutMain();
52
+
53
+ // Decide what to do
54
+ const action = await this.decideAction();
55
+ console.log(`Action: ${action.type}`);
56
+
57
+ switch (action.type) {
58
+ case 'reconcile':
59
+ await this.reconcile(action.issue);
60
+ break;
61
+ case 'investigate':
62
+ await this.investigate(action.issue);
63
+ break;
64
+ case 'implement':
65
+ await this.implement(action.task, action.issue);
66
+ break;
67
+ case 'idle':
68
+ console.log('Nothing to do. All issues are either in-progress or completed.');
69
+ return;
70
+ }
71
+
72
+ if (!this.config.noSleep && i < this.config.maxIterations) {
73
+ console.log('Sleeping 5s...');
74
+ await new Promise((r) => setTimeout(r, 5000));
75
+ }
76
+ }
77
+
78
+ console.log('');
79
+ console.log('=== Iteration limit reached ===');
80
+ }
81
+
82
+ /**
83
+ * Decide the next action to take
84
+ */
85
+ private async decideAction(): Promise<Action> {
86
+ // Priority 1: Reconcile — issues with tasks-accepted where all tasks are done
87
+ const acceptedIssues = await this.issues.listIssues({labels: [LABELS.TASKS_ACCEPTED]});
88
+ for (const issue of acceptedIssues) {
89
+ if (!this.tasks.hasRemainingTasks(issue.number)) {
90
+ return {type: 'reconcile', issue};
91
+ }
92
+ }
93
+
94
+ // Priority 2: Implement — find an available task
95
+ const implementAction = await this.findAvailableTask(acceptedIssues);
96
+ if (implementAction) {
97
+ return implementAction;
98
+ }
99
+
100
+ // Priority 3: Investigate — find a new issue (no whitesmith labels)
101
+ const allDevPulseLabels = Object.values(LABELS);
102
+ const newIssues = await this.issues.listIssues({noLabels: allDevPulseLabels});
103
+ if (newIssues.length > 0) {
104
+ // Pick the oldest issue
105
+ const issue = newIssues[newIssues.length - 1];
106
+ return {type: 'investigate', issue};
107
+ }
108
+
109
+ return {type: 'idle'};
110
+ }
111
+
112
+ /**
113
+ * Find an available task to implement
114
+ */
115
+ private async findAvailableTask(
116
+ acceptedIssues: Issue[],
117
+ ): Promise<{type: 'implement'; task: Task; issue: Issue} | null> {
118
+ for (const issue of acceptedIssues) {
119
+ const issueTasks = this.tasks.listTasks(issue.number);
120
+
121
+ for (const task of issueTasks) {
122
+ // Check dependencies are satisfied
123
+ if (!this.tasks.areDependenciesSatisfied(task)) {
124
+ continue;
125
+ }
126
+
127
+ // Check if someone is already working on it (branch exists)
128
+ const branch = `task/${task.id}`;
129
+ const branchExists = await this.issues.remoteBranchExists(branch);
130
+ if (branchExists) {
131
+ continue;
132
+ }
133
+
134
+ return {type: 'implement', task, issue};
135
+ }
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * Phase 1: Reconcile — mark issue as completed, close it
143
+ */
144
+ private async reconcile(issue: Issue): Promise<void> {
145
+ console.log(`Reconciling issue #${issue.number}: ${issue.title}`);
146
+ console.log('All tasks completed. Marking issue as done.');
147
+
148
+ await this.issues.addLabel(issue.number, LABELS.COMPLETED);
149
+ await this.issues.removeLabel(issue.number, LABELS.TASKS_ACCEPTED);
150
+ await this.issues.comment(
151
+ issue.number,
152
+ `✅ All tasks for this issue have been implemented and merged. Closing.`,
153
+ );
154
+ await this.issues.closeIssue(issue.number);
155
+
156
+ console.log(`Issue #${issue.number} closed.`);
157
+ }
158
+
159
+ /**
160
+ * Phase 2: Investigate — generate tasks for a new issue
161
+ */
162
+ private async investigate(issue: Issue): Promise<void> {
163
+ console.log(`Investigating issue #${issue.number}: ${issue.title}`);
164
+
165
+ // Claim the issue
166
+ await this.issues.addLabel(issue.number, LABELS.INVESTIGATING);
167
+
168
+ const branch = `investigate/${issue.number}`;
169
+ const issueTasksDir = `tasks/${issue.number}`;
170
+
171
+ try {
172
+ // Create branch from main
173
+ await this.git.deleteLocalBranch(branch);
174
+ await this.git.checkout(branch, {create: true, startPoint: 'origin/main'});
175
+
176
+ // Run agent to generate tasks
177
+ const prompt = buildInvestigatePrompt(issue, issueTasksDir);
178
+ const {exitCode} = await this.agent.run({
179
+ prompt,
180
+ workDir: this.config.workDir,
181
+ logFile: this.config.logFile,
182
+ });
183
+
184
+ if (exitCode !== 0) {
185
+ console.error(`Agent failed with exit code ${exitCode}`);
186
+ await this.issues.removeLabel(issue.number, LABELS.INVESTIGATING);
187
+ await this.git.checkoutMain();
188
+ return;
189
+ }
190
+
191
+ // Verify task files were created
192
+ await this.git.verifyBranch(branch);
193
+ const tasks = this.tasks.listTasks(issue.number);
194
+ if (tasks.length === 0) {
195
+ console.error('Agent did not create any task files');
196
+ await this.issues.removeLabel(issue.number, LABELS.INVESTIGATING);
197
+ await this.git.checkoutMain();
198
+ return;
199
+ }
200
+
201
+ // Ensure changes are committed
202
+ await this.git.commitAll(`tasks(#${issue.number}): generate implementation tasks`);
203
+
204
+ console.log(`Generated ${tasks.length} task(s) for issue #${issue.number}`);
205
+
206
+ if (this.config.noPush) {
207
+ console.log(`Branch '${branch}' ready (--no-push mode)`);
208
+ } else {
209
+ // Push and create PR
210
+ await this.git.push(branch);
211
+
212
+ const taskList = tasks.map((t) => `- [ ] **${t.id}**: ${t.title}`).join('\n');
213
+ const prUrl = await this.issues.createPR({
214
+ head: branch,
215
+ base: 'main',
216
+ title: `tasks(#${issue.number}): ${issue.title}`,
217
+ body: `## Generated Tasks for #${issue.number}\n\n${taskList}\n\n---\n*Generated by whitesmith from issue #${issue.number}*`,
218
+ });
219
+
220
+ await this.issues.removeLabel(issue.number, LABELS.INVESTIGATING);
221
+ await this.issues.addLabel(issue.number, LABELS.TASKS_PROPOSED);
222
+ await this.issues.comment(
223
+ issue.number,
224
+ `📋 Tasks have been generated. Review the PR: ${prUrl}`,
225
+ );
226
+
227
+ console.log(`PR created: ${prUrl}`);
228
+ }
229
+ } catch (error) {
230
+ console.error('Investigation failed:', error instanceof Error ? error.message : error);
231
+ await this.issues.removeLabel(issue.number, LABELS.INVESTIGATING);
232
+ }
233
+
234
+ // Return to main
235
+ await this.git.checkoutMain();
236
+ }
237
+
238
+ /**
239
+ * Phase 3: Implement — implement a task and create a PR
240
+ */
241
+ private async implement(task: Task, issue: Issue): Promise<void> {
242
+ console.log(`Implementing task ${task.id}: ${task.title}`);
243
+ console.log(`For issue #${issue.number}: ${issue.title}`);
244
+
245
+ const branch = `task/${task.id}`;
246
+
247
+ try {
248
+ // Create branch from main
249
+ await this.git.deleteLocalBranch(branch);
250
+ await this.git.checkout(branch, {create: true, startPoint: 'origin/main'});
251
+
252
+ // Run agent to implement
253
+ const prompt = buildImplementPrompt(task, issue);
254
+ const {exitCode} = await this.agent.run({
255
+ prompt,
256
+ workDir: this.config.workDir,
257
+ logFile: this.config.logFile,
258
+ });
259
+
260
+ if (exitCode !== 0) {
261
+ console.error(`Agent failed with exit code ${exitCode}`);
262
+ await this.git.checkoutMain();
263
+ return;
264
+ }
265
+
266
+ // Verify we're still on the right branch
267
+ await this.git.verifyBranch(branch);
268
+
269
+ // Ensure changes are committed
270
+ await this.git.commitAll(`feat(#${issue.number}): ${task.title}`);
271
+
272
+ if (this.config.noPush) {
273
+ console.log(`Branch '${branch}' ready (--no-push mode)`);
274
+ } else {
275
+ // Push and create PR
276
+ await this.git.push(branch);
277
+
278
+ const prUrl = await this.issues.createPR({
279
+ head: branch,
280
+ base: 'main',
281
+ title: `feat(#${issue.number}): ${task.title}`,
282
+ body: `## Task: ${task.title}\n\nImplements task \`${task.id}\` from issue #${issue.number}.\n\n### From the task spec:\n\n${task.content}\n\n---\n*Implemented by whitesmith*\n\nCloses #${issue.number} (if all tasks are complete)`,
283
+ });
284
+
285
+ console.log(`PR created: ${prUrl}`);
286
+ }
287
+ } catch (error) {
288
+ console.error('Implementation failed:', error instanceof Error ? error.message : error);
289
+ }
290
+
291
+ // Return to main
292
+ await this.git.checkoutMain();
293
+ }
294
+ }
package/src/prompts.ts ADDED
@@ -0,0 +1,114 @@
1
+ import type {Issue, Task} from './types.js';
2
+
3
+ /**
4
+ * Build the prompt for the "investigate" phase.
5
+ * The agent reads the issue, understands the codebase, and generates task files.
6
+ */
7
+ export function buildInvestigatePrompt(issue: Issue, issueTasksDir: string): string {
8
+ return `# Task: Generate implementation tasks for Issue #${issue.number}
9
+
10
+ ## Issue
11
+ **Title:** ${issue.title}
12
+ **URL:** ${issue.url}
13
+
14
+ ### Description
15
+ ${issue.body}
16
+
17
+ ## Your Job
18
+
19
+ You are an AI assistant helping break down a GitHub issue into concrete implementation tasks.
20
+
21
+ 1. **Read and understand** the issue above.
22
+ 2. **Explore the codebase** to understand the architecture, conventions, and relevant code.
23
+ 3. **Break the issue down** into 1 or more tasks. Each task should represent a single, reviewable PR's worth of work.
24
+ 4. **Write task files** to the \`${issueTasksDir}\` directory.
25
+
26
+ ## Task File Format
27
+
28
+ Each task file should be named \`<seq>-<short-slug>.md\` (e.g., \`001-add-validation.md\`) and contain:
29
+
30
+ \`\`\`markdown
31
+ ---
32
+ id: "${issue.number}-<seq>"
33
+ issue: ${issue.number}
34
+ title: "<concise title>"
35
+ depends_on: []
36
+ ---
37
+
38
+ ## Description
39
+ <detailed description of what needs to be done>
40
+
41
+ ## Acceptance Criteria
42
+ - <criterion 1>
43
+ - <criterion 2>
44
+
45
+ ## Implementation Notes
46
+ <any relevant notes about approach, files to modify, etc.>
47
+ \`\`\`
48
+
49
+ ## Rules
50
+
51
+ - Sequence numbers start at 001 and increment.
52
+ - The \`id\` field must be \`"${issue.number}-<seq>"\` (e.g., "${issue.number}-001").
53
+ - Use \`depends_on\` to list task IDs that must be completed before this task. For example, if task 002 depends on task 001, set \`depends_on: ["${issue.number}-001"]\`.
54
+ - Each task should be a meaningful, self-contained unit of work that results in one PR.
55
+ - Be specific in descriptions and acceptance criteria — another AI agent will implement these.
56
+ - Consider the existing codebase patterns and conventions.
57
+ - Create the \`${issueTasksDir}\` directory if it doesn't exist.
58
+
59
+ ## When Done
60
+
61
+ After creating all task files, commit your changes:
62
+ \`\`\`
63
+ git add tasks/
64
+ git commit -m "tasks(#${issue.number}): generate implementation tasks"
65
+ \`\`\`
66
+
67
+ Do NOT push. Do NOT create a PR. The orchestrator will handle that.
68
+ `;
69
+ }
70
+
71
+ /**
72
+ * Build the prompt for the "implement" phase.
73
+ * The agent implements a specific task and deletes the task file.
74
+ */
75
+ export function buildImplementPrompt(task: Task, issue: Issue): string {
76
+ return `# Task: Implement "${task.title}"
77
+
78
+ ## Context
79
+
80
+ You are implementing a task generated from GitHub Issue #${issue.number}: "${issue.title}"
81
+
82
+ **Issue URL:** ${issue.url}
83
+ **Task ID:** ${task.id}
84
+ **Task File:** ${task.filePath}
85
+
86
+ ## Task Details
87
+
88
+ ${task.content}
89
+
90
+ ## Your Job
91
+
92
+ 1. **Read the task** above carefully.
93
+ 2. **Explore the codebase** to understand the architecture and conventions.
94
+ 3. **Implement the changes** described in the task.
95
+ 4. **Verify** your implementation meets the acceptance criteria.
96
+ 5. **Delete the task file** at \`${task.filePath}\` — this marks the task as complete.
97
+ 6. **Clean up**: if the task directory \`tasks/${task.issue}/\` is now empty, delete it too.
98
+ 7. **Commit** all changes (implementation + task file deletion):
99
+
100
+ \`\`\`
101
+ git add -A
102
+ git commit -m "feat(#${issue.number}): ${task.title}"
103
+ \`\`\`
104
+
105
+ ## Rules
106
+
107
+ - Follow existing code conventions and patterns.
108
+ - Make clean, reviewable changes.
109
+ - Do NOT push. Do NOT create a PR. The orchestrator will handle that.
110
+ - Do NOT modify other task files.
111
+ - You MUST delete \`${task.filePath}\` as part of your commit.
112
+ - If the \`tasks/${task.issue}/\` directory is empty after deletion, remove it.
113
+ `;
114
+ }