@markjaquith/agency 2.17.0 → 2.19.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 +29 -8
- package/cli.ts +94 -8
- package/package.json +1 -1
- package/skills/agency/SKILL.md +13 -6
- package/src/cli-parser.test.ts +111 -2
- package/src/cli-parser.ts +273 -36
- package/src/cli.test.ts +105 -5
- package/src/commands/context.ts +3 -0
- package/src/commands/epic.test.ts +22 -0
- package/src/commands/epic.ts +38 -2
- package/src/commands/init.ts +3 -1
- package/src/commands/phase.ts +52 -2
- package/src/commands/status.test.ts +44 -0
- package/src/commands/status.ts +51 -2
- package/src/commands/task-phase.test.ts +20 -0
- package/src/commands/task.ts +49 -2
- package/src/commands/validate.test.ts +2 -0
- package/src/commands/work.test.ts +1 -0
- package/src/commands/work.ts +15 -7
- package/src/commands/workbase.test.ts +63 -2
- package/src/commands/workbase.ts +77 -10
- package/src/services/WorkbaseService.test.ts +147 -4
- package/src/services/WorkbaseService.ts +214 -12
- package/src/utils/table.ts +19 -0
- package/src/work-view.test.ts +151 -0
- package/src/work-view.ts +253 -0
- package/src/workbase/schemas.test.ts +17 -6
- package/src/workbase/schemas.ts +16 -1
- package/src/workbase/workbase-choice.ts +2 -0
package/src/cli.test.ts
CHANGED
|
@@ -217,6 +217,15 @@ describe("CLI", () => {
|
|
|
217
217
|
await expect(access(root)).rejects.toThrow()
|
|
218
218
|
})
|
|
219
219
|
|
|
220
|
+
test("resolves relative init paths from explicit cwd", async () => {
|
|
221
|
+
const parent = await createTempDir()
|
|
222
|
+
tempDirs.push(parent)
|
|
223
|
+
const result = parseJson(
|
|
224
|
+
await runCli(["init", "child", "--cwd", parent, "--json"], projectRoot),
|
|
225
|
+
)
|
|
226
|
+
expect(result.root).toBe(join(parent, "child"))
|
|
227
|
+
})
|
|
228
|
+
|
|
220
229
|
test("refuses guided input without a TTY or with --no-input", async () => {
|
|
221
230
|
for (const args of [
|
|
222
231
|
["task", "new"],
|
|
@@ -349,12 +358,93 @@ describe("CLI", () => {
|
|
|
349
358
|
).toEqual({
|
|
350
359
|
root,
|
|
351
360
|
})
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
361
|
+
const registration = parseJson(
|
|
362
|
+
await runCli(
|
|
363
|
+
[
|
|
364
|
+
"workbase",
|
|
365
|
+
"add",
|
|
366
|
+
"workbase",
|
|
367
|
+
"--cwd",
|
|
368
|
+
parent,
|
|
369
|
+
"--name",
|
|
370
|
+
"primary",
|
|
371
|
+
"--json",
|
|
372
|
+
],
|
|
373
|
+
projectRoot,
|
|
374
|
+
env,
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
expect(registration).toMatchObject({
|
|
378
|
+
name: "primary",
|
|
379
|
+
path: await realpath(root),
|
|
380
|
+
})
|
|
355
381
|
expect(
|
|
356
382
|
parseJson(await runCli(["workbase", "list", "--json"], parent, env)),
|
|
357
|
-
).toEqual([
|
|
383
|
+
).toEqual({ workbases: [registration] })
|
|
384
|
+
|
|
385
|
+
await mkdir(join(root, "repos", "agency"), { recursive: true })
|
|
386
|
+
parseJson(
|
|
387
|
+
await runCli(
|
|
388
|
+
[
|
|
389
|
+
"task",
|
|
390
|
+
"create",
|
|
391
|
+
"explicit",
|
|
392
|
+
"--repo",
|
|
393
|
+
"agency",
|
|
394
|
+
"--workbase",
|
|
395
|
+
"primary",
|
|
396
|
+
"--no-input",
|
|
397
|
+
"--json",
|
|
398
|
+
],
|
|
399
|
+
parent,
|
|
400
|
+
env,
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
const shown = parseJson(
|
|
404
|
+
await runCli(
|
|
405
|
+
[
|
|
406
|
+
"task",
|
|
407
|
+
"show",
|
|
408
|
+
"--task",
|
|
409
|
+
"explicit",
|
|
410
|
+
"--workbase",
|
|
411
|
+
registration.id,
|
|
412
|
+
"--no-input",
|
|
413
|
+
"--json",
|
|
414
|
+
],
|
|
415
|
+
parent,
|
|
416
|
+
env,
|
|
417
|
+
),
|
|
418
|
+
)
|
|
419
|
+
expect(shown.id).toBe("explicit")
|
|
420
|
+
const inferred = parseJson(
|
|
421
|
+
await runCli(
|
|
422
|
+
["context", "--cwd", join(root, "tasks", "explicit"), "--json"],
|
|
423
|
+
projectRoot,
|
|
424
|
+
env,
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
expect(inferred.target).toMatchObject({
|
|
428
|
+
kind: "task",
|
|
429
|
+
taskId: "explicit",
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
parseJson(
|
|
433
|
+
await runCli(["workbase", "default", "primary", "--json"], parent, env),
|
|
434
|
+
)
|
|
435
|
+
expect(
|
|
436
|
+
parseJson(await runCli(["task", "list", "--json"], parent, env)),
|
|
437
|
+
).toHaveLength(1)
|
|
438
|
+
const explicitOutside = await runCli(
|
|
439
|
+
["task", "list", "--cwd", parent, "--json"],
|
|
440
|
+
projectRoot,
|
|
441
|
+
env,
|
|
442
|
+
)
|
|
443
|
+
expect(explicitOutside.exitCode).toBe(1)
|
|
444
|
+
expect(explicitOutside.stderr).toBe("")
|
|
445
|
+
expect(JSON.parse(explicitOutside.stdout).error.message).toContain(
|
|
446
|
+
"No Agency workbase found from",
|
|
447
|
+
)
|
|
358
448
|
})
|
|
359
449
|
|
|
360
450
|
test("exports equivalent JSON and JSONL graph contracts", async () => {
|
|
@@ -684,10 +774,20 @@ status: open
|
|
|
684
774
|
await runCli(["phase", "list", "ship", "--json"], root),
|
|
685
775
|
)
|
|
686
776
|
expect(phaseList.map((phase: { id: string }) => phase.id)).toEqual([
|
|
687
|
-
"implement",
|
|
688
777
|
"prepare",
|
|
778
|
+
"implement",
|
|
689
779
|
"release",
|
|
690
780
|
])
|
|
781
|
+
const phaseTable = await runCli(
|
|
782
|
+
["phase", "list", "ship", "--repository", "agency", "--no-pr"],
|
|
783
|
+
root,
|
|
784
|
+
)
|
|
785
|
+
expect(phaseTable.stdout).toMatch(
|
|
786
|
+
/PHASE\s+PARENT\s+STATUS\s+READINESS\s+REPOSITORIES\s+BRANCH/,
|
|
787
|
+
)
|
|
788
|
+
expect(phaseTable.stdout.indexOf("prepare")).toBeLessThan(
|
|
789
|
+
phaseTable.stdout.indexOf("implement"),
|
|
790
|
+
)
|
|
691
791
|
|
|
692
792
|
const phaseShow = parseJson(
|
|
693
793
|
await runCli(["phase", "show", "ship", "release", "--json"], root),
|
package/src/commands/context.ts
CHANGED
|
@@ -26,4 +26,7 @@ defaults to the current directory and may be an entity path or task ID.
|
|
|
26
26
|
Options:
|
|
27
27
|
--json Output a versioned machine result
|
|
28
28
|
--compact Omit prose bodies and low-level Git details
|
|
29
|
+
--epic <id> Select an epic
|
|
30
|
+
--task <id> Select a task
|
|
31
|
+
--phase <id> Select a phase together with --task
|
|
29
32
|
`
|
|
@@ -100,4 +100,26 @@ describe("epic command", () => {
|
|
|
100
100
|
},
|
|
101
101
|
})
|
|
102
102
|
})
|
|
103
|
+
|
|
104
|
+
test("renders a readable operational table", async () => {
|
|
105
|
+
await runTestEffect(
|
|
106
|
+
epic({
|
|
107
|
+
subcommand: "create",
|
|
108
|
+
args: ["example"],
|
|
109
|
+
ticketUrl: "https://example.com/epic",
|
|
110
|
+
repos: ["agency:main"],
|
|
111
|
+
cwd: root,
|
|
112
|
+
silent: true,
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const logs = await captureLogs(() =>
|
|
117
|
+
runTestEffect(epic({ subcommand: "list", args: [], cwd: root })),
|
|
118
|
+
)
|
|
119
|
+
expect(logs[0]).toContain(
|
|
120
|
+
"EPIC STATUS READINESS REPOSITORIES PR WORKTREE",
|
|
121
|
+
)
|
|
122
|
+
expect(logs[0]).toContain("example open waiting agency")
|
|
123
|
+
expect(logs[0]).not.toMatch(/[\u{e000}-\u{f8ff}]/u)
|
|
124
|
+
})
|
|
103
125
|
})
|
package/src/commands/epic.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { Effect } from "effect"
|
|
|
2
2
|
import type { BaseCommandOptions } from "../utils/command"
|
|
3
3
|
import { EpicService } from "../services/EpicService"
|
|
4
4
|
import { createLoggers } from "../utils/effect"
|
|
5
|
+
import { formatTable } from "../utils/table"
|
|
6
|
+
import { getWorkViews } from "../work-view"
|
|
5
7
|
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
6
8
|
|
|
7
9
|
interface EpicOptions extends BaseCommandOptions {
|
|
@@ -11,6 +13,11 @@ interface EpicOptions extends BaseCommandOptions {
|
|
|
11
13
|
readonly description?: string
|
|
12
14
|
readonly repos?: readonly string[]
|
|
13
15
|
readonly json?: boolean
|
|
16
|
+
readonly statuses?: readonly string[]
|
|
17
|
+
readonly repositories?: readonly string[]
|
|
18
|
+
readonly ready?: boolean
|
|
19
|
+
readonly blocked?: boolean
|
|
20
|
+
readonly pr?: boolean
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export const epic = (options: EpicOptions) =>
|
|
@@ -47,16 +54,40 @@ export const epic = (options: EpicOptions) =>
|
|
|
47
54
|
|
|
48
55
|
case "list": {
|
|
49
56
|
const records = yield* epics.list(cwd)
|
|
57
|
+
const { epicRows } = yield* getWorkViews({
|
|
58
|
+
cwd,
|
|
59
|
+
statuses: options.statuses,
|
|
60
|
+
repositories: options.repositories,
|
|
61
|
+
ready: options.ready,
|
|
62
|
+
blocked: options.blocked,
|
|
63
|
+
pr: options.pr,
|
|
64
|
+
})
|
|
65
|
+
const ordered = epicRows.flatMap((row) => {
|
|
66
|
+
const record = records.find((item) => item.id === row.key)
|
|
67
|
+
return record ? [record] : []
|
|
68
|
+
})
|
|
50
69
|
if (options.json) {
|
|
51
70
|
log(
|
|
52
71
|
JSON.stringify(
|
|
53
|
-
|
|
72
|
+
ordered.map(({ content: _, ...record }) => record),
|
|
54
73
|
null,
|
|
55
74
|
2,
|
|
56
75
|
),
|
|
57
76
|
)
|
|
58
77
|
} else {
|
|
59
|
-
|
|
78
|
+
log(
|
|
79
|
+
formatTable(
|
|
80
|
+
["EPIC", "STATUS", "READINESS", "REPOSITORIES", "PR", "WORKTREE"],
|
|
81
|
+
epicRows.map((row) => [
|
|
82
|
+
row.id,
|
|
83
|
+
row.status,
|
|
84
|
+
row.readiness,
|
|
85
|
+
row.repositories,
|
|
86
|
+
row.pr,
|
|
87
|
+
row.worktree,
|
|
88
|
+
]),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
60
91
|
}
|
|
61
92
|
return
|
|
62
93
|
}
|
|
@@ -98,4 +129,9 @@ Create options:
|
|
|
98
129
|
|
|
99
130
|
Options:
|
|
100
131
|
--json Output results as JSON
|
|
132
|
+
--status <status> Filter list by status; repeatable
|
|
133
|
+
--repository <alias> Filter list by repository; repeatable
|
|
134
|
+
--ready Include only ready epics
|
|
135
|
+
--blocked Include only blocked epics
|
|
136
|
+
--pr / --no-pr Filter by recorded PR presence
|
|
101
137
|
`
|
package/src/commands/init.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
|
+
import { resolve } from "node:path"
|
|
2
3
|
import type { BaseCommandOptions } from "../utils/command"
|
|
3
4
|
import { WorkbaseService } from "../services/WorkbaseService"
|
|
4
5
|
import { createLoggers } from "../utils/effect"
|
|
@@ -11,8 +12,9 @@ export const init = (options: InitOptions = {}) =>
|
|
|
11
12
|
Effect.gen(function* () {
|
|
12
13
|
const workbase = yield* WorkbaseService
|
|
13
14
|
const { log } = createLoggers(options)
|
|
15
|
+
const cwd = options.cwd ?? process.cwd()
|
|
14
16
|
const root = yield* workbase.initialize(
|
|
15
|
-
options.path
|
|
17
|
+
options.path ? resolve(cwd, options.path) : cwd,
|
|
16
18
|
)
|
|
17
19
|
log(
|
|
18
20
|
options.json
|
package/src/commands/phase.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { Effect } from "effect"
|
|
|
2
2
|
import type { BaseCommandOptions } from "../utils/command"
|
|
3
3
|
import { PhaseService } from "../services/PhaseService"
|
|
4
4
|
import { createLoggers } from "../utils/effect"
|
|
5
|
+
import { formatTable } from "../utils/table"
|
|
6
|
+
import { getWorkViews } from "../work-view"
|
|
5
7
|
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
6
8
|
|
|
7
9
|
interface PhaseOptions extends BaseCommandOptions {
|
|
@@ -15,6 +17,11 @@ interface PhaseOptions extends BaseCommandOptions {
|
|
|
15
17
|
readonly dependsOn?: readonly string[]
|
|
16
18
|
readonly firstPhase?: string
|
|
17
19
|
readonly json?: boolean
|
|
20
|
+
readonly statuses?: readonly string[]
|
|
21
|
+
readonly repositories?: readonly string[]
|
|
22
|
+
readonly ready?: boolean
|
|
23
|
+
readonly blocked?: boolean
|
|
24
|
+
readonly pr?: boolean
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export const phase = (options: PhaseOptions) =>
|
|
@@ -64,15 +71,53 @@ export const phase = (options: PhaseOptions) =>
|
|
|
64
71
|
case "list": {
|
|
65
72
|
if (!taskId) return yield* Effect.fail(new Error("Task ID is required"))
|
|
66
73
|
const records = yield* phases.list(taskId, cwd)
|
|
74
|
+
const { phaseRows } = yield* getWorkViews({
|
|
75
|
+
cwd,
|
|
76
|
+
statuses: options.statuses,
|
|
77
|
+
repositories: options.repositories,
|
|
78
|
+
ready: options.ready,
|
|
79
|
+
blocked: options.blocked,
|
|
80
|
+
pr: options.pr,
|
|
81
|
+
})
|
|
82
|
+
const rows = phaseRows.filter((row) => row.parent === taskId)
|
|
83
|
+
const ordered = rows.flatMap((row) => {
|
|
84
|
+
const record = records.find((item) => item.id === row.id)
|
|
85
|
+
return record ? [record] : []
|
|
86
|
+
})
|
|
67
87
|
if (options.json) {
|
|
68
88
|
log(
|
|
69
89
|
JSON.stringify(
|
|
70
|
-
|
|
90
|
+
ordered.map(({ content: _, ...record }) => record),
|
|
71
91
|
null,
|
|
72
92
|
2,
|
|
73
93
|
),
|
|
74
94
|
)
|
|
75
|
-
} else
|
|
95
|
+
} else {
|
|
96
|
+
log(
|
|
97
|
+
formatTable(
|
|
98
|
+
[
|
|
99
|
+
"PHASE",
|
|
100
|
+
"PARENT",
|
|
101
|
+
"STATUS",
|
|
102
|
+
"READINESS",
|
|
103
|
+
"REPOSITORIES",
|
|
104
|
+
"BRANCH",
|
|
105
|
+
"PR",
|
|
106
|
+
"WORKTREE",
|
|
107
|
+
],
|
|
108
|
+
rows.map((row) => [
|
|
109
|
+
row.id,
|
|
110
|
+
row.parent,
|
|
111
|
+
row.status,
|
|
112
|
+
row.readiness,
|
|
113
|
+
row.repositories,
|
|
114
|
+
row.branch,
|
|
115
|
+
row.pr,
|
|
116
|
+
row.worktree,
|
|
117
|
+
]),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
76
121
|
return
|
|
77
122
|
}
|
|
78
123
|
case "show": {
|
|
@@ -139,4 +184,9 @@ Create options:
|
|
|
139
184
|
|
|
140
185
|
Options:
|
|
141
186
|
--json Output results as JSON
|
|
187
|
+
--status <status> Filter list by status; repeatable
|
|
188
|
+
--repository <alias> Filter list by repository; repeatable
|
|
189
|
+
--ready Include only ready phases
|
|
190
|
+
--blocked Include only blocked phases
|
|
191
|
+
--pr / --no-pr Filter by recorded PR presence
|
|
142
192
|
`
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
runTestEffect,
|
|
9
9
|
} from "../test-utils"
|
|
10
10
|
import { status } from "./status"
|
|
11
|
+
import { task } from "./task"
|
|
11
12
|
|
|
12
13
|
describe("status command", () => {
|
|
13
14
|
let root: string
|
|
@@ -44,4 +45,47 @@ describe("status command", () => {
|
|
|
44
45
|
},
|
|
45
46
|
])
|
|
46
47
|
})
|
|
48
|
+
|
|
49
|
+
test("renders and filters the execution dashboard", async () => {
|
|
50
|
+
await runTestEffect(
|
|
51
|
+
task({
|
|
52
|
+
subcommand: "create",
|
|
53
|
+
args: ["example"],
|
|
54
|
+
repo: "agency",
|
|
55
|
+
branch: "feat/example",
|
|
56
|
+
base: "main",
|
|
57
|
+
cwd: root,
|
|
58
|
+
silent: true,
|
|
59
|
+
}),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const logs = await captureLogs(() =>
|
|
63
|
+
runTestEffect(
|
|
64
|
+
status({ cwd: root, repositories: ["agency"], ready: true, pr: false }),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
expect(logs).toContain("Repositories: 1")
|
|
68
|
+
expect(logs.at(-1)).toContain(
|
|
69
|
+
"KIND WORK PARENT STATUS READINESS REPOSITORIES BRANCH",
|
|
70
|
+
)
|
|
71
|
+
expect(logs.at(-1)).toContain(
|
|
72
|
+
"task example - open ready agency feat/example absent absent",
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test("reports validation issues without requiring a decodable graph", async () => {
|
|
77
|
+
await mkdir(join(root, "tasks/broken"), { recursive: true })
|
|
78
|
+
await Bun.write(
|
|
79
|
+
join(root, "tasks/broken/TASK.md"),
|
|
80
|
+
"---\nrepo: agency\nstatus: invalid\n---\n",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const logs = await captureLogs(() =>
|
|
84
|
+
runTestEffect(status({ cwd: root, json: true })),
|
|
85
|
+
)
|
|
86
|
+
const output = JSON.parse(logs[0]!)
|
|
87
|
+
expect(output.valid).toBe(false)
|
|
88
|
+
expect(output.issues.length).toBeGreaterThan(0)
|
|
89
|
+
expect(output.work).toEqual([])
|
|
90
|
+
})
|
|
47
91
|
})
|
package/src/commands/status.ts
CHANGED
|
@@ -3,9 +3,16 @@ import type { BaseCommandOptions } from "../utils/command"
|
|
|
3
3
|
import { WorkbaseService } from "../services/WorkbaseService"
|
|
4
4
|
import { RepositoryService } from "../services/RepositoryService"
|
|
5
5
|
import { createLoggers } from "../utils/effect"
|
|
6
|
+
import { formatTable } from "../utils/table"
|
|
7
|
+
import { getWorkViews } from "../work-view"
|
|
6
8
|
|
|
7
9
|
interface StatusOptions extends BaseCommandOptions {
|
|
8
10
|
readonly json?: boolean
|
|
11
|
+
readonly statuses?: readonly string[]
|
|
12
|
+
readonly repositories?: readonly string[]
|
|
13
|
+
readonly ready?: boolean
|
|
14
|
+
readonly blocked?: boolean
|
|
15
|
+
readonly pr?: boolean
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
export const status = (options: StatusOptions = {}) =>
|
|
@@ -16,7 +23,17 @@ export const status = (options: StatusOptions = {}) =>
|
|
|
16
23
|
const cwd = options.cwd ?? process.cwd()
|
|
17
24
|
const report = yield* workbase.validate(cwd)
|
|
18
25
|
const repos = yield* repositories.list(report.root)
|
|
19
|
-
const
|
|
26
|
+
const executionRows = report.valid
|
|
27
|
+
? (yield* getWorkViews({
|
|
28
|
+
cwd,
|
|
29
|
+
statuses: options.statuses,
|
|
30
|
+
repositories: options.repositories,
|
|
31
|
+
ready: options.ready,
|
|
32
|
+
blocked: options.blocked,
|
|
33
|
+
pr: options.pr,
|
|
34
|
+
})).executionRows
|
|
35
|
+
: []
|
|
36
|
+
const data = { ...report, repositories: repos, work: executionRows }
|
|
20
37
|
|
|
21
38
|
if (options.json) {
|
|
22
39
|
log(JSON.stringify(data, null, 2))
|
|
@@ -31,6 +48,33 @@ export const status = (options: StatusOptions = {}) =>
|
|
|
31
48
|
log(
|
|
32
49
|
`Validation: ${report.valid ? "valid" : `${report.issues.length} issues`}`,
|
|
33
50
|
)
|
|
51
|
+
log("")
|
|
52
|
+
log(
|
|
53
|
+
formatTable(
|
|
54
|
+
[
|
|
55
|
+
"KIND",
|
|
56
|
+
"WORK",
|
|
57
|
+
"PARENT",
|
|
58
|
+
"STATUS",
|
|
59
|
+
"READINESS",
|
|
60
|
+
"REPOSITORIES",
|
|
61
|
+
"BRANCH",
|
|
62
|
+
"PR",
|
|
63
|
+
"WORKTREE",
|
|
64
|
+
],
|
|
65
|
+
executionRows.map((row) => [
|
|
66
|
+
row.kind,
|
|
67
|
+
row.kind === "phase" ? row.key : row.id,
|
|
68
|
+
row.parent,
|
|
69
|
+
row.status,
|
|
70
|
+
row.readiness,
|
|
71
|
+
row.repositories,
|
|
72
|
+
row.branch,
|
|
73
|
+
row.pr,
|
|
74
|
+
row.worktree,
|
|
75
|
+
]),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
34
78
|
})
|
|
35
79
|
|
|
36
80
|
export const help = `
|
|
@@ -39,5 +83,10 @@ Usage: agency status [options]
|
|
|
39
83
|
Show workbase repository, entity, and validation status.
|
|
40
84
|
|
|
41
85
|
Options:
|
|
42
|
-
--json
|
|
86
|
+
--json Output status as JSON
|
|
87
|
+
--status <status> Filter by status; repeatable
|
|
88
|
+
--repository <alias> Filter by repository; repeatable
|
|
89
|
+
--ready Include only ready work
|
|
90
|
+
--blocked Include only blocked work
|
|
91
|
+
--pr / --no-pr Filter by recorded PR presence
|
|
43
92
|
`
|
|
@@ -124,6 +124,26 @@ describe("task and phase command JSON output", () => {
|
|
|
124
124
|
})
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
+
test("renders task and phase operational tables", async () => {
|
|
128
|
+
const taskLogs = await captureLogs(() =>
|
|
129
|
+
runTestEffect(task({ subcommand: "list", args: [], cwd: root })),
|
|
130
|
+
)
|
|
131
|
+
expect(taskLogs[0]).toContain(
|
|
132
|
+
"TASK PARENT STATUS READINESS REPOSITORIES BRANCH",
|
|
133
|
+
)
|
|
134
|
+
expect(taskLogs[0]).toContain("multi - open ready")
|
|
135
|
+
|
|
136
|
+
const phaseLogs = await captureLogs(() =>
|
|
137
|
+
runTestEffect(phase({ subcommand: "list", args: ["multi"], cwd: root })),
|
|
138
|
+
)
|
|
139
|
+
expect(phaseLogs[0]).toContain(
|
|
140
|
+
"PHASE PARENT STATUS READINESS REPOSITORIES BRANCH",
|
|
141
|
+
)
|
|
142
|
+
expect(phaseLogs[0]).toContain(
|
|
143
|
+
"first multi open ready agency task/first absent absent",
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
|
|
127
147
|
test("outputs created task and phase records as JSON", async () => {
|
|
128
148
|
const taskLogs = await captureLogs(() =>
|
|
129
149
|
runTestEffect(
|
package/src/commands/task.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { createLoggers } from "../utils/effect"
|
|
|
8
8
|
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
9
9
|
import { WorkbaseService } from "../services/WorkbaseService"
|
|
10
10
|
import { choose } from "../utils/chooser"
|
|
11
|
+
import { formatTable } from "../utils/table"
|
|
12
|
+
import { getWorkViews } from "../work-view"
|
|
11
13
|
|
|
12
14
|
interface TaskOptions extends BaseCommandOptions {
|
|
13
15
|
readonly subcommand?: string
|
|
@@ -21,6 +23,11 @@ interface TaskOptions extends BaseCommandOptions {
|
|
|
21
23
|
readonly base?: string
|
|
22
24
|
readonly multiPhase?: boolean
|
|
23
25
|
readonly json?: boolean
|
|
26
|
+
readonly statuses?: readonly string[]
|
|
27
|
+
readonly repositories?: readonly string[]
|
|
28
|
+
readonly ready?: boolean
|
|
29
|
+
readonly blocked?: boolean
|
|
30
|
+
readonly pr?: boolean
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export interface TaskInteraction {
|
|
@@ -208,16 +215,51 @@ export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
|
|
|
208
215
|
}
|
|
209
216
|
case "list": {
|
|
210
217
|
const records = yield* tasks.list(cwd)
|
|
218
|
+
const { taskRows } = yield* getWorkViews({
|
|
219
|
+
cwd,
|
|
220
|
+
statuses: options.statuses,
|
|
221
|
+
repositories: options.repositories,
|
|
222
|
+
ready: options.ready,
|
|
223
|
+
blocked: options.blocked,
|
|
224
|
+
pr: options.pr,
|
|
225
|
+
})
|
|
226
|
+
const ordered = taskRows.flatMap((row) => {
|
|
227
|
+
const record = records.find((item) => item.id === row.key)
|
|
228
|
+
return record ? [record] : []
|
|
229
|
+
})
|
|
211
230
|
if (options.json) {
|
|
212
231
|
log(
|
|
213
232
|
JSON.stringify(
|
|
214
|
-
|
|
233
|
+
ordered.map(({ content: _, ...record }) => record),
|
|
215
234
|
null,
|
|
216
235
|
2,
|
|
217
236
|
),
|
|
218
237
|
)
|
|
219
238
|
} else {
|
|
220
|
-
|
|
239
|
+
log(
|
|
240
|
+
formatTable(
|
|
241
|
+
[
|
|
242
|
+
"TASK",
|
|
243
|
+
"PARENT",
|
|
244
|
+
"STATUS",
|
|
245
|
+
"READINESS",
|
|
246
|
+
"REPOSITORIES",
|
|
247
|
+
"BRANCH",
|
|
248
|
+
"PR",
|
|
249
|
+
"WORKTREE",
|
|
250
|
+
],
|
|
251
|
+
taskRows.map((row) => [
|
|
252
|
+
row.id,
|
|
253
|
+
row.parent,
|
|
254
|
+
row.status,
|
|
255
|
+
row.readiness,
|
|
256
|
+
row.repositories,
|
|
257
|
+
row.branch,
|
|
258
|
+
row.pr,
|
|
259
|
+
row.worktree,
|
|
260
|
+
]),
|
|
261
|
+
),
|
|
262
|
+
)
|
|
221
263
|
}
|
|
222
264
|
return
|
|
223
265
|
}
|
|
@@ -286,4 +328,9 @@ through task new, which fails when --no-input is set or no TTY is available.
|
|
|
286
328
|
Options:
|
|
287
329
|
--json Output results as JSON
|
|
288
330
|
--no-input Never open interactive task creation
|
|
331
|
+
--status <status> Filter list by status; repeatable
|
|
332
|
+
--repository <alias> Filter list by repository; repeatable
|
|
333
|
+
--ready Include only ready tasks
|
|
334
|
+
--blocked Include only blocked tasks
|
|
335
|
+
--pr / --no-pr Filter by recorded PR presence
|
|
289
336
|
`
|
|
@@ -80,6 +80,7 @@ pr: null
|
|
|
80
80
|
: Effect.succeed(path)
|
|
81
81
|
},
|
|
82
82
|
listRegistered: () => Effect.succeed(["/first", "/selected"]),
|
|
83
|
+
getDefault: () => Effect.succeed(undefined),
|
|
83
84
|
validate: (path: string) =>
|
|
84
85
|
Effect.succeed({
|
|
85
86
|
root: path,
|
|
@@ -116,6 +117,7 @@ pr: null
|
|
|
116
117
|
message: "No Agency workbase found from /outside",
|
|
117
118
|
}),
|
|
118
119
|
listRegistered: () => Effect.succeed(["/selected"]),
|
|
120
|
+
getDefault: () => Effect.succeed(undefined),
|
|
119
121
|
}
|
|
120
122
|
const fs = {
|
|
121
123
|
runCommand: () => Effect.fail(new Error("unexpected fzf probe")),
|
|
@@ -98,6 +98,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
98
98
|
})
|
|
99
99
|
: Effect.succeed("/workbase"),
|
|
100
100
|
listRegistered: () => Effect.succeed(options.registeredWorkbases ?? []),
|
|
101
|
+
getDefault: () => Effect.succeed(undefined),
|
|
101
102
|
loadConfig: () =>
|
|
102
103
|
Effect.succeed({
|
|
103
104
|
root: "/workbase",
|
package/src/commands/work.ts
CHANGED
|
@@ -297,9 +297,13 @@ export const workPrepare = (options: WorkOptions = {}) =>
|
|
|
297
297
|
const isDirectory = yield* fs.isDirectory(targetPath)
|
|
298
298
|
const root = yield* workbase.discover(isDirectory ? targetPath : cwd)
|
|
299
299
|
|
|
300
|
-
let taskId
|
|
301
|
-
let phaseId
|
|
302
|
-
if (
|
|
300
|
+
let taskId = options.taskId
|
|
301
|
+
let phaseId = options.phaseId
|
|
302
|
+
if (taskId) {
|
|
303
|
+
const task = yield* tasks.show(taskId, root)
|
|
304
|
+
taskId = task.id
|
|
305
|
+
if (phaseId) phaseId = (yield* phases.show(task.id, phaseId, root)).id
|
|
306
|
+
} else if (options.directory && !isDirectory) {
|
|
303
307
|
const task = yield* tasks.show(options.directory, root)
|
|
304
308
|
taskId = task.id
|
|
305
309
|
} else {
|
|
@@ -353,12 +357,16 @@ launching an agent or changing lifecycle status. --dry-run reports planned Git
|
|
|
353
357
|
changes without fetching, creating branches, or creating worktrees.
|
|
354
358
|
|
|
355
359
|
Options:
|
|
356
|
-
--epic <id>
|
|
360
|
+
--epic <id> Work on an epic
|
|
361
|
+
--task <id> Work on a task
|
|
362
|
+
--phase <id> Work on a phase selected with --task
|
|
363
|
+
--workbase <target> Select a workbase by ID, name, or path
|
|
364
|
+
--cwd <path> Resolve context from a specific directory
|
|
357
365
|
--opencode Require OpenCode
|
|
358
366
|
--claude Require Claude Code
|
|
359
367
|
--force Override readiness and terminal-state guards
|
|
360
|
-
--no-input
|
|
368
|
+
--no-input Never open an interactive selector
|
|
361
369
|
|
|
362
|
-
Without interactive input, provide
|
|
363
|
-
|
|
370
|
+
Without interactive input, provide an explicit workbase or cwd and an entity
|
|
371
|
+
selector. Workbase and target selection otherwise fail.
|
|
364
372
|
`
|