@markjaquith/agency 2.31.0 → 2.32.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 +50 -38
- package/cli.ts +2 -1
- package/package.json +5 -2
- package/skills/agency/SKILL.md +7 -6
- package/skills/agency/references/commands.md +17 -14
- package/skills/agency/references/contracts.md +4 -5
- package/skills/agency/references/recipes.md +13 -10
- package/src/cli-parser.test.ts +12 -2
- package/src/cli-parser.ts +5 -2
- package/src/cli.test.ts +34 -10
- package/src/commands/doctor.test.ts +11 -1
- package/src/commands/phase.ts +1 -1
- package/src/commands/task.test.ts +34 -1
- package/src/commands/task.ts +16 -20
- package/src/commands/work.test.ts +65 -65
- package/src/commands/work.ts +43 -35
- package/src/opentui.d.ts +1 -0
- package/src/services/DoctorService.ts +22 -0
- package/src/services/IntegrationService.test.ts +2 -1
- package/src/services/PhaseService.ts +2 -2
- package/src/services/ReadinessService.test.ts +54 -0
- package/src/services/ReadinessService.ts +39 -2
- package/src/services/TaskPhaseService.test.ts +23 -9
- package/src/services/TaskService.ts +2 -2
- package/src/utils/chooser.test.ts +49 -12
- package/src/utils/chooser.ts +20 -37
- package/src/utils/interactive-loader.ts +6 -0
- package/src/utils/interactive.test.tsx +258 -0
- package/src/utils/interactive.tsx +290 -0
- package/src/workbase/AGENTS.md +5 -5
- package/src/workbase/runner-command.test.ts +30 -4
- package/src/workbase/runner-command.ts +21 -6
- package/src/workbase/schemas.test.ts +4 -2
- package/src/workbase/schemas.ts +4 -0
package/src/commands/task.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
|
-
import { createInterface } from "node:readline/promises"
|
|
3
2
|
import type { BaseCommandOptions } from "../utils/command"
|
|
4
3
|
import { TaskService } from "../services/TaskService"
|
|
5
4
|
import { EpicService } from "../services/EpicService"
|
|
@@ -51,17 +50,10 @@ const defaultInteraction = (
|
|
|
51
50
|
): TaskInteraction => ({
|
|
52
51
|
text: (prompt) =>
|
|
53
52
|
Effect.tryPromise({
|
|
54
|
-
try: async () =>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
})
|
|
59
|
-
try {
|
|
60
|
-
return await input.question(prompt)
|
|
61
|
-
} finally {
|
|
62
|
-
input.close()
|
|
63
|
-
}
|
|
64
|
-
},
|
|
53
|
+
try: async () =>
|
|
54
|
+
(
|
|
55
|
+
await (await import("../utils/interactive-loader")).loadInteractive()
|
|
56
|
+
).promptText(prompt),
|
|
65
57
|
catch: (cause) => new Error("Failed to read task input", { cause }),
|
|
66
58
|
}),
|
|
67
59
|
select: (prompt, choices) =>
|
|
@@ -157,14 +149,18 @@ export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
|
|
|
157
149
|
),
|
|
158
150
|
)
|
|
159
151
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
152
|
+
if (records.length === 1) {
|
|
153
|
+
repo = records[0]!.alias
|
|
154
|
+
} else {
|
|
155
|
+
const selected = yield* activeInteraction.select(
|
|
156
|
+
"Writable repository",
|
|
157
|
+
records.map((record) => record.alias),
|
|
158
|
+
)
|
|
159
|
+
if (!selected) {
|
|
160
|
+
return yield* Effect.fail(new Error("Task creation cancelled"))
|
|
161
|
+
}
|
|
162
|
+
repo = selected
|
|
166
163
|
}
|
|
167
|
-
repo = selected
|
|
168
164
|
}
|
|
169
165
|
|
|
170
166
|
const record = yield* tasks.create(
|
|
@@ -410,7 +406,7 @@ Subcommands:
|
|
|
410
406
|
create <id> Create a task without prompting
|
|
411
407
|
list List tasks
|
|
412
408
|
show <id> Show a task
|
|
413
|
-
status <id> <status> Set open, done, or dropped
|
|
409
|
+
status <id> <status> Set open, working, done, or dropped
|
|
414
410
|
update <id> Update task metadata
|
|
415
411
|
rename <id> <new-id> Rename a task and update graph references
|
|
416
412
|
move <id> Move a task with --epic or --no-epic
|
|
@@ -6,7 +6,6 @@ import { EpicService } from "../services/EpicService"
|
|
|
6
6
|
import { TaskService } from "../services/TaskService"
|
|
7
7
|
import { PhaseService } from "../services/PhaseService"
|
|
8
8
|
import { WorktreeService } from "../services/WorktreeService"
|
|
9
|
-
import { ClaimService } from "../services/ClaimService"
|
|
10
9
|
import { ReadinessService } from "../services/ReadinessService"
|
|
11
10
|
import { IntegrationService } from "../services/IntegrationService"
|
|
12
11
|
import { captureErrors, captureLogs } from "../test-utils"
|
|
@@ -45,6 +44,8 @@ const multiPhaseWorkspace: ExecutionWorkspace = {
|
|
|
45
44
|
operations: [],
|
|
46
45
|
}
|
|
47
46
|
|
|
47
|
+
const taskDirectory = "/workbase/tasks/example"
|
|
48
|
+
|
|
48
49
|
interface HarnessOptions {
|
|
49
50
|
readonly workspace?: ExecutionWorkspace
|
|
50
51
|
readonly materializeError?: Error
|
|
@@ -54,7 +55,9 @@ interface HarnessOptions {
|
|
|
54
55
|
string,
|
|
55
56
|
{
|
|
56
57
|
command: readonly [string, ...string[]]
|
|
58
|
+
autoCommand?: readonly [string, ...string[]]
|
|
57
59
|
resumeCommand?: readonly [string, ...string[]]
|
|
60
|
+
autoResumeCommand?: readonly [string, ...string[]]
|
|
58
61
|
environment?: Record<string, string>
|
|
59
62
|
}
|
|
60
63
|
>
|
|
@@ -66,7 +69,7 @@ interface HarnessOptions {
|
|
|
66
69
|
readonly registeredWorkbases?: readonly string[]
|
|
67
70
|
readonly existingDirectories?: readonly string[]
|
|
68
71
|
readonly guardError?: Error
|
|
69
|
-
readonly
|
|
72
|
+
readonly workTargetIds?: readonly string[]
|
|
70
73
|
readonly opencodeIntegrationState?: "managed" | "customized"
|
|
71
74
|
}
|
|
72
75
|
|
|
@@ -161,35 +164,11 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
161
164
|
return Effect.void
|
|
162
165
|
},
|
|
163
166
|
}
|
|
164
|
-
const claims = {
|
|
165
|
-
inspect: (taskId: string, phaseId?: string) =>
|
|
166
|
-
Effect.succeed({
|
|
167
|
-
target: {
|
|
168
|
-
kind: phaseId ? "phase" : "task",
|
|
169
|
-
taskId,
|
|
170
|
-
phaseId,
|
|
171
|
-
path: phaseId
|
|
172
|
-
? `/workbase/tasks/${taskId}/phases/${phaseId}/PHASE.md`
|
|
173
|
-
: `/workbase/tasks/${taskId}/TASK.md`,
|
|
174
|
-
label: phaseId ? `phase '${taskId}/${phaseId}'` : `task '${taskId}'`,
|
|
175
|
-
},
|
|
176
|
-
revision: "0".repeat(64),
|
|
177
|
-
data: {},
|
|
178
|
-
}),
|
|
179
|
-
claim: (input: { taskId: string; phaseId?: string }) => {
|
|
180
|
-
statusUpdates.push(
|
|
181
|
-
input.phaseId
|
|
182
|
-
? `phase:${input.taskId}:${input.phaseId}:working`
|
|
183
|
-
: `task:${input.taskId}:working`,
|
|
184
|
-
)
|
|
185
|
-
return Effect.succeed({ revision: "1".repeat(64) })
|
|
186
|
-
},
|
|
187
|
-
}
|
|
188
167
|
const readiness = {
|
|
189
|
-
|
|
168
|
+
getWorkTargetIds: () =>
|
|
190
169
|
Effect.succeed(
|
|
191
170
|
new Set(
|
|
192
|
-
options.
|
|
171
|
+
options.workTargetIds ?? [
|
|
193
172
|
...(options.epicRecords ?? []).map(
|
|
194
173
|
(record: any) => `epic:${record.id}`,
|
|
195
174
|
),
|
|
@@ -271,7 +250,6 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
271
250
|
Effect.provideService(EpicService, epics as never),
|
|
272
251
|
Effect.provideService(TaskService, tasks as never),
|
|
273
252
|
Effect.provideService(PhaseService, phases as never),
|
|
274
|
-
Effect.provideService(ClaimService, claims as never),
|
|
275
253
|
Effect.provideService(ReadinessService, readiness as never),
|
|
276
254
|
Effect.provideService(IntegrationService, integrations as never),
|
|
277
255
|
) as Effect.Effect<void, unknown, never>,
|
|
@@ -338,7 +316,7 @@ describe("work command", () => {
|
|
|
338
316
|
})
|
|
339
317
|
})
|
|
340
318
|
|
|
341
|
-
test("offers only
|
|
319
|
+
test("offers only launchable targets to the interactive chooser", async () => {
|
|
342
320
|
const harness = createHarness({
|
|
343
321
|
taskRecords: [
|
|
344
322
|
{
|
|
@@ -352,7 +330,7 @@ describe("work command", () => {
|
|
|
352
330
|
data: { status: "open" },
|
|
353
331
|
},
|
|
354
332
|
],
|
|
355
|
-
|
|
333
|
+
workTargetIds: ["execution-unit:task/ready"],
|
|
356
334
|
})
|
|
357
335
|
let labels: readonly string[] = []
|
|
358
336
|
const pick: PickWorkTarget = (choices) => {
|
|
@@ -392,6 +370,7 @@ describe("work command", () => {
|
|
|
392
370
|
cwd: "/workbase/epics/delivery",
|
|
393
371
|
directory: ".",
|
|
394
372
|
opencode: true,
|
|
373
|
+
auto: true,
|
|
395
374
|
})
|
|
396
375
|
|
|
397
376
|
expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
|
|
@@ -435,7 +414,7 @@ describe("work command", () => {
|
|
|
435
414
|
})
|
|
436
415
|
|
|
437
416
|
expect(harness.shownTasks).toEqual(["delivery"])
|
|
438
|
-
expect(harness.launches[0]?.cwd).toBe(
|
|
417
|
+
expect(harness.launches[0]?.cwd).toBe(taskDirectory)
|
|
439
418
|
})
|
|
440
419
|
|
|
441
420
|
test("treats a positional value as a task ID when it is not a directory", async () => {
|
|
@@ -448,7 +427,7 @@ describe("work command", () => {
|
|
|
448
427
|
})
|
|
449
428
|
|
|
450
429
|
expect(harness.shownTasks).toEqual(["delivery"])
|
|
451
|
-
expect(harness.launches[0]?.cwd).toBe(
|
|
430
|
+
expect(harness.launches[0]?.cwd).toBe(taskDirectory)
|
|
452
431
|
})
|
|
453
432
|
|
|
454
433
|
test("launches a multi-phase task agent without materializing", async () => {
|
|
@@ -458,6 +437,7 @@ describe("work command", () => {
|
|
|
458
437
|
cwd: "/workbase/tasks/delivery",
|
|
459
438
|
directory: ".",
|
|
460
439
|
opencode: true,
|
|
440
|
+
auto: true,
|
|
461
441
|
})
|
|
462
442
|
|
|
463
443
|
expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
|
|
@@ -482,6 +462,7 @@ describe("work command", () => {
|
|
|
482
462
|
cwd: "/workbase/tasks/example/phases/implementation/code/agency/src",
|
|
483
463
|
directory: ".",
|
|
484
464
|
opencode: true,
|
|
465
|
+
auto: true,
|
|
485
466
|
})
|
|
486
467
|
|
|
487
468
|
expect(harness.events).toEqual([
|
|
@@ -496,7 +477,7 @@ describe("work command", () => {
|
|
|
496
477
|
"--prompt",
|
|
497
478
|
"Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
498
479
|
],
|
|
499
|
-
cwd:
|
|
480
|
+
cwd: taskDirectory,
|
|
500
481
|
})
|
|
501
482
|
expect(harness.statusUpdates).toEqual([
|
|
502
483
|
"phase:example:implementation:working",
|
|
@@ -516,7 +497,7 @@ describe("work command", () => {
|
|
|
516
497
|
})
|
|
517
498
|
|
|
518
499
|
expect(harness.events[0]).toBe("materialize")
|
|
519
|
-
expect(harness.launches[0]?.cwd).toBe(
|
|
500
|
+
expect(harness.launches[0]?.cwd).toBe(taskDirectory)
|
|
520
501
|
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBe(
|
|
521
502
|
"/workbase/.opencode/opencode.jsonc",
|
|
522
503
|
)
|
|
@@ -552,7 +533,7 @@ describe("work command", () => {
|
|
|
552
533
|
"probe:opencode",
|
|
553
534
|
"launch:opencode",
|
|
554
535
|
])
|
|
555
|
-
expect(harness.launches[0]?.cwd).toBe(
|
|
536
|
+
expect(harness.launches[0]?.cwd).toBe(taskDirectory)
|
|
556
537
|
})
|
|
557
538
|
|
|
558
539
|
test("requires an explicit target when input is disabled", async () => {
|
|
@@ -678,7 +659,7 @@ describe("work command", () => {
|
|
|
678
659
|
expect(harness.events).toEqual([])
|
|
679
660
|
})
|
|
680
661
|
|
|
681
|
-
test("launches OpenCode in the
|
|
662
|
+
test("launches OpenCode in the task directory with explicit context", async () => {
|
|
682
663
|
const harness = createHarness()
|
|
683
664
|
|
|
684
665
|
await harness.run({ taskId: "example", opencode: true })
|
|
@@ -691,14 +672,11 @@ describe("work command", () => {
|
|
|
691
672
|
expect(harness.launches).toEqual([
|
|
692
673
|
{
|
|
693
674
|
cli: "opencode",
|
|
694
|
-
args: [
|
|
695
|
-
|
|
696
|
-
"--prompt",
|
|
697
|
-
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
698
|
-
],
|
|
699
|
-
cwd: singlePhaseWorkspace.writablePath,
|
|
675
|
+
args: ["opencode"],
|
|
676
|
+
cwd: taskDirectory,
|
|
700
677
|
},
|
|
701
678
|
])
|
|
679
|
+
expect(harness.launchEnvironments[0]?.AGENCY_PROMPT).toBe("")
|
|
702
680
|
expect(harness.statusUpdates).toEqual(["task:example:working"])
|
|
703
681
|
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBe(
|
|
704
682
|
"/workbase/.opencode/opencode.jsonc",
|
|
@@ -709,6 +687,21 @@ describe("work command", () => {
|
|
|
709
687
|
])
|
|
710
688
|
})
|
|
711
689
|
|
|
690
|
+
test("sends the generated prompt only with --auto", async () => {
|
|
691
|
+
const harness = createHarness()
|
|
692
|
+
|
|
693
|
+
await harness.run({ taskId: "example", opencode: true, auto: true })
|
|
694
|
+
|
|
695
|
+
expect(harness.launches[0]?.args).toEqual([
|
|
696
|
+
"opencode",
|
|
697
|
+
"--prompt",
|
|
698
|
+
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
699
|
+
])
|
|
700
|
+
expect(harness.launchEnvironments[0]?.AGENCY_PROMPT).toBe(
|
|
701
|
+
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
702
|
+
)
|
|
703
|
+
})
|
|
704
|
+
|
|
712
705
|
test("resumes OpenCode deterministically when a session identity exists", async () => {
|
|
713
706
|
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
714
707
|
process.env.AGENCY_SESSION_ID = "existing-session"
|
|
@@ -717,6 +710,7 @@ describe("work command", () => {
|
|
|
717
710
|
taskId: "example",
|
|
718
711
|
phaseId: "implementation",
|
|
719
712
|
opencode: true,
|
|
713
|
+
auto: true,
|
|
720
714
|
})
|
|
721
715
|
} finally {
|
|
722
716
|
delete process.env.AGENCY_SESSION_ID
|
|
@@ -730,16 +724,17 @@ describe("work command", () => {
|
|
|
730
724
|
"--prompt",
|
|
731
725
|
"Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
732
726
|
],
|
|
733
|
-
cwd:
|
|
727
|
+
cwd: taskDirectory,
|
|
734
728
|
})
|
|
735
729
|
})
|
|
736
730
|
|
|
737
|
-
test("expands a named runner with shared context
|
|
731
|
+
test("expands a named runner with shared context", async () => {
|
|
738
732
|
const harness = createHarness({
|
|
739
733
|
available: { codex: true },
|
|
740
734
|
runners: {
|
|
741
735
|
custom: {
|
|
742
|
-
command: ["codex"
|
|
736
|
+
command: ["codex"],
|
|
737
|
+
autoCommand: ["codex", "--task", "{task}", "{prompt}"],
|
|
743
738
|
environment: {
|
|
744
739
|
CUSTOM_TARGET: "{target}",
|
|
745
740
|
AGENCY_TARGET: "cannot-override",
|
|
@@ -748,7 +743,7 @@ describe("work command", () => {
|
|
|
748
743
|
},
|
|
749
744
|
})
|
|
750
745
|
|
|
751
|
-
await harness.run({ taskId: "example", runner: "custom" })
|
|
746
|
+
await harness.run({ taskId: "example", runner: "custom", auto: true })
|
|
752
747
|
|
|
753
748
|
expect(harness.probes).toEqual(["codex"])
|
|
754
749
|
expect(harness.launches[0]).toEqual({
|
|
@@ -759,7 +754,7 @@ describe("work command", () => {
|
|
|
759
754
|
"example",
|
|
760
755
|
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
761
756
|
],
|
|
762
|
-
cwd:
|
|
757
|
+
cwd: taskDirectory,
|
|
763
758
|
})
|
|
764
759
|
expect(harness.launchEnvironments[0]).toMatchObject({
|
|
765
760
|
AGENCY_RUNNER: "custom",
|
|
@@ -768,7 +763,7 @@ describe("work command", () => {
|
|
|
768
763
|
AGENCY_TARGET: "execution-unit:task/example",
|
|
769
764
|
AGENCY_TASK_ID: "example",
|
|
770
765
|
AGENCY_PHASE_ID: "",
|
|
771
|
-
AGENCY_CLAIM_REVISION: "
|
|
766
|
+
AGENCY_CLAIM_REVISION: "",
|
|
772
767
|
CUSTOM_TARGET: "execution-unit:task/example",
|
|
773
768
|
})
|
|
774
769
|
})
|
|
@@ -778,7 +773,8 @@ describe("work command", () => {
|
|
|
778
773
|
available: { agent: true },
|
|
779
774
|
runners: {
|
|
780
775
|
custom: {
|
|
781
|
-
command: ["agent"
|
|
776
|
+
command: ["agent"],
|
|
777
|
+
autoCommand: ["agent", "{prompt}"],
|
|
782
778
|
environment: {
|
|
783
779
|
VISIBLE: "{task}",
|
|
784
780
|
API_TOKEN: "do-not-print",
|
|
@@ -797,11 +793,10 @@ describe("work command", () => {
|
|
|
797
793
|
const printed = JSON.parse(output.join("\n"))
|
|
798
794
|
|
|
799
795
|
expect(harness.launches).toEqual([])
|
|
800
|
-
expect(
|
|
801
|
-
expect(printed.
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
])
|
|
796
|
+
expect(harness.statusUpdates).toEqual([])
|
|
797
|
+
expect(printed.cwd).toBe(taskDirectory)
|
|
798
|
+
expect(printed.argv).toEqual(["agent"])
|
|
799
|
+
expect(printed.environment.AGENCY_PROMPT).toBe("")
|
|
805
800
|
expect(printed.environment.VISIBLE).toBe("example")
|
|
806
801
|
expect(printed.environment.API_TOKEN).toBeUndefined()
|
|
807
802
|
})
|
|
@@ -816,16 +811,21 @@ describe("work command", () => {
|
|
|
816
811
|
expect(printed.environment.OPENCODE_CONFIG).toBe(
|
|
817
812
|
"/workbase/.opencode/opencode.jsonc",
|
|
818
813
|
)
|
|
814
|
+
const edit = {
|
|
815
|
+
"*": "deny",
|
|
816
|
+
"tasks/example/code/agency/**": "allow",
|
|
817
|
+
"code/agency/**": "allow",
|
|
818
|
+
}
|
|
819
819
|
expect(JSON.parse(printed.environment.OPENCODE_CONFIG_CONTENT)).toEqual({
|
|
820
820
|
permission: {
|
|
821
821
|
external_directory: { "/workbase/**": "allow" },
|
|
822
|
-
edit
|
|
822
|
+
edit,
|
|
823
823
|
},
|
|
824
824
|
agent: {
|
|
825
|
-
build: { permission: { edit
|
|
826
|
-
plan: { permission: { edit
|
|
827
|
-
general: { permission: { edit
|
|
828
|
-
explore: { permission: { edit
|
|
825
|
+
build: { permission: { edit } },
|
|
826
|
+
plan: { permission: { edit } },
|
|
827
|
+
general: { permission: { edit } },
|
|
828
|
+
explore: { permission: { edit } },
|
|
829
829
|
},
|
|
830
830
|
})
|
|
831
831
|
})
|
|
@@ -848,8 +848,8 @@ describe("work command", () => {
|
|
|
848
848
|
expect(harness.probes).toEqual(["opencode", "claude"])
|
|
849
849
|
expect(harness.launches[0]).toEqual({
|
|
850
850
|
cli: "claude",
|
|
851
|
-
args: ["claude"
|
|
852
|
-
cwd:
|
|
851
|
+
args: ["claude"],
|
|
852
|
+
cwd: taskDirectory,
|
|
853
853
|
})
|
|
854
854
|
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBeUndefined()
|
|
855
855
|
})
|
|
@@ -873,8 +873,8 @@ describe("work command", () => {
|
|
|
873
873
|
expect(harness.probes).toEqual(["claude"])
|
|
874
874
|
expect(harness.launches[0]).toEqual({
|
|
875
875
|
cli: "claude",
|
|
876
|
-
args: ["claude"
|
|
877
|
-
cwd:
|
|
876
|
+
args: ["claude"],
|
|
877
|
+
cwd: taskDirectory,
|
|
878
878
|
})
|
|
879
879
|
})
|
|
880
880
|
|
|
@@ -911,7 +911,7 @@ describe("work command", () => {
|
|
|
911
911
|
verboseHarness.run({ taskId: "example", verbose: true }),
|
|
912
912
|
)
|
|
913
913
|
expect(verboseLogs).toEqual([
|
|
914
|
-
"Launching command: opencode
|
|
914
|
+
"Launching command: opencode (cwd: /workbase/tasks/example)",
|
|
915
915
|
])
|
|
916
916
|
expect(verboseHarness.materializeOptions[0]?.verbose).toBe(true)
|
|
917
917
|
|
package/src/commands/work.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { WorkbaseService } from "../services/WorkbaseService"
|
|
|
7
7
|
import { EpicService } from "../services/EpicService"
|
|
8
8
|
import { TaskService } from "../services/TaskService"
|
|
9
9
|
import { PhaseService } from "../services/PhaseService"
|
|
10
|
-
import { ClaimService } from "../services/ClaimService"
|
|
11
10
|
import { ReadinessService } from "../services/ReadinessService"
|
|
12
11
|
import { IntegrationService } from "../services/IntegrationService"
|
|
13
12
|
import { createLoggers } from "../utils/effect"
|
|
@@ -39,6 +38,7 @@ interface WorkOptions extends BaseCommandOptions {
|
|
|
39
38
|
readonly claude?: boolean
|
|
40
39
|
readonly runner?: string
|
|
41
40
|
readonly printCommand?: boolean
|
|
41
|
+
readonly auto?: boolean
|
|
42
42
|
readonly force?: boolean
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -105,7 +105,6 @@ export const work = (
|
|
|
105
105
|
const epics = yield* EpicService
|
|
106
106
|
const tasks = yield* TaskService
|
|
107
107
|
const phases = yield* PhaseService
|
|
108
|
-
const claims = yield* ClaimService
|
|
109
108
|
const readiness = yield* ReadinessService
|
|
110
109
|
const integrations = yield* IntegrationService
|
|
111
110
|
const { log, verboseLog } = createLoggers(options)
|
|
@@ -207,13 +206,13 @@ export const work = (
|
|
|
207
206
|
taskRecords,
|
|
208
207
|
phaseRecords,
|
|
209
208
|
)
|
|
210
|
-
const
|
|
209
|
+
const workTargetIds = options.force
|
|
211
210
|
? null
|
|
212
|
-
: yield* readiness.
|
|
211
|
+
: yield* readiness.getWorkTargetIds(root)
|
|
213
212
|
const choices = options.force
|
|
214
213
|
? allChoices
|
|
215
214
|
: allChoices.filter((choice) =>
|
|
216
|
-
|
|
215
|
+
workTargetIds!.has(targetNodeId(choice.target)),
|
|
217
216
|
)
|
|
218
217
|
if (choices.length === 0) {
|
|
219
218
|
return yield* Effect.fail(
|
|
@@ -227,6 +226,7 @@ export const work = (
|
|
|
227
226
|
|
|
228
227
|
let prompt: string
|
|
229
228
|
let launchPath: string
|
|
229
|
+
let writablePath: string | undefined
|
|
230
230
|
if (target.kind === "epic") {
|
|
231
231
|
prompt = `Work on the epic. Read ${target.path}.`
|
|
232
232
|
launchPath = dirname(target.path)
|
|
@@ -250,7 +250,8 @@ export const work = (
|
|
|
250
250
|
prompt = workspace.phasePath
|
|
251
251
|
? `Start the task. Read ${workspace.taskPath} and ${workspace.phasePath}.`
|
|
252
252
|
: `Start the task. Read ${workspace.taskPath}.`
|
|
253
|
-
launchPath = workspace.
|
|
253
|
+
launchPath = dirname(workspace.taskPath)
|
|
254
|
+
writablePath = workspace.writablePath
|
|
254
255
|
}
|
|
255
256
|
|
|
256
257
|
const explicitlyRequested = Boolean(
|
|
@@ -267,22 +268,22 @@ export const work = (
|
|
|
267
268
|
const sessionId =
|
|
268
269
|
process.env.AGENCY_SESSION_ID ?? `${process.pid}-${Date.now()}`
|
|
269
270
|
const resume = process.env.AGENCY_SESSION_ID !== undefined
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
prompt,
|
|
271
|
+
const variables = {
|
|
272
|
+
prompt: options.auto ? prompt : "",
|
|
273
273
|
workbase: root,
|
|
274
274
|
target: targetNodeId(target),
|
|
275
275
|
task: target.kind === "epic" ? "" : target.taskId,
|
|
276
276
|
phase: target.kind === "phase" ? target.phaseId : "",
|
|
277
277
|
claimant,
|
|
278
278
|
sessionId,
|
|
279
|
-
claimRevision,
|
|
279
|
+
claimRevision: "",
|
|
280
280
|
}
|
|
281
281
|
let resolved = resolveRunnerCommand(
|
|
282
282
|
runner,
|
|
283
283
|
config.runners,
|
|
284
284
|
variables,
|
|
285
285
|
resume,
|
|
286
|
+
options.auto,
|
|
286
287
|
)
|
|
287
288
|
let cli = resolved.argv[0]!
|
|
288
289
|
let available = yield* fs.runCommand(["which", cli], {
|
|
@@ -294,35 +295,26 @@ export const work = (
|
|
|
294
295
|
runner === "opencode"
|
|
295
296
|
) {
|
|
296
297
|
runner = "claude"
|
|
297
|
-
resolved = resolveRunnerCommand(
|
|
298
|
+
resolved = resolveRunnerCommand(
|
|
299
|
+
runner,
|
|
300
|
+
config.runners,
|
|
301
|
+
variables,
|
|
302
|
+
resume,
|
|
303
|
+
options.auto,
|
|
304
|
+
)
|
|
298
305
|
cli = resolved.argv[0]!
|
|
299
306
|
available = yield* fs.runCommand(["which", cli], { captureOutput: true })
|
|
300
307
|
}
|
|
301
308
|
if (available.exitCode !== 0) {
|
|
302
309
|
return yield* Effect.fail(new Error(`${cli} CLI tool not found`))
|
|
303
310
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
{
|
|
312
|
-
taskId: target.taskId,
|
|
313
|
-
...(phaseId ? { phaseId } : {}),
|
|
314
|
-
claimant,
|
|
315
|
-
runner,
|
|
316
|
-
sessionId,
|
|
317
|
-
revision: current.revision,
|
|
318
|
-
},
|
|
319
|
-
root,
|
|
320
|
-
)
|
|
321
|
-
claimRevision = acquired.revision
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
variables = { ...variables, claimRevision }
|
|
325
|
-
resolved = resolveRunnerCommand(runner, config.runners, variables, resume)
|
|
311
|
+
resolved = resolveRunnerCommand(
|
|
312
|
+
runner,
|
|
313
|
+
config.runners,
|
|
314
|
+
variables,
|
|
315
|
+
resume,
|
|
316
|
+
options.auto,
|
|
317
|
+
)
|
|
326
318
|
cli = resolved.argv[0]!
|
|
327
319
|
const environment = {
|
|
328
320
|
...resolved.environment,
|
|
@@ -333,7 +325,17 @@ export const work = (
|
|
|
333
325
|
const execution =
|
|
334
326
|
target.kind === "phase" ||
|
|
335
327
|
(target.kind === "task" && !target.multiPhase)
|
|
336
|
-
const edit =
|
|
328
|
+
const edit = execution
|
|
329
|
+
? {
|
|
330
|
+
"*": "deny" as const,
|
|
331
|
+
...Object.fromEntries(
|
|
332
|
+
[root, launchPath].map((base) => [
|
|
333
|
+
join(relative(base, writablePath!), "**").split(sep).join("/"),
|
|
334
|
+
"allow" as const,
|
|
335
|
+
]),
|
|
336
|
+
),
|
|
337
|
+
}
|
|
338
|
+
: { "*": "deny" as const }
|
|
337
339
|
environment.OPENCODE_CONFIG_CONTENT = JSON.stringify({
|
|
338
340
|
permission: {
|
|
339
341
|
external_directory: { [join(root, "**")]: "allow" },
|
|
@@ -361,6 +363,11 @@ export const work = (
|
|
|
361
363
|
)
|
|
362
364
|
return
|
|
363
365
|
}
|
|
366
|
+
if (target.kind === "phase") {
|
|
367
|
+
yield* phases.setStatus(target.taskId, target.phaseId, "working", root)
|
|
368
|
+
} else if (target.kind === "task" && !target.multiPhase) {
|
|
369
|
+
yield* tasks.setStatus(target.taskId, "working", root)
|
|
370
|
+
}
|
|
364
371
|
for (const [key, value] of Object.entries(environment)) {
|
|
365
372
|
process.env[key] = value
|
|
366
373
|
}
|
|
@@ -438,7 +445,7 @@ export const workPrepare = (options: WorkOptions = {}) =>
|
|
|
438
445
|
})
|
|
439
446
|
|
|
440
447
|
export const help = `
|
|
441
|
-
Usage: agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>]
|
|
448
|
+
Usage: agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] [--auto]
|
|
442
449
|
agency work prepare [target] [--dry-run] [--json]
|
|
443
450
|
|
|
444
451
|
Launch an agent for an epic, task, or phase. With no directory, select one
|
|
@@ -458,6 +465,7 @@ Options:
|
|
|
458
465
|
--workbase <target> Select a workbase by ID, name, or path
|
|
459
466
|
--cwd <path> Resolve context from a specific directory
|
|
460
467
|
--runner <name> Select a configured runner or built-in preset
|
|
468
|
+
--auto Send the generated context prompt to the runner
|
|
461
469
|
--print-command Print cwd, argv, and non-secret environment without launch
|
|
462
470
|
--opencode Require the OpenCode preset
|
|
463
471
|
--claude Require the Claude Code preset
|
package/src/opentui.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "@opentui/solid/preload"
|
|
@@ -170,6 +170,17 @@ export class DoctorService extends Effect.Service<DoctorService>()(
|
|
|
170
170
|
`Configured runner '${name}'`,
|
|
171
171
|
] as const,
|
|
172
172
|
),
|
|
173
|
+
...Object.entries(config.runners ?? {}).flatMap(([name, runner]) =>
|
|
174
|
+
runner.autoCommand
|
|
175
|
+
? [
|
|
176
|
+
[
|
|
177
|
+
`integration.runner.${name}.auto`,
|
|
178
|
+
runner.autoCommand,
|
|
179
|
+
`Configured runner '${name}' auto`,
|
|
180
|
+
] as const,
|
|
181
|
+
]
|
|
182
|
+
: [],
|
|
183
|
+
),
|
|
173
184
|
...Object.entries(config.runners ?? {}).flatMap(([name, runner]) =>
|
|
174
185
|
runner.resumeCommand
|
|
175
186
|
? [
|
|
@@ -181,6 +192,17 @@ export class DoctorService extends Effect.Service<DoctorService>()(
|
|
|
181
192
|
]
|
|
182
193
|
: [],
|
|
183
194
|
),
|
|
195
|
+
...Object.entries(config.runners ?? {}).flatMap(([name, runner]) =>
|
|
196
|
+
runner.autoResumeCommand
|
|
197
|
+
? [
|
|
198
|
+
[
|
|
199
|
+
`integration.runner.${name}.auto-resume`,
|
|
200
|
+
runner.autoResumeCommand,
|
|
201
|
+
`Configured runner '${name}' auto resume`,
|
|
202
|
+
] as const,
|
|
203
|
+
]
|
|
204
|
+
: [],
|
|
205
|
+
),
|
|
184
206
|
...(config.delivery
|
|
185
207
|
? [
|
|
186
208
|
[
|
|
@@ -76,7 +76,8 @@ describe("IntegrationService", () => {
|
|
|
76
76
|
|
|
77
77
|
expect(body).toContain("agency context . --json")
|
|
78
78
|
expect(body).toContain("authority.writable.checkoutPath")
|
|
79
|
-
expect(body).toContain("
|
|
79
|
+
expect(body).toContain("`agency work` is the local launch flow")
|
|
80
|
+
expect(body).toContain("External orchestrators claim before launching")
|
|
80
81
|
expect(body).toContain("Run `agency validate`")
|
|
81
82
|
expect(body).toContain("only with explicit user intent")
|
|
82
83
|
expect(body).toContain("An execution unit is `working`")
|
|
@@ -474,10 +474,10 @@ export class PhaseService extends Effect.Service<PhaseService>()(
|
|
|
474
474
|
const fs = yield* FileSystemService
|
|
475
475
|
const service = yield* PhaseService
|
|
476
476
|
const validStatus = yield* decodeStatus(status)
|
|
477
|
-
if (validStatus === "
|
|
477
|
+
if (validStatus === "delegated") {
|
|
478
478
|
return yield* new PhaseError({
|
|
479
479
|
message:
|
|
480
|
-
"
|
|
480
|
+
"Delegation requires explicit ownership; use 'agency claim'",
|
|
481
481
|
})
|
|
482
482
|
}
|
|
483
483
|
const record = yield* service.show(taskId, id, startPath)
|