lalph 0.3.12 → 0.3.14

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 CHANGED
@@ -8,11 +8,12 @@ A LLM agent orchestrator driven by your chosen source of issues.
8
8
 
9
9
  ## Features
10
10
 
11
- - Integrate with various issue sources (GitHub Issues, Linear, etc.)
12
- - Plan mode to generate issues from high-level goals
13
- - Uses git worktrees to allow for multiple concurrent iterations
14
- - Creates pull requests for each task, with optional auto-merge for vibe coding
15
- - Support issue dependencies to ensure correct task order
11
+ - Pull work from an issue source (GitHub Issues, Linear, etc.) and keep task state in sync
12
+ - Projects to group execution settings (enabled state, concurrency, target branch, git flow, review agent)
13
+ - Agent presets to control which CLI agent runs tasks, with optional label-based routing
14
+ - Plan mode to turn a high-level plan into a spec and generate PRD tasks
15
+ - Git worktrees to support multiple concurrent iterations
16
+ - Optional PR flow with auto-merge and support for issue dependencies
16
17
 
17
18
  ## Installation
18
19
 
@@ -28,15 +29,78 @@ npx -y lalph@latest
28
29
 
29
30
  ## CLI usage
30
31
 
31
- - Run the main loop: `lalph`
32
- - Run multiple iterations with concurrency: `lalph --iterations 4 --concurrency 2`
32
+ - Run the main loop across enabled projects: `lalph`
33
+ - Run a bounded set of iterations per enabled project: `lalph --iterations 1`
34
+ - Configure projects and per-project concurrency: `lalph projects add`
35
+ - Inspect and configure agent presets: `lalph agents ls`
33
36
  - Start plan mode: `lalph plan`
34
- - Start plan mode without permission prompts: `lalph plan --dangerous`
35
- - Choose your issue source: `lalph source`
37
+ - Create an issue from your editor: `lalph issue`
38
+ - Choose your issue source integration (applies to all projects): `lalph source`
36
39
 
37
40
  It is recommended to add `.lalph/` to your `.gitignore` to avoid committing your
38
41
  credentials.
39
42
 
43
+ ## Agent presets
44
+
45
+ Agent presets define which CLI agent runs tasks (and with what arguments). Lalph
46
+ always needs a default preset and will prompt you to create one on first run if
47
+ it's missing.
48
+
49
+ Some issue sources support routing: you can associate a preset with a label, and
50
+ issues with that label will run with that preset; anything else uses the default.
51
+
52
+ ```bash
53
+ lalph agents ls
54
+ lalph agents add
55
+ ```
56
+
57
+ ## Projects
58
+
59
+ Projects bundle execution settings for the current repo: whether it is enabled
60
+ for runs, how many tasks can run concurrently, which branch to target, what git
61
+ flow to use, and whether review is enabled.
62
+
63
+ `lalph` runs across all enabled projects in parallel; for single-project
64
+ commands, you'll be prompted to choose an active project when needed.
65
+
66
+ ```bash
67
+ lalph projects add
68
+ lalph projects toggle
69
+ ```
70
+
71
+ ## Plan mode
72
+
73
+ Plan mode opens an editor so you can write a high-level plan. On save, lalph
74
+ generates a specification under `--specs` and then creates PRD tasks from it.
75
+
76
+ Use `--dangerous` to skip permission prompts during spec generation, and `--new`
77
+ to create a project before starting plan mode.
78
+
79
+ ```bash
80
+ lalph plan
81
+ lalph plan tasks .specs/my-spec.md
82
+ ```
83
+
84
+ ## Creating issues
85
+
86
+ `lalph issue` opens a new-issue template in your editor. When you save and close
87
+ the file, the issue is created in the current issue source.
88
+
89
+ Anything below the front matter is used as the issue description.
90
+
91
+ Front matter fields:
92
+
93
+ - `title`: short issue title
94
+ - `priority`: number (0 = none, 1 = urgent, 2 = high, 3 = normal, 4 = low)
95
+ - `estimate`: number of points, or `null`
96
+ - `blockedBy`: array of issue identifiers
97
+ - `autoMerge`: whether to mark this issue for auto-merge when applicable
98
+
99
+ ```bash
100
+ lalph issue
101
+ lalph i
102
+ ```
103
+
40
104
  ## Development
41
105
 
42
106
  - Install dependencies: `pnpm install`
package/dist/cli.mjs CHANGED
@@ -144155,16 +144155,49 @@ var LinearError = class extends ErrorClass("lalph/LinearError")({
144155
144155
  cause: Defect
144156
144156
  }) {};
144157
144157
  const selectedProjectId = new ProjectSetting("linear.selectedProjectId", String$1);
144158
- const selectProject$1 = gen(function* () {
144158
+ const createLinearProject = gen(function* () {
144159
144159
  const linear = yield* Linear;
144160
144160
  const projects = yield* runCollect(linear.projects);
144161
- const project = yield* autoComplete({
144162
- message: "Select a Linear project",
144163
- choices: projects.map((project) => ({
144164
- title: project.name,
144165
- value: project
144161
+ const projectName = yield* text$2({
144162
+ message: "Project name",
144163
+ validate(input) {
144164
+ const name = input.trim();
144165
+ if (name.length === 0) return fail$4("Project name cannot be empty");
144166
+ if (projects.some((project) => project.name.toLowerCase() === name.toLowerCase())) return fail$4("A project with this name already exists");
144167
+ return succeed$1(name);
144168
+ }
144169
+ });
144170
+ const teams = yield* linear.use((client) => client.teams()).pipe(map$8((teamConnection) => teamConnection.nodes));
144171
+ const teamId = yield* autoComplete({
144172
+ message: "Select a team for the new project",
144173
+ choices: teams.map((team) => ({
144174
+ title: team.name,
144175
+ value: team.id
144166
144176
  }))
144167
144177
  });
144178
+ const created = yield* linear.use((client) => client.createProject({
144179
+ name: projectName,
144180
+ teamIds: [teamId]
144181
+ }));
144182
+ return yield* linear.use(() => created.project);
144183
+ });
144184
+ const selectProject$1 = gen(function* () {
144185
+ const linear = yield* Linear;
144186
+ const choices = [{
144187
+ title: "Create new",
144188
+ value: { _tag: "create" }
144189
+ }, ...(yield* runCollect(linear.projects)).map((project) => ({
144190
+ title: project.name,
144191
+ value: {
144192
+ _tag: "existing",
144193
+ project
144194
+ }
144195
+ }))];
144196
+ const selected = yield* autoComplete({
144197
+ message: "Select a Linear project",
144198
+ choices
144199
+ });
144200
+ const project = selected._tag === "existing" ? selected.project : yield* createLinearProject;
144168
144201
  yield* Settings.setProject(selectedProjectId, some$2(project.id));
144169
144202
  return project;
144170
144203
  });
@@ -152025,18 +152058,18 @@ const runProject = fnUntraced(function* (options) {
152025
152058
  }
152026
152059
  yield* awaitEmpty(fibers);
152027
152060
  }, (effect, options) => annotateLogs(effect, { project: options.project.id }));
152028
- const iterations = integer("iterations").pipe(withDescription$1("Number of iterations to run, defaults to unlimited"), withAlias("i"), withDefault(Number.POSITIVE_INFINITY));
152029
- const maxIterationMinutes = integer("max-minutes").pipe(withDescription$1("Maximum number of minutes to allow an iteration to run. Defaults to 90 minutes. Env variable: LALPH_MAX_MINUTES"), withFallbackConfig(int("LALPH_MAX_MINUTES")), withDefault(90));
152030
- const stallMinutes = integer("stall-minutes").pipe(withDescription$1("If no activity occurs for this many minutes, the iteration will be stopped. Defaults to 5 minutes. Env variable: LALPH_STALL_MINUTES"), withFallbackConfig(int("LALPH_STALL_MINUTES")), withDefault(5));
152031
- const specsDirectory = directory("specs").pipe(withDescription$1("Directory to store plan specifications. Env variable: LALPH_SPECS"), withAlias("s"), withFallbackConfig(string$1("LALPH_SPECS")), withDefault(".specs"));
152032
- const verbose = boolean("verbose").pipe(withDescription$1("Enable verbose logging"), withAlias("v"));
152061
+ const iterations = integer("iterations").pipe(withDescription$1("Limit how many task iterations run per enabled project (default: unlimited). Use -i 1 to run a single iteration and exit."), withAlias("i"), withDefault(Number.POSITIVE_INFINITY));
152062
+ const maxIterationMinutes = integer("max-minutes").pipe(withDescription$1("Timeout an iteration if execution (and review, if enabled) exceeds this many minutes (default: LALPH_MAX_MINUTES or 90)."), withFallbackConfig(int("LALPH_MAX_MINUTES")), withDefault(90));
152063
+ const stallMinutes = integer("stall-minutes").pipe(withDescription$1("Fail an iteration if the agent stops responding for this many minutes (default: LALPH_STALL_MINUTES or 5)."), withFallbackConfig(int("LALPH_STALL_MINUTES")), withDefault(5));
152064
+ const specsDirectory = directory("specs").pipe(withDescription$1("Directory where plan specs are written and read (default: LALPH_SPECS or .specs)."), withAlias("s"), withFallbackConfig(string$1("LALPH_SPECS")), withDefault(".specs"));
152065
+ const verbose = boolean("verbose").pipe(withDescription$1("Increase log output for debugging. Use -v when you need detailed logs."), withAlias("v"));
152033
152066
  const commandRoot = make$35("lalph", {
152034
152067
  iterations,
152035
152068
  maxIterationMinutes,
152036
152069
  stallMinutes,
152037
152070
  specsDirectory,
152038
152071
  verbose
152039
- }).pipe(withHandler(fnUntraced(function* ({ iterations, maxIterationMinutes, stallMinutes, specsDirectory }) {
152072
+ }).pipe(withDescription("Run the task loop across all enabled projects in parallel: pull issues from the current issue source and execute them with your configured agent preset(s). Use --iterations for a bounded run, and configure per-project concurrency via lalph projects edit."), withHandler(fnUntraced(function* ({ iterations, maxIterationMinutes, stallMinutes, specsDirectory }) {
152040
152073
  yield* getDefaultCliAgentPreset;
152041
152074
  let allProjects = yield* getAllProjects;
152042
152075
  if (allProjects.length === 0) {
@@ -152115,8 +152148,8 @@ const agentTasker = fnUntraced(function* (options) {
152115
152148
  const specificationPath = path("spec", {
152116
152149
  pathType: "file",
152117
152150
  mustExist: true
152118
- }).pipe(withDescription$2("Path to the specification file to convert into tasks"));
152119
- const commandPlanTasks = make$35("tasks", { specificationPath }).pipe(withDescription("Convert a specification into tasks"), withHandler(fnUntraced(function* ({ specificationPath }) {
152151
+ }).pipe(withDescription$2("Required. Path to an existing specification file to convert into tasks"));
152152
+ const commandPlanTasks = make$35("tasks", { specificationPath }).pipe(withDescription("Convert an existing specification file into PRD tasks (without re-running plan mode)"), withHandler(fnUntraced(function* ({ specificationPath }) {
152120
152153
  const { specsDirectory } = yield* commandRoot;
152121
152154
  const fs = yield* FileSystem;
152122
152155
  const pathService = yield* Path$1;
@@ -152176,12 +152209,12 @@ var Editor = class extends Service$1()("lalph/Editor", { make: gen(function* ()
152176
152209
 
152177
152210
  //#endregion
152178
152211
  //#region src/commands/plan.ts
152179
- const dangerous = boolean("dangerous").pipe(withAlias("d"), withDescription$1("Enable dangerous mode (skip permission prompts) during plan generation"));
152180
- const withNewProject = boolean("new").pipe(withAlias("n"), withDescription$1("Create a new project before starting plan mode"));
152212
+ const dangerous = boolean("dangerous").pipe(withAlias("d"), withDescription$1("Skip permission prompts while generating the specification from your plan"));
152213
+ const withNewProject = boolean("new").pipe(withAlias("n"), withDescription$1("Create a new project (via prompts) before starting plan mode"));
152181
152214
  const commandPlan = make$35("plan", {
152182
152215
  dangerous,
152183
152216
  withNewProject
152184
- }).pipe(withDescription("Iterate on an issue plan and create PRD tasks"), withHandler(fnUntraced(function* ({ dangerous, withNewProject }) {
152217
+ }).pipe(withDescription("Open an editor to draft a plan; on save, generate a specification under --specs and then create PRD tasks from it. Use --new to create a project first; use --dangerous to skip permission prompts during spec generation."), withHandler(fnUntraced(function* ({ dangerous, withNewProject }) {
152185
152218
  const thePlan = yield* (yield* Editor).editTemp({ suffix: ".md" });
152186
152219
  if (isNone(thePlan)) return;
152187
152220
  yield* gen(function* () {
@@ -152207,6 +152240,10 @@ const plan = fnUntraced(function* (options) {
152207
152240
  preset
152208
152241
  });
152209
152242
  const planDetails = yield* pipe(fs.readFileString(pathService.join(worktree.directory, ".lalph", "plan.json")), flatMap$2(decodeEffect(PlanDetails)), mapError$2(() => new SpecNotFound()));
152243
+ if (isSome(options.targetBranch)) yield* commitAndPushSpecification({
152244
+ specsDirectory: options.specsDirectory,
152245
+ targetBranch: options.targetBranch.value
152246
+ });
152210
152247
  yield* log$1("Converting specification into tasks");
152211
152248
  yield* agentTasker({
152212
152249
  specificationPath: planDetails.specification,
@@ -152224,6 +152261,28 @@ const plan = fnUntraced(function* (options) {
152224
152261
  var SpecNotFound = class extends TaggedError("SpecNotFound") {
152225
152262
  message = "The AI agent failed to produce a specification.";
152226
152263
  };
152264
+ var SpecGitError = class extends TaggedError("SpecGitError") {};
152265
+ const commitAndPushSpecification = fnUntraced(function* (options) {
152266
+ const worktree = yield* Worktree;
152267
+ const absSpecsDirectory = (yield* Path$1).join(worktree.directory, options.specsDirectory);
152268
+ const git = (args) => make$23("git", [...args], {
152269
+ cwd: worktree.directory,
152270
+ stdout: "inherit",
152271
+ stderr: "inherit"
152272
+ }).pipe(exitCode);
152273
+ if ((yield* git(["add", absSpecsDirectory])) !== 0) return yield* new SpecGitError({ message: "Failed to stage specification changes." });
152274
+ if ((yield* git([
152275
+ "commit",
152276
+ "-m",
152277
+ "Update plan specification"
152278
+ ])) !== 0) return yield* new SpecGitError({ message: "Failed to commit the generated specification changes." });
152279
+ const parsed = parseBranch(options.targetBranch);
152280
+ yield* git([
152281
+ "push",
152282
+ parsed.remote,
152283
+ `HEAD:${parsed.branch}`
152284
+ ]);
152285
+ }, ignore({ log: "Warn" }));
152227
152286
  const PlanDetails = fromJsonString(Struct({ specification: String$1 }));
152228
152287
 
152229
152288
  //#endregion
@@ -152279,8 +152338,8 @@ const handler$1 = flow(withHandler(fnUntraced(function* () {
152279
152338
  console.log(`URL: ${created.url}`);
152280
152339
  }).pipe(provide$1([layerProjectIdPrompt, CurrentIssueSource.layer]));
152281
152340
  })), provide(Editor.layer));
152282
- const commandIssue = make$35("issue").pipe(withDescription("Create a new issue in the selected issue source"), handler$1);
152283
- const commandIssueAlias = make$35("i").pipe(withDescription("Alias for 'issue' command"), handler$1);
152341
+ const commandIssue = make$35("issue").pipe(withDescription("Create a new issue in your editor."), handler$1);
152342
+ const commandIssueAlias = make$35("i").pipe(withDescription("Alias for 'issue' (create a new issue in your editor)."), handler$1);
152284
152343
 
152285
152344
  //#endregion
152286
152345
  //#region src/commands/edit.ts
@@ -152288,20 +152347,20 @@ const handler = withHandler(fnUntraced(function* () {
152288
152347
  const prd = yield* Prd;
152289
152348
  yield* (yield* Editor).edit(prd.path);
152290
152349
  }, provide$1([Prd.layerLocalProvided.pipe(provideMerge(layerProjectIdPrompt)), Editor.layer])));
152291
- const commandEdit = make$35("edit").pipe(withDescription("Open the prd.yml file in your editor"), handler);
152292
- const commandEditAlias = make$35("e").pipe(withDescription("Alias for 'edit' command"), handler);
152350
+ const commandEdit = make$35("edit").pipe(withDescription("Open the selected project's .lalph/prd.yml in your editor."), handler);
152351
+ const commandEditAlias = make$35("e").pipe(withDescription("Alias for 'edit' (open the selected project's .lalph/prd.yml in your editor)."), handler);
152293
152352
 
152294
152353
  //#endregion
152295
152354
  //#region src/commands/source.ts
152296
- const commandSource = make$35("source").pipe(withDescription("Select the issue source to use"), withHandler(() => selectIssueSource), provide(Settings.layer));
152355
+ const commandSource = make$35("source").pipe(withDescription("Select the issue source to use (e.g. GitHub Issues or Linear). This applies to all projects."), withHandler(() => selectIssueSource), provide(Settings.layer));
152297
152356
 
152298
152357
  //#endregion
152299
152358
  //#region package.json
152300
- var version = "0.3.12";
152359
+ var version = "0.3.14";
152301
152360
 
152302
152361
  //#endregion
152303
152362
  //#region src/commands/projects/ls.ts
152304
- const commandProjectsLs = make$35("ls").pipe(withDescription("List all configured projects and their settings"), withHandler(fnUntraced(function* () {
152363
+ const commandProjectsLs = make$35("ls").pipe(withDescription("List configured projects and how they run (enabled state, concurrency, branch, git flow, review agent)."), withHandler(fnUntraced(function* () {
152305
152364
  const meta = yield* CurrentIssueSource;
152306
152365
  const source = yield* IssueSource;
152307
152366
  console.log("Issue source:", meta.name);
@@ -152325,11 +152384,11 @@ const commandProjectsLs = make$35("ls").pipe(withDescription("List all configure
152325
152384
 
152326
152385
  //#endregion
152327
152386
  //#region src/commands/projects/add.ts
152328
- const commandProjectsAdd = make$35("add").pipe(withDescription("Add a new project"), withHandler(() => addOrUpdateProject()), provide(Settings.layer), provide(CurrentIssueSource.layer));
152387
+ const commandProjectsAdd = make$35("add").pipe(withDescription("Add a project and configure its execution settings (concurrency, target branch, git flow, review agent) and issue source settings."), withHandler(() => addOrUpdateProject()), provide(Settings.layer), provide(CurrentIssueSource.layer));
152329
152388
 
152330
152389
  //#endregion
152331
152390
  //#region src/commands/projects/rm.ts
152332
- const commandProjectsRm = make$35("rm").pipe(withDescription("Remove a project"), withHandler(fnUntraced(function* () {
152391
+ const commandProjectsRm = make$35("rm").pipe(withDescription("Remove a project from the configured list and delete its stored state under .lalph/projects."), withHandler(fnUntraced(function* () {
152333
152392
  const fs = yield* FileSystem;
152334
152393
  const pathService = yield* Path$1;
152335
152394
  const projects = yield* getAllProjects;
@@ -152343,14 +152402,14 @@ const commandProjectsRm = make$35("rm").pipe(withDescription("Remove a project")
152343
152402
 
152344
152403
  //#endregion
152345
152404
  //#region src/commands/projects/edit.ts
152346
- const commandProjectsEdit = make$35("edit").pipe(withDescription("Modify a project"), withHandler(fnUntraced(function* () {
152405
+ const commandProjectsEdit = make$35("edit").pipe(withDescription("Edit a project's execution settings (concurrency, target branch, git flow, review agent) and issue source settings."), withHandler(fnUntraced(function* () {
152347
152406
  if ((yield* getAllProjects).length === 0) return yield* log$1("No projects available to edit.");
152348
152407
  yield* addOrUpdateProject(yield* selectProject);
152349
152408
  })), provide(Settings.layer), provide(CurrentIssueSource.layer));
152350
152409
 
152351
152410
  //#endregion
152352
152411
  //#region src/commands/projects/toggle.ts
152353
- const commandProjectsToggle = make$35("toggle").pipe(withDescription("Enable or disable projects"), withHandler(fnUntraced(function* () {
152412
+ const commandProjectsToggle = make$35("toggle").pipe(withDescription("Enable or disable configured projects for lalph runs."), withHandler(fnUntraced(function* () {
152354
152413
  const projects = yield* getAllProjects;
152355
152414
  if (projects.length === 0) return yield* log$1("No projects available to toggle.");
152356
152415
  const enabled = yield* multiSelect({
@@ -152376,12 +152435,12 @@ const subcommands$1 = withSubcommands([
152376
152435
  commandProjectsToggle,
152377
152436
  commandProjectsRm
152378
152437
  ]);
152379
- const commandProjects = make$35("projects").pipe(withDescription("Manage projects"), subcommands$1);
152380
- const commandProjectsAlias = make$35("p").pipe(withDescription("Alias for 'projects' command"), subcommands$1);
152438
+ const commandProjects = make$35("projects").pipe(withDescription("Manage projects and their execution settings (enabled state, concurrency, target branch, git flow, review agent). Use 'ls' to inspect and 'add', 'edit', or 'toggle' to configure."), subcommands$1);
152439
+ const commandProjectsAlias = make$35("p").pipe(withDescription("Alias for 'projects'."), subcommands$1);
152381
152440
 
152382
152441
  //#endregion
152383
152442
  //#region src/commands/sh.ts
152384
- const commandSh = make$35("sh").pipe(withDescription("Enter an interactive shell in the worktree"), withHandler(fnUntraced(function* () {
152443
+ const commandSh = make$35("sh").pipe(withDescription("Launch an interactive shell in the active project's worktree."), withHandler(fnUntraced(function* () {
152385
152444
  const worktree = yield* Worktree;
152386
152445
  const fs = yield* FileSystem;
152387
152446
  const pathService = yield* Path$1;
@@ -152397,7 +152456,7 @@ const commandSh = make$35("sh").pipe(withDescription("Enter an interactive shell
152397
152456
 
152398
152457
  //#endregion
152399
152458
  //#region src/commands/agents/ls.ts
152400
- const commandAgentsLs = make$35("ls").pipe(withDescription("List all configured CLI agent presets"), withHandler(fnUntraced(function* () {
152459
+ const commandAgentsLs = make$35("ls").pipe(withDescription("List configured agent presets (preset ids, agent, arguments, and any issue-source options)."), withHandler(fnUntraced(function* () {
152401
152460
  const meta = yield* CurrentIssueSource;
152402
152461
  const source = yield* IssueSource;
152403
152462
  console.log("Issue source:", meta.name);
@@ -152419,11 +152478,11 @@ const commandAgentsLs = make$35("ls").pipe(withDescription("List all configured
152419
152478
 
152420
152479
  //#endregion
152421
152480
  //#region src/commands/agents/add.ts
152422
- const commandAgentsAdd = make$35("add").pipe(withDescription("Add a new CLI agent preset"), withHandler(() => addOrUpdatePreset()), provide(Settings.layer), provide(CurrentIssueSource.layer));
152481
+ const commandAgentsAdd = make$35("add").pipe(withDescription("Add a new agent preset (interactive prompt for CLI agent, arguments, and any issue-source options)."), withHandler(() => addOrUpdatePreset()), provide(Settings.layer), provide(CurrentIssueSource.layer));
152423
152482
 
152424
152483
  //#endregion
152425
152484
  //#region src/commands/agents/rm.ts
152426
- const commandAgentsRm = make$35("rm").pipe(withDescription("Remove a CLI agent preset"), withHandler(fnUntraced(function* () {
152485
+ const commandAgentsRm = make$35("rm").pipe(withDescription("Remove an agent preset (select a preset to delete from your configuration)."), withHandler(fnUntraced(function* () {
152427
152486
  const presets = yield* getAllCliAgentPresets;
152428
152487
  if (presets.length === 0) return yield* log$1("There are no presets to remove.");
152429
152488
  const preset = yield* selectCliAgentPreset;
@@ -152433,7 +152492,7 @@ const commandAgentsRm = make$35("rm").pipe(withDescription("Remove a CLI agent p
152433
152492
 
152434
152493
  //#endregion
152435
152494
  //#region src/commands/agents/edit.ts
152436
- const commandAgentsEdit = make$35("edit").pipe(withDescription("Modify a CLI agent preset"), withHandler(fnUntraced(function* () {
152495
+ const commandAgentsEdit = make$35("edit").pipe(withDescription("Edit an existing agent preset (interactive prompt to update agent, arguments, and any issue-source options)."), withHandler(fnUntraced(function* () {
152437
152496
  if ((yield* getAllCliAgentPresets).length === 0) return yield* log$1("No presets available to edit.");
152438
152497
  yield* addOrUpdatePreset({ existing: yield* selectCliAgentPreset });
152439
152498
  })), provide(Settings.layer), provide(CurrentIssueSource.layer));
@@ -152446,8 +152505,8 @@ const subcommands = withSubcommands([
152446
152505
  commandAgentsEdit,
152447
152506
  commandAgentsRm
152448
152507
  ]);
152449
- const commandAgents = make$35("agents").pipe(withDescription("Manage CLI agent presets"), subcommands);
152450
- const commandAgentsAlias = make$35("a").pipe(withDescription("Alias for 'agents' command"), subcommands);
152508
+ const commandAgents = make$35("agents").pipe(withDescription("Manage agent presets used to run tasks. Use 'ls' to inspect presets and 'add'/'edit' to configure agents, arguments, and any issue-source options."), subcommands);
152509
+ const commandAgentsAlias = make$35("a").pipe(withDescription("Alias for 'agents' (manage agent presets used to run tasks)."), subcommands);
152451
152510
 
152452
152511
  //#endregion
152453
152512
  //#region src/cli.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lalph",
3
3
  "type": "module",
4
- "version": "0.3.12",
4
+ "version": "0.3.14",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
package/src/Linear.ts CHANGED
@@ -539,19 +539,81 @@ const selectedProjectId = new ProjectSetting(
539
539
  "linear.selectedProjectId",
540
540
  Schema.String,
541
541
  )
542
+ type ProjectSelection =
543
+ | {
544
+ readonly _tag: "create"
545
+ }
546
+ | {
547
+ readonly _tag: "existing"
548
+ readonly project: LinearProject
549
+ }
550
+ const createLinearProject = Effect.gen(function* () {
551
+ const linear = yield* Linear
552
+ const projects = yield* Stream.runCollect(linear.projects)
553
+ const projectName = yield* Prompt.text({
554
+ message: "Project name",
555
+ validate(input) {
556
+ const name = input.trim()
557
+ if (name.length === 0) {
558
+ return Effect.fail("Project name cannot be empty")
559
+ }
560
+ if (
561
+ projects.some(
562
+ (project) => project.name.toLowerCase() === name.toLowerCase(),
563
+ )
564
+ ) {
565
+ return Effect.fail("A project with this name already exists")
566
+ }
567
+ return Effect.succeed(name)
568
+ },
569
+ })
570
+ const teams = yield* linear
571
+ .use((client) => client.teams())
572
+ .pipe(Effect.map((teamConnection) => teamConnection.nodes))
573
+ const teamId = yield* Prompt.autoComplete({
574
+ message: "Select a team for the new project",
575
+ choices: teams.map((team) => ({
576
+ title: team.name,
577
+ value: team.id,
578
+ })),
579
+ })
580
+ const created = yield* linear.use((client) =>
581
+ client.createProject({
582
+ name: projectName,
583
+ teamIds: [teamId],
584
+ }),
585
+ )
586
+ return yield* linear.use(() => created.project!)
587
+ })
542
588
  const selectProject = Effect.gen(function* () {
543
589
  const linear = yield* Linear
544
590
 
545
591
  const projects = yield* Stream.runCollect(linear.projects)
546
-
547
- const project = yield* Prompt.autoComplete({
548
- message: "Select a Linear project",
549
- choices: projects.map((project) => ({
592
+ const choices: ReadonlyArray<{
593
+ readonly title: string
594
+ readonly value: ProjectSelection
595
+ }> = [
596
+ {
597
+ title: "Create new",
598
+ value: { _tag: "create" },
599
+ },
600
+ ...projects.map((project) => ({
550
601
  title: project.name,
551
- value: project,
602
+ value: {
603
+ _tag: "existing" as const,
604
+ project,
605
+ },
552
606
  })),
607
+ ]
608
+
609
+ const selected = yield* Prompt.autoComplete({
610
+ message: "Select a Linear project",
611
+ choices,
553
612
  })
554
613
 
614
+ const project =
615
+ selected._tag === "existing" ? selected.project : yield* createLinearProject
616
+
555
617
  yield* Settings.setProject(selectedProjectId, Option.some(project.id))
556
618
 
557
619
  return project
@@ -4,7 +4,9 @@ import { Settings } from "../../Settings.ts"
4
4
  import { addOrUpdatePreset } from "../../Presets.ts"
5
5
 
6
6
  export const commandAgentsAdd = Command.make("add").pipe(
7
- Command.withDescription("Add a new CLI agent preset"),
7
+ Command.withDescription(
8
+ "Add a new agent preset (interactive prompt for CLI agent, arguments, and any issue-source options).",
9
+ ),
8
10
  Command.withHandler(() => addOrUpdatePreset()),
9
11
  Command.provide(Settings.layer),
10
12
  Command.provide(CurrentIssueSource.layer),
@@ -9,11 +9,13 @@ import {
9
9
  } from "../../Presets.ts"
10
10
 
11
11
  export const commandAgentsEdit = Command.make("edit").pipe(
12
- Command.withDescription("Modify a CLI agent preset"),
12
+ Command.withDescription(
13
+ "Edit an existing agent preset (interactive prompt to update agent, arguments, and any issue-source options).",
14
+ ),
13
15
  Command.withHandler(
14
16
  Effect.fnUntraced(function* () {
15
- const projects = yield* getAllCliAgentPresets
16
- if (projects.length === 0) {
17
+ const presets = yield* getAllCliAgentPresets
18
+ if (presets.length === 0) {
17
19
  return yield* Effect.log("No presets available to edit.")
18
20
  }
19
21
  const preset = yield* selectCliAgentPreset
@@ -6,7 +6,9 @@ import { Settings } from "../../Settings.ts"
6
6
  import { getAllCliAgentPresets } from "../../Presets.ts"
7
7
 
8
8
  export const commandAgentsLs = Command.make("ls").pipe(
9
- Command.withDescription("List all configured CLI agent presets"),
9
+ Command.withDescription(
10
+ "List configured agent presets (preset ids, agent, arguments, and any issue-source options).",
11
+ ),
10
12
  Command.withHandler(
11
13
  Effect.fnUntraced(function* () {
12
14
  const meta = yield* CurrentIssueSource
@@ -9,7 +9,9 @@ import {
9
9
  } from "../../Presets.ts"
10
10
 
11
11
  export const commandAgentsRm = Command.make("rm").pipe(
12
- Command.withDescription("Remove a CLI agent preset"),
12
+ Command.withDescription(
13
+ "Remove an agent preset (select a preset to delete from your configuration).",
14
+ ),
13
15
  Command.withHandler(
14
16
  Effect.fnUntraced(function* () {
15
17
  const presets = yield* getAllCliAgentPresets
@@ -12,11 +12,15 @@ const subcommands = Command.withSubcommands([
12
12
  ])
13
13
 
14
14
  export const commandAgents = Command.make("agents").pipe(
15
- Command.withDescription("Manage CLI agent presets"),
15
+ Command.withDescription(
16
+ "Manage agent presets used to run tasks. Use 'ls' to inspect presets and 'add'/'edit' to configure agents, arguments, and any issue-source options.",
17
+ ),
16
18
  subcommands,
17
19
  )
18
20
 
19
21
  export const commandAgentsAlias = Command.make("a").pipe(
20
- Command.withDescription("Alias for 'agents' command"),
22
+ Command.withDescription(
23
+ "Alias for 'agents' (manage agent presets used to run tasks).",
24
+ ),
21
25
  subcommands,
22
26
  )
@@ -19,11 +19,15 @@ const handler = Command.withHandler(
19
19
  )
20
20
 
21
21
  export const commandEdit = Command.make("edit").pipe(
22
- Command.withDescription("Open the prd.yml file in your editor"),
22
+ Command.withDescription(
23
+ "Open the selected project's .lalph/prd.yml in your editor.",
24
+ ),
23
25
  handler,
24
26
  )
25
27
 
26
28
  export const commandEditAlias = Command.make("e").pipe(
27
- Command.withDescription("Alias for 'edit' command"),
29
+ Command.withDescription(
30
+ "Alias for 'edit' (open the selected project's .lalph/prd.yml in your editor).",
31
+ ),
28
32
  handler,
29
33
  )
@@ -87,11 +87,13 @@ const handler = flow(
87
87
  )
88
88
 
89
89
  export const commandIssue = Command.make("issue").pipe(
90
- Command.withDescription("Create a new issue in the selected issue source"),
90
+ Command.withDescription("Create a new issue in your editor."),
91
91
  handler,
92
92
  )
93
93
 
94
94
  export const commandIssueAlias = Command.make("i").pipe(
95
- Command.withDescription("Alias for 'issue' command"),
95
+ Command.withDescription(
96
+ "Alias for 'issue' (create a new issue in your editor).",
97
+ ),
96
98
  handler,
97
99
  )
@@ -15,14 +15,16 @@ const specificationPath = Argument.path("spec", {
15
15
  mustExist: true,
16
16
  }).pipe(
17
17
  Argument.withDescription(
18
- "Path to the specification file to convert into tasks",
18
+ "Required. Path to an existing specification file to convert into tasks",
19
19
  ),
20
20
  )
21
21
 
22
22
  export const commandPlanTasks = Command.make("tasks", {
23
23
  specificationPath,
24
24
  }).pipe(
25
- Command.withDescription("Convert a specification into tasks"),
25
+ Command.withDescription(
26
+ "Convert an existing specification file into PRD tasks (without re-running plan mode)",
27
+ ),
26
28
  Command.withHandler(
27
29
  Effect.fnUntraced(
28
30
  function* ({ specificationPath }) {
@@ -12,24 +12,30 @@ import { agentTasker } from "../Agents/tasker.ts"
12
12
  import { commandPlanTasks } from "./plan/tasks.ts"
13
13
  import { Editor } from "../Editor.ts"
14
14
  import { getDefaultCliAgentPreset } from "../Presets.ts"
15
+ import { ChildProcess } from "effect/unstable/process"
16
+ import { parseBranch } from "../shared/git.ts"
15
17
 
16
18
  const dangerous = Flag.boolean("dangerous").pipe(
17
19
  Flag.withAlias("d"),
18
20
  Flag.withDescription(
19
- "Enable dangerous mode (skip permission prompts) during plan generation",
21
+ "Skip permission prompts while generating the specification from your plan",
20
22
  ),
21
23
  )
22
24
 
23
25
  const withNewProject = Flag.boolean("new").pipe(
24
26
  Flag.withAlias("n"),
25
- Flag.withDescription("Create a new project before starting plan mode"),
27
+ Flag.withDescription(
28
+ "Create a new project (via prompts) before starting plan mode",
29
+ ),
26
30
  )
27
31
 
28
32
  export const commandPlan = Command.make("plan", {
29
33
  dangerous,
30
34
  withNewProject,
31
35
  }).pipe(
32
- Command.withDescription("Iterate on an issue plan and create PRD tasks"),
36
+ Command.withDescription(
37
+ "Open an editor to draft a plan; on save, generate a specification under --specs and then create PRD tasks from it. Use --new to create a project first; use --dangerous to skip permission prompts during spec generation.",
38
+ ),
33
39
  Command.withHandler(
34
40
  Effect.fnUntraced(function* ({ dangerous, withNewProject }) {
35
41
  const editor = yield* Editor
@@ -86,6 +92,13 @@ const plan = Effect.fnUntraced(
86
92
  Effect.mapError(() => new SpecNotFound()),
87
93
  )
88
94
 
95
+ if (Option.isSome(options.targetBranch)) {
96
+ yield* commitAndPushSpecification({
97
+ specsDirectory: options.specsDirectory,
98
+ targetBranch: options.targetBranch.value,
99
+ })
100
+ }
101
+
89
102
  yield* Effect.log("Converting specification into tasks")
90
103
 
91
104
  yield* agentTasker({
@@ -119,6 +132,50 @@ export class SpecNotFound extends Data.TaggedError("SpecNotFound") {
119
132
  readonly message = "The AI agent failed to produce a specification."
120
133
  }
121
134
 
135
+ export class SpecGitError extends Data.TaggedError("SpecGitError")<{
136
+ readonly message: string
137
+ }> {}
138
+
139
+ const commitAndPushSpecification = Effect.fnUntraced(
140
+ function* (options: {
141
+ readonly specsDirectory: string
142
+ readonly targetBranch: string
143
+ }) {
144
+ const worktree = yield* Worktree
145
+ const pathService = yield* Path.Path
146
+
147
+ const absSpecsDirectory = pathService.join(
148
+ worktree.directory,
149
+ options.specsDirectory,
150
+ )
151
+
152
+ const git = (args: ReadonlyArray<string>) =>
153
+ ChildProcess.make("git", [...args], {
154
+ cwd: worktree.directory,
155
+ stdout: "inherit",
156
+ stderr: "inherit",
157
+ }).pipe(ChildProcess.exitCode)
158
+
159
+ const addCode = yield* git(["add", absSpecsDirectory])
160
+ if (addCode !== 0) {
161
+ return yield* new SpecGitError({
162
+ message: "Failed to stage specification changes.",
163
+ })
164
+ }
165
+
166
+ const commitCode = yield* git(["commit", "-m", "Update plan specification"])
167
+ if (commitCode !== 0) {
168
+ return yield* new SpecGitError({
169
+ message: "Failed to commit the generated specification changes.",
170
+ })
171
+ }
172
+
173
+ const parsed = parseBranch(options.targetBranch)
174
+ yield* git(["push", parsed.remote, `HEAD:${parsed.branch}`])
175
+ },
176
+ Effect.ignore({ log: "Warn" }),
177
+ )
178
+
122
179
  const PlanDetails = Schema.fromJsonString(
123
180
  Schema.Struct({
124
181
  specification: Schema.String,
@@ -4,7 +4,9 @@ import { CurrentIssueSource } from "../../CurrentIssueSource.ts"
4
4
  import { Settings } from "../../Settings.ts"
5
5
 
6
6
  export const commandProjectsAdd = Command.make("add").pipe(
7
- Command.withDescription("Add a new project"),
7
+ Command.withDescription(
8
+ "Add a project and configure its execution settings (concurrency, target branch, git flow, review agent) and issue source settings.",
9
+ ),
8
10
  Command.withHandler(() => addOrUpdateProject()),
9
11
  Command.provide(Settings.layer),
10
12
  Command.provide(CurrentIssueSource.layer),
@@ -9,7 +9,9 @@ import { Settings } from "../../Settings.ts"
9
9
  import { CurrentIssueSource } from "../../CurrentIssueSource.ts"
10
10
 
11
11
  export const commandProjectsEdit = Command.make("edit").pipe(
12
- Command.withDescription("Modify a project"),
12
+ Command.withDescription(
13
+ "Edit a project's execution settings (concurrency, target branch, git flow, review agent) and issue source settings.",
14
+ ),
13
15
  Command.withHandler(
14
16
  Effect.fnUntraced(function* () {
15
17
  const projects = yield* getAllProjects
@@ -6,7 +6,9 @@ import { getAllProjects } from "../../Projects.ts"
6
6
  import { Settings } from "../../Settings.ts"
7
7
 
8
8
  export const commandProjectsLs = Command.make("ls").pipe(
9
- Command.withDescription("List all configured projects and their settings"),
9
+ Command.withDescription(
10
+ "List configured projects and how they run (enabled state, concurrency, branch, git flow, review agent).",
11
+ ),
10
12
  Command.withHandler(
11
13
  Effect.fnUntraced(function* () {
12
14
  const meta = yield* CurrentIssueSource
@@ -5,7 +5,9 @@ import { Settings } from "../../Settings.ts"
5
5
  import { CurrentIssueSource } from "../../CurrentIssueSource.ts"
6
6
 
7
7
  export const commandProjectsRm = Command.make("rm").pipe(
8
- Command.withDescription("Remove a project"),
8
+ Command.withDescription(
9
+ "Remove a project from the configured list and delete its stored state under .lalph/projects.",
10
+ ),
9
11
  Command.withHandler(
10
12
  Effect.fnUntraced(function* () {
11
13
  const fs = yield* FileSystem.FileSystem
@@ -5,7 +5,9 @@ import { Settings } from "../../Settings.ts"
5
5
  import { Project } from "../../domain/Project.ts"
6
6
 
7
7
  export const commandProjectsToggle = Command.make("toggle").pipe(
8
- Command.withDescription("Enable or disable projects"),
8
+ Command.withDescription(
9
+ "Enable or disable configured projects for lalph runs.",
10
+ ),
9
11
  Command.withHandler(
10
12
  Effect.fnUntraced(function* () {
11
13
  const projects = yield* getAllProjects
@@ -14,11 +14,13 @@ const subcommands = Command.withSubcommands([
14
14
  ])
15
15
 
16
16
  export const commandProjects = Command.make("projects").pipe(
17
- Command.withDescription("Manage projects"),
17
+ Command.withDescription(
18
+ "Manage projects and their execution settings (enabled state, concurrency, target branch, git flow, review agent). Use 'ls' to inspect and 'add', 'edit', or 'toggle' to configure.",
19
+ ),
18
20
  subcommands,
19
21
  )
20
22
 
21
23
  export const commandProjectsAlias = Command.make("p").pipe(
22
- Command.withDescription("Alias for 'projects' command"),
24
+ Command.withDescription("Alias for 'projects'."),
23
25
  subcommands,
24
26
  )
@@ -323,14 +323,16 @@ const runProject = Effect.fnUntraced(
323
323
  // Command
324
324
 
325
325
  const iterations = Flag.integer("iterations").pipe(
326
- Flag.withDescription("Number of iterations to run, defaults to unlimited"),
326
+ Flag.withDescription(
327
+ "Limit how many task iterations run per enabled project (default: unlimited). Use -i 1 to run a single iteration and exit.",
328
+ ),
327
329
  Flag.withAlias("i"),
328
330
  Flag.withDefault(Number.POSITIVE_INFINITY),
329
331
  )
330
332
 
331
333
  const maxIterationMinutes = Flag.integer("max-minutes").pipe(
332
334
  Flag.withDescription(
333
- "Maximum number of minutes to allow an iteration to run. Defaults to 90 minutes. Env variable: LALPH_MAX_MINUTES",
335
+ "Timeout an iteration if execution (and review, if enabled) exceeds this many minutes (default: LALPH_MAX_MINUTES or 90).",
334
336
  ),
335
337
  Flag.withFallbackConfig(Config.int("LALPH_MAX_MINUTES")),
336
338
  Flag.withDefault(90),
@@ -338,7 +340,7 @@ const maxIterationMinutes = Flag.integer("max-minutes").pipe(
338
340
 
339
341
  const stallMinutes = Flag.integer("stall-minutes").pipe(
340
342
  Flag.withDescription(
341
- "If no activity occurs for this many minutes, the iteration will be stopped. Defaults to 5 minutes. Env variable: LALPH_STALL_MINUTES",
343
+ "Fail an iteration if the agent stops responding for this many minutes (default: LALPH_STALL_MINUTES or 5).",
342
344
  ),
343
345
  Flag.withFallbackConfig(Config.int("LALPH_STALL_MINUTES")),
344
346
  Flag.withDefault(5),
@@ -346,7 +348,7 @@ const stallMinutes = Flag.integer("stall-minutes").pipe(
346
348
 
347
349
  const specsDirectory = Flag.directory("specs").pipe(
348
350
  Flag.withDescription(
349
- "Directory to store plan specifications. Env variable: LALPH_SPECS",
351
+ "Directory where plan specs are written and read (default: LALPH_SPECS or .specs).",
350
352
  ),
351
353
  Flag.withAlias("s"),
352
354
  Flag.withFallbackConfig(Config.string("LALPH_SPECS")),
@@ -354,7 +356,9 @@ const specsDirectory = Flag.directory("specs").pipe(
354
356
  )
355
357
 
356
358
  const verbose = Flag.boolean("verbose").pipe(
357
- Flag.withDescription("Enable verbose logging"),
359
+ Flag.withDescription(
360
+ "Increase log output for debugging. Use -v when you need detailed logs.",
361
+ ),
358
362
  Flag.withAlias("v"),
359
363
  )
360
364
 
@@ -365,6 +369,9 @@ export const commandRoot = Command.make("lalph", {
365
369
  specsDirectory,
366
370
  verbose,
367
371
  }).pipe(
372
+ Command.withDescription(
373
+ "Run the task loop across all enabled projects in parallel: pull issues from the current issue source and execute them with your configured agent preset(s). Use --iterations for a bounded run, and configure per-project concurrency via lalph projects edit.",
374
+ ),
368
375
  Command.withHandler(
369
376
  Effect.fnUntraced(
370
377
  function* ({
@@ -6,7 +6,9 @@ import { Worktree } from "../Worktree.ts"
6
6
  import { layerProjectIdPrompt } from "../Projects.ts"
7
7
 
8
8
  export const commandSh = Command.make("sh").pipe(
9
- Command.withDescription("Enter an interactive shell in the worktree"),
9
+ Command.withDescription(
10
+ "Launch an interactive shell in the active project's worktree.",
11
+ ),
10
12
  Command.withHandler(
11
13
  Effect.fnUntraced(
12
14
  function* () {
@@ -3,7 +3,9 @@ import { selectIssueSource } from "../CurrentIssueSource.ts"
3
3
  import { Settings } from "../Settings.ts"
4
4
 
5
5
  export const commandSource = Command.make("source").pipe(
6
- Command.withDescription("Select the issue source to use"),
6
+ Command.withDescription(
7
+ "Select the issue source to use (e.g. GitHub Issues or Linear). This applies to all projects.",
8
+ ),
7
9
  Command.withHandler(() => selectIssueSource),
8
10
  Command.provide(Settings.layer),
9
11
  )