@markjaquith/agency 2.13.0 → 2.15.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/README.md +62 -8
- package/cli.ts +66 -0
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +28 -1
- package/skills/agency/SKILL.md +14 -3
- package/src/cli-parser.test.ts +81 -0
- package/src/cli-parser.ts +71 -0
- package/src/cli.test.ts +85 -0
- package/src/commands/claim.ts +97 -0
- package/src/commands/phase.ts +1 -1
- package/src/commands/task-phase.test.ts +4 -4
- package/src/commands/task.ts +32 -35
- package/src/commands/validate.ts +1 -6
- package/src/commands/work.test.ts +46 -12
- package/src/commands/work.ts +38 -19
- package/src/protocol.test.ts +25 -0
- package/src/protocol.ts +16 -0
- package/src/services/ClaimService.test.ts +270 -0
- package/src/services/ClaimService.ts +446 -0
- package/src/services/PhaseService.ts +13 -0
- package/src/services/TaskPhaseService.test.ts +10 -9
- package/src/services/TaskService.ts +11 -0
- package/src/test-utils.ts +2 -0
- package/src/utils/chooser.test.ts +108 -0
- package/src/utils/chooser.ts +222 -0
- package/src/workbase/AGENTS.md +2 -2
- package/src/workbase/document-revision.ts +2 -0
- package/src/workbase/frontmatter.ts +59 -53
- package/src/workbase/schemas.test.ts +39 -0
- package/src/workbase/schemas.ts +22 -0
- package/src/workbase/work-target.test.ts +10 -0
- package/src/workbase/work-target.ts +45 -35
- package/src/workbase/workbase-choice.ts +12 -37
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import type { WorkStatus } from "./schemas"
|
|
3
|
+
import { choose } from "../utils/chooser"
|
|
3
4
|
|
|
4
5
|
export type WorkTarget =
|
|
5
6
|
| {
|
|
@@ -52,6 +53,7 @@ interface PhaseRecord {
|
|
|
52
53
|
|
|
53
54
|
export interface WorkTargetChoice {
|
|
54
55
|
readonly label: string
|
|
56
|
+
readonly plainLabel: string
|
|
55
57
|
readonly target: WorkTarget
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -78,6 +80,15 @@ const label = (
|
|
|
78
80
|
}[kind]
|
|
79
81
|
} ${id}${description === undefined ? "" : `\x1b[2m - ${description}\x1b[0m`}`
|
|
80
82
|
|
|
83
|
+
const plainLabel = (
|
|
84
|
+
indent: string,
|
|
85
|
+
kind: WorkTarget["kind"],
|
|
86
|
+
id: string,
|
|
87
|
+
description?: string,
|
|
88
|
+
status?: WorkStatus,
|
|
89
|
+
) =>
|
|
90
|
+
`${indent}${status === undefined ? "" : `[${status}] `}${kind} ${id}${description === undefined ? "" : ` - ${description}`}`
|
|
91
|
+
|
|
81
92
|
const taskChoices = (
|
|
82
93
|
task: TaskRecord,
|
|
83
94
|
phaseRecords: readonly PhaseRecord[],
|
|
@@ -93,6 +104,13 @@ const taskChoices = (
|
|
|
93
104
|
task.data.description,
|
|
94
105
|
multiPhase ? undefined : (task.data.status ?? "open"),
|
|
95
106
|
),
|
|
107
|
+
plainLabel: plainLabel(
|
|
108
|
+
indent,
|
|
109
|
+
"task",
|
|
110
|
+
task.id,
|
|
111
|
+
task.data.description,
|
|
112
|
+
multiPhase ? undefined : (task.data.status ?? "open"),
|
|
113
|
+
),
|
|
96
114
|
target: {
|
|
97
115
|
kind: "task",
|
|
98
116
|
taskId: task.id,
|
|
@@ -117,6 +135,13 @@ const taskChoices = (
|
|
|
117
135
|
record.data.description,
|
|
118
136
|
record.data.status ?? "open",
|
|
119
137
|
),
|
|
138
|
+
plainLabel: plainLabel(
|
|
139
|
+
`${indent} `,
|
|
140
|
+
"phase",
|
|
141
|
+
record.id,
|
|
142
|
+
record.data.description,
|
|
143
|
+
record.data.status ?? "open",
|
|
144
|
+
),
|
|
120
145
|
target: {
|
|
121
146
|
kind: "phase",
|
|
122
147
|
taskId: task.id,
|
|
@@ -135,6 +160,13 @@ const taskChoices = (
|
|
|
135
160
|
record.data.description,
|
|
136
161
|
record.data.status ?? "open",
|
|
137
162
|
),
|
|
163
|
+
plainLabel: plainLabel(
|
|
164
|
+
`${indent} `,
|
|
165
|
+
"phase",
|
|
166
|
+
record.id,
|
|
167
|
+
record.data.description,
|
|
168
|
+
record.data.status ?? "open",
|
|
169
|
+
),
|
|
138
170
|
target: {
|
|
139
171
|
kind: "phase",
|
|
140
172
|
taskId: task.id,
|
|
@@ -164,6 +196,7 @@ export const buildWorkTargetChoices = (
|
|
|
164
196
|
for (const epic of epicRecords) {
|
|
165
197
|
choices.push({
|
|
166
198
|
label: label("", "epic", epic.id, epic.data.description),
|
|
199
|
+
plainLabel: plainLabel("", "epic", epic.id, epic.data.description),
|
|
167
200
|
target: { kind: "epic", epicId: epic.id, path: epic.path },
|
|
168
201
|
})
|
|
169
202
|
for (const child of epic.data.tasks) {
|
|
@@ -184,40 +217,17 @@ export const buildWorkTargetChoices = (
|
|
|
184
217
|
|
|
185
218
|
export type PickWorkTarget = (
|
|
186
219
|
choices: readonly WorkTargetChoice[],
|
|
220
|
+
command?: readonly string[],
|
|
187
221
|
) => Effect.Effect<WorkTarget | null, Error>
|
|
188
222
|
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
) => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const input = choices
|
|
201
|
-
.map((choice, index) => `${index}\t${choice.label}`)
|
|
202
|
-
.join("\n")
|
|
203
|
-
const process = Bun.spawn(
|
|
204
|
-
[
|
|
205
|
-
"fzf",
|
|
206
|
-
"--ansi",
|
|
207
|
-
"--delimiter=\t",
|
|
208
|
-
"--with-nth=2..",
|
|
209
|
-
"--prompt=Work on> ",
|
|
210
|
-
],
|
|
211
|
-
{ stdin: new Blob([input]), stdout: "pipe", stderr: "inherit" },
|
|
212
|
-
)
|
|
213
|
-
const [exitCode, output] = await Promise.all([
|
|
214
|
-
process.exited,
|
|
215
|
-
new Response(process.stdout).text(),
|
|
216
|
-
])
|
|
217
|
-
if (exitCode === 1 || exitCode === 130) return null
|
|
218
|
-
if (exitCode !== 0) throw new Error(`fzf exited with code ${exitCode}`)
|
|
219
|
-
return parseWorkTargetSelection(output, choices)
|
|
220
|
-
},
|
|
221
|
-
catch: (cause) =>
|
|
222
|
-
new Error("Failed to select a work target with fzf", { cause }),
|
|
223
|
-
})
|
|
223
|
+
export const pickWorkTarget: PickWorkTarget = (choices, command) =>
|
|
224
|
+
choose(
|
|
225
|
+
"Work on",
|
|
226
|
+
choices.map((choice, index) => ({
|
|
227
|
+
key: String(index),
|
|
228
|
+
label: choice.label,
|
|
229
|
+
plainLabel: choice.plainLabel,
|
|
230
|
+
value: choice.target,
|
|
231
|
+
})),
|
|
232
|
+
command,
|
|
233
|
+
)
|
|
@@ -1,43 +1,30 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import { resolve } from "node:path"
|
|
3
|
-
import { FileSystemService } from "../services/FileSystemService"
|
|
4
3
|
import { WorkbaseService } from "../services/WorkbaseService"
|
|
4
|
+
import { choose } from "../utils/chooser"
|
|
5
5
|
|
|
6
6
|
export type PickWorkbase = (
|
|
7
7
|
workbases: readonly string[],
|
|
8
|
+
command?: readonly string[],
|
|
8
9
|
) => Effect.Effect<string | null, Error>
|
|
9
10
|
|
|
10
|
-
export const pickWorkbase: PickWorkbase = (workbases) =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const [exitCode, output] = await Promise.all([
|
|
21
|
-
process.exited,
|
|
22
|
-
new Response(process.stdout).text(),
|
|
23
|
-
])
|
|
24
|
-
if (exitCode === 1 || exitCode === 130) return null
|
|
25
|
-
if (exitCode !== 0) throw new Error(`fzf exited with code ${exitCode}`)
|
|
26
|
-
const index = Number.parseInt(output.split("\t", 1)[0] ?? "", 10)
|
|
27
|
-
return workbases[index] ?? null
|
|
28
|
-
},
|
|
29
|
-
catch: (cause) =>
|
|
30
|
-
new Error("Failed to select a workbase with fzf", { cause }),
|
|
31
|
-
})
|
|
11
|
+
export const pickWorkbase: PickWorkbase = (workbases, command) =>
|
|
12
|
+
choose(
|
|
13
|
+
"Workbase",
|
|
14
|
+
workbases.map((workbase, index) => ({
|
|
15
|
+
key: String(index),
|
|
16
|
+
label: workbase,
|
|
17
|
+
value: workbase,
|
|
18
|
+
})),
|
|
19
|
+
command,
|
|
20
|
+
)
|
|
32
21
|
|
|
33
22
|
export const resolveWorkbase = (
|
|
34
23
|
startPath: string,
|
|
35
|
-
log: (message: string) => void,
|
|
36
24
|
pick: PickWorkbase = pickWorkbase,
|
|
37
25
|
inputAllowed = true,
|
|
38
26
|
) =>
|
|
39
27
|
Effect.gen(function* () {
|
|
40
|
-
const fs = yield* FileSystemService
|
|
41
28
|
const workbase = yield* WorkbaseService
|
|
42
29
|
|
|
43
30
|
return yield* workbase.discover(startPath).pipe(
|
|
@@ -59,18 +46,6 @@ export const resolveWorkbase = (
|
|
|
59
46
|
)
|
|
60
47
|
}
|
|
61
48
|
|
|
62
|
-
const fzf = yield* fs.runCommand(["which", "fzf"], {
|
|
63
|
-
captureOutput: true,
|
|
64
|
-
})
|
|
65
|
-
if (fzf.exitCode !== 0) {
|
|
66
|
-
for (const path of registered) log(path)
|
|
67
|
-
return yield* Effect.fail(
|
|
68
|
-
new Error(
|
|
69
|
-
"fzf is required to select a workbase; install fzf or run Agency from a registered workbase",
|
|
70
|
-
),
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
49
|
const selected = yield* pick(registered)
|
|
75
50
|
return selected ? yield* workbase.discover(selected) : null
|
|
76
51
|
}),
|