lalph 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -16
- package/dist/cli.mjs +143 -32
- package/package.json +1 -1
- package/src/Prd.ts +15 -7
- package/src/PromptGen.ts +15 -15
- package/src/Runner.ts +8 -1
- package/src/Worktree.ts +59 -0
- package/src/cli.ts +75 -8
package/README.md
CHANGED
|
@@ -5,24 +5,15 @@ A small CLI that connects to Linear, pulls the next set of unstarted issues into
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
7
|
- Install dependencies: `pnpm install`
|
|
8
|
-
- Build the CLI: `pnpm
|
|
8
|
+
- Build the CLI: `pnpm build`
|
|
9
|
+
- Add `.lalph/` to `.gitignore` to keep local state private
|
|
9
10
|
|
|
10
11
|
## CLI usage
|
|
11
12
|
|
|
12
|
-
- Run the main loop: `
|
|
13
|
-
-
|
|
14
|
-
- Select a
|
|
15
|
-
- Select a
|
|
13
|
+
- Run the main loop: `npx -y lalph@latest`
|
|
14
|
+
- Run multiple iterations with concurrency: `npx -y lalph@latest --iterations 4 --concurrency 2`
|
|
15
|
+
- Select a Linear project: `npx -y lalph@latest select-project`
|
|
16
|
+
- Select a label filter: `npx -y lalph@latest select-label`
|
|
17
|
+
- Select a CLI agent: `npx -y lalph@latest select-agent`
|
|
16
18
|
|
|
17
19
|
The first run opens a Linear OAuth flow and stores the token locally.
|
|
18
|
-
|
|
19
|
-
## Generated files
|
|
20
|
-
|
|
21
|
-
- `.lalph/prd.json`: synced task list pulled from Linear; update task states here.
|
|
22
|
-
- `PROGRESS.md`: append-only log of work completed by the agent.
|
|
23
|
-
- `.lalph/config`: local key-value store for Linear tokens and user selections.
|
|
24
|
-
|
|
25
|
-
## Checks
|
|
26
|
-
|
|
27
|
-
- Type check: `pnpm exec tsc --noEmit`
|
|
28
|
-
- Format check: `pnpm exec prettier --check .`
|
package/dist/cli.mjs
CHANGED
|
@@ -8270,8 +8270,8 @@ const whileLoop$1 = /* @__PURE__ */ makePrimitive$1({
|
|
|
8270
8270
|
/** @internal */
|
|
8271
8271
|
const forEach$2 = /* @__PURE__ */ dual((args$1) => typeof args$1[1] === "function", (iterable, f, options) => withFiber$1((parent) => {
|
|
8272
8272
|
const concurrencyOption = options?.concurrency === "inherit" ? parent.getRef(CurrentConcurrency) : options?.concurrency ?? 1;
|
|
8273
|
-
const concurrency = concurrencyOption === "unbounded" ? Number.POSITIVE_INFINITY : Math.max(1, concurrencyOption);
|
|
8274
|
-
if (concurrency === 1) return forEachSequential(iterable, f, options);
|
|
8273
|
+
const concurrency$1 = concurrencyOption === "unbounded" ? Number.POSITIVE_INFINITY : Math.max(1, concurrencyOption);
|
|
8274
|
+
if (concurrency$1 === 1) return forEachSequential(iterable, f, options);
|
|
8275
8275
|
const items = fromIterable$2(iterable);
|
|
8276
8276
|
let length = items.length;
|
|
8277
8277
|
if (length === 0) return options?.discard ? void_$3 : succeed$4([]);
|
|
@@ -8288,7 +8288,7 @@ const forEach$2 = /* @__PURE__ */ dual((args$1) => typeof args$1[1] === "functio
|
|
|
8288
8288
|
let interrupted = false;
|
|
8289
8289
|
function pump() {
|
|
8290
8290
|
pumping = true;
|
|
8291
|
-
while (inProgress < concurrency && index < length) {
|
|
8291
|
+
while (inProgress < concurrency$1 && index < length) {
|
|
8292
8292
|
const currentIndex = index;
|
|
8293
8293
|
const item = items[currentIndex];
|
|
8294
8294
|
index++;
|
|
@@ -8312,7 +8312,7 @@ const forEach$2 = /* @__PURE__ */ dual((args$1) => typeof args$1[1] === "functio
|
|
|
8312
8312
|
doneCount++;
|
|
8313
8313
|
inProgress--;
|
|
8314
8314
|
if (doneCount === length) resume(failures.length > 0 ? exitFailCause(causeFromFailures(failures)) : succeed$4(out));
|
|
8315
|
-
else if (!pumping && !failed && inProgress < concurrency) pump();
|
|
8315
|
+
else if (!pumping && !failed && inProgress < concurrency$1) pump();
|
|
8316
8316
|
});
|
|
8317
8317
|
} catch (err) {
|
|
8318
8318
|
failed = true;
|
|
@@ -18354,6 +18354,35 @@ const logWithLevel = logWithLevel$1;
|
|
|
18354
18354
|
*/
|
|
18355
18355
|
const log$1 = /* @__PURE__ */ logWithLevel$1();
|
|
18356
18356
|
/**
|
|
18357
|
+
* Logs one or more messages at the WARNING level.
|
|
18358
|
+
*
|
|
18359
|
+
* @example
|
|
18360
|
+
* ```ts
|
|
18361
|
+
* import { Effect } from "effect"
|
|
18362
|
+
*
|
|
18363
|
+
* const program = Effect.gen(function*() {
|
|
18364
|
+
* yield* Effect.logWarning("API rate limit approaching")
|
|
18365
|
+
* yield* Effect.logWarning("Retries remaining:", 2, "Operation:", "fetchData")
|
|
18366
|
+
*
|
|
18367
|
+
* // Useful for non-critical issues
|
|
18368
|
+
* const deprecated = true
|
|
18369
|
+
* if (deprecated) {
|
|
18370
|
+
* yield* Effect.logWarning("Using deprecated API endpoint")
|
|
18371
|
+
* }
|
|
18372
|
+
* })
|
|
18373
|
+
*
|
|
18374
|
+
* Effect.runPromise(program)
|
|
18375
|
+
* // Output:
|
|
18376
|
+
* // timestamp=2023-... level=WARN message="API rate limit approaching"
|
|
18377
|
+
* // timestamp=2023-... level=WARN message="Retries remaining: 2 Operation: fetchData"
|
|
18378
|
+
* // timestamp=2023-... level=WARN message="Using deprecated API endpoint"
|
|
18379
|
+
* ```
|
|
18380
|
+
*
|
|
18381
|
+
* @since 2.0.0
|
|
18382
|
+
* @category logging
|
|
18383
|
+
*/
|
|
18384
|
+
const logWarning = /* @__PURE__ */ logWithLevel$1("Warn");
|
|
18385
|
+
/**
|
|
18357
18386
|
* Logs one or more messages at the ERROR level.
|
|
18358
18387
|
*
|
|
18359
18388
|
* @example
|
|
@@ -53211,12 +53240,12 @@ var require_limiter = /* @__PURE__ */ __commonJSMin$1(((exports, module) => {
|
|
|
53211
53240
|
* @param {Number} [concurrency=Infinity] The maximum number of jobs allowed
|
|
53212
53241
|
* to run concurrently
|
|
53213
53242
|
*/
|
|
53214
|
-
constructor(concurrency) {
|
|
53243
|
+
constructor(concurrency$1) {
|
|
53215
53244
|
this[kDone] = () => {
|
|
53216
53245
|
this.pending--;
|
|
53217
53246
|
this[kRun]();
|
|
53218
53247
|
};
|
|
53219
|
-
this.concurrency = concurrency || Infinity;
|
|
53248
|
+
this.concurrency = concurrency$1 || Infinity;
|
|
53220
53249
|
this.jobs = [];
|
|
53221
53250
|
this.pending = 0;
|
|
53222
53251
|
}
|
|
@@ -132176,9 +132205,42 @@ const teamSelect = fnUntraced(function* (project) {
|
|
|
132176
132205
|
yield* selectedTeamId.set(some(teamId));
|
|
132177
132206
|
});
|
|
132178
132207
|
|
|
132208
|
+
//#endregion
|
|
132209
|
+
//#region src/Worktree.ts
|
|
132210
|
+
var Worktree = class extends Service()("lalph/Worktree", { make: gen(function* () {
|
|
132211
|
+
const fs = yield* FileSystem;
|
|
132212
|
+
const pathService = yield* Path$1;
|
|
132213
|
+
const directory = yield* fs.makeTempDirectory();
|
|
132214
|
+
yield* addFinalizer(fnUntraced(function* () {
|
|
132215
|
+
yield* execIgnore(make$21`git worktree remove ${directory}`);
|
|
132216
|
+
}));
|
|
132217
|
+
yield* exec(make$21`git worktree add ${directory} -d HEAD`);
|
|
132218
|
+
yield* fs.makeDirectory(pathService.join(directory, ".lalph"), { recursive: true });
|
|
132219
|
+
yield* forEach$1([
|
|
132220
|
+
make$21({
|
|
132221
|
+
cwd: directory,
|
|
132222
|
+
extendEnv: true,
|
|
132223
|
+
shell: process.env.SHELL ?? true
|
|
132224
|
+
})`direnv allow`,
|
|
132225
|
+
make$21({
|
|
132226
|
+
cwd: directory,
|
|
132227
|
+
extendEnv: true,
|
|
132228
|
+
shell: process.env.SHELL ?? true
|
|
132229
|
+
})`devenv allow`,
|
|
132230
|
+
make$21({ cwd: directory })`git submodule update --init --recursive`
|
|
132231
|
+
], execIgnore, { concurrency: "unbounded" });
|
|
132232
|
+
return { directory };
|
|
132233
|
+
}) }) {
|
|
132234
|
+
static layer = effect(this, this.make);
|
|
132235
|
+
};
|
|
132236
|
+
const exec = (command) => command.asEffect().pipe(flatMap((proc) => proc.exitCode), scoped$1);
|
|
132237
|
+
const execIgnore = (command) => command.asEffect().pipe(flatMap((proc) => proc.exitCode), catchCause$1(logWarning), scoped$1);
|
|
132238
|
+
|
|
132179
132239
|
//#endregion
|
|
132180
132240
|
//#region src/Prd.ts
|
|
132181
132241
|
var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
132242
|
+
const worktree = yield* Worktree;
|
|
132243
|
+
const pathService = yield* Path$1;
|
|
132182
132244
|
const fs = yield* FileSystem;
|
|
132183
132245
|
const linear = yield* Linear;
|
|
132184
132246
|
const project = yield* CurrentProject;
|
|
@@ -132190,7 +132252,7 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
|
132190
132252
|
} })).pipe(runCollect).pipe(map$4(PrdList.fromLinearIssues));
|
|
132191
132253
|
if (initial.issues.size === 0) return yield* new NoMoreWork({});
|
|
132192
132254
|
const current = yield* make$22(initial);
|
|
132193
|
-
const prdFile = `.lalph/prd.json
|
|
132255
|
+
const prdFile = pathService.join(worktree.directory, `.lalph/prd.json`);
|
|
132194
132256
|
yield* fs.writeFileString(prdFile, initial.toJson());
|
|
132195
132257
|
const sync$2 = gen(function* () {
|
|
132196
132258
|
const json = yield* fs.readFileString(prdFile);
|
|
@@ -132212,7 +132274,8 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
|
132212
132274
|
}
|
|
132213
132275
|
const existing = currentValue.issues.get(issue.id);
|
|
132214
132276
|
if (!existing || !existing.isChangedComparedTo(issue)) continue;
|
|
132215
|
-
|
|
132277
|
+
const original = currentValue.orignals.get(issue.id);
|
|
132278
|
+
yield* linear.use((c) => c.updateIssue(original.id, {
|
|
132216
132279
|
description: issue.description,
|
|
132217
132280
|
stateId: issue.stateId
|
|
132218
132281
|
}));
|
|
@@ -132223,13 +132286,12 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
|
132223
132286
|
capacity: 1,
|
|
132224
132287
|
strategy: "dropping"
|
|
132225
132288
|
}), runForEach(() => ignore(sync$2)), forkScoped);
|
|
132226
|
-
return {
|
|
132289
|
+
return {
|
|
132290
|
+
current,
|
|
132291
|
+
path: prdFile
|
|
132292
|
+
};
|
|
132227
132293
|
}) }) {
|
|
132228
|
-
static layer = effect(this, this.make).pipe(provide$2([
|
|
132229
|
-
Linear.layer,
|
|
132230
|
-
Settings.layer,
|
|
132231
|
-
CurrentProject.layer
|
|
132232
|
-
]));
|
|
132294
|
+
static layer = effect(this, this.make).pipe(provide$2([CurrentProject.layer, Worktree.layer]));
|
|
132233
132295
|
};
|
|
132234
132296
|
var NoMoreWork = class extends ErrorClass("lalph/Prd/NoMoreWork")({ _tag: tag("NoMoreWork") }) {
|
|
132235
132297
|
message = "No more work to be done!";
|
|
@@ -132251,7 +132313,7 @@ var PrdIssue = class PrdIssue extends Class("PrdIssue")({
|
|
|
132251
132313
|
};
|
|
132252
132314
|
static fromLinearIssue(issue) {
|
|
132253
132315
|
return new PrdIssue({
|
|
132254
|
-
id: issue.
|
|
132316
|
+
id: issue.identifier,
|
|
132255
132317
|
title: issue.title,
|
|
132256
132318
|
description: issue.description ?? "",
|
|
132257
132319
|
priority: issue.priority,
|
|
@@ -132266,12 +132328,17 @@ var PrdIssue = class PrdIssue extends Class("PrdIssue")({
|
|
|
132266
132328
|
var PrdList = class PrdList extends Class$1 {
|
|
132267
132329
|
static fromLinearIssues(issues) {
|
|
132268
132330
|
const map$12 = /* @__PURE__ */ new Map();
|
|
132331
|
+
const originalMap = /* @__PURE__ */ new Map();
|
|
132269
132332
|
for (const issue of issues) {
|
|
132270
132333
|
const prdIssue = PrdIssue.fromLinearIssue(issue);
|
|
132271
132334
|
if (!prdIssue.id) continue;
|
|
132272
132335
|
map$12.set(prdIssue.id, prdIssue);
|
|
132336
|
+
originalMap.set(prdIssue.id, issue);
|
|
132273
132337
|
}
|
|
132274
|
-
return new PrdList({
|
|
132338
|
+
return new PrdList({
|
|
132339
|
+
issues: map$12,
|
|
132340
|
+
orignals: originalMap
|
|
132341
|
+
});
|
|
132275
132342
|
}
|
|
132276
132343
|
static fromJson(json) {
|
|
132277
132344
|
return decodeSync(PrdIssue.ArrayFromJson)(JSON.parse(json));
|
|
@@ -132287,20 +132354,25 @@ var PrdList = class PrdList extends Class$1 {
|
|
|
132287
132354
|
//#region src/PromptGen.ts
|
|
132288
132355
|
var PromptGen = class extends Service()("lalph/PromptGen", { make: gen(function* () {
|
|
132289
132356
|
const linear = yield* Linear;
|
|
132290
|
-
const fs = yield* FileSystem;
|
|
132291
|
-
yield* scoped$1(fs.open("PROGRESS.md", { flag: "a+" }));
|
|
132292
132357
|
return { prompt: `# Instructions
|
|
132293
132358
|
|
|
132294
132359
|
1. Decide which single task to work on next from the prd.json file. This should
|
|
132295
132360
|
be the task YOU decide as the most important to work on next, not just the
|
|
132296
132361
|
first task in the list.
|
|
132297
|
-
|
|
132298
|
-
|
|
132299
|
-
|
|
132300
|
-
3.
|
|
132301
|
-
|
|
132302
|
-
|
|
132303
|
-
|
|
132362
|
+
2. Before starting the chosen task, mark it as "in progress" by updating its
|
|
132363
|
+
\`stateId\` in the prd.json file.
|
|
132364
|
+
This prevents other people or agents from working on the same task simultaneously.
|
|
132365
|
+
3. Check if there is an existing Github PR for the task, otherwise create a new
|
|
132366
|
+
branch for the task.
|
|
132367
|
+
- If there is an existing PR, checkout the branch for that PR.
|
|
132368
|
+
- If there is an existing PR, check if there are any new comments or requested
|
|
132369
|
+
changes, and address them as part of the task.
|
|
132370
|
+
4. Run any checks / feedback loops, such as type checks, unit tests, or linting.
|
|
132371
|
+
5. APPEND your progress to the PROGRESS.md file.
|
|
132372
|
+
6. Create or update the pull request with your changes once the task is complete. The title of
|
|
132373
|
+
the PR should include the task id. The PR description should include a
|
|
132374
|
+
summary of the changes made.
|
|
132375
|
+
7. Update the prd.json file to reflect any changes in task states.
|
|
132304
132376
|
- Add follow up tasks only if needed.
|
|
132305
132377
|
- Append to the \`description\` field with any notes.
|
|
132306
132378
|
- When a task is complete, set its \`stateId\` to the id that indicates
|
|
@@ -132336,6 +132408,7 @@ ${JSON.stringify(PrdIssue.jsonSchema, null, 2)}
|
|
|
132336
132408
|
//#endregion
|
|
132337
132409
|
//#region src/Runner.ts
|
|
132338
132410
|
const run = gen(function* () {
|
|
132411
|
+
const worktree = yield* Worktree;
|
|
132339
132412
|
const promptGen = yield* PromptGen;
|
|
132340
132413
|
const cliCommand = (yield* getOrSelectCliAgent).command({
|
|
132341
132414
|
prompt: promptGen.prompt,
|
|
@@ -132343,13 +132416,18 @@ const run = gen(function* () {
|
|
|
132343
132416
|
progressFilePath: "PROGRESS.md"
|
|
132344
132417
|
});
|
|
132345
132418
|
const exitCode = yield* (yield* make$21(cliCommand[0], cliCommand.slice(1), {
|
|
132419
|
+
cwd: worktree.directory,
|
|
132346
132420
|
extendEnv: true,
|
|
132347
132421
|
stdout: "inherit",
|
|
132348
132422
|
stderr: "inherit",
|
|
132349
132423
|
stdin: "inherit"
|
|
132350
132424
|
})).exitCode;
|
|
132351
132425
|
yield* log$1(`Agent exited with code: ${exitCode}`);
|
|
132352
|
-
}).pipe(scoped$1, provide([
|
|
132426
|
+
}).pipe(scoped$1, provide([
|
|
132427
|
+
PromptGen.layer,
|
|
132428
|
+
Prd.layer,
|
|
132429
|
+
Worktree.layer
|
|
132430
|
+
]));
|
|
132353
132431
|
const selectCliAgent = gen(function* () {
|
|
132354
132432
|
const agent = yield* select({
|
|
132355
132433
|
message: "Select the CLI agent to use",
|
|
@@ -132379,18 +132457,51 @@ const selectLabel = make$26("select-label").pipe(withDescription("Select the lab
|
|
|
132379
132457
|
onNone: () => "No Label",
|
|
132380
132458
|
onSome: (l) => l.name
|
|
132381
132459
|
})}`);
|
|
132382
|
-
}
|
|
132460
|
+
})));
|
|
132383
132461
|
const selectAgent = make$26("select-agent").pipe(withDescription("Select the CLI agent to use"), withHandler(() => selectCliAgent));
|
|
132384
|
-
const iterations = integer("iterations").pipe(withAlias("i"), withDefault(
|
|
132385
|
-
const
|
|
132386
|
-
|
|
132387
|
-
|
|
132462
|
+
const iterations = integer("iterations").pipe(withDescription$1("Number of iterations to run, defaults to unlimited"), withAlias("i"), withDefault(Number.POSITIVE_INFINITY));
|
|
132463
|
+
const concurrency = integer("concurrency").pipe(withDescription$1("Number of concurrent agents, defaults to 1"), withAlias("c"), withDefault(1));
|
|
132464
|
+
const root = make$26("lalph", {
|
|
132465
|
+
iterations,
|
|
132466
|
+
concurrency
|
|
132467
|
+
}).pipe(withHandler(fnUntraced(function* ({ iterations: iterations$1, concurrency: concurrency$1 }) {
|
|
132468
|
+
const isFinite$3 = Number.isFinite(iterations$1);
|
|
132469
|
+
const iterationsDisplay = isFinite$3 ? iterations$1 : "unlimited";
|
|
132470
|
+
const runConcurrency = Math.max(1, concurrency$1);
|
|
132471
|
+
const semaphore = makeSemaphoreUnsafe(runConcurrency);
|
|
132472
|
+
yield* log$1(`Executing ${iterationsDisplay} iteration(s) with concurrency ${runConcurrency}`);
|
|
132473
|
+
let iteration = 0;
|
|
132474
|
+
let lastStartedAt = makeUnsafe$3(0);
|
|
132475
|
+
let inProgress = 0;
|
|
132476
|
+
while (true) {
|
|
132477
|
+
yield* semaphore.take(1);
|
|
132478
|
+
if (isFinite$3 && iteration >= iterations$1) break;
|
|
132479
|
+
const currentIteration = iteration;
|
|
132480
|
+
if (inProgress > 0) {
|
|
132481
|
+
const nextEarliestStart = lastStartedAt.pipe(add$1({ seconds: 30 }));
|
|
132482
|
+
const diff = distance(yield* now, nextEarliestStart);
|
|
132483
|
+
if (diff > 0) yield* sleep(diff);
|
|
132484
|
+
}
|
|
132485
|
+
lastStartedAt = yield* now;
|
|
132486
|
+
inProgress++;
|
|
132487
|
+
yield* run.pipe(catchTag("NoMoreWork", (e) => {
|
|
132488
|
+
if (isFinite$3) {
|
|
132489
|
+
iterations$1 = currentIteration;
|
|
132490
|
+
return fail$3(e);
|
|
132491
|
+
}
|
|
132492
|
+
return log$1("No more work to process, waiting 30 seconds...").pipe(andThen(sleep("30 seconds")));
|
|
132493
|
+
}), catchCause$1(logWarning), annotateLogs({ iteration: currentIteration }), ensuring(suspend$2(() => {
|
|
132494
|
+
inProgress--;
|
|
132495
|
+
return semaphore.release(1);
|
|
132496
|
+
})), forkChild);
|
|
132497
|
+
iteration++;
|
|
132498
|
+
}
|
|
132388
132499
|
})), withSubcommands([
|
|
132389
132500
|
selectProject,
|
|
132390
132501
|
selectLabel,
|
|
132391
132502
|
selectAgent
|
|
132392
132503
|
]));
|
|
132393
|
-
run$1(root, { version: "0.1.0" }).pipe(provide(Settings.layer.pipe(provideMerge(layer$1))), runMain);
|
|
132504
|
+
run$1(root, { version: "0.1.0" }).pipe(provide(mergeAll(Settings.layer, Linear.layer).pipe(provideMerge(layer$1))), runMain);
|
|
132394
132505
|
|
|
132395
132506
|
//#endregion
|
|
132396
132507
|
export { };
|
package/package.json
CHANGED
package/src/Prd.ts
CHANGED
|
@@ -4,17 +4,21 @@ import {
|
|
|
4
4
|
FileSystem,
|
|
5
5
|
Layer,
|
|
6
6
|
Option,
|
|
7
|
+
Path,
|
|
7
8
|
Schema,
|
|
8
9
|
ServiceMap,
|
|
9
10
|
Stream,
|
|
10
11
|
SubscriptionRef,
|
|
11
12
|
} from "effect"
|
|
12
13
|
import { CurrentProject, Linear } from "./Linear.ts"
|
|
13
|
-
import { selectedLabelId, selectedTeamId
|
|
14
|
+
import { selectedLabelId, selectedTeamId } from "./Settings.ts"
|
|
14
15
|
import type { Issue } from "@linear/sdk"
|
|
16
|
+
import { Worktree } from "./Worktree.ts"
|
|
15
17
|
|
|
16
18
|
export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
17
19
|
make: Effect.gen(function* () {
|
|
20
|
+
const worktree = yield* Worktree
|
|
21
|
+
const pathService = yield* Path.Path
|
|
18
22
|
const fs = yield* FileSystem.FileSystem
|
|
19
23
|
const linear = yield* Linear
|
|
20
24
|
const project = yield* CurrentProject
|
|
@@ -46,7 +50,7 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
46
50
|
|
|
47
51
|
const current = yield* SubscriptionRef.make(initial)
|
|
48
52
|
|
|
49
|
-
const prdFile = `.lalph/prd.json`
|
|
53
|
+
const prdFile = pathService.join(worktree.directory, `.lalph/prd.json`)
|
|
50
54
|
|
|
51
55
|
yield* fs.writeFileString(prdFile, initial.toJson())
|
|
52
56
|
|
|
@@ -74,10 +78,11 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
74
78
|
}
|
|
75
79
|
const existing = currentValue.issues.get(issue.id)
|
|
76
80
|
if (!existing || !existing.isChangedComparedTo(issue)) continue
|
|
81
|
+
const original = currentValue.orignals.get(issue.id)!
|
|
77
82
|
|
|
78
83
|
// update existing issue
|
|
79
84
|
yield* linear.use((c) =>
|
|
80
|
-
c.updateIssue(
|
|
85
|
+
c.updateIssue(original.id, {
|
|
81
86
|
description: issue.description,
|
|
82
87
|
stateId: issue.stateId,
|
|
83
88
|
}),
|
|
@@ -96,11 +101,11 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
96
101
|
Effect.forkScoped,
|
|
97
102
|
)
|
|
98
103
|
|
|
99
|
-
return { current } as const
|
|
104
|
+
return { current, path: prdFile } as const
|
|
100
105
|
}),
|
|
101
106
|
}) {
|
|
102
107
|
static layer = Layer.effect(this, this.make).pipe(
|
|
103
|
-
Layer.provide([
|
|
108
|
+
Layer.provide([CurrentProject.layer, Worktree.layer]),
|
|
104
109
|
)
|
|
105
110
|
}
|
|
106
111
|
|
|
@@ -146,7 +151,7 @@ export class PrdIssue extends Schema.Class<PrdIssue>("PrdIssue")({
|
|
|
146
151
|
|
|
147
152
|
static fromLinearIssue(issue: Issue): PrdIssue {
|
|
148
153
|
return new PrdIssue({
|
|
149
|
-
id: issue.
|
|
154
|
+
id: issue.identifier,
|
|
150
155
|
title: issue.title,
|
|
151
156
|
description: issue.description ?? "",
|
|
152
157
|
priority: issue.priority,
|
|
@@ -164,15 +169,18 @@ export class PrdIssue extends Schema.Class<PrdIssue>("PrdIssue")({
|
|
|
164
169
|
|
|
165
170
|
export class PrdList extends Data.Class<{
|
|
166
171
|
readonly issues: ReadonlyMap<string, PrdIssue>
|
|
172
|
+
readonly orignals: ReadonlyMap<string, Issue>
|
|
167
173
|
}> {
|
|
168
174
|
static fromLinearIssues(issues: Issue[]): PrdList {
|
|
169
175
|
const map = new Map<string, PrdIssue>()
|
|
176
|
+
const originalMap = new Map<string, Issue>()
|
|
170
177
|
for (const issue of issues) {
|
|
171
178
|
const prdIssue = PrdIssue.fromLinearIssue(issue)
|
|
172
179
|
if (!prdIssue.id) continue
|
|
173
180
|
map.set(prdIssue.id, prdIssue)
|
|
181
|
+
originalMap.set(prdIssue.id, issue)
|
|
174
182
|
}
|
|
175
|
-
return new PrdList({ issues: map })
|
|
183
|
+
return new PrdList({ issues: map, orignals: originalMap })
|
|
176
184
|
}
|
|
177
185
|
|
|
178
186
|
static fromJson(json: string): ReadonlyArray<PrdIssue> {
|
package/src/PromptGen.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect,
|
|
1
|
+
import { Effect, Layer, ServiceMap } from "effect"
|
|
2
2
|
import { Linear } from "./Linear.ts"
|
|
3
3
|
import { PrdIssue } from "./Prd.ts"
|
|
4
4
|
|
|
@@ -7,26 +7,26 @@ export class PromptGen extends ServiceMap.Service<PromptGen>()(
|
|
|
7
7
|
{
|
|
8
8
|
make: Effect.gen(function* () {
|
|
9
9
|
const linear = yield* Linear
|
|
10
|
-
const fs = yield* FileSystem.FileSystem
|
|
11
|
-
|
|
12
|
-
yield* Effect.scoped(
|
|
13
|
-
fs.open("PROGRESS.md", {
|
|
14
|
-
flag: "a+",
|
|
15
|
-
}),
|
|
16
|
-
)
|
|
17
10
|
|
|
18
11
|
const prompt = `# Instructions
|
|
19
12
|
|
|
20
13
|
1. Decide which single task to work on next from the prd.json file. This should
|
|
21
14
|
be the task YOU decide as the most important to work on next, not just the
|
|
22
15
|
first task in the list.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
3.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
2. Before starting the chosen task, mark it as "in progress" by updating its
|
|
17
|
+
\`stateId\` in the prd.json file.
|
|
18
|
+
This prevents other people or agents from working on the same task simultaneously.
|
|
19
|
+
3. Check if there is an existing Github PR for the task, otherwise create a new
|
|
20
|
+
branch for the task.
|
|
21
|
+
- If there is an existing PR, checkout the branch for that PR.
|
|
22
|
+
- If there is an existing PR, check if there are any new comments or requested
|
|
23
|
+
changes, and address them as part of the task.
|
|
24
|
+
4. Run any checks / feedback loops, such as type checks, unit tests, or linting.
|
|
25
|
+
5. APPEND your progress to the PROGRESS.md file.
|
|
26
|
+
6. Create or update the pull request with your changes once the task is complete. The title of
|
|
27
|
+
the PR should include the task id. The PR description should include a
|
|
28
|
+
summary of the changes made.
|
|
29
|
+
7. Update the prd.json file to reflect any changes in task states.
|
|
30
30
|
- Add follow up tasks only if needed.
|
|
31
31
|
- Append to the \`description\` field with any notes.
|
|
32
32
|
- When a task is complete, set its \`stateId\` to the id that indicates
|
package/src/Runner.ts
CHANGED
|
@@ -5,16 +5,20 @@ import { ChildProcess } from "effect/unstable/process"
|
|
|
5
5
|
import { Prompt } from "effect/unstable/cli"
|
|
6
6
|
import { allCliAgents } from "./domain/CliAgent.ts"
|
|
7
7
|
import { selectedCliAgentId } from "./Settings.ts"
|
|
8
|
+
import { Worktree } from "./Worktree.ts"
|
|
8
9
|
|
|
9
10
|
export const run = Effect.gen(function* () {
|
|
11
|
+
const worktree = yield* Worktree
|
|
10
12
|
const promptGen = yield* PromptGen
|
|
11
13
|
const cliAgent = yield* getOrSelectCliAgent
|
|
14
|
+
|
|
12
15
|
const cliCommand = cliAgent.command({
|
|
13
16
|
prompt: promptGen.prompt,
|
|
14
17
|
prdFilePath: ".lalph/prd.json",
|
|
15
18
|
progressFilePath: "PROGRESS.md",
|
|
16
19
|
})
|
|
17
20
|
const command = ChildProcess.make(cliCommand[0]!, cliCommand.slice(1), {
|
|
21
|
+
cwd: worktree.directory,
|
|
18
22
|
extendEnv: true,
|
|
19
23
|
stdout: "inherit",
|
|
20
24
|
stderr: "inherit",
|
|
@@ -25,7 +29,10 @@ export const run = Effect.gen(function* () {
|
|
|
25
29
|
const exitCode = yield* agent.exitCode
|
|
26
30
|
|
|
27
31
|
yield* Effect.log(`Agent exited with code: ${exitCode}`)
|
|
28
|
-
}).pipe(
|
|
32
|
+
}).pipe(
|
|
33
|
+
Effect.scoped,
|
|
34
|
+
Effect.provide([PromptGen.layer, Prd.layer, Worktree.layer]),
|
|
35
|
+
)
|
|
29
36
|
|
|
30
37
|
export const selectCliAgent = Effect.gen(function* () {
|
|
31
38
|
const agent = yield* Prompt.select({
|
package/src/Worktree.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Effect, FileSystem, Layer, Path, ServiceMap } from "effect"
|
|
2
|
+
import { ChildProcess } from "effect/unstable/process"
|
|
3
|
+
|
|
4
|
+
export class Worktree extends ServiceMap.Service<Worktree>()("lalph/Worktree", {
|
|
5
|
+
make: Effect.gen(function* () {
|
|
6
|
+
const fs = yield* FileSystem.FileSystem
|
|
7
|
+
const pathService = yield* Path.Path
|
|
8
|
+
const directory = yield* fs.makeTempDirectory()
|
|
9
|
+
|
|
10
|
+
yield* Effect.addFinalizer(
|
|
11
|
+
Effect.fnUntraced(function* () {
|
|
12
|
+
yield* execIgnore(ChildProcess.make`git worktree remove ${directory}`)
|
|
13
|
+
}),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
yield* exec(ChildProcess.make`git worktree add ${directory} -d HEAD`)
|
|
17
|
+
|
|
18
|
+
yield* fs.makeDirectory(pathService.join(directory, ".lalph"), {
|
|
19
|
+
recursive: true,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
yield* Effect.forEach(
|
|
23
|
+
[
|
|
24
|
+
ChildProcess.make({
|
|
25
|
+
cwd: directory,
|
|
26
|
+
extendEnv: true,
|
|
27
|
+
shell: process.env.SHELL ?? true,
|
|
28
|
+
})`direnv allow`,
|
|
29
|
+
ChildProcess.make({
|
|
30
|
+
cwd: directory,
|
|
31
|
+
extendEnv: true,
|
|
32
|
+
shell: process.env.SHELL ?? true,
|
|
33
|
+
})`devenv allow`,
|
|
34
|
+
ChildProcess.make({
|
|
35
|
+
cwd: directory,
|
|
36
|
+
})`git submodule update --init --recursive`,
|
|
37
|
+
],
|
|
38
|
+
execIgnore,
|
|
39
|
+
{ concurrency: "unbounded" },
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return { directory } as const
|
|
43
|
+
}),
|
|
44
|
+
}) {
|
|
45
|
+
static layer = Layer.effect(this, this.make)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const exec = (command: ChildProcess.Command) =>
|
|
49
|
+
command.asEffect().pipe(
|
|
50
|
+
Effect.flatMap((proc) => proc.exitCode),
|
|
51
|
+
Effect.scoped,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const execIgnore = (command: ChildProcess.Command) =>
|
|
55
|
+
command.asEffect().pipe(
|
|
56
|
+
Effect.flatMap((proc) => proc.exitCode),
|
|
57
|
+
Effect.catchCause(Effect.logWarning),
|
|
58
|
+
Effect.scoped,
|
|
59
|
+
)
|
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command, Flag } from "effect/unstable/cli"
|
|
4
|
-
import { Effect, Layer, Option } from "effect"
|
|
4
|
+
import { DateTime, Effect, Layer, Option } from "effect"
|
|
5
5
|
import { NodeRuntime, NodeServices } from "@effect/platform-node"
|
|
6
6
|
import { CurrentProject, labelSelect, Linear } from "./Linear.ts"
|
|
7
7
|
import { layerKvs } from "./Kvs.ts"
|
|
@@ -34,7 +34,7 @@ const selectLabel = Command.make("select-label").pipe(
|
|
|
34
34
|
onSome: (l) => l.name,
|
|
35
35
|
})}`,
|
|
36
36
|
)
|
|
37
|
-
}
|
|
37
|
+
}),
|
|
38
38
|
),
|
|
39
39
|
)
|
|
40
40
|
|
|
@@ -44,17 +44,80 @@ const selectAgent = Command.make("select-agent").pipe(
|
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
const iterations = Flag.integer("iterations").pipe(
|
|
47
|
+
Flag.withDescription("Number of iterations to run, defaults to unlimited"),
|
|
47
48
|
Flag.withAlias("i"),
|
|
49
|
+
Flag.withDefault(Number.POSITIVE_INFINITY),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const concurrency = Flag.integer("concurrency").pipe(
|
|
53
|
+
Flag.withDescription("Number of concurrent agents, defaults to 1"),
|
|
54
|
+
Flag.withAlias("c"),
|
|
48
55
|
Flag.withDefault(1),
|
|
49
56
|
)
|
|
50
57
|
|
|
51
|
-
const root = Command.make("lalph", { iterations }).pipe(
|
|
58
|
+
const root = Command.make("lalph", { iterations, concurrency }).pipe(
|
|
52
59
|
Command.withHandler(
|
|
53
|
-
Effect.fnUntraced(function* ({ iterations }) {
|
|
54
|
-
|
|
60
|
+
Effect.fnUntraced(function* ({ iterations, concurrency }) {
|
|
61
|
+
const isFinite = Number.isFinite(iterations)
|
|
62
|
+
const iterationsDisplay = isFinite ? iterations : "unlimited"
|
|
63
|
+
const runConcurrency = Math.max(1, concurrency)
|
|
64
|
+
const semaphore = Effect.makeSemaphoreUnsafe(runConcurrency)
|
|
65
|
+
|
|
66
|
+
yield* Effect.log(
|
|
67
|
+
`Executing ${iterationsDisplay} iteration(s) with concurrency ${runConcurrency}`,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
let iteration = 0
|
|
71
|
+
let lastStartedAt = DateTime.makeUnsafe(0)
|
|
72
|
+
let inProgress = 0
|
|
55
73
|
|
|
56
|
-
|
|
57
|
-
yield*
|
|
74
|
+
while (true) {
|
|
75
|
+
yield* semaphore.take(1)
|
|
76
|
+
if (isFinite && iteration >= iterations) {
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const currentIteration = iteration
|
|
81
|
+
|
|
82
|
+
if (inProgress > 0) {
|
|
83
|
+
// add delay to try keep task list in sync
|
|
84
|
+
const nextEarliestStart = lastStartedAt.pipe(
|
|
85
|
+
DateTime.add({ seconds: 30 }),
|
|
86
|
+
)
|
|
87
|
+
const diff = DateTime.distance(yield* DateTime.now, nextEarliestStart)
|
|
88
|
+
if (diff > 0) {
|
|
89
|
+
yield* Effect.sleep(diff)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lastStartedAt = yield* DateTime.now
|
|
94
|
+
inProgress++
|
|
95
|
+
|
|
96
|
+
yield* run.pipe(
|
|
97
|
+
Effect.catchTag("NoMoreWork", (e) => {
|
|
98
|
+
if (isFinite) {
|
|
99
|
+
// If we have a finite number of iterations, we exit when no more
|
|
100
|
+
// work is found
|
|
101
|
+
iterations = currentIteration
|
|
102
|
+
return Effect.fail(e)
|
|
103
|
+
}
|
|
104
|
+
return Effect.log(
|
|
105
|
+
"No more work to process, waiting 30 seconds...",
|
|
106
|
+
).pipe(Effect.andThen(Effect.sleep("30 seconds")))
|
|
107
|
+
}),
|
|
108
|
+
Effect.catchCause(Effect.logWarning),
|
|
109
|
+
Effect.annotateLogs({
|
|
110
|
+
iteration: currentIteration,
|
|
111
|
+
}),
|
|
112
|
+
Effect.ensuring(
|
|
113
|
+
Effect.suspend(() => {
|
|
114
|
+
inProgress--
|
|
115
|
+
return semaphore.release(1)
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
Effect.forkChild,
|
|
119
|
+
)
|
|
120
|
+
iteration++
|
|
58
121
|
}
|
|
59
122
|
}),
|
|
60
123
|
),
|
|
@@ -64,6 +127,10 @@ const root = Command.make("lalph", { iterations }).pipe(
|
|
|
64
127
|
Command.run(root, {
|
|
65
128
|
version: "0.1.0",
|
|
66
129
|
}).pipe(
|
|
67
|
-
Effect.provide(
|
|
130
|
+
Effect.provide(
|
|
131
|
+
Layer.mergeAll(Settings.layer, Linear.layer).pipe(
|
|
132
|
+
Layer.provideMerge(NodeServices.layer),
|
|
133
|
+
),
|
|
134
|
+
),
|
|
68
135
|
NodeRuntime.runMain,
|
|
69
136
|
)
|