@markjaquith/agency 2.71.17 → 2.71.18
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 +2 -1
- package/src/commands/epic.test.ts +29 -0
- package/src/commands/epic.ts +7 -6
- package/src/readiness.ts +15 -11
- package/src/services/GraphService.ts +15 -9
- package/src/work-view.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "2.71.
|
|
3
|
+
"version": "2.71.18",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"benchmark:context": "bun scripts/benchmark-context.ts",
|
|
76
76
|
"benchmark:claim": "bun scripts/benchmark-claim.ts",
|
|
77
77
|
"benchmark:finish": "bun scripts/benchmark-finish.ts",
|
|
78
|
+
"benchmark:epic": "bun scripts/benchmark-epic.ts",
|
|
78
79
|
"benchmark:phase": "bun scripts/benchmark-phase.ts",
|
|
79
80
|
"benchmark:push": "bun scripts/benchmark-push.ts",
|
|
80
81
|
"benchmark:archive": "bun scripts/benchmark-archive.ts",
|
|
@@ -111,6 +111,35 @@ describe("epic command", () => {
|
|
|
111
111
|
})
|
|
112
112
|
})
|
|
113
113
|
|
|
114
|
+
test("reads each epic document once when listing JSON", async () => {
|
|
115
|
+
await runTestEffect(
|
|
116
|
+
epic({
|
|
117
|
+
subcommand: "create",
|
|
118
|
+
args: ["example"],
|
|
119
|
+
ticketUrl: "https://example.com/epic",
|
|
120
|
+
repos: ["agency:main"],
|
|
121
|
+
cwd: root,
|
|
122
|
+
silent: true,
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
const original = Bun.file
|
|
126
|
+
let reads = 0
|
|
127
|
+
Bun.file = ((path: Parameters<typeof Bun.file>[0], ...args: never[]) => {
|
|
128
|
+
if (String(path).endsWith("/epics/example/EPIC.md")) reads += 1
|
|
129
|
+
return original(path as unknown as string, ...args)
|
|
130
|
+
}) as typeof Bun.file
|
|
131
|
+
try {
|
|
132
|
+
await captureLogs(() =>
|
|
133
|
+
runTestEffect(
|
|
134
|
+
epic({ subcommand: "list", args: [], cwd: root, json: true }),
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
} finally {
|
|
138
|
+
Bun.file = original
|
|
139
|
+
}
|
|
140
|
+
expect(reads).toBe(2)
|
|
141
|
+
})
|
|
142
|
+
|
|
114
143
|
test("renders a readable operational table", async () => {
|
|
115
144
|
await runTestEffect(
|
|
116
145
|
epic({
|
package/src/commands/epic.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
|
+
import { join } from "node:path"
|
|
2
3
|
import type { BaseCommandOptions } from "../utils/command"
|
|
3
4
|
import { EpicService } from "../services/EpicService"
|
|
4
5
|
import { createLoggers } from "../utils/effect"
|
|
@@ -71,7 +72,6 @@ export const epic = (options: EpicOptions, work: StartWork = startWork) =>
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
case "list": {
|
|
74
|
-
const records = yield* epics.list(cwd)
|
|
75
75
|
const { epicRows } = yield* getWorkViews({
|
|
76
76
|
cwd,
|
|
77
77
|
statuses: options.statuses,
|
|
@@ -80,14 +80,15 @@ export const epic = (options: EpicOptions, work: StartWork = startWork) =>
|
|
|
80
80
|
blocked: options.blocked,
|
|
81
81
|
pr: options.pr,
|
|
82
82
|
})
|
|
83
|
-
const ordered = epicRows.flatMap((row) => {
|
|
84
|
-
const record = records.find((item) => item.id === row.key)
|
|
85
|
-
return record ? [record] : []
|
|
86
|
-
})
|
|
87
83
|
if (options.json) {
|
|
88
84
|
log(
|
|
89
85
|
JSON.stringify(
|
|
90
|
-
|
|
86
|
+
epicRows.map((row) => ({
|
|
87
|
+
id: row.id,
|
|
88
|
+
path: join(cwd, "epics", row.id, "EPIC.md"),
|
|
89
|
+
revision: row.revision,
|
|
90
|
+
data: row.data,
|
|
91
|
+
})),
|
|
91
92
|
null,
|
|
92
93
|
2,
|
|
93
94
|
),
|
package/src/readiness.ts
CHANGED
|
@@ -24,23 +24,27 @@ export const canTransitionStatus = (from: WorkStatus, to: WorkStatus) =>
|
|
|
24
24
|
export const aggregateProgress = (statuses: readonly WorkStatus[]) => {
|
|
25
25
|
const counts = {
|
|
26
26
|
total: statuses.length,
|
|
27
|
-
open:
|
|
28
|
-
working:
|
|
29
|
-
delegated:
|
|
30
|
-
done:
|
|
31
|
-
dropped:
|
|
32
|
-
terminal:
|
|
27
|
+
open: 0,
|
|
28
|
+
working: 0,
|
|
29
|
+
delegated: 0,
|
|
30
|
+
done: 0,
|
|
31
|
+
dropped: 0,
|
|
32
|
+
terminal: 0,
|
|
33
|
+
}
|
|
34
|
+
for (const status of statuses) {
|
|
35
|
+
counts[status] += 1
|
|
36
|
+
if (isTerminalStatus(status)) counts.terminal += 1
|
|
33
37
|
}
|
|
34
38
|
const status: WorkStatus =
|
|
35
|
-
|
|
39
|
+
counts.total === 0
|
|
36
40
|
? "open"
|
|
37
|
-
:
|
|
41
|
+
: counts.done === counts.total
|
|
38
42
|
? "done"
|
|
39
|
-
:
|
|
43
|
+
: counts.terminal === counts.total
|
|
40
44
|
? "dropped"
|
|
41
|
-
:
|
|
45
|
+
: counts.working > 0
|
|
42
46
|
? "working"
|
|
43
|
-
:
|
|
47
|
+
: counts.delegated > 0
|
|
44
48
|
? "delegated"
|
|
45
49
|
: "open"
|
|
46
50
|
return { status, ...counts }
|
|
@@ -298,22 +298,28 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
298
298
|
}
|
|
299
299
|
const taskDependencies = (taskId: string) =>
|
|
300
300
|
taskDeclarations.get(taskId)?.dependsOn ?? []
|
|
301
|
+
const taskLeafStatusCache = new Map<string, WorkStatus[]>()
|
|
301
302
|
const taskLeafStatuses = (taskId: string): WorkStatus[] => {
|
|
303
|
+
const cached = taskLeafStatusCache.get(taskId)
|
|
304
|
+
if (cached) return cached
|
|
302
305
|
const task = tasks.get(taskId)
|
|
303
306
|
if (!task) return []
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
307
|
+
const statuses =
|
|
308
|
+
"phases" in task.data
|
|
309
|
+
? task.data.phases.map(
|
|
310
|
+
(item) =>
|
|
311
|
+
phases.get(`${taskId}/${item.id}`)?.data.status ?? "open",
|
|
312
|
+
)
|
|
313
|
+
: [task.data.status]
|
|
314
|
+
taskLeafStatusCache.set(taskId, statuses)
|
|
315
|
+
return statuses
|
|
310
316
|
}
|
|
311
|
-
const
|
|
317
|
+
const taskStatusCache = new Map<string, WorkStatus>()
|
|
312
318
|
const taskStatus = (taskId: string) => {
|
|
313
|
-
const cached =
|
|
319
|
+
const cached = taskStatusCache.get(taskId)
|
|
314
320
|
if (cached) return cached
|
|
315
321
|
const status = aggregateProgress(taskLeafStatuses(taskId)).status
|
|
316
|
-
|
|
322
|
+
taskStatusCache.set(taskId, status)
|
|
317
323
|
return status
|
|
318
324
|
}
|
|
319
325
|
const dependencyBlockers = (
|
package/src/work-view.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface WorkViewRow {
|
|
|
34
34
|
readonly pr: string
|
|
35
35
|
readonly worktree: string
|
|
36
36
|
readonly hasPr: boolean
|
|
37
|
+
readonly data: Readonly<Record<string, unknown>>
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
const allowedStatuses = new Set<WorkStatus>([
|
|
@@ -122,6 +123,9 @@ const rowFor = (
|
|
|
122
123
|
hasPr: executions.some(
|
|
123
124
|
(execution) => "pr" in execution.data && Boolean(execution.data.pr),
|
|
124
125
|
),
|
|
126
|
+
data: Object.fromEntries(
|
|
127
|
+
Object.entries(node.data).filter(([key]) => key !== "sha256"),
|
|
128
|
+
),
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
|