lalph 0.2.10 → 0.2.12

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 CHANGED
@@ -151168,6 +151168,139 @@ const withWorkerState = (projectId) => (effect) => AtomRegistry.use((registry) =
151168
151168
  }));
151169
151169
  });
151170
151170
 
151171
+ //#endregion
151172
+ //#region src/Projects.ts
151173
+ const layerProjectIdPrompt = effect$1(CurrentProjectId, gen(function* () {
151174
+ return (yield* selectProject).id;
151175
+ })).pipe(provide$3(Settings.layer), provide$3(CurrentIssueSource.layer));
151176
+ const allProjects = new Setting("projects", Array$1(Project$1));
151177
+ const getAllProjects = Settings.get(allProjects).pipe(map$8(getOrElse$1(() => [])));
151178
+ const projectById = fnUntraced(function* (projectId) {
151179
+ const projects = yield* getAllProjects;
151180
+ return findFirst$3(projects, (p) => p.id === projectId);
151181
+ });
151182
+ const allProjectsAtom = (function() {
151183
+ const read = Settings.runtime.atom(fnUntraced(function* () {
151184
+ const projects = yield* (yield* Settings).get(allProjects);
151185
+ return getOrElse$1(projects, () => []);
151186
+ }));
151187
+ const set = Settings.runtime.fn()(fnUntraced(function* (value, get) {
151188
+ yield* (yield* Settings).set(allProjects, some$2(value));
151189
+ get.refresh(read);
151190
+ }));
151191
+ return writable((get) => {
151192
+ get.mount(set);
151193
+ return get(read);
151194
+ }, (ctx, value) => {
151195
+ ctx.set(set, value);
151196
+ }, (r) => r(read));
151197
+ })();
151198
+ const projectAtom = family((projectId) => {
151199
+ const read = make(fnUntraced(function* (get) {
151200
+ const projects = yield* get.result(allProjectsAtom);
151201
+ return findFirst$3(projects, (p) => p.id === projectId);
151202
+ }));
151203
+ const set = Settings.runtime.fn()(fnUntraced(function* (value, get) {
151204
+ const projects = yield* get.result(allProjectsAtom);
151205
+ const updatedProjects = match$7(value, {
151206
+ onNone: () => filter$6(projects, (p) => p.id !== projectId),
151207
+ onSome: (project) => map$12(projects, (p) => p.id === projectId ? project : p)
151208
+ });
151209
+ get.set(allProjectsAtom, updatedProjects);
151210
+ }));
151211
+ return writable((get) => {
151212
+ get.mount(set);
151213
+ return get(read);
151214
+ }, (ctx, value) => {
151215
+ ctx.set(set, value);
151216
+ }, (refresh) => refresh(read));
151217
+ });
151218
+ const selectProject = gen(function* () {
151219
+ const projects = yield* getAllProjects;
151220
+ if (projects.length === 0) return yield* welcomeWizard;
151221
+ else if (projects.length === 1) {
151222
+ const project = projects[0];
151223
+ yield* log$1(`Using project: ${project.id}`);
151224
+ return project;
151225
+ }
151226
+ return yield* autoComplete({
151227
+ message: "Select a project:",
151228
+ choices: projects.map((p) => ({
151229
+ title: p.id,
151230
+ value: p
151231
+ }))
151232
+ });
151233
+ });
151234
+ const welcomeWizard = gen(function* () {
151235
+ const welcome = [
151236
+ " .--.",
151237
+ " |^()^| lalph",
151238
+ " '--'",
151239
+ "",
151240
+ "Welcome! Let's add your first project.",
151241
+ "Projects let you configure how lalph runs tasks.",
151242
+ ""
151243
+ ].join("\n");
151244
+ console.log(welcome);
151245
+ return yield* addOrUpdateProject();
151246
+ });
151247
+ const addOrUpdateProject = fnUntraced(function* (existing) {
151248
+ const projects = yield* getAllProjects;
151249
+ const id = existing ? existing.id : yield* text$2({
151250
+ message: "Project name",
151251
+ validate(input) {
151252
+ input = input.trim();
151253
+ if (input.length === 0) return fail$4("Project name cannot be empty");
151254
+ else if (projects.some((p) => p.id === input)) return fail$4("Project already exists");
151255
+ return succeed$1(input);
151256
+ }
151257
+ });
151258
+ const concurrency = yield* integer$2({
151259
+ message: "Concurrency (number of tasks to run in parallel)",
151260
+ min: 1
151261
+ });
151262
+ const targetBranch = pipe(yield* text$2({ message: "Target branch (leave empty to use HEAD)" }), trim, liftPredicate(isNonEmpty));
151263
+ const gitFlow = yield* select({
151264
+ message: "Git flow",
151265
+ choices: [{
151266
+ title: "Pull Request",
151267
+ description: "Create a pull request for each task",
151268
+ value: "pr"
151269
+ }, {
151270
+ title: "Commit",
151271
+ description: "Tasks are committed directly to the target branch",
151272
+ value: "commit"
151273
+ }]
151274
+ });
151275
+ const reviewAgent = yield* toggle({ message: "Enable review agent?" });
151276
+ const project = new Project$1({
151277
+ id: ProjectId.makeUnsafe(id),
151278
+ enabled: existing ? existing.enabled : true,
151279
+ concurrency,
151280
+ targetBranch,
151281
+ gitFlow,
151282
+ reviewAgent
151283
+ });
151284
+ yield* Settings.set(allProjects, some$2(existing ? projects.map((p) => p.id === project.id ? project : p) : [...projects, project]));
151285
+ const source = yield* IssueSource;
151286
+ yield* source.reset.pipe(provideService(CurrentProjectId, project.id));
151287
+ yield* source.settings(project.id);
151288
+ return project;
151289
+ });
151290
+
151291
+ //#endregion
151292
+ //#region src/shared/git.ts
151293
+ const parseBranch = (ref) => {
151294
+ const parts = ref.split("/");
151295
+ const remote = parts.length > 1 ? parts[0] : "origin";
151296
+ const branch = parts.length > 1 ? parts.slice(1).join("/") : parts[0];
151297
+ return {
151298
+ remote,
151299
+ branch,
151300
+ branchWithRemote: `${remote}/${branch}`
151301
+ };
151302
+ };
151303
+
151171
151304
  //#endregion
151172
151305
  //#region src/Worktree.ts
151173
151306
  var Worktree = class extends Service$1()("lalph/Worktree", { make: gen(function* () {
@@ -151188,17 +151321,16 @@ var Worktree = class extends Service$1()("lalph/Worktree", { make: gen(function*
151188
151321
  }));
151189
151322
  yield* make$23`git worktree add ${directory} -d HEAD`.pipe(exitCode);
151190
151323
  yield* fs.makeDirectory(pathService.join(directory, ".lalph"), { recursive: true });
151191
- const setupPath = pathService.resolve("scripts", "worktree-setup.sh");
151192
- yield* seedSetupScript(setupPath);
151193
- yield* seedCheckoutScript(pathService.resolve("scripts", "checkout-setup.sh"));
151194
- if (yield* fs.exists(setupPath)) yield* make$23({
151195
- cwd: directory,
151196
- shell: process.env.SHELL ?? true
151197
- })`${setupPath}`.pipe(exitCode);
151324
+ const execHelpers = yield* makeExecHelpers({ directory });
151325
+ yield* setupWorktree({
151326
+ directory,
151327
+ exec: execHelpers.exec,
151328
+ pathService
151329
+ });
151198
151330
  return {
151199
151331
  directory,
151200
151332
  inExisting,
151201
- ...yield* makeExecHelpers({ directory })
151333
+ ...execHelpers
151202
151334
  };
151203
151335
  }).pipe(withSpan("Worktree.build")) }) {
151204
151336
  static layer = effect$1(this, this.make);
@@ -151223,11 +151355,31 @@ const seedSetupScript = fnUntraced(function* (setupPath) {
151223
151355
  yield* fs.writeFileString(setupPath, setupScriptTemplate(baseBranch));
151224
151356
  yield* fs.chmod(setupPath, 493);
151225
151357
  });
151226
- const seedCheckoutScript = fnUntraced(function* (setupPath) {
151358
+ const setupWorktree = fnUntraced(function* (options) {
151227
151359
  const fs = yield* FileSystem;
151228
- if (yield* fs.exists(setupPath)) return;
151229
- yield* fs.writeFileString(setupPath, checkoutScriptTemplate);
151230
- yield* fs.chmod(setupPath, 493);
151360
+ const targetBranch = yield* getTargetBranch;
151361
+ const shouldUseWorktree = isSome(targetBranch);
151362
+ if (shouldUseWorktree) {
151363
+ const parsed = parseBranch(targetBranch.value);
151364
+ if ((yield* options.exec`git checkout ${parsed.branchWithRemote}`) !== 0) {
151365
+ yield* options.exec`git checkout -b ${parsed.branch}`;
151366
+ yield* options.exec`git push -u ${parsed.remote} ${parsed.branch}`;
151367
+ }
151368
+ }
151369
+ const setupPath = shouldUseWorktree ? options.pathService.join(options.directory, "scripts", "worktree-setup.sh") : options.pathService.resolve("scripts", "worktree-setup.sh");
151370
+ yield* seedSetupScript(setupPath);
151371
+ if (yield* fs.exists(setupPath)) {
151372
+ const setupCwd = shouldUseWorktree ? options.directory : options.pathService.resolve(".");
151373
+ yield* make$23({
151374
+ cwd: setupCwd,
151375
+ shell: process.env.SHELL ?? true
151376
+ })`${setupPath}`.pipe(exitCode);
151377
+ }
151378
+ });
151379
+ const getTargetBranch = gen(function* () {
151380
+ const project = yield* projectById(yield* CurrentProjectId);
151381
+ if (isNone(project)) return none$4();
151382
+ return project.value.targetBranch;
151231
151383
  });
151232
151384
  const discoverBaseBranch = gen(function* () {
151233
151385
  const originHead = yield* make$23`git symbolic-ref --short refs/remotes/origin/HEAD`.pipe(string, catch_$1((_) => succeed$1("")), map$8((output) => output.trim()));
@@ -151243,13 +151395,6 @@ git checkout origin/${baseBranch}
151243
151395
 
151244
151396
  # Seeded by lalph. Customize this to prepare new worktrees.
151245
151397
  `;
151246
- const checkoutScriptTemplate = `#!/usr/bin/env bash
151247
- set -euo pipefail
151248
-
151249
- pnpm install || true
151250
-
151251
- # Seeded by lalph. Customize this to prepare branches after checkout.
151252
- `;
151253
151398
  const makeExecHelpers = fnUntraced(function* (options) {
151254
151399
  const spawner = yield* ChildProcessSpawner;
151255
151400
  const provide = provideService(ChildProcessSpawner, spawner);
@@ -151559,19 +151704,6 @@ const makeWaitForFile = gen(function* () {
151559
151704
  return (directory, name) => pipe(fs.watch(directory), filter$2((e) => pathService.basename(e.path) === name), runHead);
151560
151705
  });
151561
151706
 
151562
- //#endregion
151563
- //#region src/shared/git.ts
151564
- const parseBranch = (ref) => {
151565
- const parts = ref.split("/");
151566
- const remote = parts.length > 1 ? parts[0] : "origin";
151567
- const branch = parts.length > 1 ? parts.slice(1).join("/") : parts[0];
151568
- return {
151569
- remote,
151570
- branch,
151571
- branchWithRemote: `${remote}/${branch}`
151572
- };
151573
- };
151574
-
151575
151707
  //#endregion
151576
151708
  //#region src/GitFlow.ts
151577
151709
  var GitFlow = class extends Service$1()("lalph/GitFlow") {};
@@ -151730,126 +151862,6 @@ const agentTimeout = fnUntraced(function* (options) {
151730
151862
  }));
151731
151863
  });
151732
151864
 
151733
- //#endregion
151734
- //#region src/Projects.ts
151735
- const layerProjectIdPrompt = effect$1(CurrentProjectId, gen(function* () {
151736
- return (yield* selectProject).id;
151737
- })).pipe(provide$3(Settings.layer), provide$3(CurrentIssueSource.layer));
151738
- const allProjects = new Setting("projects", Array$1(Project$1));
151739
- const getAllProjects = Settings.get(allProjects).pipe(map$8(getOrElse$1(() => [])));
151740
- const projectById = fnUntraced(function* (projectId) {
151741
- const projects = yield* getAllProjects;
151742
- return findFirst$3(projects, (p) => p.id === projectId);
151743
- });
151744
- const allProjectsAtom = (function() {
151745
- const read = Settings.runtime.atom(fnUntraced(function* () {
151746
- const projects = yield* (yield* Settings).get(allProjects);
151747
- return getOrElse$1(projects, () => []);
151748
- }));
151749
- const set = Settings.runtime.fn()(fnUntraced(function* (value, get) {
151750
- yield* (yield* Settings).set(allProjects, some$2(value));
151751
- get.refresh(read);
151752
- }));
151753
- return writable((get) => {
151754
- get.mount(set);
151755
- return get(read);
151756
- }, (ctx, value) => {
151757
- ctx.set(set, value);
151758
- }, (r) => r(read));
151759
- })();
151760
- const projectAtom = family((projectId) => {
151761
- const read = make(fnUntraced(function* (get) {
151762
- const projects = yield* get.result(allProjectsAtom);
151763
- return findFirst$3(projects, (p) => p.id === projectId);
151764
- }));
151765
- const set = Settings.runtime.fn()(fnUntraced(function* (value, get) {
151766
- const projects = yield* get.result(allProjectsAtom);
151767
- const updatedProjects = match$7(value, {
151768
- onNone: () => filter$6(projects, (p) => p.id !== projectId),
151769
- onSome: (project) => map$12(projects, (p) => p.id === projectId ? project : p)
151770
- });
151771
- get.set(allProjectsAtom, updatedProjects);
151772
- }));
151773
- return writable((get) => {
151774
- get.mount(set);
151775
- return get(read);
151776
- }, (ctx, value) => {
151777
- ctx.set(set, value);
151778
- }, (refresh) => refresh(read));
151779
- });
151780
- const selectProject = gen(function* () {
151781
- const projects = yield* getAllProjects;
151782
- if (projects.length === 0) return yield* welcomeWizard;
151783
- else if (projects.length === 1) {
151784
- const project = projects[0];
151785
- yield* log$1(`Using project: ${project.id}`);
151786
- return project;
151787
- }
151788
- return yield* autoComplete({
151789
- message: "Select a project:",
151790
- choices: projects.map((p) => ({
151791
- title: p.id,
151792
- value: p
151793
- }))
151794
- });
151795
- });
151796
- const welcomeWizard = gen(function* () {
151797
- const welcome = [
151798
- " .--.",
151799
- " |^()^| lalph",
151800
- " '--'",
151801
- "",
151802
- "Welcome! Let's add your first project.",
151803
- "Projects let you configure how lalph runs tasks.",
151804
- ""
151805
- ].join("\n");
151806
- console.log(welcome);
151807
- return yield* addOrUpdateProject();
151808
- });
151809
- const addOrUpdateProject = fnUntraced(function* (existing) {
151810
- const projects = yield* getAllProjects;
151811
- const id = existing ? existing.id : yield* text$2({
151812
- message: "Project name",
151813
- validate(input) {
151814
- input = input.trim();
151815
- if (input.length === 0) return fail$4("Project name cannot be empty");
151816
- else if (projects.some((p) => p.id === input)) return fail$4("Project already exists");
151817
- return succeed$1(input);
151818
- }
151819
- });
151820
- const concurrency = yield* integer$2({
151821
- message: "Concurrency (number of tasks to run in parallel)",
151822
- min: 1
151823
- });
151824
- const targetBranch = pipe(yield* text$2({ message: "Target branch (leave empty to use HEAD)" }), trim, liftPredicate(isNonEmpty));
151825
- const gitFlow = yield* select({
151826
- message: "Git flow",
151827
- choices: [{
151828
- title: "Pull Request",
151829
- description: "Create a pull request for each task",
151830
- value: "pr"
151831
- }, {
151832
- title: "Commit",
151833
- description: "Tasks are committed directly to the target branch",
151834
- value: "commit"
151835
- }]
151836
- });
151837
- const reviewAgent = yield* toggle({ message: "Enable review agent?" });
151838
- const project = new Project$1({
151839
- id: ProjectId.makeUnsafe(id),
151840
- enabled: existing ? existing.enabled : true,
151841
- concurrency,
151842
- targetBranch,
151843
- gitFlow,
151844
- reviewAgent
151845
- });
151846
- yield* Settings.set(allProjects, some$2(existing ? projects.map((p) => p.id === project.id ? project : p) : [...projects, project]));
151847
- const source = yield* IssueSource;
151848
- yield* source.reset.pipe(provideService(CurrentProjectId, project.id));
151849
- yield* source.settings(project.id);
151850
- return project;
151851
- });
151852
-
151853
151865
  //#endregion
151854
151866
  //#region src/commands/root.ts
151855
151867
  const run = fnUntraced(function* (options) {
@@ -151864,19 +151876,6 @@ const run = fnUntraced(function* (options) {
151864
151876
  const gitFlow = yield* GitFlow;
151865
151877
  const currentWorker = yield* CurrentWorkerState;
151866
151878
  const registry = yield* AtomRegistry;
151867
- if (isSome(options.targetBranch)) {
151868
- const parsed = parseBranch(options.targetBranch.value);
151869
- if ((yield* worktree.exec`git checkout ${parsed.branchWithRemote}`) !== 0) {
151870
- yield* worktree.exec`git checkout -b ${parsed.branch}`;
151871
- yield* worktree.exec`git push -u ${parsed.remote} ${parsed.branch}`;
151872
- }
151873
- }
151874
- if (gitFlow.branch) {
151875
- yield* worktree.exec`git branch -D ${gitFlow.branch}`;
151876
- yield* worktree.exec`git checkout -b ${gitFlow.branch}`;
151877
- }
151878
- const checkoutScript = pathService.resolve("scripts", "checkout-setup.sh");
151879
- if (yield* fs.exists(checkoutScript)) yield* worktree.exec`${checkoutScript}`;
151880
151879
  yield* addFinalizer(fnUntraced(function* () {
151881
151880
  const currentBranchName = yield* worktree.currentBranch(worktree.directory).pipe(option$1, map$8(getOrUndefined));
151882
151881
  if (!currentBranchName) return;
@@ -151886,9 +151885,12 @@ const run = fnUntraced(function* (options) {
151886
151885
  let taskId = void 0;
151887
151886
  yield* addFinalizer(fnUntraced(function* (exit) {
151888
151887
  if (exit._tag === "Success") return;
151889
- const prd = yield* Prd;
151890
- if (taskId) yield* prd.maybeRevertIssue({ issueId: taskId });
151891
- else yield* prd.revertUpdatedIssues;
151888
+ if (taskId) yield* source.updateIssue({
151889
+ projectId,
151890
+ issueId: taskId,
151891
+ state: "todo"
151892
+ });
151893
+ else yield* (yield* Prd).revertUpdatedIssues;
151892
151894
  }, ignore()));
151893
151895
  registry.update(currentWorker.state, (s) => s.transitionTo(WorkerStatus.ChoosingTask()));
151894
151896
  const chosenTask = yield* agentChooser({
@@ -151979,7 +151981,7 @@ const runProject = fnUntraced(function* (options) {
151979
151981
  commandPrefix: options.commandPrefix,
151980
151982
  review: options.project.reviewAgent
151981
151983
  }).pipe(provide$1(options.project.gitFlow === "commit" ? GitFlowCommit : GitFlowPR, { local: true }), withWorkerState(options.project.id))), catchTags({
151982
- NoMoreWork(_) {
151984
+ NoMoreWork(_error) {
151983
151985
  if (isFinite) {
151984
151986
  iterations = currentIteration;
151985
151987
  return log$1(`No more work to process, ending after ${currentIteration} iteration(s).`);
@@ -151987,7 +151989,7 @@ const runProject = fnUntraced(function* (options) {
151987
151989
  const log = size$3(fibers) <= 1 ? log$1("No more work to process, waiting 30 seconds...") : void_$1;
151988
151990
  return andThen(log, sleep(seconds(30)));
151989
151991
  },
151990
- QuitError(_) {
151992
+ QuitError(_error) {
151991
151993
  quit = true;
151992
151994
  return void_$1;
151993
151995
  }
@@ -152094,7 +152096,7 @@ const commandPlanTasks = make$35("tasks", { specificationPath }).pipe(withDescri
152094
152096
  Settings.layer,
152095
152097
  PromptGen.layer,
152096
152098
  Prd.layerProvided.pipe(provide$3(layerProjectIdPrompt)),
152097
- Worktree.layer
152099
+ Worktree.layer.pipe(provide$3(layerProjectIdPrompt))
152098
152100
  ]))));
152099
152101
 
152100
152102
  //#endregion
@@ -152120,11 +152122,6 @@ const plan = fnUntraced(function* (options) {
152120
152122
  const pathService = yield* Path$1;
152121
152123
  const worktree = yield* Worktree;
152122
152124
  const cliAgent = yield* getOrSelectCliAgent;
152123
- const exec = (template, ...args) => exitCode(make$23({
152124
- cwd: worktree.directory,
152125
- extendEnv: true
152126
- })(template, ...args));
152127
- if (isSome(options.targetBranch)) yield* exec`git checkout ${options.targetBranch.value.includes("/") ? options.targetBranch.value : `origin/${options.targetBranch.value}`}`;
152128
152125
  yield* agentPlanner({
152129
152126
  specsDirectory: options.specsDirectory,
152130
152127
  commandPrefix: options.commandPrefix,
@@ -152142,7 +152139,7 @@ const plan = fnUntraced(function* (options) {
152142
152139
  }, scoped$1, provide$1([
152143
152140
  PromptGen.layer,
152144
152141
  Prd.layerProvided,
152145
- Worktree.layer,
152142
+ Worktree.layer.pipe(provide$3(layerProjectIdPrompt)),
152146
152143
  Settings.layer,
152147
152144
  CurrentIssueSource.layer
152148
152145
  ]));
@@ -152232,7 +152229,7 @@ const commandSource = make$35("source").pipe(withDescription("Select the issue s
152232
152229
 
152233
152230
  //#endregion
152234
152231
  //#region package.json
152235
- var version = "0.2.10";
152232
+ var version = "0.2.12";
152236
152233
 
152237
152234
  //#endregion
152238
152235
  //#region src/commands/projects/ls.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lalph",
3
3
  "type": "module",
4
- "version": "0.2.10",
4
+ "version": "0.2.12",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
package/src/Worktree.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  Layer,
10
10
  Option,
11
11
  Path,
12
+ PlatformError,
12
13
  Schema,
13
14
  ServiceMap,
14
15
  Stream,
@@ -18,6 +19,9 @@ import { RunnerStalled } from "./domain/Errors.ts"
18
19
  import type { CliAgent } from "./domain/CliAgent.ts"
19
20
  import { constWorkerMaxOutputChunks, CurrentWorkerState } from "./Workers.ts"
20
21
  import { AtomRegistry } from "effect/unstable/reactivity"
22
+ import { CurrentProjectId } from "./Settings.ts"
23
+ import { projectById } from "./Projects.ts"
24
+ import { parseBranch } from "./shared/git.ts"
21
25
 
22
26
  export class Worktree extends ServiceMap.Service<Worktree>()("lalph/Worktree", {
23
27
  make: Effect.gen(function* () {
@@ -52,22 +56,17 @@ export class Worktree extends ServiceMap.Service<Worktree>()("lalph/Worktree", {
52
56
  recursive: true,
53
57
  })
54
58
 
55
- const setupPath = pathService.resolve("scripts", "worktree-setup.sh")
56
- yield* seedSetupScript(setupPath)
57
- yield* seedCheckoutScript(
58
- pathService.resolve("scripts", "checkout-setup.sh"),
59
- )
60
- if (yield* fs.exists(setupPath)) {
61
- yield* ChildProcess.make({
62
- cwd: directory,
63
- shell: process.env.SHELL ?? true,
64
- })`${setupPath}`.pipe(ChildProcess.exitCode)
65
- }
59
+ const execHelpers = yield* makeExecHelpers({ directory })
60
+ yield* setupWorktree({
61
+ directory,
62
+ exec: execHelpers.exec,
63
+ pathService,
64
+ })
66
65
 
67
66
  return {
68
67
  directory,
69
68
  inExisting,
70
- ...(yield* makeExecHelpers({ directory })),
69
+ ...execHelpers,
71
70
  } as const
72
71
  }).pipe(Effect.withSpan("Worktree.build")),
73
72
  }) {
@@ -107,13 +106,53 @@ const seedSetupScript = Effect.fnUntraced(function* (setupPath: string) {
107
106
  yield* fs.chmod(setupPath, 0o755)
108
107
  })
109
108
 
110
- const seedCheckoutScript = Effect.fnUntraced(function* (setupPath: string) {
109
+ const setupWorktree = Effect.fnUntraced(function* (options: {
110
+ readonly directory: string
111
+ readonly exec: (
112
+ template: TemplateStringsArray,
113
+ ...args: Array<string | number | boolean>
114
+ ) => Effect.Effect<ChildProcessSpawner.ExitCode, PlatformError.PlatformError>
115
+ readonly pathService: Path.Path
116
+ }) {
111
117
  const fs = yield* FileSystem.FileSystem
118
+ const targetBranch = yield* getTargetBranch
119
+ const shouldUseWorktree = Option.isSome(targetBranch)
120
+
121
+ if (shouldUseWorktree) {
122
+ const parsed = parseBranch(targetBranch.value)
123
+ const code = yield* options.exec`git checkout ${parsed.branchWithRemote}`
124
+ if (code !== 0) {
125
+ yield* options.exec`git checkout -b ${parsed.branch}`
126
+ yield* options.exec`git push -u ${parsed.remote} ${parsed.branch}`
127
+ }
128
+ }
112
129
 
113
- if (yield* fs.exists(setupPath)) return
130
+ const setupPath = shouldUseWorktree
131
+ ? options.pathService.join(
132
+ options.directory,
133
+ "scripts",
134
+ "worktree-setup.sh",
135
+ )
136
+ : options.pathService.resolve("scripts", "worktree-setup.sh")
137
+ yield* seedSetupScript(setupPath)
138
+ if (yield* fs.exists(setupPath)) {
139
+ const setupCwd = shouldUseWorktree
140
+ ? options.directory
141
+ : options.pathService.resolve(".")
142
+ yield* ChildProcess.make({
143
+ cwd: setupCwd,
144
+ shell: process.env.SHELL ?? true,
145
+ })`${setupPath}`.pipe(ChildProcess.exitCode)
146
+ }
147
+ })
114
148
 
115
- yield* fs.writeFileString(setupPath, checkoutScriptTemplate)
116
- yield* fs.chmod(setupPath, 0o755)
149
+ const getTargetBranch = Effect.gen(function* () {
150
+ const projectId = yield* CurrentProjectId
151
+ const project = yield* projectById(projectId)
152
+ if (Option.isNone(project)) {
153
+ return Option.none<string>()
154
+ }
155
+ return project.value.targetBranch
117
156
  })
118
157
 
119
158
  const discoverBaseBranch = Effect.gen(function* () {
@@ -148,14 +187,6 @@ git checkout origin/${baseBranch}
148
187
 
149
188
  # Seeded by lalph. Customize this to prepare new worktrees.
150
189
  `
151
- const checkoutScriptTemplate = `#!/usr/bin/env bash
152
- set -euo pipefail
153
-
154
- pnpm install || true
155
-
156
- # Seeded by lalph. Customize this to prepare branches after checkout.
157
- `
158
-
159
190
  const makeExecHelpers = Effect.fnUntraced(function* (options: {
160
191
  readonly directory: string
161
192
  }) {
@@ -54,7 +54,7 @@ export const commandPlanTasks = Command.make("tasks", {
54
54
  Settings.layer,
55
55
  PromptGen.layer,
56
56
  Prd.layerProvided.pipe(Layer.provide(layerProjectIdPrompt)),
57
- Worktree.layer,
57
+ Worktree.layer.pipe(Layer.provide(layerProjectIdPrompt)),
58
58
  ]),
59
59
  ),
60
60
  ),
@@ -1,14 +1,27 @@
1
- import { Data, Effect, FileSystem, Option, Path, pipe, Schema } from "effect"
1
+ import {
2
+ Data,
3
+ Effect,
4
+ FileSystem,
5
+ Layer,
6
+ Option,
7
+ Path,
8
+ pipe,
9
+ Schema,
10
+ } from "effect"
2
11
  import { PromptGen } from "../PromptGen.ts"
3
12
  import { Prd } from "../Prd.ts"
4
- import { ChildProcess } from "effect/unstable/process"
5
13
  import { Worktree } from "../Worktree.ts"
14
+ import type { ChildProcess } from "effect/unstable/process"
6
15
  import { getCommandPrefix, getOrSelectCliAgent } from "./agent.ts"
7
16
  import { Command, Flag } from "effect/unstable/cli"
8
17
  import { CurrentIssueSource } from "../CurrentIssueSource.ts"
9
18
  import { commandRoot } from "./root.ts"
10
19
  import { CurrentProjectId, Settings } from "../Settings.ts"
11
- import { addOrUpdateProject, selectProject } from "../Projects.ts"
20
+ import {
21
+ addOrUpdateProject,
22
+ layerProjectIdPrompt,
23
+ selectProject,
24
+ } from "../Projects.ts"
12
25
  import { agentPlanner } from "../Agents/planner.ts"
13
26
  import { agentTasker } from "../Agents/tasker.ts"
14
27
  import { commandPlanTasks } from "./plan/tasks.ts"
@@ -64,24 +77,6 @@ const plan = Effect.fnUntraced(
64
77
  const worktree = yield* Worktree
65
78
  const cliAgent = yield* getOrSelectCliAgent
66
79
 
67
- const exec = (
68
- template: TemplateStringsArray,
69
- ...args: Array<string | number | boolean>
70
- ) =>
71
- ChildProcess.exitCode(
72
- ChildProcess.make({
73
- cwd: worktree.directory,
74
- extendEnv: true,
75
- })(template, ...args),
76
- )
77
-
78
- if (Option.isSome(options.targetBranch)) {
79
- const targetWithRemote = options.targetBranch.value.includes("/")
80
- ? options.targetBranch.value
81
- : `origin/${options.targetBranch.value}`
82
- yield* exec`git checkout ${targetWithRemote}`
83
- }
84
-
85
80
  yield* agentPlanner({
86
81
  specsDirectory: options.specsDirectory,
87
82
  commandPrefix: options.commandPrefix,
@@ -119,7 +114,7 @@ const plan = Effect.fnUntraced(
119
114
  Effect.provide([
120
115
  PromptGen.layer,
121
116
  Prd.layerProvided,
122
- Worktree.layer,
117
+ Worktree.layer.pipe(Layer.provide(layerProjectIdPrompt)),
123
118
  Settings.layer,
124
119
  CurrentIssueSource.layer,
125
120
  ]),
@@ -36,7 +36,6 @@ import {
36
36
  } from "../Workers.ts"
37
37
  import { WorkerStatus } from "../domain/WorkerState.ts"
38
38
  import { GitFlow, GitFlowCommit, GitFlowPR } from "../GitFlow.ts"
39
- import { parseBranch } from "../shared/git.ts"
40
39
  import { getAllProjects, welcomeWizard } from "../Projects.ts"
41
40
  import type { Project } from "../domain/Project.ts"
42
41
 
@@ -66,24 +65,6 @@ const run = Effect.fnUntraced(
66
65
  const currentWorker = yield* CurrentWorkerState
67
66
  const registry = yield* AtomRegistry.AtomRegistry
68
67
 
69
- if (Option.isSome(options.targetBranch)) {
70
- const parsed = parseBranch(options.targetBranch.value)
71
- const code = yield* worktree.exec`git checkout ${parsed.branchWithRemote}`
72
- if (code !== 0) {
73
- yield* worktree.exec`git checkout -b ${parsed.branch}`
74
- yield* worktree.exec`git push -u ${parsed.remote} ${parsed.branch}`
75
- }
76
- }
77
- if (gitFlow.branch) {
78
- yield* worktree.exec`git branch -D ${gitFlow.branch}`
79
- yield* worktree.exec`git checkout -b ${gitFlow.branch}`
80
- }
81
-
82
- const checkoutScript = pathService.resolve("scripts", "checkout-setup.sh")
83
- if (yield* fs.exists(checkoutScript)) {
84
- yield* worktree.exec`${checkoutScript}`
85
- }
86
-
87
68
  // ensure cleanup of branch after run
88
69
  yield* Effect.addFinalizer(
89
70
  Effect.fnUntraced(function* () {
@@ -105,12 +86,14 @@ const run = Effect.fnUntraced(
105
86
  yield* Effect.addFinalizer(
106
87
  Effect.fnUntraced(function* (exit) {
107
88
  if (exit._tag === "Success") return
108
- const prd = yield* Prd
109
89
  if (taskId) {
110
- yield* prd.maybeRevertIssue({
90
+ yield* source.updateIssue({
91
+ projectId,
111
92
  issueId: taskId,
93
+ state: "todo",
112
94
  })
113
95
  } else {
96
+ const prd = yield* Prd
114
97
  yield* prd.revertUpdatedIssues
115
98
  }
116
99
  }, Effect.ignore()),
@@ -284,7 +267,7 @@ const runProject = Effect.fnUntraced(
284
267
  ),
285
268
  ),
286
269
  Effect.catchTags({
287
- NoMoreWork(_) {
270
+ NoMoreWork(_error) {
288
271
  if (isFinite) {
289
272
  // If we have a finite number of iterations, we exit when no more
290
273
  // work is found
@@ -299,7 +282,7 @@ const runProject = Effect.fnUntraced(
299
282
  : Effect.void
300
283
  return Effect.andThen(log, Effect.sleep(Duration.seconds(30)))
301
284
  },
302
- QuitError(_) {
285
+ QuitError(_error) {
303
286
  quit = true
304
287
  return Effect.void
305
288
  },