lalph 0.1.16 → 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 +136 -94
- package/package.json +3 -2
- package/src/Github.ts +10 -9
- package/src/Linear.ts +13 -1
- package/src/Planner.ts +2 -2
- package/src/Prd.ts +45 -31
- package/src/PromptGen.ts +53 -35
- package/src/Runner.ts +45 -5
- package/src/domain/PrdIssue.ts +10 -26
package/dist/cli.mjs
CHANGED
|
@@ -59594,7 +59594,7 @@ const selectedCliAgentId = new Setting("selectedCliAgentId", Literals(allCliAgen
|
|
|
59594
59594
|
|
|
59595
59595
|
//#endregion
|
|
59596
59596
|
//#region src/domain/PrdIssue.ts
|
|
59597
|
-
var PrdIssue = class extends Class("PrdIssue")({
|
|
59597
|
+
var PrdIssue = class PrdIssue extends Class("PrdIssue")({
|
|
59598
59598
|
id: NullOr(String$1).annotate({ description: "The unique identifier of the issue. If null, it is considered a new issue." }),
|
|
59599
59599
|
title: String$1.annotate({ description: "The title of the issue" }),
|
|
59600
59600
|
description: String$1.annotate({ description: "The description of the issue in markdown format." }),
|
|
@@ -59607,8 +59607,13 @@ var PrdIssue = class extends Class("PrdIssue")({
|
|
|
59607
59607
|
}) {
|
|
59608
59608
|
static Array = Array$1(this);
|
|
59609
59609
|
static ArrayFromJson = toCodecJson(this.Array);
|
|
59610
|
-
static
|
|
59611
|
-
|
|
59610
|
+
static arrayToYaml(issues) {
|
|
59611
|
+
const json = encodeSync(this.ArrayFromJson)(issues);
|
|
59612
|
+
return import_dist.stringify(json, { blockQuote: "literal" });
|
|
59613
|
+
}
|
|
59614
|
+
static arrayFromYaml(yaml) {
|
|
59615
|
+
const json = import_dist.parse(yaml);
|
|
59616
|
+
return decodeSync(PrdIssue.ArrayFromJson)(json);
|
|
59612
59617
|
}
|
|
59613
59618
|
static jsonSchemaDoc = toJsonSchemaDocument(this);
|
|
59614
59619
|
static jsonSchema = {
|
|
@@ -59619,18 +59624,6 @@ var PrdIssue = class extends Class("PrdIssue")({
|
|
|
59619
59624
|
return this.title !== issue.title || this.description !== issue.description || this.stateId !== issue.stateId || !makeEquivalence$1(asEquivalence())(this.blockedBy, issue.blockedBy);
|
|
59620
59625
|
}
|
|
59621
59626
|
};
|
|
59622
|
-
var PrdList = class extends Class$1 {
|
|
59623
|
-
static fromJson(json) {
|
|
59624
|
-
return decodeSync(PrdIssue.ArrayFromJson)(JSON.parse(json));
|
|
59625
|
-
}
|
|
59626
|
-
toJson() {
|
|
59627
|
-
const issuesArray = fromIterable$2(this.issues.values());
|
|
59628
|
-
return PrdIssue.arrayToJson(issuesArray);
|
|
59629
|
-
}
|
|
59630
|
-
cast() {
|
|
59631
|
-
return this;
|
|
59632
|
-
}
|
|
59633
|
-
};
|
|
59634
59627
|
|
|
59635
59628
|
//#endregion
|
|
59636
59629
|
//#region src/IssueSource.ts
|
|
@@ -133887,7 +133880,13 @@ const LinearIssueSource = effect(IssueSource, gen(function* () {
|
|
|
133887
133880
|
"started",
|
|
133888
133881
|
"completed"
|
|
133889
133882
|
] } }
|
|
133890
|
-
} })).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) {
|
|
133891
133890
|
identifierMap.set(issue.identifier, issue.id);
|
|
133892
133891
|
const state = linear.states.get(issue.stateId);
|
|
133893
133892
|
const blockedBy = yield* runCollect(linear.blockedBy(issue));
|
|
@@ -133898,7 +133897,7 @@ const LinearIssueSource = effect(IssueSource, gen(function* () {
|
|
|
133898
133897
|
priority: issue.priority,
|
|
133899
133898
|
estimate: issue.estimate ?? null,
|
|
133900
133899
|
stateId: issue.stateId,
|
|
133901
|
-
complete: state.type === "completed",
|
|
133900
|
+
complete: state.type === "completed" || state.name.toLowerCase().includes("review"),
|
|
133902
133901
|
blockedBy: blockedBy.map((i) => i.identifier),
|
|
133903
133902
|
githubPrNumber: null
|
|
133904
133903
|
});
|
|
@@ -140192,14 +140191,15 @@ const GithubIssueSource = effect(IssueSource, gen(function* () {
|
|
|
140192
140191
|
labels: getOrUndefined(labelFilter$1)
|
|
140193
140192
|
})).pipe(merge$2(recentlyClosed), filter((issue) => issue.pull_request === void 0), mapEffect$1(fnUntraced(function* (issue) {
|
|
140194
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";
|
|
140195
140195
|
return new PrdIssue({
|
|
140196
140196
|
id: `#${issue.number}`,
|
|
140197
140197
|
title: issue.title,
|
|
140198
140198
|
description: issue.body ?? "",
|
|
140199
140199
|
priority: 0,
|
|
140200
140200
|
estimate: null,
|
|
140201
|
-
stateId
|
|
140202
|
-
complete:
|
|
140201
|
+
stateId,
|
|
140202
|
+
complete: stateId === "closed" || stateId === "in-review",
|
|
140203
140203
|
blockedBy: dependencies.map((dep) => `#${dep.number}`),
|
|
140204
140204
|
githubPrNumber: null
|
|
140205
140205
|
});
|
|
@@ -140371,9 +140371,9 @@ var CurrentIssueSource = class CurrentIssueSource extends Service()("lalph/Curre
|
|
|
140371
140371
|
var PromptGen = class extends Service()("lalph/PromptGen", { make: gen(function* () {
|
|
140372
140372
|
const sourceMeta = yield* CurrentIssueSource;
|
|
140373
140373
|
const states = yield* (yield* IssueSource).states;
|
|
140374
|
-
const prdNotes = `## prd.
|
|
140374
|
+
const prdNotes = `## prd.yml format
|
|
140375
140375
|
|
|
140376
|
-
Each item in the prd.
|
|
140376
|
+
Each item in the prd.yml file represents a task for the current project.
|
|
140377
140377
|
|
|
140378
140378
|
The \`stateId\` field indicates the current state of the task. The possible states
|
|
140379
140379
|
are:
|
|
@@ -140382,35 +140382,68 @@ ${Array.from(states.values(), (state) => `- **${state.name}** (stateId: \`${stat
|
|
|
140382
140382
|
|
|
140383
140383
|
### Adding tasks
|
|
140384
140384
|
|
|
140385
|
-
To add a new task, append a new item to the prd.
|
|
140385
|
+
To add a new task, append a new item to the prd.yml file with the id set to
|
|
140386
140386
|
\`null\`.
|
|
140387
140387
|
|
|
140388
140388
|
When adding a new task, it will take about 5 seconds for the system to update the
|
|
140389
|
-
prd.
|
|
140389
|
+
prd.yml file with a new id for the task.
|
|
140390
140390
|
|
|
140391
140391
|
### Removing tasks
|
|
140392
140392
|
|
|
140393
|
-
To remove a task, simply delete the item from the prd.
|
|
140393
|
+
To remove a task, simply delete the item from the prd.yml file.
|
|
140394
140394
|
|
|
140395
|
-
### prd.
|
|
140395
|
+
### prd.yml json schema
|
|
140396
140396
|
|
|
140397
140397
|
\`\`\`json
|
|
140398
140398
|
${JSON.stringify(PrdIssue.jsonSchema, null, 2)}
|
|
140399
140399
|
\`\`\``;
|
|
140400
|
-
const
|
|
140400
|
+
const promptChoose = `# Instructions
|
|
140401
140401
|
|
|
140402
|
-
|
|
140403
|
-
|
|
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.
|
|
140404
140405
|
|
|
140405
|
-
1. Decide which single task to work on next from the prd.
|
|
140406
|
+
1. Decide which single task to work on next from the prd.yml file. This should
|
|
140406
140407
|
be the task YOU decide as the most important to work on next, not just the
|
|
140407
140408
|
first task in the list.
|
|
140408
140409
|
- Only start tasks that are in a "todo" state (i.e., not started yet).
|
|
140409
140410
|
- If the \`blockedBy\` field is not empty, skip the task.
|
|
140410
140411
|
2. **Before doing anything else**, mark the task as "in progress" by updating its
|
|
140411
|
-
\`stateId\` in the prd.
|
|
140412
|
+
\`stateId\` in the prd.yml file.
|
|
140412
140413
|
This prevents other people or agents from working on the same task simultaneously.
|
|
140413
|
-
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
|
|
140414
140447
|
branch for the task.
|
|
140415
140448
|
- If there is an existing PR, checkout the branch for that PR.
|
|
140416
140449
|
- If there is an existing PR, check if there are any new comments or requested
|
|
@@ -140419,10 +140452,7 @@ permission.
|
|
|
140419
140452
|
HEAD as the base.
|
|
140420
140453
|
- New branches should be named using the format \`{task id}/description\`.
|
|
140421
140454
|
- When checking for PR reviews, make sure to check the "reviews" field and read ALL unresolved comments.
|
|
140422
|
-
4.
|
|
140423
|
-
break it down into smaller tasks and add them to the prd.json file, marking the
|
|
140424
|
-
original task as "closed" by updating its \`stateId\`.
|
|
140425
|
-
Otherwise, implement the task.
|
|
140455
|
+
4. Implement the task.
|
|
140426
140456
|
5. Run any checks / feedback loops, such as type checks, unit tests, or linting.
|
|
140427
140457
|
6. Create or update the pull request with your progress.
|
|
140428
140458
|
${sourceMeta.githubPrInstructions}
|
|
@@ -140430,25 +140460,12 @@ permission.
|
|
|
140430
140460
|
- None of the files in the \`.lalph\` directory should be committed.
|
|
140431
140461
|
- You have permission to create or update the PR as needed. You have full
|
|
140432
140462
|
permission to push branches, create PRs or create git commits.
|
|
140433
|
-
7. Update the prd.
|
|
140463
|
+
7. Update the prd.yml file to reflect any changes in task states.
|
|
140434
140464
|
- Add follow up tasks only if needed.
|
|
140435
140465
|
- Append to the \`description\` field with any notes or important discoveries.
|
|
140436
140466
|
- If you believe the task is complete, update the \`stateId\` for "review".
|
|
140437
140467
|
Only if no "review" state exists, use a completed state.
|
|
140438
140468
|
|
|
140439
|
-
Remember, only work on a single task at a time, that you decide is the most
|
|
140440
|
-
important to work on next.
|
|
140441
|
-
|
|
140442
|
-
## Important: Task sizing
|
|
140443
|
-
|
|
140444
|
-
If at any point you decide that a task is too large or complex to complete in a
|
|
140445
|
-
single iteration, break it down into smaller tasks and add them to the prd.json
|
|
140446
|
-
file. Then, mark the original task as "closed" by updating its \`stateId\`.
|
|
140447
|
-
|
|
140448
|
-
Each task should be small and specific.
|
|
140449
|
-
Instead of creating tasks like "Refactor the authentication system", create
|
|
140450
|
-
smaller tasks like "Implement OAuth2 login endpoint", "Add JWT token refresh mechanism", etc.
|
|
140451
|
-
|
|
140452
140469
|
## Handling blockers
|
|
140453
140470
|
|
|
140454
140471
|
If for any reason you get stuck on a task, mark the task back as "todo" by updating its
|
|
@@ -140464,27 +140481,28 @@ ${prdNotes}`;
|
|
|
140464
140481
|
Users idea / request: ${idea}
|
|
140465
140482
|
|
|
140466
140483
|
1. For the users idea / request above, break it down into multiple smaller tasks
|
|
140467
|
-
that can be added to the prd.
|
|
140484
|
+
that can be added to the prd.yml file.
|
|
140468
140485
|
- Make sure to research the codebase before creating any tasks, to ensure they
|
|
140469
140486
|
are relevant and feasible.
|
|
140470
|
-
- Check if similar tasks already exist in the prd.
|
|
140487
|
+
- Check if similar tasks already exist in the prd.yml file to avoid duplication.
|
|
140471
140488
|
2. Each task should have a id of \`null\`, a title, and a concise description that
|
|
140472
140489
|
includes a short summary of the task and a brief list of steps to complete it.
|
|
140473
140490
|
- The tasks should start in a "Todo" state (i.e., not started yet).
|
|
140474
140491
|
- Each task should be small and specific.
|
|
140475
140492
|
Instead of creating tasks like "Refactor the authentication system", create
|
|
140476
140493
|
smaller tasks like "Implement OAuth2 login endpoint", "Add JWT token refresh mechanism", etc.
|
|
140477
|
-
3. Add the new tasks to the prd.
|
|
140494
|
+
3. Add the new tasks to the prd.yml file.
|
|
140478
140495
|
4. Add a brief outline of the plan to a "lalph-plan.md" file, that will help guide future
|
|
140479
140496
|
iterations.
|
|
140480
140497
|
|
|
140481
140498
|
${prdNotes}`;
|
|
140482
140499
|
return {
|
|
140500
|
+
promptChoose,
|
|
140483
140501
|
prompt,
|
|
140484
140502
|
planPrompt,
|
|
140485
140503
|
planContinuePrompt: `# Instructions
|
|
140486
140504
|
|
|
140487
|
-
1. Review the existing prd.
|
|
140505
|
+
1. Review the existing prd.yml file and lalph-plan.md file to understand the current
|
|
140488
140506
|
plan and tasks.
|
|
140489
140507
|
2. Ask the user for feedback to iterate on the existing plan.
|
|
140490
140508
|
|
|
@@ -140536,17 +140554,17 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
|
140536
140554
|
const fs = yield* FileSystem;
|
|
140537
140555
|
const source = yield* IssueSource;
|
|
140538
140556
|
const lalphDir = pathService.join(worktree.directory, `.lalph`);
|
|
140539
|
-
const prdFile = pathService.join(worktree.directory, `.lalph`, `prd.
|
|
140557
|
+
const prdFile = pathService.join(worktree.directory, `.lalph`, `prd.yml`);
|
|
140540
140558
|
let current = yield* source.issues;
|
|
140541
|
-
yield* fs.writeFileString(prdFile, PrdIssue.
|
|
140559
|
+
yield* fs.writeFileString(prdFile, PrdIssue.arrayToYaml(current));
|
|
140542
140560
|
const updatedIssues = /* @__PURE__ */ new Map();
|
|
140543
140561
|
yield* fs.watch(lalphDir).pipe(buffer({
|
|
140544
140562
|
capacity: 1,
|
|
140545
140563
|
strategy: "dropping"
|
|
140546
140564
|
}), runForEach((_) => ignore(sync$3)), retry$1(forever$1), forkScoped);
|
|
140547
140565
|
const sync$3 = gen(function* () {
|
|
140548
|
-
const
|
|
140549
|
-
const updated =
|
|
140566
|
+
const yaml = yield* fs.readFileString(prdFile);
|
|
140567
|
+
const updated = PrdIssue.arrayFromYaml(yaml);
|
|
140550
140568
|
if (!(updated.length !== current.length || updated.some((u, i) => u.isChangedComparedTo(current[i])))) return;
|
|
140551
140569
|
const githubPrs = /* @__PURE__ */ new Map();
|
|
140552
140570
|
const toRemove = new Set(current.filter((i) => i.id !== null).map((i) => i.id));
|
|
@@ -140568,20 +140586,14 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
|
140568
140586
|
stateId: issue.stateId,
|
|
140569
140587
|
blockedBy: issue.blockedBy
|
|
140570
140588
|
});
|
|
140571
|
-
|
|
140572
|
-
|
|
140573
|
-
|
|
140574
|
-
|
|
140575
|
-
originalStateId: existing.stateId,
|
|
140576
|
-
count: 0
|
|
140577
|
-
};
|
|
140578
|
-
updatedIssues.set(issue.id, entry);
|
|
140579
|
-
}
|
|
140580
|
-
entry.count++;
|
|
140589
|
+
if (!updatedIssues.has(issue.id)) updatedIssues.set(issue.id, {
|
|
140590
|
+
issue,
|
|
140591
|
+
originalStateId: existing.stateId
|
|
140592
|
+
});
|
|
140581
140593
|
}
|
|
140582
140594
|
yield* forEach$1(toRemove, (issueId) => source.cancelIssue(issueId), { concurrency: "unbounded" });
|
|
140583
140595
|
current = yield* source.issues;
|
|
140584
|
-
yield* fs.writeFileString(prdFile, PrdIssue.
|
|
140596
|
+
yield* fs.writeFileString(prdFile, PrdIssue.arrayToYaml(current.map((issue) => {
|
|
140585
140597
|
const prNumber = githubPrs.get(issue.id);
|
|
140586
140598
|
if (!prNumber) return issue;
|
|
140587
140599
|
return new PrdIssue({
|
|
@@ -140590,28 +140602,35 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
|
140590
140602
|
});
|
|
140591
140603
|
})));
|
|
140592
140604
|
}).pipe(uninterruptible);
|
|
140593
|
-
const mergableGithubPrs = gen(function* () {
|
|
140594
|
-
const json = yield* fs.readFileString(prdFile);
|
|
140595
|
-
const updated = PrdList.fromJson(json);
|
|
140596
|
-
const prs = empty$11();
|
|
140597
|
-
for (const issue of updated) {
|
|
140598
|
-
const entry = updatedIssues.get(issue.id ?? "");
|
|
140599
|
-
if (!entry || !issue.githubPrNumber || issue.stateId === entry.originalStateId) continue;
|
|
140600
|
-
prs.push(issue.githubPrNumber);
|
|
140601
|
-
}
|
|
140602
|
-
return prs;
|
|
140603
|
-
});
|
|
140604
|
-
const revertStateIds = (options) => suspend$2(() => forEach$1(updatedIssues.values(), ({ issue, originalStateId, count }) => options.reason === "error" || count === 1 ? source.updateIssue({
|
|
140605
|
-
issueId: issue.id,
|
|
140606
|
-
stateId: originalStateId
|
|
140607
|
-
}) : void_$1, {
|
|
140608
|
-
concurrency: "unbounded",
|
|
140609
|
-
discard: true
|
|
140610
|
-
}));
|
|
140611
140605
|
return {
|
|
140612
140606
|
path: prdFile,
|
|
140613
|
-
mergableGithubPrs
|
|
140614
|
-
|
|
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
|
+
})
|
|
140615
140634
|
};
|
|
140616
140635
|
}) }) {
|
|
140617
140636
|
static layer = effect(this, this.make).pipe(provide$3(Worktree.layer));
|
|
@@ -140639,14 +140658,28 @@ const getOrSelectCliAgent = gen(function* () {
|
|
|
140639
140658
|
//#endregion
|
|
140640
140659
|
//#region src/Runner.ts
|
|
140641
140660
|
const run = fnUntraced(function* (options) {
|
|
140661
|
+
const fs = yield* FileSystem;
|
|
140642
140662
|
const pathService = yield* Path$1;
|
|
140643
140663
|
const worktree = yield* Worktree;
|
|
140644
140664
|
const promptGen = yield* PromptGen;
|
|
140645
140665
|
const cliAgent = yield* getOrSelectCliAgent;
|
|
140646
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);
|
|
140647
140680
|
const cliCommand = cliAgent.command({
|
|
140648
|
-
prompt: promptGen.prompt,
|
|
140649
|
-
prdFilePath: pathService.join(".lalph", "prd.
|
|
140681
|
+
prompt: promptGen.prompt(task.id),
|
|
140682
|
+
prdFilePath: pathService.join(".lalph", "prd.yml")
|
|
140650
140683
|
});
|
|
140651
140684
|
const handle = yield* make$22(cliCommand[0], cliCommand.slice(1), {
|
|
140652
140685
|
cwd: worktree.directory,
|
|
@@ -140671,11 +140704,14 @@ const run = fnUntraced(function* (options) {
|
|
|
140671
140704
|
const exitCode$1 = yield* handle.exitCode;
|
|
140672
140705
|
yield* log$1(`Agent exited with code: ${exitCode$1}`);
|
|
140673
140706
|
const prs = yield* prd.mergableGithubPrs;
|
|
140674
|
-
if (prs.length === 0) yield* prd.
|
|
140707
|
+
if (prs.length === 0) yield* prd.maybeRevertIssue({
|
|
140708
|
+
...task,
|
|
140709
|
+
issueId: task.id
|
|
140710
|
+
});
|
|
140675
140711
|
else if (options.autoMerge) for (const pr of prs) yield* make$22`gh pr merge ${pr} -sd`.pipe(exitCode);
|
|
140676
140712
|
}, onError(fnUntraced(function* () {
|
|
140677
140713
|
const prd = yield* Prd;
|
|
140678
|
-
yield* ignore(prd.revertStateIds
|
|
140714
|
+
yield* ignore(prd.revertStateIds);
|
|
140679
140715
|
})), scoped$1, provide$1([
|
|
140680
140716
|
PromptGen.layer,
|
|
140681
140717
|
Prd.layer,
|
|
@@ -140684,6 +140720,12 @@ const run = fnUntraced(function* (options) {
|
|
|
140684
140720
|
var RunnerStalled = class extends TaggedError("RunnerStalled") {
|
|
140685
140721
|
message = "The runner has stalled due to inactivity.";
|
|
140686
140722
|
};
|
|
140723
|
+
const ChosenTask = fromJsonString(Struct({
|
|
140724
|
+
id: String$1,
|
|
140725
|
+
todoStateId: String$1,
|
|
140726
|
+
inProgressStateId: String$1,
|
|
140727
|
+
reviewStateId: String$1
|
|
140728
|
+
}));
|
|
140687
140729
|
|
|
140688
140730
|
//#endregion
|
|
140689
140731
|
//#region src/Planner.ts
|
|
@@ -140698,7 +140740,7 @@ const plan = gen(function* () {
|
|
|
140698
140740
|
yield* scoped$1(fs.open(lalphPlanPath, { flag: "a+" }));
|
|
140699
140741
|
const cliCommand = cliAgent.commandPlan({
|
|
140700
140742
|
prompt: promptGen.planPrompt(idea),
|
|
140701
|
-
prdFilePath: pathService.join(worktree.directory, ".lalph", "prd.
|
|
140743
|
+
prdFilePath: pathService.join(worktree.directory, ".lalph", "prd.yml")
|
|
140702
140744
|
});
|
|
140703
140745
|
const exitCode$1 = yield* make$22(cliCommand[0], cliCommand.slice(1), {
|
|
140704
140746
|
cwd: worktree.directory,
|
|
@@ -140726,7 +140768,7 @@ const planContinue = gen(function* () {
|
|
|
140726
140768
|
yield* scoped$1(fs.open(lalphPlanPath, { flag: "a+" }));
|
|
140727
140769
|
const cliCommand = cliAgent.commandPlan({
|
|
140728
140770
|
prompt: promptGen.planContinuePrompt,
|
|
140729
|
-
prdFilePath: pathService.join(worktree.directory, ".lalph", "prd.
|
|
140771
|
+
prdFilePath: pathService.join(worktree.directory, ".lalph", "prd.yml")
|
|
140730
140772
|
});
|
|
140731
140773
|
const exitCode$1 = yield* make$22(cliCommand[0], cliCommand.slice(1), {
|
|
140732
140774
|
cwd: worktree.directory,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lalph",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.18",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"octokit": "^5.0.5",
|
|
32
32
|
"prettier": "^3.7.4",
|
|
33
33
|
"tsdown": "^0.19.0",
|
|
34
|
-
"typescript": "^5.9.3"
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"yaml": "^2.8.2"
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
38
|
"check": "tsc --noEmit && prettier --check .",
|
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/Planner.ts
CHANGED
|
@@ -22,7 +22,7 @@ export const plan = Effect.gen(function* () {
|
|
|
22
22
|
|
|
23
23
|
const cliCommand = cliAgent.commandPlan({
|
|
24
24
|
prompt: promptGen.planPrompt(idea),
|
|
25
|
-
prdFilePath: pathService.join(worktree.directory, ".lalph", "prd.
|
|
25
|
+
prdFilePath: pathService.join(worktree.directory, ".lalph", "prd.yml"),
|
|
26
26
|
})
|
|
27
27
|
const exitCode = yield* ChildProcess.make(
|
|
28
28
|
cliCommand[0]!,
|
|
@@ -60,7 +60,7 @@ export const planContinue = Effect.gen(function* () {
|
|
|
60
60
|
|
|
61
61
|
const cliCommand = cliAgent.commandPlan({
|
|
62
62
|
prompt: promptGen.planContinuePrompt,
|
|
63
|
-
prdFilePath: pathService.join(worktree.directory, ".lalph", "prd.
|
|
63
|
+
prdFilePath: pathService.join(worktree.directory, ".lalph", "prd.yml"),
|
|
64
64
|
})
|
|
65
65
|
const exitCode = yield* ChildProcess.make(
|
|
66
66
|
cliCommand[0]!,
|
package/src/Prd.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
Stream,
|
|
10
10
|
} from "effect"
|
|
11
11
|
import { Worktree } from "./Worktree.ts"
|
|
12
|
-
import { PrdIssue
|
|
12
|
+
import { PrdIssue } from "./domain/PrdIssue.ts"
|
|
13
13
|
import { IssueSource } from "./IssueSource.ts"
|
|
14
14
|
|
|
15
15
|
export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
@@ -20,17 +20,16 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
20
20
|
const source = yield* IssueSource
|
|
21
21
|
|
|
22
22
|
const lalphDir = pathService.join(worktree.directory, `.lalph`)
|
|
23
|
-
const prdFile = pathService.join(worktree.directory, `.lalph`, `prd.
|
|
23
|
+
const prdFile = pathService.join(worktree.directory, `.lalph`, `prd.yml`)
|
|
24
24
|
|
|
25
25
|
let current = yield* source.issues
|
|
26
|
-
yield* fs.writeFileString(prdFile, PrdIssue.
|
|
26
|
+
yield* fs.writeFileString(prdFile, PrdIssue.arrayToYaml(current))
|
|
27
27
|
|
|
28
28
|
const updatedIssues = new Map<
|
|
29
29
|
string,
|
|
30
30
|
{
|
|
31
31
|
readonly issue: PrdIssue
|
|
32
32
|
readonly originalStateId: string
|
|
33
|
-
count: number
|
|
34
33
|
}
|
|
35
34
|
>()
|
|
36
35
|
|
|
@@ -45,8 +44,8 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
45
44
|
)
|
|
46
45
|
|
|
47
46
|
const sync = Effect.gen(function* () {
|
|
48
|
-
const
|
|
49
|
-
const updated =
|
|
47
|
+
const yaml = yield* fs.readFileString(prdFile)
|
|
48
|
+
const updated = PrdIssue.arrayFromYaml(yaml)
|
|
50
49
|
const anyChanges =
|
|
51
50
|
updated.length !== current.length ||
|
|
52
51
|
updated.some((u, i) => u.isChangedComparedTo(current[i]!))
|
|
@@ -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(
|
|
@@ -101,7 +100,7 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
101
100
|
current = yield* source.issues
|
|
102
101
|
yield* fs.writeFileString(
|
|
103
102
|
prdFile,
|
|
104
|
-
PrdIssue.
|
|
103
|
+
PrdIssue.arrayToYaml(
|
|
105
104
|
current.map((issue) => {
|
|
106
105
|
const prNumber = githubPrs.get(issue.id!)
|
|
107
106
|
if (!prNumber) return issue
|
|
@@ -112,8 +111,8 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
112
111
|
}).pipe(Effect.uninterruptible)
|
|
113
112
|
|
|
114
113
|
const mergableGithubPrs = Effect.gen(function* () {
|
|
115
|
-
const
|
|
116
|
-
const updated =
|
|
114
|
+
const yaml = yield* fs.readFileString(prdFile)
|
|
115
|
+
const updated = PrdIssue.arrayFromYaml(yaml)
|
|
117
116
|
const prs = Array.empty<number>()
|
|
118
117
|
for (const issue of updated) {
|
|
119
118
|
const entry = updatedIssues.get(issue.id ?? "")
|
|
@@ -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
|
@@ -11,9 +11,9 @@ export class PromptGen extends ServiceMap.Service<PromptGen>()(
|
|
|
11
11
|
const source = yield* IssueSource
|
|
12
12
|
const states = yield* source.states
|
|
13
13
|
|
|
14
|
-
const prdNotes = `## prd.
|
|
14
|
+
const prdNotes = `## prd.yml format
|
|
15
15
|
|
|
16
|
-
Each item in the prd.
|
|
16
|
+
Each item in the prd.yml file represents a task for the current project.
|
|
17
17
|
|
|
18
18
|
The \`stateId\` field indicates the current state of the task. The possible states
|
|
19
19
|
are:
|
|
@@ -22,36 +22,70 @@ ${Array.from(states.values(), (state) => `- **${state.name}** (stateId: \`${stat
|
|
|
22
22
|
|
|
23
23
|
### Adding tasks
|
|
24
24
|
|
|
25
|
-
To add a new task, append a new item to the prd.
|
|
25
|
+
To add a new task, append a new item to the prd.yml file with the id set to
|
|
26
26
|
\`null\`.
|
|
27
27
|
|
|
28
28
|
When adding a new task, it will take about 5 seconds for the system to update the
|
|
29
|
-
prd.
|
|
29
|
+
prd.yml file with a new id for the task.
|
|
30
30
|
|
|
31
31
|
### Removing tasks
|
|
32
32
|
|
|
33
|
-
To remove a task, simply delete the item from the prd.
|
|
33
|
+
To remove a task, simply delete the item from the prd.yml file.
|
|
34
34
|
|
|
35
|
-
### prd.
|
|
35
|
+
### prd.yml json schema
|
|
36
36
|
|
|
37
37
|
\`\`\`json
|
|
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
|
-
1. Decide which single task to work on next from the prd.
|
|
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
|
|
48
49
|
first task in the list.
|
|
49
50
|
- Only start tasks that are in a "todo" state (i.e., not started yet).
|
|
50
51
|
- If the \`blockedBy\` field is not empty, skip the task.
|
|
51
52
|
2. **Before doing anything else**, mark the task as "in progress" by updating its
|
|
52
|
-
\`stateId\` in the prd.
|
|
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.json 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}
|
|
@@ -71,25 +102,12 @@ permission.
|
|
|
71
102
|
- None of the files in the \`.lalph\` directory should be committed.
|
|
72
103
|
- You have permission to create or update the PR as needed. You have full
|
|
73
104
|
permission to push branches, create PRs or create git commits.
|
|
74
|
-
7. Update the prd.
|
|
105
|
+
7. Update the prd.yml file to reflect any changes in task states.
|
|
75
106
|
- Add follow up tasks only if needed.
|
|
76
107
|
- Append to the \`description\` field with any notes or important discoveries.
|
|
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.json
|
|
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
|
|
@@ -106,17 +124,17 @@ ${prdNotes}`
|
|
|
106
124
|
Users idea / request: ${idea}
|
|
107
125
|
|
|
108
126
|
1. For the users idea / request above, break it down into multiple smaller tasks
|
|
109
|
-
that can be added to the prd.
|
|
127
|
+
that can be added to the prd.yml file.
|
|
110
128
|
- Make sure to research the codebase before creating any tasks, to ensure they
|
|
111
129
|
are relevant and feasible.
|
|
112
|
-
- Check if similar tasks already exist in the prd.
|
|
130
|
+
- Check if similar tasks already exist in the prd.yml file to avoid duplication.
|
|
113
131
|
2. Each task should have a id of \`null\`, a title, and a concise description that
|
|
114
132
|
includes a short summary of the task and a brief list of steps to complete it.
|
|
115
133
|
- The tasks should start in a "Todo" state (i.e., not started yet).
|
|
116
134
|
- Each task should be small and specific.
|
|
117
135
|
Instead of creating tasks like "Refactor the authentication system", create
|
|
118
136
|
smaller tasks like "Implement OAuth2 login endpoint", "Add JWT token refresh mechanism", etc.
|
|
119
|
-
3. Add the new tasks to the prd.
|
|
137
|
+
3. Add the new tasks to the prd.yml file.
|
|
120
138
|
4. Add a brief outline of the plan to a "lalph-plan.md" file, that will help guide future
|
|
121
139
|
iterations.
|
|
122
140
|
|
|
@@ -124,7 +142,7 @@ ${prdNotes}`
|
|
|
124
142
|
|
|
125
143
|
const planContinuePrompt = `# Instructions
|
|
126
144
|
|
|
127
|
-
1. Review the existing prd.
|
|
145
|
+
1. Review the existing prd.yml file and lalph-plan.md file to understand the current
|
|
128
146
|
plan and tasks.
|
|
129
147
|
2. Ask the user for feedback to iterate on the existing plan.
|
|
130
148
|
|
|
@@ -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,15 +19,34 @@ 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,
|
|
21
|
-
prdFilePath: pathService.join(".lalph", "prd.
|
|
48
|
+
prompt: promptGen.prompt(task.id),
|
|
49
|
+
prdFilePath: pathService.join(".lalph", "prd.yml"),
|
|
22
50
|
})
|
|
23
51
|
const handle = yield* ChildProcess.make(
|
|
24
52
|
cliCommand[0]!,
|
|
@@ -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
|
+
)
|
package/src/domain/PrdIssue.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Schema,
|
|
1
|
+
import { Schema, Array, Equal } from "effect"
|
|
2
|
+
import * as Yaml from "yaml"
|
|
2
3
|
|
|
3
4
|
export class PrdIssue extends Schema.Class<PrdIssue>("PrdIssue")({
|
|
4
5
|
id: Schema.NullOr(Schema.String).annotate({
|
|
@@ -36,12 +37,14 @@ export class PrdIssue extends Schema.Class<PrdIssue>("PrdIssue")({
|
|
|
36
37
|
}) {
|
|
37
38
|
static Array = Schema.Array(this)
|
|
38
39
|
static ArrayFromJson = Schema.toCodecJson(this.Array)
|
|
39
|
-
static
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
40
|
+
static arrayToYaml(issues: ReadonlyArray<PrdIssue>): string {
|
|
41
|
+
const json = Schema.encodeSync(this.ArrayFromJson)(issues)
|
|
42
|
+
return Yaml.stringify(json, { blockQuote: "literal" })
|
|
43
|
+
}
|
|
44
|
+
static arrayFromYaml(yaml: string): ReadonlyArray<PrdIssue> {
|
|
45
|
+
const json = Yaml.parse(yaml)
|
|
46
|
+
const issues = Schema.decodeSync(PrdIssue.ArrayFromJson)(json)
|
|
47
|
+
return issues
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
static jsonSchemaDoc = Schema.toJsonSchemaDocument(this)
|
|
@@ -62,22 +65,3 @@ export class PrdIssue extends Schema.Class<PrdIssue>("PrdIssue")({
|
|
|
62
65
|
)
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
|
-
|
|
66
|
-
export class PrdList<O = unknown> extends Data.Class<{
|
|
67
|
-
readonly issues: ReadonlyMap<string, PrdIssue>
|
|
68
|
-
readonly orignals: ReadonlyMap<string, O>
|
|
69
|
-
}> {
|
|
70
|
-
static fromJson(json: string): ReadonlyArray<PrdIssue> {
|
|
71
|
-
const issues = Schema.decodeSync(PrdIssue.ArrayFromJson)(JSON.parse(json))
|
|
72
|
-
return issues
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
toJson(): string {
|
|
76
|
-
const issuesArray = Array.fromIterable(this.issues.values())
|
|
77
|
-
return PrdIssue.arrayToJson(issuesArray)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
cast<T>(): PrdList<T> {
|
|
81
|
-
return this as any
|
|
82
|
-
}
|
|
83
|
-
}
|