lalph 0.2.19 → 0.2.21

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
  })));
@@ -151016,6 +150984,7 @@ ${options.task.description}
151016
150984
  5. ${options.gitFlow.commitInstructions({
151017
150985
  githubPrInstructions: sourceMeta.githubPrInstructions,
151018
150986
  githubPrNumber: options.githubPrNumber,
150987
+ taskId: options.task.id ?? "unknown",
151019
150988
  targetBranch: options.targetBranch
151020
150989
  })}
151021
150990
  6. **After ${options.gitFlow.requiresGithubPr ? "pushing" : "committing"}**
@@ -151063,8 +151032,13 @@ permission.
151063
151032
  5. If any specifications need updating based on your new understanding, update them.
151064
151033
 
151065
151034
  ${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.
151035
+ const planPrompt = (options) => `<request><![CDATA[
151036
+ ${options.plan}
151037
+ ]]></request>
151038
+
151039
+ ## Instructions
151040
+
151041
+ 1. Your job is to create a detailed specification to fulfill the request and save it as a file.
151068
151042
  First do some research to understand the request, then interview the user
151069
151043
  to gather all the necessary requirements and details for the specification.
151070
151044
  - If the user asks you to update an existing specification, find the relevant
@@ -151733,9 +151707,9 @@ const GitFlowPR = succeed$2(GitFlow, GitFlow.of({
151733
151707
  branch: void 0,
151734
151708
  setupInstructions: ({ githubPrNumber }) => githubPrNumber ? `The Github PR #${githubPrNumber} has been detected for this task and the branch has been checked out.
151735
151709
  - Review feedback in the .lalph/feedback.md file (same folder as the prd.yml file).` : `Create a new branch for the task using the format \`{task id}/description\`, using the current HEAD as the base (don't checkout any other branches first).`,
151736
- commitInstructions: ({ githubPrInstructions, githubPrNumber, targetBranch }) => `${!githubPrNumber ? `Create a pull request for this task. If the target branch does not exist, create it first.` : "Commit and push your changes to the pull request."}
151737
- ${githubPrInstructions}
151738
- The PR description should include a summary of the changes made.${targetBranch ? `\n - The target branch for the PR should be \`${targetBranch}\`.` : ""}
151710
+ commitInstructions: (options) => `${!options.githubPrNumber ? `Create a pull request for this task. If the target branch does not exist, create it first.` : "Commit and push your changes to the pull request."}
151711
+ ${options.githubPrInstructions}
151712
+ The PR description should include a summary of the changes made.${options.targetBranch ? `\n - The target branch for the PR should be \`${options.targetBranch}\`.` : ""}
151739
151713
  - **DO NOT** commit any of the files in the \`.lalph\` directory.
151740
151714
  - You have full permission to push branches, create PRs or create git commits.`,
151741
151715
  reviewInstructions: `You are already on the PR branch with their changes.
@@ -151768,13 +151742,15 @@ const GitFlowCommit = effect$1(GitFlow, gen(function* () {
151768
151742
  requiresGithubPr: false,
151769
151743
  branch: `lalph/worker-${workerState.id}`,
151770
151744
  setupInstructions: () => `You are already on a new branch for this task. You do not need to checkout any other branches.`,
151771
- commitInstructions: () => `When you have completed your changes, **you must** commit them to the current local branch. Do not git push your changes or switch branches.
151745
+ commitInstructions: (options) => `When you have completed your changes, **you must** commit them to the current local branch. Do not git push your changes or switch branches.
151746
+ - Include \`References ${options.taskId}\` in each commit message.
151772
151747
  - **DO NOT** commit any of the files in the \`.lalph\` directory.`,
151773
151748
  reviewInstructions: `You are already on the branch with their changes.
151774
151749
  After making any changes, commit them to the same branch. Do not git push your changes or switch branches.
151775
151750
 
151776
- - **DO NOT** commit any of the files in the \`.lalph\` directory.
151777
- - You have full permission to create git commits.`,
151751
+ - Include \`References {task id}\` in each commit message.
151752
+ - **DO NOT** commit any of the files in the \`.lalph\` directory.
151753
+ - You have full permission to create git commits.`,
151778
151754
  postWork: fnUntraced(function* ({ worktree, targetBranch, issueId }) {
151779
151755
  if (!targetBranch) return yield* logWarning("GitFlowCommit: No target branch specified, skipping postWork.");
151780
151756
  const prd = yield* Prd;
@@ -152120,6 +152096,41 @@ const commandPlanTasks = make$35("tasks", { specificationPath }).pipe(withDescri
152120
152096
  Worktree.layer.pipe(provide$3(layerProjectIdPrompt))
152121
152097
  ]))));
152122
152098
 
152099
+ //#endregion
152100
+ //#region src/shared/config.ts
152101
+ const configEditor = string$1("LALPH_EDITOR").pipe(orElse(() => string$1("EDITOR")), map$5(parseCommand), withDefault$3(() => ["nano"]));
152102
+
152103
+ //#endregion
152104
+ //#region src/Editor.ts
152105
+ var Editor = class extends Service$1()("lalph/Editor", { make: gen(function* () {
152106
+ const fs = yield* FileSystem;
152107
+ const editor = yield* configEditor;
152108
+ const spawner = yield* ChildProcessSpawner;
152109
+ const edit = (path) => make$23(editor[0], [...editor.slice(1), path], {
152110
+ stdin: "inherit",
152111
+ stdout: "inherit",
152112
+ stderr: "inherit"
152113
+ }).pipe(exitCode, provideService(ChildProcessSpawner, spawner), orDie$2);
152114
+ return {
152115
+ edit,
152116
+ editTemp: fnUntraced(function* (options) {
152117
+ const initialContent = options.initialContent ?? "";
152118
+ const file = yield* fs.makeTempFileScoped({ suffix: options.suffix ?? ".txt" });
152119
+ if (initialContent) yield* fs.writeFileString(file, initialContent);
152120
+ if ((yield* make$23(editor[0], [...editor.slice(1), file], {
152121
+ stdin: "inherit",
152122
+ stdout: "inherit",
152123
+ stderr: "inherit"
152124
+ }).pipe(exitCode)) !== 0) return yield* new NoSuchElementError();
152125
+ const content = (yield* fs.readFileString(file)).trim();
152126
+ if (content === initialContent) return yield* new NoSuchElementError();
152127
+ return content;
152128
+ }, scoped$1, provideService(ChildProcessSpawner, spawner), option$1)
152129
+ };
152130
+ }) }) {
152131
+ static layer = effect$1(this, this.make).pipe(provide$3(PlatformServices));
152132
+ };
152133
+
152123
152134
  //#endregion
152124
152135
  //#region src/commands/plan.ts
152125
152136
  const dangerous = boolean("dangerous").pipe(withAlias("d"), withDescription$1("Enable dangerous mode (skip permission prompts) during plan generation"));
@@ -152128,30 +152139,39 @@ const commandPlan = make$35("plan", {
152128
152139
  dangerous,
152129
152140
  withNewProject
152130
152141
  }).pipe(withDescription("Iterate on an issue plan and create PRD tasks"), withHandler(fnUntraced(function* ({ dangerous, withNewProject }) {
152142
+ const thePlan = yield* (yield* Editor).editTemp({ suffix: ".md" });
152143
+ if (isNone(thePlan)) return;
152131
152144
  const project = withNewProject ? yield* addOrUpdateProject() : yield* selectProject;
152132
152145
  const { specsDirectory } = yield* commandRoot;
152133
152146
  const commandPrefix = yield* getCommandPrefix;
152134
152147
  yield* plan({
152148
+ plan: thePlan.value,
152135
152149
  specsDirectory,
152136
152150
  targetBranch: project.targetBranch,
152137
152151
  commandPrefix,
152138
152152
  dangerous
152139
152153
  }).pipe(provideService(CurrentProjectId, project.id));
152140
- }, provide$1([Settings.layer, CurrentIssueSource.layer]))), withSubcommands([commandPlanTasks]));
152154
+ }, provide$1([
152155
+ Settings.layer,
152156
+ CurrentIssueSource.layer,
152157
+ Editor.layer
152158
+ ]))), withSubcommands([commandPlanTasks]));
152141
152159
  const plan = fnUntraced(function* (options) {
152142
152160
  const fs = yield* FileSystem;
152143
152161
  const pathService = yield* Path$1;
152144
152162
  const worktree = yield* Worktree;
152145
152163
  const cliAgent = yield* getOrSelectCliAgent;
152146
152164
  yield* agentPlanner({
152165
+ plan: options.plan,
152147
152166
  specsDirectory: options.specsDirectory,
152148
152167
  commandPrefix: options.commandPrefix,
152149
152168
  dangerous: options.dangerous,
152150
152169
  cliAgent
152151
152170
  });
152171
+ const planDetails = yield* pipe(fs.readFileString(pathService.join(worktree.directory, ".lalph", "plan.json")), flatMap$2(decodeEffect(PlanDetails)), mapError$2(() => new SpecNotFound()));
152152
152172
  yield* log$1("Converting specification into tasks");
152153
152173
  yield* agentTasker({
152154
- specificationPath: (yield* pipe(fs.readFileString(pathService.join(worktree.directory, ".lalph", "plan.json")), flatMap$2(decodeEffect(PlanDetails)))).specification,
152174
+ specificationPath: planDetails.specification,
152155
152175
  specsDirectory: options.specsDirectory,
152156
152176
  commandPrefix: options.commandPrefix,
152157
152177
  cliAgent
@@ -152160,16 +152180,15 @@ const plan = fnUntraced(function* (options) {
152160
152180
  }, scoped$1, provide$1([
152161
152181
  PromptGen.layer,
152162
152182
  Prd.layerProvided,
152163
- Worktree.layer.pipe(provide$3(layerProjectIdPrompt)),
152183
+ Worktree.layer,
152164
152184
  Settings.layer,
152165
152185
  CurrentIssueSource.layer
152166
152186
  ]));
152187
+ var SpecNotFound = class extends TaggedError("SpecNotFound") {
152188
+ message = "The AI agent failed to produce a specification.";
152189
+ };
152167
152190
  const PlanDetails = fromJsonString(Struct({ specification: String$1 }));
152168
152191
 
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
152192
  //#endregion
152174
152193
  //#region src/commands/issue.ts
152175
152194
  const issueTemplate = `---
@@ -152189,20 +152208,12 @@ const FrontMatterSchema = toCodecJson(Struct({
152189
152208
  autoMerge: Boolean$2
152190
152209
  }));
152191
152210
  const handler$1 = flow(withHandler(fnUntraced(function* () {
152192
- const source = yield* IssueSource;
152193
- const fs = yield* FileSystem;
152194
- 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");
152211
+ const content = yield* (yield* Editor).editTemp({
152212
+ suffix: ".md",
152213
+ initialContent: issueTemplate
152214
+ });
152215
+ if (isNone(content)) return;
152216
+ const lines = content.value.split("\n");
152206
152217
  const yamlLines = [];
152207
152218
  let descriptionStartIndex = 0;
152208
152219
  for (let i = 0; i < lines.length; i++) {
@@ -152217,15 +152228,19 @@ const handler$1 = flow(withHandler(fnUntraced(function* () {
152217
152228
  const yamlContent = yamlLines.join("\n");
152218
152229
  const frontMatter = yield* decodeEffect(FrontMatterSchema)(import_dist.parse(yamlContent));
152219
152230
  const description = lines.slice(descriptionStartIndex).join("\n").trim();
152220
- const created = yield* source.createIssue(projectId, new PrdIssue({
152221
- id: null,
152222
- ...frontMatter,
152223
- description,
152224
- state: "todo"
152225
- }));
152226
- console.log(`Created issue with ID: ${created.id}`);
152227
- console.log(`URL: ${created.url}`);
152228
- }, scoped$1)), provide(merge$5(layerProjectIdPrompt, CurrentIssueSource.layer)));
152231
+ yield* gen(function* () {
152232
+ const source = yield* IssueSource;
152233
+ const projectId = yield* CurrentProjectId;
152234
+ const created = yield* source.createIssue(projectId, new PrdIssue({
152235
+ id: null,
152236
+ ...frontMatter,
152237
+ description,
152238
+ state: "todo"
152239
+ }));
152240
+ console.log(`Created issue with ID: ${created.id}`);
152241
+ console.log(`URL: ${created.url}`);
152242
+ }).pipe(provide$1([layerProjectIdPrompt, CurrentIssueSource.layer]));
152243
+ })), provide(Editor.layer));
152229
152244
  const commandIssue = make$35("issue").pipe(withDescription("Create a new issue in the selected issue source"), handler$1);
152230
152245
  const commandIssueAlias = make$35("i").pipe(withDescription("Alias for 'issue' command"), handler$1);
152231
152246
 
@@ -152233,14 +152248,8 @@ const commandIssueAlias = make$35("i").pipe(withDescription("Alias for 'issue' c
152233
152248
  //#region src/commands/edit.ts
152234
152249
  const handler = withHandler(fnUntraced(function* () {
152235
152250
  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)))));
152251
+ yield* (yield* Editor).edit(prd.path);
152252
+ }, provide$1([Prd.layerLocalProvided.pipe(provideMerge(layerProjectIdPrompt)), Editor.layer])));
152244
152253
  const commandEdit = make$35("edit").pipe(withDescription("Open the prd.yml file in your editor"), handler);
152245
152254
  const commandEditAlias = make$35("e").pipe(withDescription("Alias for 'edit' command"), handler);
152246
152255
 
@@ -152250,7 +152259,7 @@ const commandSource = make$35("source").pipe(withDescription("Select the issue s
152250
152259
 
152251
152260
  //#endregion
152252
152261
  //#region package.json
152253
- var version = "0.2.19";
152262
+ var version = "0.2.21";
152254
152263
 
152255
152264
  //#endregion
152256
152265
  //#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.21",
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/GitFlow.ts CHANGED
@@ -22,6 +22,7 @@ export class GitFlow extends ServiceMap.Service<
22
22
  readonly githubPrNumber: number | undefined
23
23
  readonly githubPrInstructions: string
24
24
  readonly targetBranch: string | undefined
25
+ readonly taskId: string
25
26
  }) => string
26
27
  readonly reviewInstructions: string
27
28
  readonly postWork: (options: {
@@ -63,13 +64,11 @@ export const GitFlowPR = Layer.succeed(
63
64
  - Review feedback in the .lalph/feedback.md file (same folder as the prd.yml file).`
64
65
  : `Create a new branch for the task using the format \`{task id}/description\`, using the current HEAD as the base (don't checkout any other branches first).`,
65
66
 
66
- commitInstructions: ({
67
- githubPrInstructions,
68
- githubPrNumber,
69
- targetBranch,
70
- }) => `${!githubPrNumber ? `Create a pull request for this task. If the target branch does not exist, create it first.` : "Commit and push your changes to the pull request."}
71
- ${githubPrInstructions}
72
- The PR description should include a summary of the changes made.${targetBranch ? `\n - The target branch for the PR should be \`${targetBranch}\`.` : ""}
67
+ commitInstructions: (
68
+ options,
69
+ ) => `${!options.githubPrNumber ? `Create a pull request for this task. If the target branch does not exist, create it first.` : "Commit and push your changes to the pull request."}
70
+ ${options.githubPrInstructions}
71
+ The PR description should include a summary of the changes made.${options.targetBranch ? `\n - The target branch for the PR should be \`${options.targetBranch}\`.` : ""}
73
72
  - **DO NOT** commit any of the files in the \`.lalph\` directory.
74
73
  - You have full permission to push branches, create PRs or create git commits.`,
75
74
 
@@ -124,15 +123,18 @@ export const GitFlowCommit = Layer.effect(
124
123
  setupInstructions: () =>
125
124
  `You are already on a new branch for this task. You do not need to checkout any other branches.`,
126
125
 
127
- commitInstructions:
128
- () => `When you have completed your changes, **you must** commit them to the current local branch. Do not git push your changes or switch branches.
126
+ commitInstructions: (
127
+ options,
128
+ ) => `When you have completed your changes, **you must** commit them to the current local branch. Do not git push your changes or switch branches.
129
+ - Include \`References ${options.taskId}\` in each commit message.
129
130
  - **DO NOT** commit any of the files in the \`.lalph\` directory.`,
130
131
 
131
132
  reviewInstructions: `You are already on the branch with their changes.
132
133
  After making any changes, commit them to the same branch. Do not git push your changes or switch branches.
133
134
 
134
- - **DO NOT** commit any of the files in the \`.lalph\` directory.
135
- - You have full permission to create git commits.`,
135
+ - Include \`References {task id}\` in each commit message.
136
+ - **DO NOT** commit any of the files in the \`.lalph\` directory.
137
+ - You have full permission to create git commits.`,
136
138
 
137
139
  postWork: Effect.fnUntraced(function* ({
138
140
  worktree,
package/src/PromptGen.ts CHANGED
@@ -172,6 +172,7 @@ ${options.task.description}
172
172
  5. ${options.gitFlow.commitInstructions({
173
173
  githubPrInstructions: sourceMeta.githubPrInstructions,
174
174
  githubPrNumber: options.githubPrNumber,
175
+ taskId: options.task.id ?? "unknown",
175
176
  targetBranch: options.targetBranch,
176
177
  })}
177
178
  6. **After ${options.gitFlow.requiresGithubPr ? "pushing" : "committing"}**
@@ -233,9 +234,15 @@ permission.
233
234
  ${prdNotes(options)}`
234
235
 
235
236
  const planPrompt = (options: {
237
+ readonly plan: string
236
238
  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.
239
+ }) => `<request><![CDATA[
240
+ ${options.plan}
241
+ ]]></request>
242
+
243
+ ## Instructions
244
+
245
+ 1. Your job is to create a detailed specification to fulfill the request and save it as a file.
239
246
  First do some research to understand the request, then interview the user
240
247
  to gather all the necessary requirements and details for the specification.
241
248
  - 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, 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
@@ -32,32 +31,17 @@ const FrontMatterSchema = Schema.toCodecJson(
32
31
  const handler = flow(
33
32
  Command.withHandler(
34
33
  Effect.fnUntraced(function* () {
35
- const source = yield* IssueSource
36
- const fs = yield* FileSystem.FileSystem
37
- const projectId = yield* CurrentProjectId
38
- const tempFile = yield* fs.makeTempFileScoped({
34
+ const editor = yield* Editor
35
+
36
+ const content = yield* editor.editTemp({
39
37
  suffix: ".md",
38
+ initialContent: issueTemplate,
40
39
  })
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()) {
40
+ if (Option.isNone(content)) {
57
41
  return
58
42
  }
59
43
 
60
- const lines = content.split("\n")
44
+ const lines = content.value.split("\n")
61
45
  const yamlLines: string[] = []
62
46
  let descriptionStartIndex = 0
63
47
  for (let i = 0; i < lines.length; i++) {
@@ -80,20 +64,24 @@ const handler = flow(
80
64
  )
81
65
  const description = lines.slice(descriptionStartIndex).join("\n").trim()
82
66
 
83
- const created = yield* source.createIssue(
84
- projectId,
85
- new PrdIssue({
86
- id: null,
87
- ...frontMatter,
88
- description,
89
- state: "todo",
90
- }),
91
- )
92
- console.log(`Created issue with ID: ${created.id}`)
93
- console.log(`URL: ${created.url}`)
94
- }, Effect.scoped),
67
+ yield* Effect.gen(function* () {
68
+ const source = yield* IssueSource
69
+ const projectId = yield* CurrentProjectId
70
+ const created = yield* source.createIssue(
71
+ projectId,
72
+ new PrdIssue({
73
+ id: null,
74
+ ...frontMatter,
75
+ description,
76
+ state: "todo",
77
+ }),
78
+ )
79
+ console.log(`Created issue with ID: ${created.id}`)
80
+ console.log(`URL: ${created.url}`)
81
+ }).pipe(Effect.provide([layerProjectIdPrompt, CurrentIssueSource.layer]))
82
+ }),
95
83
  ),
96
- Command.provide(Layer.merge(layerProjectIdPrompt, CurrentIssueSource.layer)),
84
+ Command.provide(Editor.layer),
97
85
  )
98
86
 
99
87
  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
  ]),