lalph 0.2.10 → 0.2.11

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;
@@ -151979,7 +151978,7 @@ const runProject = fnUntraced(function* (options) {
151979
151978
  commandPrefix: options.commandPrefix,
151980
151979
  review: options.project.reviewAgent
151981
151980
  }).pipe(provide$1(options.project.gitFlow === "commit" ? GitFlowCommit : GitFlowPR, { local: true }), withWorkerState(options.project.id))), catchTags({
151982
- NoMoreWork(_) {
151981
+ NoMoreWork(_error) {
151983
151982
  if (isFinite) {
151984
151983
  iterations = currentIteration;
151985
151984
  return log$1(`No more work to process, ending after ${currentIteration} iteration(s).`);
@@ -151987,7 +151986,7 @@ const runProject = fnUntraced(function* (options) {
151987
151986
  const log = size$3(fibers) <= 1 ? log$1("No more work to process, waiting 30 seconds...") : void_$1;
151988
151987
  return andThen(log, sleep(seconds(30)));
151989
151988
  },
151990
- QuitError(_) {
151989
+ QuitError(_error) {
151991
151990
  quit = true;
151992
151991
  return void_$1;
151993
151992
  }
@@ -152094,7 +152093,7 @@ const commandPlanTasks = make$35("tasks", { specificationPath }).pipe(withDescri
152094
152093
  Settings.layer,
152095
152094
  PromptGen.layer,
152096
152095
  Prd.layerProvided.pipe(provide$3(layerProjectIdPrompt)),
152097
- Worktree.layer
152096
+ Worktree.layer.pipe(provide$3(layerProjectIdPrompt))
152098
152097
  ]))));
152099
152098
 
152100
152099
  //#endregion
@@ -152120,11 +152119,6 @@ const plan = fnUntraced(function* (options) {
152120
152119
  const pathService = yield* Path$1;
152121
152120
  const worktree = yield* Worktree;
152122
152121
  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
152122
  yield* agentPlanner({
152129
152123
  specsDirectory: options.specsDirectory,
152130
152124
  commandPrefix: options.commandPrefix,
@@ -152142,7 +152136,7 @@ const plan = fnUntraced(function* (options) {
152142
152136
  }, scoped$1, provide$1([
152143
152137
  PromptGen.layer,
152144
152138
  Prd.layerProvided,
152145
- Worktree.layer,
152139
+ Worktree.layer.pipe(provide$3(layerProjectIdPrompt)),
152146
152140
  Settings.layer,
152147
152141
  CurrentIssueSource.layer
152148
152142
  ]));
@@ -152232,7 +152226,7 @@ const commandSource = make$35("source").pipe(withDescription("Select the issue s
152232
152226
 
152233
152227
  //#endregion
152234
152228
  //#region package.json
152235
- var version = "0.2.10";
152229
+ var version = "0.2.11";
152236
152230
 
152237
152231
  //#endregion
152238
152232
  //#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.11",
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* () {
@@ -284,7 +265,7 @@ const runProject = Effect.fnUntraced(
284
265
  ),
285
266
  ),
286
267
  Effect.catchTags({
287
- NoMoreWork(_) {
268
+ NoMoreWork(_error) {
288
269
  if (isFinite) {
289
270
  // If we have a finite number of iterations, we exit when no more
290
271
  // work is found
@@ -299,7 +280,7 @@ const runProject = Effect.fnUntraced(
299
280
  : Effect.void
300
281
  return Effect.andThen(log, Effect.sleep(Duration.seconds(30)))
301
282
  },
302
- QuitError(_) {
283
+ QuitError(_error) {
303
284
  quit = true
304
285
  return Effect.void
305
286
  },