lalph 0.2.19 → 0.2.20

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
@@ -7235,7 +7235,7 @@ const getOption = /* @__PURE__ */ dual(2, (self, service) => {
7235
7235
  * @since 4.0.0
7236
7236
  * @category Utils
7237
7237
  */
7238
- const merge$6 = /* @__PURE__ */ dual(2, (self, that) => {
7238
+ const merge$5 = /* @__PURE__ */ dual(2, (self, that) => {
7239
7239
  if (self.mapUnsafe.size === 0) return that;
7240
7240
  if (that.mapUnsafe.size === 0) return self;
7241
7241
  const map = new Map(self.mapUnsafe);
@@ -9229,7 +9229,7 @@ const servicesWith$1 = (f) => withFiber$1((fiber) => f(fiber.services));
9229
9229
  /** @internal */
9230
9230
  const provideServices$1 = /* @__PURE__ */ dual(2, (self, services) => {
9231
9231
  if (effectIsExit(self)) return self;
9232
- return updateServices$1(self, merge$6(services));
9232
+ return updateServices$1(self, merge$5(services));
9233
9233
  });
9234
9234
  /** @internal */
9235
9235
  const provideService$1 = function() {
@@ -12030,38 +12030,6 @@ const mergeAllEffect = (layers, memoMap, scope) => {
12030
12030
  * @category zipping
12031
12031
  */
12032
12032
  const mergeAll = (...layers) => fromBuild((memoMap, scope) => mergeAllEffect(layers, memoMap, scope));
12033
- /**
12034
- * Merges this layer with the specified layer concurrently, producing a new layer with combined input and output types.
12035
- *
12036
- * This is a binary version of `mergeAll` that merges exactly two layers or one layer with an array of layers.
12037
- * The layers are built concurrently and their outputs are combined.
12038
- *
12039
- * @example
12040
- * ```ts
12041
- * import { Effect, Layer, ServiceMap } from "effect"
12042
- *
12043
- * class Database extends ServiceMap.Service<Database, {
12044
- * readonly query: (sql: string) => Effect.Effect<string>
12045
- * }>()("Database") {}
12046
- *
12047
- * class Logger extends ServiceMap.Service<Logger, {
12048
- * readonly log: (msg: string) => Effect.Effect<void>
12049
- * }>()("Logger") {}
12050
- *
12051
- * const dbLayer = Layer.succeed(Database)({
12052
- * query: (sql: string) => Effect.succeed("result")
12053
- * })
12054
- * const loggerLayer = Layer.succeed(Logger)({
12055
- * log: (msg: string) => Effect.sync(() => console.log(msg))
12056
- * })
12057
- *
12058
- * const mergedLayer = Layer.merge(dbLayer, loggerLayer)
12059
- * ```
12060
- *
12061
- * @since 2.0.0
12062
- * @category zipping
12063
- */
12064
- const merge$5 = /* @__PURE__ */ dual(2, (self, that) => mergeAll(self, ...Array.isArray(that) ? that : [that]));
12065
12033
  const provideWith = (self, that, f) => fromBuild((memoMap, scope) => flatMap$4(Array.isArray(that) ? mergeAllEffect(that, memoMap, scope) : that.build(memoMap, scope), (context) => self.build(memoMap, scope).pipe(provideServices$1(context), map$11((merged) => f(merged, context)))));
12066
12034
  /**
12067
12035
  * Feeds the output services of this builder into the input of the specified
@@ -12204,7 +12172,7 @@ const provide$3 = /* @__PURE__ */ dual(2, (self, that) => provideWith(self, that
12204
12172
  * @since 2.0.0
12205
12173
  * @category utils
12206
12174
  */
12207
- const provideMerge = /* @__PURE__ */ dual(2, (self, that) => provideWith(self, that, (self, that) => merge$6(that, self)));
12175
+ const provideMerge = /* @__PURE__ */ dual(2, (self, that) => provideWith(self, that, (self, that) => merge$5(that, self)));
12208
12176
  /**
12209
12177
  * Constructs a layer dynamically based on the output of this layer.
12210
12178
  *
@@ -51194,7 +51162,7 @@ const TypeId$29 = "~effect/Cache";
51194
51162
  */
51195
51163
  const makeWith$1 = (options) => servicesWith$1((services) => {
51196
51164
  const self = Object.create(Proto$14);
51197
- self.lookup = (key) => updateServices$1(options.lookup(key), (input) => merge$6(services, input));
51165
+ self.lookup = (key) => updateServices$1(options.lookup(key), (input) => merge$5(services, input));
51198
51166
  self.map = make$45();
51199
51167
  self.capacity = options.capacity;
51200
51168
  self.timeToLive = options.timeToLive ? (exit, key) => fromDurationInputUnsafe(options.timeToLive(exit, key)) : defaultTimeToLive;
@@ -59031,7 +58999,7 @@ const SpanNameGenerator$1 = /* @__PURE__ */ Reference("effect/http/HttpClient/Sp
59031
58999
  /**
59032
59000
  * @since 4.0.0
59033
59001
  */
59034
- const layerMergedServices = (effect) => effect$1(HttpClient)(servicesWith((services) => map$8(effect, (client) => transformResponse(client, updateServices((input) => merge$6(services, input))))));
59002
+ const layerMergedServices = (effect) => effect$1(HttpClient)(servicesWith((services) => map$8(effect, (client) => transformResponse(client, updateServices((input) => merge$5(services, input))))));
59035
59003
  const responseRegistry = /* @__PURE__ */ (() => {
59036
59004
  if ("FinalizationRegistry" in globalThis && globalThis.FinalizationRegistry) {
59037
59005
  const registry = /* @__PURE__ */ new FinalizationRegistry((controller) => {
@@ -60328,7 +60296,7 @@ const fromWebSocket = (acquire, options) => withFiber((fiber) => {
60328
60296
  latch.openUnsafe();
60329
60297
  if (opts?.onOpen) yield* opts.onOpen;
60330
60298
  return yield* join(fiberSet).pipe(catchFilter(SocketCloseError.filterClean((_) => !closeCodeIsError(_)), (_) => void_$1));
60331
- })).pipe(updateServices((input) => merge$6(acquireContext, input)), ensuring$2(sync(() => {
60299
+ })).pipe(updateServices((input) => merge$5(acquireContext, input)), ensuring$2(sync(() => {
60332
60300
  latch.closeUnsafe();
60333
60301
  currentWS = void 0;
60334
60302
  })));
@@ -151063,8 +151031,13 @@ permission.
151063
151031
  5. If any specifications need updating based on your new understanding, update them.
151064
151032
 
151065
151033
  ${prdNotes(options)}`;
151066
- const planPrompt = (options) => `1. Ask the user for the idea / request, then your job is to create a detailed
151067
- specification to fulfill the request and save it as a file.
151034
+ const planPrompt = (options) => `<request><![CDATA[
151035
+ ${options.plan}
151036
+ ]]></request>
151037
+
151038
+ ## Instructions
151039
+
151040
+ 1. Your job is to create a detailed specification to fulfill the request and save it as a file.
151068
151041
  First do some research to understand the request, then interview the user
151069
151042
  to gather all the necessary requirements and details for the specification.
151070
151043
  - If the user asks you to update an existing specification, find the relevant
@@ -152120,6 +152093,41 @@ const commandPlanTasks = make$35("tasks", { specificationPath }).pipe(withDescri
152120
152093
  Worktree.layer.pipe(provide$3(layerProjectIdPrompt))
152121
152094
  ]))));
152122
152095
 
152096
+ //#endregion
152097
+ //#region src/shared/config.ts
152098
+ const configEditor = string$1("LALPH_EDITOR").pipe(orElse(() => string$1("EDITOR")), map$5(parseCommand), withDefault$3(() => ["nano"]));
152099
+
152100
+ //#endregion
152101
+ //#region src/Editor.ts
152102
+ var Editor = class extends Service$1()("lalph/Editor", { make: gen(function* () {
152103
+ const fs = yield* FileSystem;
152104
+ const editor = yield* configEditor;
152105
+ const spawner = yield* ChildProcessSpawner;
152106
+ const edit = (path) => make$23(editor[0], [...editor.slice(1), path], {
152107
+ stdin: "inherit",
152108
+ stdout: "inherit",
152109
+ stderr: "inherit"
152110
+ }).pipe(exitCode, provideService(ChildProcessSpawner, spawner), orDie$2);
152111
+ return {
152112
+ edit,
152113
+ editTemp: fnUntraced(function* (options) {
152114
+ const initialContent = options.initialContent ?? "";
152115
+ const file = yield* fs.makeTempFileScoped({ suffix: options.suffix ?? ".txt" });
152116
+ if (initialContent) yield* fs.writeFileString(file, initialContent);
152117
+ if ((yield* make$23(editor[0], [...editor.slice(1), file], {
152118
+ stdin: "inherit",
152119
+ stdout: "inherit",
152120
+ stderr: "inherit"
152121
+ }).pipe(exitCode)) !== 0) return yield* new NoSuchElementError();
152122
+ const content = (yield* fs.readFileString(file)).trim();
152123
+ if (content === initialContent) return yield* new NoSuchElementError();
152124
+ return content;
152125
+ }, scoped$1, provideService(ChildProcessSpawner, spawner), option$1)
152126
+ };
152127
+ }) }) {
152128
+ static layer = effect$1(this, this.make).pipe(provide$3(PlatformServices));
152129
+ };
152130
+
152123
152131
  //#endregion
152124
152132
  //#region src/commands/plan.ts
152125
152133
  const dangerous = boolean("dangerous").pipe(withAlias("d"), withDescription$1("Enable dangerous mode (skip permission prompts) during plan generation"));
@@ -152128,30 +152136,39 @@ const commandPlan = make$35("plan", {
152128
152136
  dangerous,
152129
152137
  withNewProject
152130
152138
  }).pipe(withDescription("Iterate on an issue plan and create PRD tasks"), withHandler(fnUntraced(function* ({ dangerous, withNewProject }) {
152139
+ const thePlan = yield* (yield* Editor).editTemp({ suffix: ".md" });
152140
+ if (isNone(thePlan)) return;
152131
152141
  const project = withNewProject ? yield* addOrUpdateProject() : yield* selectProject;
152132
152142
  const { specsDirectory } = yield* commandRoot;
152133
152143
  const commandPrefix = yield* getCommandPrefix;
152134
152144
  yield* plan({
152145
+ plan: thePlan.value,
152135
152146
  specsDirectory,
152136
152147
  targetBranch: project.targetBranch,
152137
152148
  commandPrefix,
152138
152149
  dangerous
152139
152150
  }).pipe(provideService(CurrentProjectId, project.id));
152140
- }, provide$1([Settings.layer, CurrentIssueSource.layer]))), withSubcommands([commandPlanTasks]));
152151
+ }, provide$1([
152152
+ Settings.layer,
152153
+ CurrentIssueSource.layer,
152154
+ Editor.layer
152155
+ ]))), withSubcommands([commandPlanTasks]));
152141
152156
  const plan = fnUntraced(function* (options) {
152142
152157
  const fs = yield* FileSystem;
152143
152158
  const pathService = yield* Path$1;
152144
152159
  const worktree = yield* Worktree;
152145
152160
  const cliAgent = yield* getOrSelectCliAgent;
152146
152161
  yield* agentPlanner({
152162
+ plan: options.plan,
152147
152163
  specsDirectory: options.specsDirectory,
152148
152164
  commandPrefix: options.commandPrefix,
152149
152165
  dangerous: options.dangerous,
152150
152166
  cliAgent
152151
152167
  });
152168
+ const planDetails = yield* pipe(fs.readFileString(pathService.join(worktree.directory, ".lalph", "plan.json")), flatMap$2(decodeEffect(PlanDetails)), mapError$2(() => new SpecNotFound()));
152152
152169
  yield* log$1("Converting specification into tasks");
152153
152170
  yield* agentTasker({
152154
- specificationPath: (yield* pipe(fs.readFileString(pathService.join(worktree.directory, ".lalph", "plan.json")), flatMap$2(decodeEffect(PlanDetails)))).specification,
152171
+ specificationPath: planDetails.specification,
152155
152172
  specsDirectory: options.specsDirectory,
152156
152173
  commandPrefix: options.commandPrefix,
152157
152174
  cliAgent
@@ -152160,16 +152177,15 @@ const plan = fnUntraced(function* (options) {
152160
152177
  }, scoped$1, provide$1([
152161
152178
  PromptGen.layer,
152162
152179
  Prd.layerProvided,
152163
- Worktree.layer.pipe(provide$3(layerProjectIdPrompt)),
152180
+ Worktree.layer,
152164
152181
  Settings.layer,
152165
152182
  CurrentIssueSource.layer
152166
152183
  ]));
152184
+ var SpecNotFound = class extends TaggedError("SpecNotFound") {
152185
+ message = "The AI agent failed to produce a specification.";
152186
+ };
152167
152187
  const PlanDetails = fromJsonString(Struct({ specification: String$1 }));
152168
152188
 
152169
- //#endregion
152170
- //#region src/shared/config.ts
152171
- const configEditor = string$1("LALPH_EDITOR").pipe(orElse(() => string$1("EDITOR")), map$5(parseCommand), withDefault$3(() => ["nano"]));
152172
-
152173
152189
  //#endregion
152174
152190
  //#region src/commands/issue.ts
152175
152191
  const issueTemplate = `---
@@ -152190,19 +152206,13 @@ const FrontMatterSchema = toCodecJson(Struct({
152190
152206
  }));
152191
152207
  const handler$1 = flow(withHandler(fnUntraced(function* () {
152192
152208
  const source = yield* IssueSource;
152193
- const fs = yield* FileSystem;
152194
152209
  const projectId = yield* CurrentProjectId;
152195
- const tempFile = yield* fs.makeTempFileScoped({ suffix: ".md" });
152196
- const editor = yield* configEditor;
152197
- yield* fs.writeFileString(tempFile, issueTemplate);
152198
- if ((yield* make$23(editor[0], [...editor.slice(1), tempFile], {
152199
- stdin: "inherit",
152200
- stdout: "inherit",
152201
- stderr: "inherit"
152202
- }).pipe(exitCode)) !== 0) return;
152203
- const content = yield* fs.readFileString(tempFile);
152204
- if (content.trim() === issueTemplate.trim()) return;
152205
- const lines = content.split("\n");
152210
+ const content = yield* (yield* Editor).editTemp({
152211
+ suffix: ".md",
152212
+ initialContent: issueTemplate
152213
+ });
152214
+ if (isNone(content)) return;
152215
+ const lines = content.value.split("\n");
152206
152216
  const yamlLines = [];
152207
152217
  let descriptionStartIndex = 0;
152208
152218
  for (let i = 0; i < lines.length; i++) {
@@ -152225,7 +152235,7 @@ const handler$1 = flow(withHandler(fnUntraced(function* () {
152225
152235
  }));
152226
152236
  console.log(`Created issue with ID: ${created.id}`);
152227
152237
  console.log(`URL: ${created.url}`);
152228
- }, scoped$1)), provide(merge$5(layerProjectIdPrompt, CurrentIssueSource.layer)));
152238
+ }, scoped$1)), provide(mergeAll(layerProjectIdPrompt, CurrentIssueSource.layer, Editor.layer)));
152229
152239
  const commandIssue = make$35("issue").pipe(withDescription("Create a new issue in the selected issue source"), handler$1);
152230
152240
  const commandIssueAlias = make$35("i").pipe(withDescription("Alias for 'issue' command"), handler$1);
152231
152241
 
@@ -152233,14 +152243,8 @@ const commandIssueAlias = make$35("i").pipe(withDescription("Alias for 'issue' c
152233
152243
  //#region src/commands/edit.ts
152234
152244
  const handler = withHandler(fnUntraced(function* () {
152235
152245
  const prd = yield* Prd;
152236
- const editor = yield* configEditor;
152237
- yield* make$23(editor[0], [...editor.slice(1), prd.path], {
152238
- extendEnv: true,
152239
- stdin: "inherit",
152240
- stdout: "inherit",
152241
- stderr: "inherit"
152242
- }).pipe(exitCode);
152243
- }, scoped$1, provide$1(Prd.layerLocalProvided.pipe(provideMerge(layerProjectIdPrompt)))));
152246
+ yield* (yield* Editor).edit(prd.path);
152247
+ }, provide$1([Prd.layerLocalProvided.pipe(provideMerge(layerProjectIdPrompt)), Editor.layer])));
152244
152248
  const commandEdit = make$35("edit").pipe(withDescription("Open the prd.yml file in your editor"), handler);
152245
152249
  const commandEditAlias = make$35("e").pipe(withDescription("Alias for 'edit' command"), handler);
152246
152250
 
@@ -152250,7 +152254,7 @@ const commandSource = make$35("source").pipe(withDescription("Select the issue s
152250
152254
 
152251
152255
  //#endregion
152252
152256
  //#region package.json
152253
- var version = "0.2.19";
152257
+ var version = "0.2.20";
152254
152258
 
152255
152259
  //#endregion
152256
152260
  //#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.19",
4
+ "version": "0.2.20",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -5,6 +5,7 @@ import { Worktree } from "../Worktree.ts"
5
5
  import type { CliAgent } from "../domain/CliAgent.ts"
6
6
 
7
7
  export const agentPlanner = Effect.fnUntraced(function* (options: {
8
+ readonly plan: string
8
9
  readonly specsDirectory: string
9
10
  readonly commandPrefix: (
10
11
  command: ChildProcess.Command,
package/src/Editor.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { Cause, Effect, FileSystem, Layer, ServiceMap } from "effect"
2
+ import { configEditor } from "./shared/config.ts"
3
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
4
+ import { PlatformServices } from "./shared/platform.ts"
5
+
6
+ export class Editor extends ServiceMap.Service<Editor>()("lalph/Editor", {
7
+ make: Effect.gen(function* () {
8
+ const fs = yield* FileSystem.FileSystem
9
+ const editor = yield* configEditor
10
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
11
+
12
+ const edit = (path: string) =>
13
+ ChildProcess.make(editor[0]!, [...editor.slice(1), path], {
14
+ stdin: "inherit",
15
+ stdout: "inherit",
16
+ stderr: "inherit",
17
+ }).pipe(
18
+ ChildProcess.exitCode,
19
+ Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
20
+ Effect.orDie,
21
+ )
22
+
23
+ const editTemp = Effect.fnUntraced(
24
+ function* (options: {
25
+ readonly initialContent?: string
26
+ readonly suffix?: string
27
+ }) {
28
+ const initialContent = options.initialContent ?? ""
29
+ const file = yield* fs.makeTempFileScoped({
30
+ suffix: options.suffix ?? ".txt",
31
+ })
32
+ if (initialContent) {
33
+ yield* fs.writeFileString(file, initialContent)
34
+ }
35
+
36
+ const exitCode = yield* ChildProcess.make(
37
+ editor[0]!,
38
+ [...editor.slice(1), file],
39
+ {
40
+ stdin: "inherit",
41
+ stdout: "inherit",
42
+ stderr: "inherit",
43
+ },
44
+ ).pipe(ChildProcess.exitCode)
45
+
46
+ if (exitCode !== 0) {
47
+ return yield* new Cause.NoSuchElementError()
48
+ }
49
+ const content = (yield* fs.readFileString(file)).trim()
50
+ if (content === initialContent) {
51
+ return yield* new Cause.NoSuchElementError()
52
+ }
53
+ return content
54
+ },
55
+ Effect.scoped,
56
+ Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
57
+ Effect.option,
58
+ )
59
+
60
+ return { edit, editTemp } as const
61
+ }),
62
+ }) {
63
+ static layer = Layer.effect(this, this.make).pipe(
64
+ Layer.provide(PlatformServices),
65
+ )
66
+ }
package/src/PromptGen.ts CHANGED
@@ -233,9 +233,15 @@ permission.
233
233
  ${prdNotes(options)}`
234
234
 
235
235
  const planPrompt = (options: {
236
+ readonly plan: string
236
237
  readonly specsDirectory: string
237
- }) => `1. Ask the user for the idea / request, then your job is to create a detailed
238
- specification to fulfill the request and save it as a file.
238
+ }) => `<request><![CDATA[
239
+ ${options.plan}
240
+ ]]></request>
241
+
242
+ ## Instructions
243
+
244
+ 1. Your job is to create a detailed specification to fulfill the request and save it as a file.
239
245
  First do some research to understand the request, then interview the user
240
246
  to gather all the necessary requirements and details for the specification.
241
247
  - If the user asks you to update an existing specification, find the relevant
@@ -1,27 +1,20 @@
1
1
  import { Command } from "effect/unstable/cli"
2
2
  import { Effect, Layer } from "effect"
3
- import { ChildProcess } from "effect/unstable/process"
4
3
  import { Prd } from "../Prd.ts"
5
- import { configEditor } from "../shared/config.ts"
6
4
  import { layerProjectIdPrompt } from "../Projects.ts"
5
+ import { Editor } from "../Editor.ts"
7
6
 
8
7
  const handler = Command.withHandler(
9
8
  Effect.fnUntraced(
10
9
  function* () {
11
10
  const prd = yield* Prd
12
- const editor = yield* configEditor
13
-
14
- yield* ChildProcess.make(editor[0]!, [...editor.slice(1), prd.path], {
15
- extendEnv: true,
16
- stdin: "inherit",
17
- stdout: "inherit",
18
- stderr: "inherit",
19
- }).pipe(ChildProcess.exitCode)
11
+ const editor = yield* Editor
12
+ yield* editor.edit(prd.path)
20
13
  },
21
- Effect.scoped,
22
- Effect.provide(
14
+ Effect.provide([
23
15
  Prd.layerLocalProvided.pipe(Layer.provideMerge(layerProjectIdPrompt)),
24
- ),
16
+ Editor.layer,
17
+ ]),
25
18
  ),
26
19
  )
27
20
 
@@ -1,13 +1,12 @@
1
1
  import { Command } from "effect/unstable/cli"
2
2
  import { CurrentIssueSource } from "../CurrentIssueSource.ts"
3
- import { Effect, FileSystem, flow, Layer, Schema } from "effect"
3
+ import { Effect, flow, Layer, Option, Schema } from "effect"
4
4
  import { IssueSource } from "../IssueSource.ts"
5
- import { ChildProcess } from "effect/unstable/process"
6
5
  import { PrdIssue } from "../domain/PrdIssue.ts"
7
6
  import * as Yaml from "yaml"
8
- import { configEditor } from "../shared/config.ts"
9
7
  import { CurrentProjectId } from "../Settings.ts"
10
8
  import { layerProjectIdPrompt } from "../Projects.ts"
9
+ import { Editor } from "../Editor.ts"
11
10
 
12
11
  const issueTemplate = `---
13
12
  title: Issue Title
@@ -33,31 +32,18 @@ const handler = flow(
33
32
  Command.withHandler(
34
33
  Effect.fnUntraced(function* () {
35
34
  const source = yield* IssueSource
36
- const fs = yield* FileSystem.FileSystem
37
35
  const projectId = yield* CurrentProjectId
38
- const tempFile = yield* fs.makeTempFileScoped({
36
+ const editor = yield* Editor
37
+
38
+ const content = yield* editor.editTemp({
39
39
  suffix: ".md",
40
+ initialContent: issueTemplate,
40
41
  })
41
- const editor = yield* configEditor
42
- yield* fs.writeFileString(tempFile, issueTemplate)
43
-
44
- const exitCode = yield* ChildProcess.make(
45
- editor[0]!,
46
- [...editor.slice(1), tempFile],
47
- {
48
- stdin: "inherit",
49
- stdout: "inherit",
50
- stderr: "inherit",
51
- },
52
- ).pipe(ChildProcess.exitCode)
53
- if (exitCode !== 0) return
54
-
55
- const content = yield* fs.readFileString(tempFile)
56
- if (content.trim() === issueTemplate.trim()) {
42
+ if (Option.isNone(content)) {
57
43
  return
58
44
  }
59
45
 
60
- const lines = content.split("\n")
46
+ const lines = content.value.split("\n")
61
47
  const yamlLines: string[] = []
62
48
  let descriptionStartIndex = 0
63
49
  for (let i = 0; i < lines.length; i++) {
@@ -93,7 +79,13 @@ const handler = flow(
93
79
  console.log(`URL: ${created.url}`)
94
80
  }, Effect.scoped),
95
81
  ),
96
- Command.provide(Layer.merge(layerProjectIdPrompt, CurrentIssueSource.layer)),
82
+ Command.provide(
83
+ Layer.mergeAll(
84
+ layerProjectIdPrompt,
85
+ CurrentIssueSource.layer,
86
+ Editor.layer,
87
+ ),
88
+ ),
97
89
  )
98
90
 
99
91
  export const commandIssue = Command.make("issue").pipe(
@@ -1,13 +1,4 @@
1
- import {
2
- Data,
3
- Effect,
4
- FileSystem,
5
- Layer,
6
- Option,
7
- Path,
8
- pipe,
9
- Schema,
10
- } from "effect"
1
+ import { Data, Effect, FileSystem, Option, Path, pipe, Schema } from "effect"
11
2
  import { PromptGen } from "../PromptGen.ts"
12
3
  import { Prd } from "../Prd.ts"
13
4
  import { Worktree } from "../Worktree.ts"
@@ -17,14 +8,11 @@ import { Command, Flag } from "effect/unstable/cli"
17
8
  import { CurrentIssueSource } from "../CurrentIssueSource.ts"
18
9
  import { commandRoot } from "./root.ts"
19
10
  import { CurrentProjectId, Settings } from "../Settings.ts"
20
- import {
21
- addOrUpdateProject,
22
- layerProjectIdPrompt,
23
- selectProject,
24
- } from "../Projects.ts"
11
+ import { addOrUpdateProject, selectProject } from "../Projects.ts"
25
12
  import { agentPlanner } from "../Agents/planner.ts"
26
13
  import { agentTasker } from "../Agents/tasker.ts"
27
14
  import { commandPlanTasks } from "./plan/tasks.ts"
15
+ import { Editor } from "../Editor.ts"
28
16
 
29
17
  const dangerous = Flag.boolean("dangerous").pipe(
30
18
  Flag.withAlias("d"),
@@ -46,25 +34,36 @@ export const commandPlan = Command.make("plan", {
46
34
  Command.withHandler(
47
35
  Effect.fnUntraced(
48
36
  function* ({ dangerous, withNewProject }) {
37
+ const editor = yield* Editor
38
+
39
+ const thePlan = yield* editor.editTemp({
40
+ suffix: ".md",
41
+ })
42
+ if (Option.isNone(thePlan)) return
43
+
49
44
  const project = withNewProject
50
45
  ? yield* addOrUpdateProject()
51
46
  : yield* selectProject
52
47
  const { specsDirectory } = yield* commandRoot
53
48
  const commandPrefix = yield* getCommandPrefix
49
+
54
50
  yield* plan({
51
+ plan: thePlan.value,
55
52
  specsDirectory,
56
53
  targetBranch: project.targetBranch,
57
54
  commandPrefix,
58
55
  dangerous,
59
56
  }).pipe(Effect.provideService(CurrentProjectId, project.id))
60
57
  },
61
- Effect.provide([Settings.layer, CurrentIssueSource.layer]),
58
+ Effect.provide([Settings.layer, CurrentIssueSource.layer, Editor.layer]),
62
59
  ),
63
60
  ),
64
61
  Command.withSubcommands([commandPlanTasks]),
65
62
  )
63
+
66
64
  const plan = Effect.fnUntraced(
67
65
  function* (options: {
66
+ readonly plan: string
68
67
  readonly specsDirectory: string
69
68
  readonly targetBranch: Option.Option<string>
70
69
  readonly commandPrefix: (
@@ -75,23 +74,27 @@ const plan = Effect.fnUntraced(
75
74
  const fs = yield* FileSystem.FileSystem
76
75
  const pathService = yield* Path.Path
77
76
  const worktree = yield* Worktree
77
+
78
78
  const cliAgent = yield* getOrSelectCliAgent
79
79
 
80
80
  yield* agentPlanner({
81
+ plan: options.plan,
81
82
  specsDirectory: options.specsDirectory,
82
83
  commandPrefix: options.commandPrefix,
83
84
  dangerous: options.dangerous,
84
85
  cliAgent,
85
86
  })
86
87
 
87
- yield* Effect.log("Converting specification into tasks")
88
88
  const planDetails = yield* pipe(
89
89
  fs.readFileString(
90
90
  pathService.join(worktree.directory, ".lalph", "plan.json"),
91
91
  ),
92
92
  Effect.flatMap(Schema.decodeEffect(PlanDetails)),
93
+ Effect.mapError(() => new SpecNotFound()),
93
94
  )
94
95
 
96
+ yield* Effect.log("Converting specification into tasks")
97
+
95
98
  yield* agentTasker({
96
99
  specificationPath: planDetails.specification,
97
100
  specsDirectory: options.specsDirectory,
@@ -114,7 +117,7 @@ const plan = Effect.fnUntraced(
114
117
  Effect.provide([
115
118
  PromptGen.layer,
116
119
  Prd.layerProvided,
117
- Worktree.layer.pipe(Layer.provide(layerProjectIdPrompt)),
120
+ Worktree.layer,
118
121
  Settings.layer,
119
122
  CurrentIssueSource.layer,
120
123
  ]),