@markjaquith/agency 2.32.1 → 2.33.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/package.json
CHANGED
|
@@ -71,6 +71,8 @@ interface HarnessOptions {
|
|
|
71
71
|
readonly guardError?: Error
|
|
72
72
|
readonly workTargetIds?: readonly string[]
|
|
73
73
|
readonly opencodeIntegrationState?: "managed" | "customized"
|
|
74
|
+
readonly taskStatus?: "open" | "working" | "delegated" | "done" | "dropped"
|
|
75
|
+
readonly phaseStatus?: "open" | "working" | "delegated" | "done" | "dropped"
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
const createHarness = (options: HarnessOptions = {}) => {
|
|
@@ -141,7 +143,12 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
141
143
|
path: `/workbase/tasks/${id}/TASK.md`,
|
|
142
144
|
data: options.multiPhaseTasks?.includes(id)
|
|
143
145
|
? { phases: [] }
|
|
144
|
-
: {
|
|
146
|
+
: {
|
|
147
|
+
repo: "agency",
|
|
148
|
+
branch: `task/${id}`,
|
|
149
|
+
base: "main",
|
|
150
|
+
status: options.taskStatus ?? "open",
|
|
151
|
+
},
|
|
145
152
|
})
|
|
146
153
|
},
|
|
147
154
|
list: () => Effect.succeed(options.taskRecords ?? []),
|
|
@@ -156,7 +163,12 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
156
163
|
taskId,
|
|
157
164
|
id,
|
|
158
165
|
path: `/workbase/tasks/${taskId}/phases/${id}/PHASE.md`,
|
|
159
|
-
data: {
|
|
166
|
+
data: {
|
|
167
|
+
repo: "agency",
|
|
168
|
+
branch: `task/${id}`,
|
|
169
|
+
base: "main",
|
|
170
|
+
status: options.phaseStatus ?? "open",
|
|
171
|
+
},
|
|
160
172
|
}),
|
|
161
173
|
list: () => Effect.succeed(options.phaseRecords ?? []),
|
|
162
174
|
setStatus: (taskId: string, id: string, status: string) => {
|
|
@@ -702,6 +714,22 @@ describe("work command", () => {
|
|
|
702
714
|
)
|
|
703
715
|
})
|
|
704
716
|
|
|
717
|
+
test("continues existing work with the resume command", async () => {
|
|
718
|
+
const harness = createHarness({ taskStatus: "working" })
|
|
719
|
+
|
|
720
|
+
await harness.run({ taskId: "example", opencode: true, auto: true })
|
|
721
|
+
|
|
722
|
+
expect(harness.launches[0]?.args).toEqual([
|
|
723
|
+
"opencode",
|
|
724
|
+
"--continue",
|
|
725
|
+
"--prompt",
|
|
726
|
+
"Continue the task. Read /workbase/tasks/example/TASK.md.",
|
|
727
|
+
])
|
|
728
|
+
expect(harness.launchEnvironments[0]?.AGENCY_PROMPT).toBe(
|
|
729
|
+
"Continue the task. Read /workbase/tasks/example/TASK.md.",
|
|
730
|
+
)
|
|
731
|
+
})
|
|
732
|
+
|
|
705
733
|
test("resumes OpenCode deterministically when a session identity exists", async () => {
|
|
706
734
|
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
707
735
|
process.env.AGENCY_SESSION_ID = "existing-session"
|
|
@@ -722,7 +750,7 @@ describe("work command", () => {
|
|
|
722
750
|
"opencode",
|
|
723
751
|
"--continue",
|
|
724
752
|
"--prompt",
|
|
725
|
-
"
|
|
753
|
+
"Continue the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
726
754
|
],
|
|
727
755
|
cwd: taskDirectory,
|
|
728
756
|
})
|
package/src/commands/work.ts
CHANGED
|
@@ -138,6 +138,7 @@ export const work = (
|
|
|
138
138
|
taskId: task.id,
|
|
139
139
|
phaseId: phase.id,
|
|
140
140
|
path: phase.path,
|
|
141
|
+
status: phase.data.status,
|
|
141
142
|
}
|
|
142
143
|
} else {
|
|
143
144
|
target = {
|
|
@@ -145,6 +146,7 @@ export const work = (
|
|
|
145
146
|
taskId: task.id,
|
|
146
147
|
path: task.path,
|
|
147
148
|
multiPhase: "phases" in task.data,
|
|
149
|
+
status: "phases" in task.data ? undefined : task.data.status,
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
} else if (options.directory && !isDirectory) {
|
|
@@ -154,6 +156,7 @@ export const work = (
|
|
|
154
156
|
taskId: task.id,
|
|
155
157
|
path: task.path,
|
|
156
158
|
multiPhase: "phases" in task.data,
|
|
159
|
+
status: "phases" in task.data ? undefined : task.data.status,
|
|
157
160
|
}
|
|
158
161
|
} else if (directoryPath) {
|
|
159
162
|
const path = relative(root, startPath)
|
|
@@ -173,6 +176,7 @@ export const work = (
|
|
|
173
176
|
taskId: task.id,
|
|
174
177
|
phaseId: phase.id,
|
|
175
178
|
path: phase.path,
|
|
179
|
+
status: phase.data.status,
|
|
176
180
|
}
|
|
177
181
|
} else {
|
|
178
182
|
target = {
|
|
@@ -180,6 +184,7 @@ export const work = (
|
|
|
180
184
|
taskId: task.id,
|
|
181
185
|
path: task.path,
|
|
182
186
|
multiPhase: "phases" in task.data,
|
|
187
|
+
status: "phases" in task.data ? undefined : task.data.status,
|
|
183
188
|
}
|
|
184
189
|
}
|
|
185
190
|
}
|
|
@@ -224,6 +229,11 @@ export const work = (
|
|
|
224
229
|
}
|
|
225
230
|
yield* readiness.guardWorkTarget(targetNodeId(target), root, options.force)
|
|
226
231
|
|
|
232
|
+
const continuing =
|
|
233
|
+
target.kind !== "epic" &&
|
|
234
|
+
!(target.kind === "task" && target.multiPhase) &&
|
|
235
|
+
(process.env.AGENCY_SESSION_ID !== undefined ||
|
|
236
|
+
(target.status !== undefined && target.status !== "open"))
|
|
227
237
|
let prompt: string
|
|
228
238
|
let launchPath: string
|
|
229
239
|
let writablePath: string | undefined
|
|
@@ -247,9 +257,10 @@ export const work = (
|
|
|
247
257
|
Effect.sync(() => progress.fail("Workspace preparation failed")),
|
|
248
258
|
),
|
|
249
259
|
)
|
|
260
|
+
const action = continuing ? "Continue" : "Start"
|
|
250
261
|
prompt = workspace.phasePath
|
|
251
|
-
?
|
|
252
|
-
:
|
|
262
|
+
? `${action} the task. Read ${workspace.taskPath} and ${workspace.phasePath}.`
|
|
263
|
+
: `${action} the task. Read ${workspace.taskPath}.`
|
|
253
264
|
launchPath = dirname(workspace.taskPath)
|
|
254
265
|
writablePath = workspace.writablePath
|
|
255
266
|
}
|
|
@@ -267,7 +278,9 @@ export const work = (
|
|
|
267
278
|
const claimant = process.env.AGENCY_CLAIMANT ?? process.env.USER ?? "agency"
|
|
268
279
|
const sessionId =
|
|
269
280
|
process.env.AGENCY_SESSION_ID ?? `${process.pid}-${Date.now()}`
|
|
270
|
-
const resume =
|
|
281
|
+
const resume =
|
|
282
|
+
process.env.AGENCY_SESSION_ID !== undefined ||
|
|
283
|
+
(Boolean(options.auto) && continuing)
|
|
271
284
|
const variables = {
|
|
272
285
|
prompt: options.auto ? prompt : "",
|
|
273
286
|
workbase: root,
|
|
@@ -44,7 +44,7 @@ describe("OpenTUI interaction", () => {
|
|
|
44
44
|
test("uses the split-footer renderer contract", () => {
|
|
45
45
|
expect(interactiveRendererConfig).toMatchObject({
|
|
46
46
|
screenMode: "split-footer",
|
|
47
|
-
footerHeight:
|
|
47
|
+
footerHeight: 5,
|
|
48
48
|
externalOutputMode: "capture-stdout",
|
|
49
49
|
clearOnShutdown: false,
|
|
50
50
|
})
|
|
@@ -212,6 +212,26 @@ describe("OpenTUI interaction", () => {
|
|
|
212
212
|
}
|
|
213
213
|
})
|
|
214
214
|
|
|
215
|
+
test("wraps text prompt input onto another line", async () => {
|
|
216
|
+
const setup = await testRender(
|
|
217
|
+
() => <InteractiveTextPrompt prompt="Task ID" onDone={() => {}} />,
|
|
218
|
+
{ width: 16, height: 5 },
|
|
219
|
+
)
|
|
220
|
+
try {
|
|
221
|
+
await setup.renderer.setupTerminal()
|
|
222
|
+
await setup.renderOnce()
|
|
223
|
+
await Bun.sleep(0)
|
|
224
|
+
await setup.mockInput.typeText("alpha beta gamma delta")
|
|
225
|
+
await setup.flush()
|
|
226
|
+
const frame = setup.captureCharFrame()
|
|
227
|
+
expect(frame).toContain("alpha beta")
|
|
228
|
+
expect(frame).toContain("gamma")
|
|
229
|
+
expect(frame).toMatch(/alpha beta\s*\ngamma delta/)
|
|
230
|
+
} finally {
|
|
231
|
+
setup.renderer.destroy()
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
215
235
|
test("submits text and cancels with ctrl-c or escape", async () => {
|
|
216
236
|
let submitted: string | null | undefined
|
|
217
237
|
const input = await testRender(
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
createCliRenderer,
|
|
5
5
|
type CliRenderer,
|
|
6
6
|
type CliRendererConfig,
|
|
7
|
-
type
|
|
7
|
+
type TextareaRenderable,
|
|
8
8
|
} from "@opentui/core"
|
|
9
9
|
import { render, useKeyboard, type JSX } from "@opentui/solid"
|
|
10
10
|
import { createMemo, createSignal, For } from "solid-js"
|
|
@@ -16,7 +16,7 @@ export interface InteractiveChoice {
|
|
|
16
16
|
|
|
17
17
|
export const interactiveRendererConfig = {
|
|
18
18
|
screenMode: "split-footer",
|
|
19
|
-
footerHeight:
|
|
19
|
+
footerHeight: 5,
|
|
20
20
|
externalOutputMode: "capture-stdout",
|
|
21
21
|
consoleMode: "disabled",
|
|
22
22
|
clearOnShutdown: false,
|
|
@@ -35,7 +35,7 @@ const isCancel = (key: { name: string; ctrl: boolean }) =>
|
|
|
35
35
|
key.name === "escape" || (key.ctrl && key.name === "c")
|
|
36
36
|
|
|
37
37
|
export const InteractiveTextPrompt = (props: PromptProps<string>) => {
|
|
38
|
-
let input:
|
|
38
|
+
let input: TextareaRenderable | undefined
|
|
39
39
|
let value = ""
|
|
40
40
|
useKeyboard((key) => {
|
|
41
41
|
if (isCancel(key)) {
|
|
@@ -53,10 +53,13 @@ export const InteractiveTextPrompt = (props: PromptProps<string>) => {
|
|
|
53
53
|
return (
|
|
54
54
|
<box flexDirection="column" width="100%" height="100%">
|
|
55
55
|
<text fg="#7aa2f7">{props.prompt}</text>
|
|
56
|
-
<
|
|
56
|
+
<textarea
|
|
57
57
|
focused
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
height={2}
|
|
59
|
+
wrapMode="word"
|
|
60
|
+
keyBindings={[{ name: "return", action: "submit" }]}
|
|
61
|
+
onContentChange={() => {
|
|
62
|
+
value = input?.plainText ?? ""
|
|
60
63
|
}}
|
|
61
64
|
ref={(next) => {
|
|
62
65
|
input = next
|
|
@@ -140,7 +143,7 @@ export const fuzzyChoices = (
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
export const InteractiveSelectPrompt = (props: SelectPromptProps) => {
|
|
143
|
-
let input:
|
|
146
|
+
let input: TextareaRenderable | undefined
|
|
144
147
|
const [query, setQuery] = createSignal("")
|
|
145
148
|
const [selected, setSelected] = createSignal(0)
|
|
146
149
|
const choices = createMemo(() => fuzzyChoices(props.choices, query()))
|
|
@@ -195,13 +198,16 @@ export const InteractiveSelectPrompt = (props: SelectPromptProps) => {
|
|
|
195
198
|
{props.prompt}
|
|
196
199
|
</text>
|
|
197
200
|
<text fg="#7aa2f7">{" > "}</text>
|
|
198
|
-
<
|
|
201
|
+
<textarea
|
|
199
202
|
focused
|
|
200
203
|
flexGrow={1}
|
|
201
204
|
minWidth={8}
|
|
205
|
+
height={2}
|
|
206
|
+
wrapMode="word"
|
|
202
207
|
placeholder="filter"
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
keyBindings={[{ name: "return", action: "submit" }]}
|
|
209
|
+
onContentChange={() => {
|
|
210
|
+
setQuery(input?.plainText ?? "")
|
|
205
211
|
setSelected(0)
|
|
206
212
|
}}
|
|
207
213
|
ref={(next) => {
|
|
@@ -266,7 +272,6 @@ async function runInteractive<T>(
|
|
|
266
272
|
} finally {
|
|
267
273
|
if (renderer) {
|
|
268
274
|
await shutdown(renderer)
|
|
269
|
-
process.stdout.write("\n")
|
|
270
275
|
}
|
|
271
276
|
}
|
|
272
277
|
}
|
|
@@ -45,10 +45,13 @@ describe("spawnProcess", () => {
|
|
|
45
45
|
const lineCount = 256
|
|
46
46
|
const script = [
|
|
47
47
|
`const line = ${JSON.stringify(line)}`,
|
|
48
|
-
`
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
48
|
+
`const indexes = Array.from({ length: ${lineCount} }, (_, i) => i)`,
|
|
49
|
+
"const stdout = indexes.map((i) => `out:${i}:${line}`).join(`\n`)",
|
|
50
|
+
"const stderr = indexes.map((i) => `err:${i}:${line}`).join(`\n`)",
|
|
51
|
+
"const write = (stream, output) => new Promise((resolve, reject) => {",
|
|
52
|
+
"stream.write(output, (error) => error ? reject(error) : resolve())",
|
|
53
|
+
"})",
|
|
54
|
+
"await Promise.all([write(process.stdout, stdout), write(process.stderr, stderr)])",
|
|
52
55
|
].join("\n")
|
|
53
56
|
|
|
54
57
|
const result = await Effect.runPromise(
|
|
@@ -13,12 +13,14 @@ export type WorkTarget =
|
|
|
13
13
|
readonly taskId: string
|
|
14
14
|
readonly path: string
|
|
15
15
|
readonly multiPhase: boolean
|
|
16
|
+
readonly status?: WorkStatus
|
|
16
17
|
}
|
|
17
18
|
| {
|
|
18
19
|
readonly kind: "phase"
|
|
19
20
|
readonly taskId: string
|
|
20
21
|
readonly phaseId: string
|
|
21
22
|
readonly path: string
|
|
23
|
+
readonly status?: WorkStatus
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
interface EpicRecord {
|
|
@@ -116,6 +118,7 @@ const taskChoices = (
|
|
|
116
118
|
taskId: task.id,
|
|
117
119
|
path: task.path,
|
|
118
120
|
multiPhase,
|
|
121
|
+
status: multiPhase ? undefined : (task.data.status ?? "open"),
|
|
119
122
|
},
|
|
120
123
|
},
|
|
121
124
|
]
|
|
@@ -147,6 +150,7 @@ const taskChoices = (
|
|
|
147
150
|
taskId: task.id,
|
|
148
151
|
phaseId: record.id,
|
|
149
152
|
path: record.path,
|
|
153
|
+
status: record.data.status ?? "open",
|
|
150
154
|
},
|
|
151
155
|
})
|
|
152
156
|
}
|
|
@@ -172,6 +176,7 @@ const taskChoices = (
|
|
|
172
176
|
taskId: task.id,
|
|
173
177
|
phaseId: record.id,
|
|
174
178
|
path: record.path,
|
|
179
|
+
status: record.data.status ?? "open",
|
|
175
180
|
},
|
|
176
181
|
})
|
|
177
182
|
}
|