lalph 0.2.18 → 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
  })));
@@ -150368,32 +150336,41 @@ const PollResponse = Union([TokenResponse, PollErrorResponse]);
150368
150336
 
150369
150337
  //#endregion
150370
150338
  //#region src/domain/GithubComment.ts
150371
- var Author = class extends Class("github/Author")({ login: String$1 }) {};
150372
- var Comment = class extends Class("github/Comment")({
150339
+ var Author = class extends Class("Author")({ login: String$1 }) {};
150340
+ var Comment = class extends Class("Comment")({
150373
150341
  id: String$1,
150374
150342
  body: String$1,
150375
- author: Author
150343
+ author: Author,
150344
+ createdAt: String$1
150376
150345
  }) {};
150377
- var CommentsEdge = class extends Class("github/CommentsEdge")({ node: Comment }) {};
150378
- var PullRequestComments = class extends Class("PullRequestComments")({ edges: Array$1(CommentsEdge) }) {};
150379
- var PullRequest = class extends Class("github/PullRequest")({
150346
+ var PullRequestComments = class extends Class("PullRequestComments")({ nodes: Array$1(Comment) }) {};
150347
+ var PullRequest = class extends Class("PullRequest")({
150380
150348
  url: String$1,
150349
+ reviewDecision: Null,
150350
+ reviews: suspend(() => Reviews),
150381
150351
  reviewThreads: suspend(() => ReviewThreads),
150382
150352
  comments: PullRequestComments
150383
150353
  }) {};
150384
- var Repository = class extends Class("github/Repository")({ pullRequest: PullRequest }) {};
150385
- var Data = class extends Class("github/Data")({ repository: Repository }) {};
150386
- var CommentsData = class extends Class("github/CommentsData")({ data: Data }) {};
150387
- var ReviewComment = class extends Class("github/ReviewComment")({
150354
+ var Repository = class extends Class("Repository")({ pullRequest: PullRequest }) {};
150355
+ var Data = class extends Class("Data")({ repository: Repository }) {};
150356
+ var GithubPullRequestData = class extends Class("GithubPullRequestData")({ data: Data }) {};
150357
+ var Review = class extends Class("Review")({
150358
+ id: String$1,
150359
+ author: Author,
150360
+ body: String$1
150361
+ }) {};
150362
+ var Reviews = class extends Class("Reviews")({ nodes: Array$1(Review) }) {};
150363
+ var ReviewComment = class extends Class("ReviewComment")({
150388
150364
  id: String$1,
150389
150365
  author: Author,
150390
150366
  body: String$1,
150391
150367
  path: String$1,
150392
- originalLine: NullOr(Number$1),
150393
- diffHunk: String$1
150368
+ originalLine: Number$1,
150369
+ diffHunk: String$1,
150370
+ createdAt: String$1
150394
150371
  }) {};
150395
- var NodeComments = class extends Class("github/NodeComments")({ nodes: Array$1(ReviewComment) }) {};
150396
- var ReviewThreadNode = class extends Class("github/ReviewThreadNode")({
150372
+ var NodeComments = class extends Class("NodeComments")({ nodes: Array$1(ReviewComment) }) {};
150373
+ var ReviewThreadsNode = class extends Class("ReviewThreadsNode")({
150397
150374
  isCollapsed: Boolean$2,
150398
150375
  isOutdated: Boolean$2,
150399
150376
  isResolved: Boolean$2,
@@ -150402,8 +150379,7 @@ var ReviewThreadNode = class extends Class("github/ReviewThreadNode")({
150402
150379
  commentNodes = this.comments.nodes;
150403
150380
  shouldDisplayThread = !this.isCollapsed && !this.isOutdated;
150404
150381
  };
150405
- var ReviewThreadsEdge = class extends Class("ReviewThreadsEdge")({ node: ReviewThreadNode }) {};
150406
- var ReviewThreads = class extends Class("github/ReviewThreads")({ edges: Array$1(ReviewThreadsEdge) }) {};
150382
+ var ReviewThreads = class extends Class("ReviewThreads")({ nodes: Array$1(ReviewThreadsNode) }) {};
150407
150383
 
150408
150384
  //#endregion
150409
150385
  //#region src/Github/Cli.ts
@@ -150413,15 +150389,16 @@ var GithubCli = class extends Service$1()("lalph/Github/Cli", { make: gen(functi
150413
150389
  onNone: () => fail$4(new GithubCliRepoNotFound()),
150414
150390
  onSome: (value) => succeed$1(value)
150415
150391
  }))))).split("/");
150416
- const reviewComments = (pr) => make$23`gh api graphql -f owner=${owner} -f repo=${repo} -F pr=${pr} -f query=${githubReviewCommentsQuery}`.pipe(string, flatMap$2(decodeEffect(CommentsFromJson)), map$8((data) => {
150392
+ const reviewComments = (pr) => make$23`gh api graphql -f owner=${owner} -f repo=${repo} -F pr=${pr} -f query=${githubReviewCommentsQuery}`.pipe(string, flatMap$2(decodeEffect(PullRequestDataFromJson)), map$8((data) => {
150417
150393
  return {
150418
- comments: data.data.repository.pullRequest.comments.edges.map((edge) => edge.node),
150419
- reviewThreads: data.data.repository.pullRequest.reviewThreads.edges.map((edge) => edge.node)
150394
+ comments: data.data.repository.pullRequest.comments.nodes.filter((c) => !c.author.login.startsWith("github")),
150395
+ reviews: data.data.repository.pullRequest.reviews.nodes.filter((r) => r.body.trim().length > 0),
150396
+ reviewThreads: data.data.repository.pullRequest.reviewThreads.nodes
150420
150397
  };
150421
150398
  }), provideService(ChildProcessSpawner, spawner));
150422
- const prFeedbackMd = (pr) => reviewComments(pr).pipe(map$8(({ comments, reviewThreads }) => {
150399
+ const prFeedbackMd = (pr) => reviewComments(pr).pipe(map$8(({ comments, reviewThreads, reviews }) => {
150423
150400
  const eligibleReviewThreads = reviewThreads.filter((thread) => thread.shouldDisplayThread);
150424
- if (comments.length === 0 && eligibleReviewThreads.length === 0) return `No review comments found.`;
150401
+ if (comments.length === 0 && eligibleReviewThreads.length === 0 && reviews.length === 0) return `No review comments found.`;
150425
150402
  let content = `# PR feedback
150426
150403
 
150427
150404
  Comments are rendered in XML format.`;
@@ -150432,6 +150409,18 @@ Comments are rendered in XML format.`;
150432
150409
  ## Review Comments
150433
150410
 
150434
150411
  ${reviewCommentsMd}`;
150412
+ }
150413
+ if (reviews.length > 0) {
150414
+ const reviewsXml = reviews.map((review) => `<review author="${review.author.login}">
150415
+ <body><![CDATA[${review.body}]]></body>
150416
+ </review>`).join("\n");
150417
+ content += `
150418
+
150419
+ ## Reviews
150420
+
150421
+ <reviews>
150422
+ ${reviewsXml}
150423
+ </reviews>`;
150435
150424
  }
150436
150425
  if (comments.length > 0) {
150437
150426
  const generalCommentsXml = comments.map((comment) => renderGeneralComment(comment)).join("\n");
@@ -150470,46 +150459,55 @@ const renderReviewComments = (comment, followup) => `<comment author="${comment.
150470
150459
  const renderGeneralComment = (comment) => ` <comment author="${comment.author.login}">
150471
150460
  <body><![CDATA[${comment.body}]]></body>
150472
150461
  </comment>`;
150473
- const CommentsFromJson = fromJsonString(CommentsData);
150462
+ const PullRequestDataFromJson = fromJsonString(GithubPullRequestData);
150474
150463
  const githubReviewCommentsQuery = `
150475
- query FetchPRComments($owner: String!, $repo: String!, $pr: Int!) {
150476
- repository(owner: $owner, name: $repo) {
150477
- pullRequest(number: $pr) {
150478
- url
150479
- reviewDecision
150480
- reviewThreads(first: 100) {
150481
- edges {
150482
- node {
150483
- isCollapsed
150484
- isOutdated
150485
- isResolved
150486
- comments(first: 100) {
150487
- nodes {
150488
- id
150489
- author { login }
150490
- body
150491
- path
150492
- originalLine
150493
- diffHunk
150494
- createdAt
150495
- }
150496
- }
150497
- }
150464
+ query FetchPRComments($owner: String!, $repo: String!, $pr: Int!) {
150465
+ repository(owner: $owner, name: $repo) {
150466
+ pullRequest(number: $pr) {
150467
+ url
150468
+ reviewDecision
150469
+ reviews(first: 100) {
150470
+ nodes {
150471
+ id
150472
+ author {
150473
+ login
150498
150474
  }
150475
+ body
150499
150476
  }
150500
- comments(first: 100) {
150501
- edges {
150502
- node {
150477
+ }
150478
+ reviewThreads(first: 100) {
150479
+ nodes {
150480
+ isCollapsed
150481
+ isOutdated
150482
+ isResolved
150483
+ comments(first: 100) {
150484
+ nodes {
150503
150485
  id
150486
+ author {
150487
+ login
150488
+ }
150504
150489
  body
150505
- author { login }
150490
+ path
150491
+ originalLine
150492
+ diffHunk
150506
150493
  createdAt
150507
150494
  }
150508
150495
  }
150509
150496
  }
150510
150497
  }
150498
+ comments(first: 100) {
150499
+ nodes {
150500
+ id
150501
+ body
150502
+ author {
150503
+ login
150504
+ }
150505
+ createdAt
150506
+ }
150507
+ }
150511
150508
  }
150512
150509
  }
150510
+ }
150513
150511
  `;
150514
150512
 
150515
150513
  //#endregion
@@ -151033,8 +151031,13 @@ permission.
151033
151031
  5. If any specifications need updating based on your new understanding, update them.
151034
151032
 
151035
151033
  ${prdNotes(options)}`;
151036
- const planPrompt = (options) => `1. Ask the user for the idea / request, then your job is to create a detailed
151037
- 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.
151038
151041
  First do some research to understand the request, then interview the user
151039
151042
  to gather all the necessary requirements and details for the specification.
151040
151043
  - If the user asks you to update an existing specification, find the relevant
@@ -152090,6 +152093,41 @@ const commandPlanTasks = make$35("tasks", { specificationPath }).pipe(withDescri
152090
152093
  Worktree.layer.pipe(provide$3(layerProjectIdPrompt))
152091
152094
  ]))));
152092
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
+
152093
152131
  //#endregion
152094
152132
  //#region src/commands/plan.ts
152095
152133
  const dangerous = boolean("dangerous").pipe(withAlias("d"), withDescription$1("Enable dangerous mode (skip permission prompts) during plan generation"));
@@ -152098,30 +152136,39 @@ const commandPlan = make$35("plan", {
152098
152136
  dangerous,
152099
152137
  withNewProject
152100
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;
152101
152141
  const project = withNewProject ? yield* addOrUpdateProject() : yield* selectProject;
152102
152142
  const { specsDirectory } = yield* commandRoot;
152103
152143
  const commandPrefix = yield* getCommandPrefix;
152104
152144
  yield* plan({
152145
+ plan: thePlan.value,
152105
152146
  specsDirectory,
152106
152147
  targetBranch: project.targetBranch,
152107
152148
  commandPrefix,
152108
152149
  dangerous
152109
152150
  }).pipe(provideService(CurrentProjectId, project.id));
152110
- }, provide$1([Settings.layer, CurrentIssueSource.layer]))), withSubcommands([commandPlanTasks]));
152151
+ }, provide$1([
152152
+ Settings.layer,
152153
+ CurrentIssueSource.layer,
152154
+ Editor.layer
152155
+ ]))), withSubcommands([commandPlanTasks]));
152111
152156
  const plan = fnUntraced(function* (options) {
152112
152157
  const fs = yield* FileSystem;
152113
152158
  const pathService = yield* Path$1;
152114
152159
  const worktree = yield* Worktree;
152115
152160
  const cliAgent = yield* getOrSelectCliAgent;
152116
152161
  yield* agentPlanner({
152162
+ plan: options.plan,
152117
152163
  specsDirectory: options.specsDirectory,
152118
152164
  commandPrefix: options.commandPrefix,
152119
152165
  dangerous: options.dangerous,
152120
152166
  cliAgent
152121
152167
  });
152168
+ const planDetails = yield* pipe(fs.readFileString(pathService.join(worktree.directory, ".lalph", "plan.json")), flatMap$2(decodeEffect(PlanDetails)), mapError$2(() => new SpecNotFound()));
152122
152169
  yield* log$1("Converting specification into tasks");
152123
152170
  yield* agentTasker({
152124
- specificationPath: (yield* pipe(fs.readFileString(pathService.join(worktree.directory, ".lalph", "plan.json")), flatMap$2(decodeEffect(PlanDetails)))).specification,
152171
+ specificationPath: planDetails.specification,
152125
152172
  specsDirectory: options.specsDirectory,
152126
152173
  commandPrefix: options.commandPrefix,
152127
152174
  cliAgent
@@ -152130,16 +152177,15 @@ const plan = fnUntraced(function* (options) {
152130
152177
  }, scoped$1, provide$1([
152131
152178
  PromptGen.layer,
152132
152179
  Prd.layerProvided,
152133
- Worktree.layer.pipe(provide$3(layerProjectIdPrompt)),
152180
+ Worktree.layer,
152134
152181
  Settings.layer,
152135
152182
  CurrentIssueSource.layer
152136
152183
  ]));
152184
+ var SpecNotFound = class extends TaggedError("SpecNotFound") {
152185
+ message = "The AI agent failed to produce a specification.";
152186
+ };
152137
152187
  const PlanDetails = fromJsonString(Struct({ specification: String$1 }));
152138
152188
 
152139
- //#endregion
152140
- //#region src/shared/config.ts
152141
- const configEditor = string$1("LALPH_EDITOR").pipe(orElse(() => string$1("EDITOR")), map$5(parseCommand), withDefault$3(() => ["nano"]));
152142
-
152143
152189
  //#endregion
152144
152190
  //#region src/commands/issue.ts
152145
152191
  const issueTemplate = `---
@@ -152160,19 +152206,13 @@ const FrontMatterSchema = toCodecJson(Struct({
152160
152206
  }));
152161
152207
  const handler$1 = flow(withHandler(fnUntraced(function* () {
152162
152208
  const source = yield* IssueSource;
152163
- const fs = yield* FileSystem;
152164
152209
  const projectId = yield* CurrentProjectId;
152165
- const tempFile = yield* fs.makeTempFileScoped({ suffix: ".md" });
152166
- const editor = yield* configEditor;
152167
- yield* fs.writeFileString(tempFile, issueTemplate);
152168
- if ((yield* make$23(editor[0], [...editor.slice(1), tempFile], {
152169
- stdin: "inherit",
152170
- stdout: "inherit",
152171
- stderr: "inherit"
152172
- }).pipe(exitCode)) !== 0) return;
152173
- const content = yield* fs.readFileString(tempFile);
152174
- if (content.trim() === issueTemplate.trim()) return;
152175
- 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");
152176
152216
  const yamlLines = [];
152177
152217
  let descriptionStartIndex = 0;
152178
152218
  for (let i = 0; i < lines.length; i++) {
@@ -152195,7 +152235,7 @@ const handler$1 = flow(withHandler(fnUntraced(function* () {
152195
152235
  }));
152196
152236
  console.log(`Created issue with ID: ${created.id}`);
152197
152237
  console.log(`URL: ${created.url}`);
152198
- }, scoped$1)), provide(merge$5(layerProjectIdPrompt, CurrentIssueSource.layer)));
152238
+ }, scoped$1)), provide(mergeAll(layerProjectIdPrompt, CurrentIssueSource.layer, Editor.layer)));
152199
152239
  const commandIssue = make$35("issue").pipe(withDescription("Create a new issue in the selected issue source"), handler$1);
152200
152240
  const commandIssueAlias = make$35("i").pipe(withDescription("Alias for 'issue' command"), handler$1);
152201
152241
 
@@ -152203,14 +152243,8 @@ const commandIssueAlias = make$35("i").pipe(withDescription("Alias for 'issue' c
152203
152243
  //#region src/commands/edit.ts
152204
152244
  const handler = withHandler(fnUntraced(function* () {
152205
152245
  const prd = yield* Prd;
152206
- const editor = yield* configEditor;
152207
- yield* make$23(editor[0], [...editor.slice(1), prd.path], {
152208
- extendEnv: true,
152209
- stdin: "inherit",
152210
- stdout: "inherit",
152211
- stderr: "inherit"
152212
- }).pipe(exitCode);
152213
- }, 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])));
152214
152248
  const commandEdit = make$35("edit").pipe(withDescription("Open the prd.yml file in your editor"), handler);
152215
152249
  const commandEditAlias = make$35("e").pipe(withDescription("Alias for 'edit' command"), handler);
152216
152250
 
@@ -152220,7 +152254,7 @@ const commandSource = make$35("source").pipe(withDescription("Select the issue s
152220
152254
 
152221
152255
  //#endregion
152222
152256
  //#region package.json
152223
- var version = "0.2.18";
152257
+ var version = "0.2.20";
152224
152258
 
152225
152259
  //#endregion
152226
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.18",
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/Github/Cli.ts CHANGED
@@ -10,9 +10,9 @@ import {
10
10
  } from "effect"
11
11
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
12
12
  import {
13
- CommentsData,
14
- ReviewComment,
15
13
  Comment,
14
+ GithubPullRequestData,
15
+ ReviewComment,
16
16
  } from "../domain/GithubComment.ts"
17
17
 
18
18
  export class GithubCli extends ServiceMap.Service<GithubCli>()(
@@ -41,17 +41,19 @@ export class GithubCli extends ServiceMap.Service<GithubCli>()(
41
41
  const reviewComments = (pr: number) =>
42
42
  ChildProcess.make`gh api graphql -f owner=${owner} -f repo=${repo} -F pr=${pr} -f query=${githubReviewCommentsQuery}`.pipe(
43
43
  ChildProcess.string,
44
- Effect.flatMap(Schema.decodeEffect(CommentsFromJson)),
44
+ Effect.flatMap(Schema.decodeEffect(PullRequestDataFromJson)),
45
45
  Effect.map((data) => {
46
46
  const comments =
47
- data.data.repository.pullRequest.comments.edges.map(
48
- (edge) => edge.node,
47
+ data.data.repository.pullRequest.comments.nodes.filter(
48
+ (c) => !c.author.login.startsWith("github"),
49
49
  )
50
- const reviewThreads =
51
- data.data.repository.pullRequest.reviewThreads.edges.map(
52
- (edge) => edge.node,
50
+ const reviews =
51
+ data.data.repository.pullRequest.reviews.nodes.filter(
52
+ (r) => r.body.trim().length > 0,
53
53
  )
54
- return { comments, reviewThreads } as const
54
+ const reviewThreads =
55
+ data.data.repository.pullRequest.reviewThreads.nodes
56
+ return { comments, reviews, reviewThreads } as const
55
57
  }),
56
58
  Effect.provideService(
57
59
  ChildProcessSpawner.ChildProcessSpawner,
@@ -61,12 +63,16 @@ export class GithubCli extends ServiceMap.Service<GithubCli>()(
61
63
 
62
64
  const prFeedbackMd = (pr: number) =>
63
65
  reviewComments(pr).pipe(
64
- Effect.map(({ comments, reviewThreads }) => {
66
+ Effect.map(({ comments, reviewThreads, reviews }) => {
65
67
  const eligibleReviewThreads = reviewThreads.filter(
66
68
  (thread) => thread.shouldDisplayThread,
67
69
  )
68
70
 
69
- if (comments.length === 0 && eligibleReviewThreads.length === 0) {
71
+ if (
72
+ comments.length === 0 &&
73
+ eligibleReviewThreads.length === 0 &&
74
+ reviews.length === 0
75
+ ) {
70
76
  return `No review comments found.`
71
77
  }
72
78
 
@@ -90,6 +96,23 @@ Comments are rendered in XML format.`
90
96
  ${reviewCommentsMd}`
91
97
  }
92
98
 
99
+ if (reviews.length > 0) {
100
+ const reviewsXml = reviews
101
+ .map(
102
+ (review) => `<review author="${review.author.login}">
103
+ <body><![CDATA[${review.body}]]></body>
104
+ </review>`,
105
+ )
106
+ .join("\n")
107
+ content += `
108
+
109
+ ## Reviews
110
+
111
+ <reviews>
112
+ ${reviewsXml}
113
+ </reviews>`
114
+ }
115
+
93
116
  if (comments.length > 0) {
94
117
  const generalCommentsXml = comments
95
118
  .map((comment) => renderGeneralComment(comment))
@@ -155,45 +178,54 @@ const renderGeneralComment = (
155
178
 
156
179
  // Schema definitions and GraphQL query
157
180
 
158
- const CommentsFromJson = Schema.fromJsonString(CommentsData)
181
+ const PullRequestDataFromJson = Schema.fromJsonString(GithubPullRequestData)
159
182
 
160
183
  const githubReviewCommentsQuery = `
161
- query FetchPRComments($owner: String!, $repo: String!, $pr: Int!) {
162
- repository(owner: $owner, name: $repo) {
163
- pullRequest(number: $pr) {
164
- url
165
- reviewDecision
166
- reviewThreads(first: 100) {
167
- edges {
168
- node {
169
- isCollapsed
170
- isOutdated
171
- isResolved
172
- comments(first: 100) {
173
- nodes {
174
- id
175
- author { login }
176
- body
177
- path
178
- originalLine
179
- diffHunk
180
- createdAt
181
- }
182
- }
183
- }
184
+ query FetchPRComments($owner: String!, $repo: String!, $pr: Int!) {
185
+ repository(owner: $owner, name: $repo) {
186
+ pullRequest(number: $pr) {
187
+ url
188
+ reviewDecision
189
+ reviews(first: 100) {
190
+ nodes {
191
+ id
192
+ author {
193
+ login
184
194
  }
195
+ body
185
196
  }
186
- comments(first: 100) {
187
- edges {
188
- node {
197
+ }
198
+ reviewThreads(first: 100) {
199
+ nodes {
200
+ isCollapsed
201
+ isOutdated
202
+ isResolved
203
+ comments(first: 100) {
204
+ nodes {
189
205
  id
206
+ author {
207
+ login
208
+ }
190
209
  body
191
- author { login }
210
+ path
211
+ originalLine
212
+ diffHunk
192
213
  createdAt
193
214
  }
194
215
  }
195
216
  }
196
217
  }
218
+ comments(first: 100) {
219
+ nodes {
220
+ id
221
+ body
222
+ author {
223
+ login
224
+ }
225
+ createdAt
226
+ }
227
+ }
197
228
  }
198
229
  }
230
+ }
199
231
  `
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
  ]),
@@ -1,62 +1,70 @@
1
1
  import * as S from "effect/Schema"
2
2
 
3
- export class Author extends S.Class<Author>("github/Author")({
3
+ export class Author extends S.Class<Author>("Author")({
4
4
  login: S.String,
5
5
  }) {}
6
6
 
7
- export class Comment extends S.Class<Comment>("github/Comment")({
7
+ export class Comment extends S.Class<Comment>("Comment")({
8
8
  id: S.String,
9
9
  body: S.String,
10
10
  author: Author,
11
- // createdAt: S.String,
12
- }) {}
13
-
14
- export class CommentsEdge extends S.Class<CommentsEdge>("github/CommentsEdge")({
15
- node: Comment,
11
+ createdAt: S.String,
16
12
  }) {}
17
13
 
18
14
  export class PullRequestComments extends S.Class<PullRequestComments>(
19
15
  "PullRequestComments",
20
16
  )({
21
- edges: S.Array(CommentsEdge),
17
+ nodes: S.Array(Comment),
22
18
  }) {}
23
19
 
24
- export class PullRequest extends S.Class<PullRequest>("github/PullRequest")({
20
+ export class PullRequest extends S.Class<PullRequest>("PullRequest")({
25
21
  url: S.String,
22
+ reviewDecision: S.Null,
23
+ reviews: S.suspend(() => Reviews),
26
24
  reviewThreads: S.suspend(() => ReviewThreads),
27
25
  comments: PullRequestComments,
28
26
  }) {}
29
27
 
30
- export class Repository extends S.Class<Repository>("github/Repository")({
28
+ export class Repository extends S.Class<Repository>("Repository")({
31
29
  pullRequest: PullRequest,
32
30
  }) {}
33
31
 
34
- export class Data extends S.Class<Data>("github/Data")({
32
+ export class Data extends S.Class<Data>("Data")({
35
33
  repository: Repository,
36
34
  }) {}
37
35
 
38
- export class CommentsData extends S.Class<CommentsData>("github/CommentsData")({
36
+ export class GithubPullRequestData extends S.Class<GithubPullRequestData>(
37
+ "GithubPullRequestData",
38
+ )({
39
39
  data: Data,
40
40
  }) {}
41
41
 
42
- export class ReviewComment extends S.Class<ReviewComment>(
43
- "github/ReviewComment",
44
- )({
42
+ export class Review extends S.Class<Review>("Review")({
43
+ id: S.String,
44
+ author: Author,
45
+ body: S.String,
46
+ }) {}
47
+
48
+ export class Reviews extends S.Class<Reviews>("Reviews")({
49
+ nodes: S.Array(Review),
50
+ }) {}
51
+
52
+ export class ReviewComment extends S.Class<ReviewComment>("ReviewComment")({
45
53
  id: S.String,
46
54
  author: Author,
47
55
  body: S.String,
48
56
  path: S.String,
49
- originalLine: S.NullOr(S.Number),
57
+ originalLine: S.Number,
50
58
  diffHunk: S.String,
51
- // createdAt: S.String,
59
+ createdAt: S.String,
52
60
  }) {}
53
61
 
54
- export class NodeComments extends S.Class<NodeComments>("github/NodeComments")({
62
+ export class NodeComments extends S.Class<NodeComments>("NodeComments")({
55
63
  nodes: S.Array(ReviewComment),
56
64
  }) {}
57
65
 
58
- export class ReviewThreadNode extends S.Class<ReviewThreadNode>(
59
- "github/ReviewThreadNode",
66
+ export class ReviewThreadsNode extends S.Class<ReviewThreadsNode>(
67
+ "ReviewThreadsNode",
60
68
  )({
61
69
  isCollapsed: S.Boolean,
62
70
  isOutdated: S.Boolean,
@@ -67,14 +75,6 @@ export class ReviewThreadNode extends S.Class<ReviewThreadNode>(
67
75
  readonly shouldDisplayThread = !this.isCollapsed && !this.isOutdated
68
76
  }
69
77
 
70
- export class ReviewThreadsEdge extends S.Class<ReviewThreadsEdge>(
71
- "ReviewThreadsEdge",
72
- )({
73
- node: ReviewThreadNode,
74
- }) {}
75
-
76
- export class ReviewThreads extends S.Class<ReviewThreads>(
77
- "github/ReviewThreads",
78
- )({
79
- edges: S.Array(ReviewThreadsEdge),
78
+ export class ReviewThreads extends S.Class<ReviewThreads>("ReviewThreads")({
79
+ nodes: S.Array(ReviewThreadsNode),
80
80
  }) {}