lalph 0.1.17 → 0.1.18
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/cli.mjs +107 -58
- package/package.json +1 -1
- package/src/Github.ts +10 -9
- package/src/Linear.ts +13 -1
- package/src/Prd.ts +37 -23
- package/src/PromptGen.ts +40 -22
- package/src/Runner.ts +44 -4
package/dist/cli.mjs
CHANGED
|
@@ -133880,7 +133880,13 @@ const LinearIssueSource = effect(IssueSource, gen(function* () {
|
|
|
133880
133880
|
"started",
|
|
133881
133881
|
"completed"
|
|
133882
133882
|
] } }
|
|
133883
|
-
} })).pipe(
|
|
133883
|
+
} })).pipe(filter((issue) => {
|
|
133884
|
+
const completedAt = issue.completedAt;
|
|
133885
|
+
if (!completedAt) return true;
|
|
133886
|
+
const completed = makeUnsafe$3(completedAt);
|
|
133887
|
+
const threeDaysAgo = nowUnsafe().pipe(subtract({ days: 3 }));
|
|
133888
|
+
return isGreaterThanOrEqualTo$1(completed, threeDaysAgo);
|
|
133889
|
+
}), mapEffect$1(fnUntraced(function* (issue) {
|
|
133884
133890
|
identifierMap.set(issue.identifier, issue.id);
|
|
133885
133891
|
const state = linear.states.get(issue.stateId);
|
|
133886
133892
|
const blockedBy = yield* runCollect(linear.blockedBy(issue));
|
|
@@ -133891,7 +133897,7 @@ const LinearIssueSource = effect(IssueSource, gen(function* () {
|
|
|
133891
133897
|
priority: issue.priority,
|
|
133892
133898
|
estimate: issue.estimate ?? null,
|
|
133893
133899
|
stateId: issue.stateId,
|
|
133894
|
-
complete: state.type === "completed",
|
|
133900
|
+
complete: state.type === "completed" || state.name.toLowerCase().includes("review"),
|
|
133895
133901
|
blockedBy: blockedBy.map((i) => i.identifier),
|
|
133896
133902
|
githubPrNumber: null
|
|
133897
133903
|
});
|
|
@@ -140185,14 +140191,15 @@ const GithubIssueSource = effect(IssueSource, gen(function* () {
|
|
|
140185
140191
|
labels: getOrUndefined(labelFilter$1)
|
|
140186
140192
|
})).pipe(merge$2(recentlyClosed), filter((issue) => issue.pull_request === void 0), mapEffect$1(fnUntraced(function* (issue) {
|
|
140187
140193
|
const dependencies = yield* listOpenBlockedBy(issue.number).pipe(runCollect);
|
|
140194
|
+
const stateId = issue.state === "closed" ? "closed" : hasLabel(issue.labels, "in-progress") ? "in-progress" : hasLabel(issue.labels, "in-review") ? "in-review" : "open";
|
|
140188
140195
|
return new PrdIssue({
|
|
140189
140196
|
id: `#${issue.number}`,
|
|
140190
140197
|
title: issue.title,
|
|
140191
140198
|
description: issue.body ?? "",
|
|
140192
140199
|
priority: 0,
|
|
140193
140200
|
estimate: null,
|
|
140194
|
-
stateId
|
|
140195
|
-
complete:
|
|
140201
|
+
stateId,
|
|
140202
|
+
complete: stateId === "closed" || stateId === "in-review",
|
|
140196
140203
|
blockedBy: dependencies.map((dep) => `#${dep.number}`),
|
|
140197
140204
|
githubPrNumber: null
|
|
140198
140205
|
});
|
|
@@ -140390,10 +140397,11 @@ To remove a task, simply delete the item from the prd.yml file.
|
|
|
140390
140397
|
\`\`\`json
|
|
140391
140398
|
${JSON.stringify(PrdIssue.jsonSchema, null, 2)}
|
|
140392
140399
|
\`\`\``;
|
|
140393
|
-
const
|
|
140400
|
+
const promptChoose = `# Instructions
|
|
140394
140401
|
|
|
140395
|
-
|
|
140396
|
-
|
|
140402
|
+
Your job is to choose the next task to work on from the prd.yml file. **DO NOT** implement the task yet.
|
|
140403
|
+
|
|
140404
|
+
The following instructions should be done without interaction or asking for permission.
|
|
140397
140405
|
|
|
140398
140406
|
1. Decide which single task to work on next from the prd.yml file. This should
|
|
140399
140407
|
be the task YOU decide as the most important to work on next, not just the
|
|
@@ -140403,7 +140411,39 @@ permission.
|
|
|
140403
140411
|
2. **Before doing anything else**, mark the task as "in progress" by updating its
|
|
140404
140412
|
\`stateId\` in the prd.yml file.
|
|
140405
140413
|
This prevents other people or agents from working on the same task simultaneously.
|
|
140406
|
-
3.
|
|
140414
|
+
3. Research the task. If it seems like too many steps are needed to complete the task,
|
|
140415
|
+
break it down into smaller tasks and add them to the prd.yml file, marking the
|
|
140416
|
+
original task as "closed" by updating its \`stateId\`.
|
|
140417
|
+
4. Once you have chosen a task of reasonable size, save its information in a
|
|
140418
|
+
"task.json" file alongside the prd.yml file. Use the following format:
|
|
140419
|
+
|
|
140420
|
+
\`\`\`json
|
|
140421
|
+
{
|
|
140422
|
+
"id": "task id",
|
|
140423
|
+
"todoStateId": "id of the todo state",
|
|
140424
|
+
"inProgressStateId": "id of the in progress state",
|
|
140425
|
+
"reviewStateId": "id of the review state"
|
|
140426
|
+
}
|
|
140427
|
+
\`\`\`
|
|
140428
|
+
|
|
140429
|
+
## Important: Task sizing
|
|
140430
|
+
|
|
140431
|
+
If at any point you decide that a task is too large or complex to complete in a
|
|
140432
|
+
single iteration, break it down into smaller tasks and add them to the prd.yml
|
|
140433
|
+
file. Then, mark the original task as "closed" by updating its \`stateId\`.
|
|
140434
|
+
|
|
140435
|
+
Each task should be small and specific.
|
|
140436
|
+
Instead of creating tasks like "Refactor the authentication system", create
|
|
140437
|
+
smaller tasks like "Implement OAuth2 login endpoint", "Add JWT token refresh mechanism", etc.
|
|
140438
|
+
|
|
140439
|
+
${prdNotes}`;
|
|
140440
|
+
const prompt = (taskId) => `# Instructions
|
|
140441
|
+
|
|
140442
|
+
The following instructions should be done without interaction or asking for
|
|
140443
|
+
permission.
|
|
140444
|
+
|
|
140445
|
+
1. Your job is to complete the task with id \`${taskId}\` from the prd.yml file.
|
|
140446
|
+
2. Check if there is an existing Github PR for the task, otherwise create a new
|
|
140407
140447
|
branch for the task.
|
|
140408
140448
|
- If there is an existing PR, checkout the branch for that PR.
|
|
140409
140449
|
- If there is an existing PR, check if there are any new comments or requested
|
|
@@ -140412,10 +140452,7 @@ permission.
|
|
|
140412
140452
|
HEAD as the base.
|
|
140413
140453
|
- New branches should be named using the format \`{task id}/description\`.
|
|
140414
140454
|
- When checking for PR reviews, make sure to check the "reviews" field and read ALL unresolved comments.
|
|
140415
|
-
4.
|
|
140416
|
-
break it down into smaller tasks and add them to the prd.yml file, marking the
|
|
140417
|
-
original task as "closed" by updating its \`stateId\`.
|
|
140418
|
-
Otherwise, implement the task.
|
|
140455
|
+
4. Implement the task.
|
|
140419
140456
|
5. Run any checks / feedback loops, such as type checks, unit tests, or linting.
|
|
140420
140457
|
6. Create or update the pull request with your progress.
|
|
140421
140458
|
${sourceMeta.githubPrInstructions}
|
|
@@ -140429,19 +140466,6 @@ permission.
|
|
|
140429
140466
|
- If you believe the task is complete, update the \`stateId\` for "review".
|
|
140430
140467
|
Only if no "review" state exists, use a completed state.
|
|
140431
140468
|
|
|
140432
|
-
Remember, only work on a single task at a time, that you decide is the most
|
|
140433
|
-
important to work on next.
|
|
140434
|
-
|
|
140435
|
-
## Important: Task sizing
|
|
140436
|
-
|
|
140437
|
-
If at any point you decide that a task is too large or complex to complete in a
|
|
140438
|
-
single iteration, break it down into smaller tasks and add them to the prd.yml
|
|
140439
|
-
file. Then, mark the original task as "closed" by updating its \`stateId\`.
|
|
140440
|
-
|
|
140441
|
-
Each task should be small and specific.
|
|
140442
|
-
Instead of creating tasks like "Refactor the authentication system", create
|
|
140443
|
-
smaller tasks like "Implement OAuth2 login endpoint", "Add JWT token refresh mechanism", etc.
|
|
140444
|
-
|
|
140445
140469
|
## Handling blockers
|
|
140446
140470
|
|
|
140447
140471
|
If for any reason you get stuck on a task, mark the task back as "todo" by updating its
|
|
@@ -140473,6 +140497,7 @@ Users idea / request: ${idea}
|
|
|
140473
140497
|
|
|
140474
140498
|
${prdNotes}`;
|
|
140475
140499
|
return {
|
|
140500
|
+
promptChoose,
|
|
140476
140501
|
prompt,
|
|
140477
140502
|
planPrompt,
|
|
140478
140503
|
planContinuePrompt: `# Instructions
|
|
@@ -140561,16 +140586,10 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
|
140561
140586
|
stateId: issue.stateId,
|
|
140562
140587
|
blockedBy: issue.blockedBy
|
|
140563
140588
|
});
|
|
140564
|
-
|
|
140565
|
-
|
|
140566
|
-
|
|
140567
|
-
|
|
140568
|
-
originalStateId: existing.stateId,
|
|
140569
|
-
count: 0
|
|
140570
|
-
};
|
|
140571
|
-
updatedIssues.set(issue.id, entry);
|
|
140572
|
-
}
|
|
140573
|
-
entry.count++;
|
|
140589
|
+
if (!updatedIssues.has(issue.id)) updatedIssues.set(issue.id, {
|
|
140590
|
+
issue,
|
|
140591
|
+
originalStateId: existing.stateId
|
|
140592
|
+
});
|
|
140574
140593
|
}
|
|
140575
140594
|
yield* forEach$1(toRemove, (issueId) => source.cancelIssue(issueId), { concurrency: "unbounded" });
|
|
140576
140595
|
current = yield* source.issues;
|
|
@@ -140583,28 +140602,35 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
|
140583
140602
|
});
|
|
140584
140603
|
})));
|
|
140585
140604
|
}).pipe(uninterruptible);
|
|
140586
|
-
const mergableGithubPrs = gen(function* () {
|
|
140587
|
-
const yaml = yield* fs.readFileString(prdFile);
|
|
140588
|
-
const updated = PrdIssue.arrayFromYaml(yaml);
|
|
140589
|
-
const prs = empty$11();
|
|
140590
|
-
for (const issue of updated) {
|
|
140591
|
-
const entry = updatedIssues.get(issue.id ?? "");
|
|
140592
|
-
if (!entry || !issue.githubPrNumber || issue.stateId === entry.originalStateId) continue;
|
|
140593
|
-
prs.push(issue.githubPrNumber);
|
|
140594
|
-
}
|
|
140595
|
-
return prs;
|
|
140596
|
-
});
|
|
140597
|
-
const revertStateIds = (options) => suspend$2(() => forEach$1(updatedIssues.values(), ({ issue, originalStateId, count }) => options.reason === "error" || count === 1 ? source.updateIssue({
|
|
140598
|
-
issueId: issue.id,
|
|
140599
|
-
stateId: originalStateId
|
|
140600
|
-
}) : void_$1, {
|
|
140601
|
-
concurrency: "unbounded",
|
|
140602
|
-
discard: true
|
|
140603
|
-
}));
|
|
140604
140605
|
return {
|
|
140605
140606
|
path: prdFile,
|
|
140606
|
-
mergableGithubPrs
|
|
140607
|
-
|
|
140607
|
+
mergableGithubPrs: gen(function* () {
|
|
140608
|
+
const yaml = yield* fs.readFileString(prdFile);
|
|
140609
|
+
const updated = PrdIssue.arrayFromYaml(yaml);
|
|
140610
|
+
const prs = empty$11();
|
|
140611
|
+
for (const issue of updated) {
|
|
140612
|
+
const entry = updatedIssues.get(issue.id ?? "");
|
|
140613
|
+
if (!entry || !issue.githubPrNumber || issue.stateId === entry.originalStateId) continue;
|
|
140614
|
+
prs.push(issue.githubPrNumber);
|
|
140615
|
+
}
|
|
140616
|
+
return prs;
|
|
140617
|
+
}),
|
|
140618
|
+
revertStateIds: suspend$2(() => forEach$1(updatedIssues.values(), ({ issue, originalStateId }) => source.updateIssue({
|
|
140619
|
+
issueId: issue.id,
|
|
140620
|
+
stateId: originalStateId
|
|
140621
|
+
}), {
|
|
140622
|
+
concurrency: "unbounded",
|
|
140623
|
+
discard: true
|
|
140624
|
+
})),
|
|
140625
|
+
maybeRevertIssue: fnUntraced(function* (options) {
|
|
140626
|
+
const yaml = yield* fs.readFileString(prdFile);
|
|
140627
|
+
const issue = PrdIssue.arrayFromYaml(yaml).find((i) => i.id === options.issueId);
|
|
140628
|
+
if (!issue || issue.stateId === options.reviewStateId) return;
|
|
140629
|
+
yield* source.updateIssue({
|
|
140630
|
+
issueId: issue.id,
|
|
140631
|
+
stateId: options.todoStateId
|
|
140632
|
+
});
|
|
140633
|
+
})
|
|
140608
140634
|
};
|
|
140609
140635
|
}) }) {
|
|
140610
140636
|
static layer = effect(this, this.make).pipe(provide$3(Worktree.layer));
|
|
@@ -140632,13 +140658,27 @@ const getOrSelectCliAgent = gen(function* () {
|
|
|
140632
140658
|
//#endregion
|
|
140633
140659
|
//#region src/Runner.ts
|
|
140634
140660
|
const run = fnUntraced(function* (options) {
|
|
140661
|
+
const fs = yield* FileSystem;
|
|
140635
140662
|
const pathService = yield* Path$1;
|
|
140636
140663
|
const worktree = yield* Worktree;
|
|
140637
140664
|
const promptGen = yield* PromptGen;
|
|
140638
140665
|
const cliAgent = yield* getOrSelectCliAgent;
|
|
140639
140666
|
const prd = yield* Prd;
|
|
140667
|
+
const chooseCommand = cliAgent.command({
|
|
140668
|
+
prompt: promptGen.promptChoose,
|
|
140669
|
+
prdFilePath: pathService.join(".lalph", "prd.yml")
|
|
140670
|
+
});
|
|
140671
|
+
yield* make$22(chooseCommand[0], chooseCommand.slice(1), {
|
|
140672
|
+
cwd: worktree.directory,
|
|
140673
|
+
extendEnv: true,
|
|
140674
|
+
stdout: "inherit",
|
|
140675
|
+
stderr: "inherit",
|
|
140676
|
+
stdin: "inherit"
|
|
140677
|
+
}).pipe(exitCode);
|
|
140678
|
+
const taskJson = yield* fs.readFileString(pathService.join(worktree.directory, ".lalph", "task.json"));
|
|
140679
|
+
const task = yield* decodeEffect(ChosenTask)(taskJson);
|
|
140640
140680
|
const cliCommand = cliAgent.command({
|
|
140641
|
-
prompt: promptGen.prompt,
|
|
140681
|
+
prompt: promptGen.prompt(task.id),
|
|
140642
140682
|
prdFilePath: pathService.join(".lalph", "prd.yml")
|
|
140643
140683
|
});
|
|
140644
140684
|
const handle = yield* make$22(cliCommand[0], cliCommand.slice(1), {
|
|
@@ -140664,11 +140704,14 @@ const run = fnUntraced(function* (options) {
|
|
|
140664
140704
|
const exitCode$1 = yield* handle.exitCode;
|
|
140665
140705
|
yield* log$1(`Agent exited with code: ${exitCode$1}`);
|
|
140666
140706
|
const prs = yield* prd.mergableGithubPrs;
|
|
140667
|
-
if (prs.length === 0) yield* prd.
|
|
140707
|
+
if (prs.length === 0) yield* prd.maybeRevertIssue({
|
|
140708
|
+
...task,
|
|
140709
|
+
issueId: task.id
|
|
140710
|
+
});
|
|
140668
140711
|
else if (options.autoMerge) for (const pr of prs) yield* make$22`gh pr merge ${pr} -sd`.pipe(exitCode);
|
|
140669
140712
|
}, onError(fnUntraced(function* () {
|
|
140670
140713
|
const prd = yield* Prd;
|
|
140671
|
-
yield* ignore(prd.revertStateIds
|
|
140714
|
+
yield* ignore(prd.revertStateIds);
|
|
140672
140715
|
})), scoped$1, provide$1([
|
|
140673
140716
|
PromptGen.layer,
|
|
140674
140717
|
Prd.layer,
|
|
@@ -140677,6 +140720,12 @@ const run = fnUntraced(function* (options) {
|
|
|
140677
140720
|
var RunnerStalled = class extends TaggedError("RunnerStalled") {
|
|
140678
140721
|
message = "The runner has stalled due to inactivity.";
|
|
140679
140722
|
};
|
|
140723
|
+
const ChosenTask = fromJsonString(Struct({
|
|
140724
|
+
id: String$1,
|
|
140725
|
+
todoStateId: String$1,
|
|
140726
|
+
inProgressStateId: String$1,
|
|
140727
|
+
reviewStateId: String$1
|
|
140728
|
+
}));
|
|
140680
140729
|
|
|
140681
140730
|
//#endregion
|
|
140682
140731
|
//#region src/Planner.ts
|
package/package.json
CHANGED
package/src/Github.ts
CHANGED
|
@@ -185,21 +185,22 @@ export const GithubIssueSource = Layer.effect(
|
|
|
185
185
|
const dependencies = yield* listOpenBlockedBy(issue.number).pipe(
|
|
186
186
|
Stream.runCollect,
|
|
187
187
|
)
|
|
188
|
+
const stateId =
|
|
189
|
+
issue.state === "closed"
|
|
190
|
+
? "closed"
|
|
191
|
+
: hasLabel(issue.labels, "in-progress")
|
|
192
|
+
? "in-progress"
|
|
193
|
+
: hasLabel(issue.labels, "in-review")
|
|
194
|
+
? "in-review"
|
|
195
|
+
: "open"
|
|
188
196
|
return new PrdIssue({
|
|
189
197
|
id: `#${issue.number}`,
|
|
190
198
|
title: issue.title,
|
|
191
199
|
description: issue.body ?? "",
|
|
192
200
|
priority: 0,
|
|
193
201
|
estimate: null,
|
|
194
|
-
stateId
|
|
195
|
-
|
|
196
|
-
? "closed"
|
|
197
|
-
: hasLabel(issue.labels, "in-progress")
|
|
198
|
-
? "in-progress"
|
|
199
|
-
: hasLabel(issue.labels, "in-review")
|
|
200
|
-
? "in-review"
|
|
201
|
-
: "open",
|
|
202
|
-
complete: issue.state === "closed",
|
|
202
|
+
stateId,
|
|
203
|
+
complete: stateId === "closed" || stateId === "in-review",
|
|
203
204
|
blockedBy: dependencies.map((dep) => `#${dep.number}`),
|
|
204
205
|
githubPrNumber: null,
|
|
205
206
|
})
|
package/src/Linear.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ServiceMap,
|
|
7
7
|
Option,
|
|
8
8
|
RcMap,
|
|
9
|
+
DateTime,
|
|
9
10
|
} from "effect"
|
|
10
11
|
import {
|
|
11
12
|
Connection,
|
|
@@ -168,6 +169,15 @@ export const LinearIssueSource = Layer.effect(
|
|
|
168
169
|
}),
|
|
169
170
|
)
|
|
170
171
|
.pipe(
|
|
172
|
+
Stream.filter((issue) => {
|
|
173
|
+
const completedAt = issue.completedAt
|
|
174
|
+
if (!completedAt) return true
|
|
175
|
+
const completed = DateTime.makeUnsafe(completedAt)
|
|
176
|
+
const threeDaysAgo = DateTime.nowUnsafe().pipe(
|
|
177
|
+
DateTime.subtract({ days: 3 }),
|
|
178
|
+
)
|
|
179
|
+
return DateTime.isGreaterThanOrEqualTo(completed, threeDaysAgo)
|
|
180
|
+
}),
|
|
171
181
|
Stream.mapEffect(
|
|
172
182
|
Effect.fnUntraced(function* (issue) {
|
|
173
183
|
identifierMap.set(issue.identifier, issue.id)
|
|
@@ -180,7 +190,9 @@ export const LinearIssueSource = Layer.effect(
|
|
|
180
190
|
priority: issue.priority,
|
|
181
191
|
estimate: issue.estimate ?? null,
|
|
182
192
|
stateId: issue.stateId!,
|
|
183
|
-
complete:
|
|
193
|
+
complete:
|
|
194
|
+
state.type === "completed" ||
|
|
195
|
+
state.name.toLowerCase().includes("review"),
|
|
184
196
|
blockedBy: blockedBy.map((i) => i.identifier),
|
|
185
197
|
githubPrNumber: null,
|
|
186
198
|
})
|
package/src/Prd.ts
CHANGED
|
@@ -30,7 +30,6 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
30
30
|
{
|
|
31
31
|
readonly issue: PrdIssue
|
|
32
32
|
readonly originalStateId: string
|
|
33
|
-
count: number
|
|
34
33
|
}
|
|
35
34
|
>()
|
|
36
35
|
|
|
@@ -84,12 +83,12 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
84
83
|
blockedBy: issue.blockedBy,
|
|
85
84
|
})
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
if (!updatedIssues.has(issue.id!)) {
|
|
87
|
+
updatedIssues.set(issue.id, {
|
|
88
|
+
issue,
|
|
89
|
+
originalStateId: existing.stateId,
|
|
90
|
+
})
|
|
91
91
|
}
|
|
92
|
-
entry.count++
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
yield* Effect.forEach(
|
|
@@ -129,24 +128,39 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
129
128
|
return prs
|
|
130
129
|
})
|
|
131
130
|
|
|
132
|
-
const revertStateIds = (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
131
|
+
const revertStateIds = Effect.suspend(() =>
|
|
132
|
+
Effect.forEach(
|
|
133
|
+
updatedIssues.values(),
|
|
134
|
+
({ issue, originalStateId }) =>
|
|
135
|
+
source.updateIssue({
|
|
136
|
+
issueId: issue.id!,
|
|
137
|
+
stateId: originalStateId,
|
|
138
|
+
}),
|
|
139
|
+
{ concurrency: "unbounded", discard: true },
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
const maybeRevertIssue = Effect.fnUntraced(function* (options: {
|
|
143
|
+
readonly issueId: string
|
|
144
|
+
readonly todoStateId: string
|
|
145
|
+
readonly inProgressStateId: string
|
|
146
|
+
readonly reviewStateId: string
|
|
147
|
+
}) {
|
|
148
|
+
const yaml = yield* fs.readFileString(prdFile)
|
|
149
|
+
const updated = PrdIssue.arrayFromYaml(yaml)
|
|
150
|
+
const issue = updated.find((i) => i.id === options.issueId)
|
|
151
|
+
if (!issue || issue.stateId === options.reviewStateId) return
|
|
152
|
+
yield* source.updateIssue({
|
|
153
|
+
issueId: issue.id!,
|
|
154
|
+
stateId: options.todoStateId,
|
|
155
|
+
})
|
|
156
|
+
})
|
|
148
157
|
|
|
149
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
path: prdFile,
|
|
160
|
+
mergableGithubPrs,
|
|
161
|
+
revertStateIds,
|
|
162
|
+
maybeRevertIssue,
|
|
163
|
+
} as const
|
|
150
164
|
}),
|
|
151
165
|
}) {
|
|
152
166
|
static layer = Layer.effect(this, this.make).pipe(
|
package/src/PromptGen.ts
CHANGED
|
@@ -38,10 +38,11 @@ To remove a task, simply delete the item from the prd.yml file.
|
|
|
38
38
|
${JSON.stringify(PrdIssue.jsonSchema, null, 2)}
|
|
39
39
|
\`\`\``
|
|
40
40
|
|
|
41
|
-
const
|
|
41
|
+
const promptChoose = `# Instructions
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
Your job is to choose the next task to work on from the prd.yml file. **DO NOT** implement the task yet.
|
|
44
|
+
|
|
45
|
+
The following instructions should be done without interaction or asking for permission.
|
|
45
46
|
|
|
46
47
|
1. Decide which single task to work on next from the prd.yml file. This should
|
|
47
48
|
be the task YOU decide as the most important to work on next, not just the
|
|
@@ -51,7 +52,40 @@ permission.
|
|
|
51
52
|
2. **Before doing anything else**, mark the task as "in progress" by updating its
|
|
52
53
|
\`stateId\` in the prd.yml file.
|
|
53
54
|
This prevents other people or agents from working on the same task simultaneously.
|
|
54
|
-
3.
|
|
55
|
+
3. Research the task. If it seems like too many steps are needed to complete the task,
|
|
56
|
+
break it down into smaller tasks and add them to the prd.yml file, marking the
|
|
57
|
+
original task as "closed" by updating its \`stateId\`.
|
|
58
|
+
4. Once you have chosen a task of reasonable size, save its information in a
|
|
59
|
+
"task.json" file alongside the prd.yml file. Use the following format:
|
|
60
|
+
|
|
61
|
+
\`\`\`json
|
|
62
|
+
{
|
|
63
|
+
"id": "task id",
|
|
64
|
+
"todoStateId": "id of the todo state",
|
|
65
|
+
"inProgressStateId": "id of the in progress state",
|
|
66
|
+
"reviewStateId": "id of the review state"
|
|
67
|
+
}
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
## Important: Task sizing
|
|
71
|
+
|
|
72
|
+
If at any point you decide that a task is too large or complex to complete in a
|
|
73
|
+
single iteration, break it down into smaller tasks and add them to the prd.yml
|
|
74
|
+
file. Then, mark the original task as "closed" by updating its \`stateId\`.
|
|
75
|
+
|
|
76
|
+
Each task should be small and specific.
|
|
77
|
+
Instead of creating tasks like "Refactor the authentication system", create
|
|
78
|
+
smaller tasks like "Implement OAuth2 login endpoint", "Add JWT token refresh mechanism", etc.
|
|
79
|
+
|
|
80
|
+
${prdNotes}`
|
|
81
|
+
|
|
82
|
+
const prompt = (taskId: string) => `# Instructions
|
|
83
|
+
|
|
84
|
+
The following instructions should be done without interaction or asking for
|
|
85
|
+
permission.
|
|
86
|
+
|
|
87
|
+
1. Your job is to complete the task with id \`${taskId}\` from the prd.yml file.
|
|
88
|
+
2. Check if there is an existing Github PR for the task, otherwise create a new
|
|
55
89
|
branch for the task.
|
|
56
90
|
- If there is an existing PR, checkout the branch for that PR.
|
|
57
91
|
- If there is an existing PR, check if there are any new comments or requested
|
|
@@ -60,10 +94,7 @@ permission.
|
|
|
60
94
|
HEAD as the base.
|
|
61
95
|
- New branches should be named using the format \`{task id}/description\`.
|
|
62
96
|
- When checking for PR reviews, make sure to check the "reviews" field and read ALL unresolved comments.
|
|
63
|
-
4.
|
|
64
|
-
break it down into smaller tasks and add them to the prd.yml file, marking the
|
|
65
|
-
original task as "closed" by updating its \`stateId\`.
|
|
66
|
-
Otherwise, implement the task.
|
|
97
|
+
4. Implement the task.
|
|
67
98
|
5. Run any checks / feedback loops, such as type checks, unit tests, or linting.
|
|
68
99
|
6. Create or update the pull request with your progress.
|
|
69
100
|
${sourceMeta.githubPrInstructions}
|
|
@@ -77,19 +108,6 @@ permission.
|
|
|
77
108
|
- If you believe the task is complete, update the \`stateId\` for "review".
|
|
78
109
|
Only if no "review" state exists, use a completed state.
|
|
79
110
|
|
|
80
|
-
Remember, only work on a single task at a time, that you decide is the most
|
|
81
|
-
important to work on next.
|
|
82
|
-
|
|
83
|
-
## Important: Task sizing
|
|
84
|
-
|
|
85
|
-
If at any point you decide that a task is too large or complex to complete in a
|
|
86
|
-
single iteration, break it down into smaller tasks and add them to the prd.yml
|
|
87
|
-
file. Then, mark the original task as "closed" by updating its \`stateId\`.
|
|
88
|
-
|
|
89
|
-
Each task should be small and specific.
|
|
90
|
-
Instead of creating tasks like "Refactor the authentication system", create
|
|
91
|
-
smaller tasks like "Implement OAuth2 login endpoint", "Add JWT token refresh mechanism", etc.
|
|
92
|
-
|
|
93
111
|
## Handling blockers
|
|
94
112
|
|
|
95
113
|
If for any reason you get stuck on a task, mark the task back as "todo" by updating its
|
|
@@ -141,7 +159,7 @@ ${prdNotes}`
|
|
|
141
159
|
|
|
142
160
|
${prdNotes}`
|
|
143
161
|
|
|
144
|
-
return { prompt, planPrompt, planContinuePrompt } as const
|
|
162
|
+
return { promptChoose, prompt, planPrompt, planContinuePrompt } as const
|
|
145
163
|
}),
|
|
146
164
|
},
|
|
147
165
|
) {
|
package/src/Runner.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Data,
|
|
3
|
+
DateTime,
|
|
4
|
+
Duration,
|
|
5
|
+
Effect,
|
|
6
|
+
FileSystem,
|
|
7
|
+
Path,
|
|
8
|
+
Schema,
|
|
9
|
+
Stream,
|
|
10
|
+
} from "effect"
|
|
2
11
|
import { PromptGen } from "./PromptGen.ts"
|
|
3
12
|
import { Prd } from "./Prd.ts"
|
|
4
13
|
import { ChildProcess } from "effect/unstable/process"
|
|
@@ -10,14 +19,33 @@ export const run = Effect.fnUntraced(
|
|
|
10
19
|
readonly autoMerge: boolean
|
|
11
20
|
readonly stallTimeout: Duration.Duration
|
|
12
21
|
}) {
|
|
22
|
+
const fs = yield* FileSystem.FileSystem
|
|
13
23
|
const pathService = yield* Path.Path
|
|
14
24
|
const worktree = yield* Worktree
|
|
15
25
|
const promptGen = yield* PromptGen
|
|
16
26
|
const cliAgent = yield* getOrSelectCliAgent
|
|
17
27
|
const prd = yield* Prd
|
|
18
28
|
|
|
29
|
+
const chooseCommand = cliAgent.command({
|
|
30
|
+
prompt: promptGen.promptChoose,
|
|
31
|
+
prdFilePath: pathService.join(".lalph", "prd.yml"),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
yield* ChildProcess.make(chooseCommand[0]!, chooseCommand.slice(1), {
|
|
35
|
+
cwd: worktree.directory,
|
|
36
|
+
extendEnv: true,
|
|
37
|
+
stdout: "inherit",
|
|
38
|
+
stderr: "inherit",
|
|
39
|
+
stdin: "inherit",
|
|
40
|
+
}).pipe(ChildProcess.exitCode)
|
|
41
|
+
|
|
42
|
+
const taskJson = yield* fs.readFileString(
|
|
43
|
+
pathService.join(worktree.directory, ".lalph", "task.json"),
|
|
44
|
+
)
|
|
45
|
+
const task = yield* Schema.decodeEffect(ChosenTask)(taskJson)
|
|
46
|
+
|
|
19
47
|
const cliCommand = cliAgent.command({
|
|
20
|
-
prompt: promptGen.prompt,
|
|
48
|
+
prompt: promptGen.prompt(task.id),
|
|
21
49
|
prdFilePath: pathService.join(".lalph", "prd.yml"),
|
|
22
50
|
})
|
|
23
51
|
const handle = yield* ChildProcess.make(
|
|
@@ -62,7 +90,10 @@ export const run = Effect.fnUntraced(
|
|
|
62
90
|
|
|
63
91
|
const prs = yield* prd.mergableGithubPrs
|
|
64
92
|
if (prs.length === 0) {
|
|
65
|
-
yield* prd.
|
|
93
|
+
yield* prd.maybeRevertIssue({
|
|
94
|
+
...task,
|
|
95
|
+
issueId: task.id,
|
|
96
|
+
})
|
|
66
97
|
} else if (options.autoMerge) {
|
|
67
98
|
for (const pr of prs) {
|
|
68
99
|
yield* ChildProcess.make`gh pr merge ${pr} -sd`.pipe(
|
|
@@ -75,7 +106,7 @@ export const run = Effect.fnUntraced(
|
|
|
75
106
|
Effect.onError(
|
|
76
107
|
Effect.fnUntraced(function* () {
|
|
77
108
|
const prd = yield* Prd
|
|
78
|
-
yield* Effect.ignore(prd.revertStateIds
|
|
109
|
+
yield* Effect.ignore(prd.revertStateIds)
|
|
79
110
|
}),
|
|
80
111
|
),
|
|
81
112
|
Effect.scoped,
|
|
@@ -85,3 +116,12 @@ export const run = Effect.fnUntraced(
|
|
|
85
116
|
export class RunnerStalled extends Data.TaggedError("RunnerStalled") {
|
|
86
117
|
readonly message = "The runner has stalled due to inactivity."
|
|
87
118
|
}
|
|
119
|
+
|
|
120
|
+
const ChosenTask = Schema.fromJsonString(
|
|
121
|
+
Schema.Struct({
|
|
122
|
+
id: Schema.String,
|
|
123
|
+
todoStateId: Schema.String,
|
|
124
|
+
inProgressStateId: Schema.String,
|
|
125
|
+
reviewStateId: Schema.String,
|
|
126
|
+
}),
|
|
127
|
+
)
|