lalph 0.2.21 → 0.3.0
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 +772 -453
- package/package.json +3 -3
- package/src/Agents/chooser.ts +8 -8
- package/src/Agents/planner.ts +5 -7
- package/src/Agents/reviewer.ts +6 -8
- package/src/Agents/tasker.ts +6 -8
- package/src/Agents/timeout.ts +6 -8
- package/src/Agents/worker.ts +6 -8
- package/src/CurrentIssueSource.ts +12 -0
- package/src/Github/Cli.ts +5 -5
- package/src/Github.ts +43 -1
- package/src/IssueSource.ts +18 -1
- package/src/Linear.ts +52 -1
- package/src/Presets.ts +165 -0
- package/src/Projects.ts +7 -1
- package/src/PromptGen.ts +8 -6
- package/src/Worktree.ts +4 -4
- package/src/cli.ts +4 -3
- package/src/commands/agents/add.ts +11 -0
- package/src/commands/agents/edit.ts +25 -0
- package/src/commands/agents/ls.ts +43 -0
- package/src/commands/agents/rm.ts +26 -0
- package/src/commands/agents.ts +22 -0
- package/src/commands/plan/tasks.ts +6 -6
- package/src/commands/plan.ts +4 -13
- package/src/commands/root.ts +13 -21
- package/src/domain/CliAgent.ts +120 -51
- package/src/domain/CliAgentPreset.ts +55 -0
- package/src/commands/agent.ts +0 -70
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lalph",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
"@changesets/changelog-github": "^0.5.2",
|
|
24
24
|
"@changesets/cli": "^2.29.8",
|
|
25
25
|
"@effect/language-service": "^0.72.0",
|
|
26
|
-
"@effect/platform-node": "https://pkg.pr.new/Effect-TS/effect-smol/@effect/platform-node@
|
|
26
|
+
"@effect/platform-node": "https://pkg.pr.new/Effect-TS/effect-smol/@effect/platform-node@39ecde9",
|
|
27
27
|
"@linear/sdk": "^72.0.0",
|
|
28
28
|
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
|
29
29
|
"@octokit/types": "^16.0.0",
|
|
30
|
-
"effect": "https://pkg.pr.new/Effect-TS/effect-smol/effect@
|
|
30
|
+
"effect": "https://pkg.pr.new/Effect-TS/effect-smol/effect@39ecde9",
|
|
31
31
|
"husky": "^9.1.7",
|
|
32
32
|
"lint-staged": "^16.2.7",
|
|
33
33
|
"octokit": "^5.0.5",
|
package/src/Agents/chooser.ts
CHANGED
|
@@ -4,16 +4,13 @@ import { Prd } from "../Prd.ts"
|
|
|
4
4
|
import { ChildProcess } from "effect/unstable/process"
|
|
5
5
|
import { Worktree } from "../Worktree.ts"
|
|
6
6
|
import { RunnerStalled } from "../domain/Errors.ts"
|
|
7
|
-
import type { CliAgent } from "../domain/CliAgent.ts"
|
|
8
7
|
import { makeWaitForFile } from "../shared/fs.ts"
|
|
9
8
|
import { GitFlow } from "../GitFlow.ts"
|
|
9
|
+
import type { CliAgentPreset } from "../domain/CliAgentPreset.ts"
|
|
10
10
|
|
|
11
11
|
export const agentChooser = Effect.fnUntraced(function* (options: {
|
|
12
12
|
readonly stallTimeout: Duration.Duration
|
|
13
|
-
readonly
|
|
14
|
-
command: ChildProcess.Command,
|
|
15
|
-
) => ChildProcess.Command
|
|
16
|
-
readonly cliAgent: CliAgent
|
|
13
|
+
readonly preset: CliAgentPreset
|
|
17
14
|
}) {
|
|
18
15
|
const fs = yield* FileSystem.FileSystem
|
|
19
16
|
const pathService = yield* Path.Path
|
|
@@ -29,13 +26,16 @@ export const agentChooser = Effect.fnUntraced(function* (options: {
|
|
|
29
26
|
)
|
|
30
27
|
|
|
31
28
|
yield* pipe(
|
|
32
|
-
options.cliAgent.command({
|
|
29
|
+
options.preset.cliAgent.command({
|
|
33
30
|
prompt: promptGen.promptChoose({ gitFlow }),
|
|
34
31
|
prdFilePath: pathService.join(".lalph", "prd.yml"),
|
|
32
|
+
extraArgs: options.preset.extraArgs,
|
|
35
33
|
}),
|
|
36
34
|
ChildProcess.setCwd(worktree.directory),
|
|
37
|
-
options.
|
|
38
|
-
worktree.execWithWorkerOutput({
|
|
35
|
+
options.preset.withCommandPrefix,
|
|
36
|
+
worktree.execWithWorkerOutput({
|
|
37
|
+
cliAgent: options.preset.cliAgent,
|
|
38
|
+
}),
|
|
39
39
|
Effect.timeoutOrElse({
|
|
40
40
|
duration: options.stallTimeout,
|
|
41
41
|
onTimeout: () => Effect.fail(new RunnerStalled()),
|
package/src/Agents/planner.ts
CHANGED
|
@@ -2,29 +2,27 @@ import { Effect, Path, pipe } from "effect"
|
|
|
2
2
|
import { PromptGen } from "../PromptGen.ts"
|
|
3
3
|
import { ChildProcess } from "effect/unstable/process"
|
|
4
4
|
import { Worktree } from "../Worktree.ts"
|
|
5
|
-
import type {
|
|
5
|
+
import type { CliAgentPreset } from "../domain/CliAgentPreset.ts"
|
|
6
6
|
|
|
7
7
|
export const agentPlanner = Effect.fnUntraced(function* (options: {
|
|
8
8
|
readonly plan: string
|
|
9
9
|
readonly specsDirectory: string
|
|
10
|
-
readonly commandPrefix: (
|
|
11
|
-
command: ChildProcess.Command,
|
|
12
|
-
) => ChildProcess.Command
|
|
13
10
|
readonly dangerous: boolean
|
|
14
|
-
readonly
|
|
11
|
+
readonly preset: CliAgentPreset
|
|
15
12
|
}) {
|
|
16
13
|
const pathService = yield* Path.Path
|
|
17
14
|
const worktree = yield* Worktree
|
|
18
15
|
const promptGen = yield* PromptGen
|
|
19
16
|
|
|
20
17
|
yield* pipe(
|
|
21
|
-
options.cliAgent.commandPlan({
|
|
18
|
+
options.preset.cliAgent.commandPlan({
|
|
22
19
|
prompt: promptGen.planPrompt(options),
|
|
23
20
|
prdFilePath: pathService.join(".lalph", "prd.yml"),
|
|
24
21
|
dangerous: options.dangerous,
|
|
22
|
+
extraArgs: options.preset.extraArgs,
|
|
25
23
|
}),
|
|
26
24
|
ChildProcess.setCwd(worktree.directory),
|
|
27
|
-
options.
|
|
25
|
+
options.preset.withCommandPrefix,
|
|
28
26
|
ChildProcess.exitCode,
|
|
29
27
|
)
|
|
30
28
|
})
|
package/src/Agents/reviewer.ts
CHANGED
|
@@ -2,16 +2,13 @@ import { Duration, Effect, FileSystem, Option, Path, pipe } from "effect"
|
|
|
2
2
|
import { PromptGen } from "../PromptGen.ts"
|
|
3
3
|
import { ChildProcess } from "effect/unstable/process"
|
|
4
4
|
import { Worktree } from "../Worktree.ts"
|
|
5
|
-
import type { CliAgent } from "../domain/CliAgent.ts"
|
|
6
5
|
import { GitFlow } from "../GitFlow.ts"
|
|
6
|
+
import type { CliAgentPreset } from "../domain/CliAgentPreset.ts"
|
|
7
7
|
|
|
8
8
|
export const agentReviewer = Effect.fnUntraced(function* (options: {
|
|
9
9
|
readonly specsDirectory: string
|
|
10
10
|
readonly stallTimeout: Duration.Duration
|
|
11
|
-
readonly
|
|
12
|
-
readonly commandPrefix: (
|
|
13
|
-
command: ChildProcess.Command,
|
|
14
|
-
) => ChildProcess.Command
|
|
11
|
+
readonly preset: CliAgentPreset
|
|
15
12
|
readonly instructions: string
|
|
16
13
|
}) {
|
|
17
14
|
const fs = yield* FileSystem.FileSystem
|
|
@@ -26,7 +23,7 @@ export const agentReviewer = Effect.fnUntraced(function* (options: {
|
|
|
26
23
|
)
|
|
27
24
|
|
|
28
25
|
const cliCommand = pipe(
|
|
29
|
-
options.cliAgent.command({
|
|
26
|
+
options.preset.cliAgent.command({
|
|
30
27
|
prompt: Option.match(customInstructions, {
|
|
31
28
|
onNone: () =>
|
|
32
29
|
promptGen.promptReview({
|
|
@@ -40,14 +37,15 @@ export const agentReviewer = Effect.fnUntraced(function* (options: {
|
|
|
40
37
|
}),
|
|
41
38
|
}),
|
|
42
39
|
prdFilePath: pathService.join(".lalph", "prd.yml"),
|
|
40
|
+
extraArgs: options.preset.extraArgs,
|
|
43
41
|
}),
|
|
44
42
|
ChildProcess.setCwd(worktree.directory),
|
|
45
|
-
options.
|
|
43
|
+
options.preset.withCommandPrefix,
|
|
46
44
|
)
|
|
47
45
|
|
|
48
46
|
return yield* cliCommand.pipe(
|
|
49
47
|
worktree.execWithStallTimeout({
|
|
50
|
-
cliAgent: options.cliAgent,
|
|
48
|
+
cliAgent: options.preset.cliAgent,
|
|
51
49
|
stallTimeout: options.stallTimeout,
|
|
52
50
|
}),
|
|
53
51
|
)
|
package/src/Agents/tasker.ts
CHANGED
|
@@ -2,30 +2,28 @@ import { Effect, Path, pipe } from "effect"
|
|
|
2
2
|
import { PromptGen } from "../PromptGen.ts"
|
|
3
3
|
import { ChildProcess } from "effect/unstable/process"
|
|
4
4
|
import { Worktree } from "../Worktree.ts"
|
|
5
|
-
import type {
|
|
5
|
+
import type { CliAgentPreset } from "../domain/CliAgentPreset.ts"
|
|
6
6
|
|
|
7
7
|
export const agentTasker = Effect.fnUntraced(function* (options: {
|
|
8
8
|
readonly specsDirectory: string
|
|
9
9
|
readonly specificationPath: string
|
|
10
|
-
readonly
|
|
11
|
-
command: ChildProcess.Command,
|
|
12
|
-
) => ChildProcess.Command
|
|
13
|
-
readonly cliAgent: CliAgent
|
|
10
|
+
readonly preset: CliAgentPreset
|
|
14
11
|
}) {
|
|
15
12
|
const pathService = yield* Path.Path
|
|
16
13
|
const worktree = yield* Worktree
|
|
17
14
|
const promptGen = yield* PromptGen
|
|
18
15
|
|
|
19
16
|
return yield* pipe(
|
|
20
|
-
options.cliAgent.command({
|
|
17
|
+
options.preset.cliAgent.command({
|
|
21
18
|
prompt: promptGen.promptPlanTasks({
|
|
22
19
|
specsDirectory: options.specsDirectory,
|
|
23
20
|
specificationPath: options.specificationPath,
|
|
24
21
|
}),
|
|
25
22
|
prdFilePath: pathService.join(".lalph", "prd.yml"),
|
|
23
|
+
extraArgs: options.preset.extraArgs,
|
|
26
24
|
}),
|
|
27
25
|
ChildProcess.setCwd(worktree.directory),
|
|
28
|
-
options.
|
|
29
|
-
worktree.execWithOutput(options),
|
|
26
|
+
options.preset.withCommandPrefix,
|
|
27
|
+
worktree.execWithOutput({ cliAgent: options.preset.cliAgent }),
|
|
30
28
|
)
|
|
31
29
|
})
|
package/src/Agents/timeout.ts
CHANGED
|
@@ -2,16 +2,13 @@ import { Duration, Effect, Path, pipe } from "effect"
|
|
|
2
2
|
import { PromptGen } from "../PromptGen.ts"
|
|
3
3
|
import { ChildProcess } from "effect/unstable/process"
|
|
4
4
|
import { Worktree } from "../Worktree.ts"
|
|
5
|
-
import type { CliAgent } from "../domain/CliAgent.ts"
|
|
6
5
|
import type { PrdIssue } from "../domain/PrdIssue.ts"
|
|
6
|
+
import type { CliAgentPreset } from "../domain/CliAgentPreset.ts"
|
|
7
7
|
|
|
8
8
|
export const agentTimeout = Effect.fnUntraced(function* (options: {
|
|
9
9
|
readonly specsDirectory: string
|
|
10
10
|
readonly stallTimeout: Duration.Duration
|
|
11
|
-
readonly
|
|
12
|
-
readonly commandPrefix: (
|
|
13
|
-
command: ChildProcess.Command,
|
|
14
|
-
) => ChildProcess.Command
|
|
11
|
+
readonly preset: CliAgentPreset
|
|
15
12
|
readonly task: PrdIssue
|
|
16
13
|
}) {
|
|
17
14
|
const pathService = yield* Path.Path
|
|
@@ -19,19 +16,20 @@ export const agentTimeout = Effect.fnUntraced(function* (options: {
|
|
|
19
16
|
const promptGen = yield* PromptGen
|
|
20
17
|
|
|
21
18
|
const timeoutCommand = pipe(
|
|
22
|
-
options.cliAgent.command({
|
|
19
|
+
options.preset.cliAgent.command({
|
|
23
20
|
prompt: promptGen.promptTimeout({
|
|
24
21
|
taskId: options.task.id!,
|
|
25
22
|
specsDirectory: options.specsDirectory,
|
|
26
23
|
}),
|
|
27
24
|
prdFilePath: pathService.join(".lalph", "prd.yml"),
|
|
25
|
+
extraArgs: options.preset.extraArgs,
|
|
28
26
|
}),
|
|
29
27
|
ChildProcess.setCwd(worktree.directory),
|
|
30
|
-
options.
|
|
28
|
+
options.preset.withCommandPrefix,
|
|
31
29
|
)
|
|
32
30
|
return yield* timeoutCommand.pipe(
|
|
33
31
|
worktree.execWithStallTimeout({
|
|
34
|
-
cliAgent: options.cliAgent,
|
|
32
|
+
cliAgent: options.preset.cliAgent,
|
|
35
33
|
stallTimeout: options.stallTimeout,
|
|
36
34
|
}),
|
|
37
35
|
)
|
package/src/Agents/worker.ts
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
import { Duration, Effect, Path, pipe } from "effect"
|
|
2
2
|
import { ChildProcess } from "effect/unstable/process"
|
|
3
3
|
import { Worktree } from "../Worktree.ts"
|
|
4
|
-
import type {
|
|
4
|
+
import type { CliAgentPreset } from "../domain/CliAgentPreset.ts"
|
|
5
5
|
|
|
6
6
|
export const agentWorker = Effect.fnUntraced(function* (options: {
|
|
7
7
|
readonly stallTimeout: Duration.Duration
|
|
8
|
-
readonly
|
|
9
|
-
readonly commandPrefix: (
|
|
10
|
-
command: ChildProcess.Command,
|
|
11
|
-
) => ChildProcess.Command
|
|
8
|
+
readonly preset: CliAgentPreset
|
|
12
9
|
readonly prompt: string
|
|
13
10
|
}) {
|
|
14
11
|
const pathService = yield* Path.Path
|
|
15
12
|
const worktree = yield* Worktree
|
|
16
13
|
|
|
17
14
|
const cliCommand = pipe(
|
|
18
|
-
options.cliAgent.command({
|
|
15
|
+
options.preset.cliAgent.command({
|
|
19
16
|
prompt: options.prompt,
|
|
20
17
|
prdFilePath: pathService.join(".lalph", "prd.yml"),
|
|
18
|
+
extraArgs: [],
|
|
21
19
|
}),
|
|
22
20
|
ChildProcess.setCwd(worktree.directory),
|
|
23
|
-
options.
|
|
21
|
+
options.preset.withCommandPrefix,
|
|
24
22
|
)
|
|
25
23
|
|
|
26
24
|
return yield* cliCommand.pipe(
|
|
27
25
|
worktree.execWithStallTimeout({
|
|
28
|
-
cliAgent: options.cliAgent,
|
|
26
|
+
cliAgent: options.preset.cliAgent,
|
|
29
27
|
stallTimeout: options.stallTimeout,
|
|
30
28
|
}),
|
|
31
29
|
)
|
|
@@ -124,6 +124,18 @@ export class CurrentIssueSource extends ServiceMap.Service<
|
|
|
124
124
|
ScopedRef.get(ref).pipe(
|
|
125
125
|
Effect.flatMap((source) => source.info(projectId)),
|
|
126
126
|
),
|
|
127
|
+
issueCliAgentPreset: (issue) =>
|
|
128
|
+
ScopedRef.get(ref).pipe(
|
|
129
|
+
Effect.flatMap((source) => source.issueCliAgentPreset(issue)),
|
|
130
|
+
),
|
|
131
|
+
updateCliAgentPreset: (preset) =>
|
|
132
|
+
ScopedRef.get(ref).pipe(
|
|
133
|
+
Effect.flatMap((source) => source.updateCliAgentPreset(preset)),
|
|
134
|
+
),
|
|
135
|
+
cliAgentPresetInfo: (preset) =>
|
|
136
|
+
ScopedRef.get(ref).pipe(
|
|
137
|
+
Effect.flatMap((source) => source.cliAgentPresetInfo(preset)),
|
|
138
|
+
),
|
|
127
139
|
ensureInProgress: (projectId, issueId) =>
|
|
128
140
|
ScopedRef.get(ref).pipe(
|
|
129
141
|
Effect.flatMap((source) =>
|
package/src/Github/Cli.ts
CHANGED
|
@@ -100,7 +100,7 @@ ${reviewCommentsMd}`
|
|
|
100
100
|
const reviewsXml = reviews
|
|
101
101
|
.map(
|
|
102
102
|
(review) => `<review author="${review.author.login}">
|
|
103
|
-
<body
|
|
103
|
+
<body>${review.body}</body>
|
|
104
104
|
</review>`,
|
|
105
105
|
)
|
|
106
106
|
.join("\n")
|
|
@@ -152,8 +152,8 @@ const renderReviewComments = (
|
|
|
152
152
|
) => `<comment author="${comment.author.login}" path="${comment.path}"${
|
|
153
153
|
comment.originalLine ? ` originalLine="${comment.originalLine}"` : ""
|
|
154
154
|
}>
|
|
155
|
-
<diffHunk
|
|
156
|
-
<body
|
|
155
|
+
<diffHunk>${comment.diffHunk}</diffHunk>
|
|
156
|
+
<body>${comment.body}</body>${
|
|
157
157
|
followup.length > 0
|
|
158
158
|
? `
|
|
159
159
|
|
|
@@ -161,7 +161,7 @@ const renderReviewComments = (
|
|
|
161
161
|
.map(
|
|
162
162
|
(fc) => `
|
|
163
163
|
<comment author="${fc.author.login}">
|
|
164
|
-
<body
|
|
164
|
+
<body>${fc.body}</body>
|
|
165
165
|
</comment>`,
|
|
166
166
|
)
|
|
167
167
|
.join("")}
|
|
@@ -173,7 +173,7 @@ const renderReviewComments = (
|
|
|
173
173
|
const renderGeneralComment = (
|
|
174
174
|
comment: Comment,
|
|
175
175
|
) => ` <comment author="${comment.author.login}">
|
|
176
|
-
<body
|
|
176
|
+
<body>${comment.body}</body>
|
|
177
177
|
</comment>`
|
|
178
178
|
|
|
179
179
|
// Schema definitions and GraphQL query
|
package/src/Github.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { TokenManager } from "./Github/TokenManager.ts"
|
|
|
24
24
|
import { GithubCli } from "./Github/Cli.ts"
|
|
25
25
|
import { Reactivity } from "effect/unstable/reactivity"
|
|
26
26
|
import type { ProjectId } from "./domain/Project.ts"
|
|
27
|
+
import type { CliAgentPreset } from "./domain/CliAgentPreset.ts"
|
|
28
|
+
import { getPresetsWithMetadata } from "./Presets.ts"
|
|
27
29
|
|
|
28
30
|
export class GithubError extends Data.TaggedError("GithubError")<{
|
|
29
31
|
readonly cause: unknown
|
|
@@ -158,6 +160,9 @@ export const GithubIssueSource = Layer.effect(
|
|
|
158
160
|
Stream.filter((issue) => issue.state_reason !== "not_planned"),
|
|
159
161
|
)
|
|
160
162
|
|
|
163
|
+
const presets = yield* getPresetsWithMetadata("github", PresetMetadata)
|
|
164
|
+
const issuePresetMap = new Map<string, CliAgentPreset>()
|
|
165
|
+
|
|
161
166
|
const issues = (options: {
|
|
162
167
|
readonly labelFilter: Option.Option<string>
|
|
163
168
|
readonly autoMergeLabelName: Option.Option<string>
|
|
@@ -177,6 +182,7 @@ export const GithubIssueSource = Layer.effect(
|
|
|
177
182
|
Stream.filter((issue) => issue.pull_request === undefined),
|
|
178
183
|
Stream.mapEffect(
|
|
179
184
|
Effect.fnUntraced(function* (issue) {
|
|
185
|
+
const id = `#${issue.number}`
|
|
180
186
|
const dependencies = yield* listOpenBlockedBy(issue.number).pipe(
|
|
181
187
|
Stream.runCollect,
|
|
182
188
|
)
|
|
@@ -188,8 +194,16 @@ export const GithubIssueSource = Layer.effect(
|
|
|
188
194
|
: hasLabel(issue.labels, "in-review")
|
|
189
195
|
? "in-review"
|
|
190
196
|
: "todo"
|
|
197
|
+
|
|
198
|
+
const preset = presets.find(({ metadata }) =>
|
|
199
|
+
hasLabel(issue.labels, metadata.label),
|
|
200
|
+
)
|
|
201
|
+
if (preset) {
|
|
202
|
+
issuePresetMap.set(id, preset.preset)
|
|
203
|
+
}
|
|
204
|
+
|
|
191
205
|
return new PrdIssue({
|
|
192
|
-
id
|
|
206
|
+
id,
|
|
193
207
|
title: issue.title,
|
|
194
208
|
description: issue.body ?? "",
|
|
195
209
|
priority: 0,
|
|
@@ -436,6 +450,28 @@ export const GithubIssueSource = Layer.effect(
|
|
|
436
450
|
})}`,
|
|
437
451
|
)
|
|
438
452
|
}),
|
|
453
|
+
issueCliAgentPreset: (issue) =>
|
|
454
|
+
Effect.sync(() =>
|
|
455
|
+
Option.fromUndefinedOr(issuePresetMap.get(issue.id!)),
|
|
456
|
+
),
|
|
457
|
+
updateCliAgentPreset: Effect.fnUntraced(function* (preset) {
|
|
458
|
+
const label = yield* Prompt.text({
|
|
459
|
+
message: "Enter a label for this preset",
|
|
460
|
+
validate(value) {
|
|
461
|
+
value = value.trim()
|
|
462
|
+
if (value.length === 0) {
|
|
463
|
+
return Effect.fail("Label cannot be empty")
|
|
464
|
+
}
|
|
465
|
+
return Effect.succeed(value)
|
|
466
|
+
},
|
|
467
|
+
})
|
|
468
|
+
return yield* preset.addMetadata("github", PresetMetadata, { label })
|
|
469
|
+
}),
|
|
470
|
+
cliAgentPresetInfo: Effect.fnUntraced(function* (preset) {
|
|
471
|
+
const metadata = yield* preset.decodeMetadata("github", PresetMetadata)
|
|
472
|
+
if (Option.isNone(metadata)) return
|
|
473
|
+
console.log(` Label: ${metadata.value.label}`)
|
|
474
|
+
}),
|
|
439
475
|
ensureInProgress: Effect.fnUntraced(
|
|
440
476
|
function* (_project, issueId) {
|
|
441
477
|
const issueNumber = Number(issueId.slice(1))
|
|
@@ -520,6 +556,12 @@ const getOrSelectAutoMergeLabel = Effect.gen(function* () {
|
|
|
520
556
|
return yield* autoMergeLabelSelect
|
|
521
557
|
})
|
|
522
558
|
|
|
559
|
+
// == preset metadata
|
|
560
|
+
|
|
561
|
+
const PresetMetadata = Schema.Struct({
|
|
562
|
+
label: Schema.NonEmptyString,
|
|
563
|
+
})
|
|
564
|
+
|
|
523
565
|
// == helpers
|
|
524
566
|
|
|
525
567
|
const maybeNextPage = (page: number, linkHeader?: string) =>
|
package/src/IssueSource.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { Effect, Schema, ServiceMap } from "effect"
|
|
1
|
+
import { Effect, Option, Schema, ServiceMap } from "effect"
|
|
2
2
|
import type { PrdIssue } from "./domain/PrdIssue.ts"
|
|
3
3
|
import { Reactivity } from "effect/unstable/reactivity"
|
|
4
4
|
import type { ProjectId } from "./domain/Project.ts"
|
|
5
5
|
import type { CurrentProjectId, Settings } from "./Settings.ts"
|
|
6
|
+
import type { CliAgentPreset } from "./domain/CliAgentPreset.ts"
|
|
7
|
+
import type { Environment } from "effect/unstable/cli/Prompt"
|
|
8
|
+
import type { QuitError } from "effect/Terminal"
|
|
6
9
|
|
|
7
10
|
export class IssueSource extends ServiceMap.Service<
|
|
8
11
|
IssueSource,
|
|
@@ -43,6 +46,20 @@ export class IssueSource extends ServiceMap.Service<
|
|
|
43
46
|
projectId: ProjectId,
|
|
44
47
|
) => Effect.Effect<void, IssueSourceError>
|
|
45
48
|
|
|
49
|
+
readonly issueCliAgentPreset: (
|
|
50
|
+
issue: PrdIssue,
|
|
51
|
+
) => Effect.Effect<Option.Option<CliAgentPreset>, IssueSourceError>
|
|
52
|
+
readonly updateCliAgentPreset: (
|
|
53
|
+
preset: CliAgentPreset,
|
|
54
|
+
) => Effect.Effect<
|
|
55
|
+
CliAgentPreset,
|
|
56
|
+
IssueSourceError | QuitError,
|
|
57
|
+
Environment
|
|
58
|
+
>
|
|
59
|
+
readonly cliAgentPresetInfo: (
|
|
60
|
+
preset: CliAgentPreset,
|
|
61
|
+
) => Effect.Effect<void, IssueSourceError>
|
|
62
|
+
|
|
46
63
|
readonly ensureInProgress: (
|
|
47
64
|
projectId: ProjectId,
|
|
48
65
|
issueId: string,
|
package/src/Linear.ts
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
} from "./domain/LinearIssues.ts"
|
|
30
30
|
import { Reactivity } from "effect/unstable/reactivity"
|
|
31
31
|
import type { ProjectId } from "./domain/Project.ts"
|
|
32
|
+
import { getPresetsWithMetadata } from "./Presets.ts"
|
|
33
|
+
import type { CliAgentPreset } from "./domain/CliAgentPreset.ts"
|
|
32
34
|
|
|
33
35
|
class Linear extends ServiceMap.Service<Linear>()("lalph/Linear", {
|
|
34
36
|
make: Effect.gen(function* () {
|
|
@@ -174,8 +176,11 @@ export const LinearIssueSource = Layer.effect(
|
|
|
174
176
|
capacity: Number.POSITIVE_INFINITY,
|
|
175
177
|
})
|
|
176
178
|
|
|
179
|
+
const presets = yield* getPresetsWithMetadata("linear", PresetMetadata)
|
|
180
|
+
|
|
177
181
|
// Map of linear identifier to issue id
|
|
178
182
|
const identifierMap = new Map<string, string>()
|
|
183
|
+
const presetMap = new Map<string, CliAgentPreset>()
|
|
179
184
|
|
|
180
185
|
const backlogState =
|
|
181
186
|
linear.states.find(
|
|
@@ -258,6 +263,13 @@ export const LinearIssueSource = Layer.effect(
|
|
|
258
263
|
return pipe(
|
|
259
264
|
Array.filter(issues, (issue) => {
|
|
260
265
|
identifierMap.set(issue.identifier, issue.id)
|
|
266
|
+
const preset = presets.find((p) =>
|
|
267
|
+
issue.labelIds.includes(p.metadata.labelId),
|
|
268
|
+
)
|
|
269
|
+
if (preset) {
|
|
270
|
+
presetMap.set(issue.identifier, preset.preset)
|
|
271
|
+
}
|
|
272
|
+
|
|
261
273
|
const completedAt = issue.completedAt
|
|
262
274
|
if (!completedAt) return true
|
|
263
275
|
return DateTime.isGreaterThanOrEqualTo(completedAt, threeDaysAgo)
|
|
@@ -477,11 +489,44 @@ export const LinearIssueSource = Layer.effect(
|
|
|
477
489
|
},
|
|
478
490
|
Effect.mapError((cause) => new IssueSourceError({ cause })),
|
|
479
491
|
),
|
|
492
|
+
issueCliAgentPreset: (_issue) =>
|
|
493
|
+
Effect.sync(() => Option.fromUndefinedOr(presetMap.get(_issue.id!))),
|
|
494
|
+
updateCliAgentPreset: Effect.fnUntraced(function* (preset) {
|
|
495
|
+
const labels = yield* Stream.runCollect(linear.labels).pipe(
|
|
496
|
+
Effect.mapError((cause) => new IssueSourceError({ cause })),
|
|
497
|
+
)
|
|
498
|
+
const labelId = yield* Prompt.autoComplete({
|
|
499
|
+
message: "Select a label for this preset",
|
|
500
|
+
choices: labels.map((label) => ({
|
|
501
|
+
title: label.name,
|
|
502
|
+
value: label.id,
|
|
503
|
+
})),
|
|
504
|
+
})
|
|
505
|
+
return yield* preset.addMetadata("linear", PresetMetadata, {
|
|
506
|
+
labelId,
|
|
507
|
+
})
|
|
508
|
+
}),
|
|
509
|
+
cliAgentPresetInfo: Effect.fnUntraced(
|
|
510
|
+
function* (preset) {
|
|
511
|
+
const metadata = yield* preset.decodeMetadata(
|
|
512
|
+
"linear",
|
|
513
|
+
PresetMetadata,
|
|
514
|
+
)
|
|
515
|
+
if (Option.isNone(metadata)) return
|
|
516
|
+
const label = yield* linear.labels.pipe(
|
|
517
|
+
Stream.filter((label) => label.id === metadata.value.labelId),
|
|
518
|
+
Stream.runHead,
|
|
519
|
+
)
|
|
520
|
+
if (Option.isNone(label)) return
|
|
521
|
+
console.log(` Label: ${label.value.name}`)
|
|
522
|
+
},
|
|
523
|
+
Effect.mapError((cause) => new IssueSourceError({ cause })),
|
|
524
|
+
),
|
|
480
525
|
// linear api writes and reflected immediately in reads, so no-op
|
|
481
526
|
ensureInProgress: () => Effect.void,
|
|
482
527
|
})
|
|
483
528
|
}),
|
|
484
|
-
).pipe(Layer.provide([Linear.layer, Reactivity.layer]))
|
|
529
|
+
).pipe(Layer.provide([Linear.layer, Reactivity.layer, Settings.layer]))
|
|
485
530
|
|
|
486
531
|
export class LinearError extends Schema.ErrorClass("lalph/LinearError")({
|
|
487
532
|
_tag: Schema.tag("LinearError"),
|
|
@@ -615,6 +660,12 @@ const getOrSelectAutoMergeLabel = Effect.gen(function* () {
|
|
|
615
660
|
return yield* autoMergeLabelIdSelect
|
|
616
661
|
})
|
|
617
662
|
|
|
663
|
+
// preset metadata schema
|
|
664
|
+
|
|
665
|
+
const PresetMetadata = Schema.Struct({
|
|
666
|
+
labelId: Schema.String,
|
|
667
|
+
})
|
|
668
|
+
|
|
618
669
|
// graphql queries
|
|
619
670
|
const issueQueryFields = `
|
|
620
671
|
id
|