@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.71.17",
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({
@@ -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
- ordered.map(({ content: _, ...record }) => record),
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: statuses.filter((status) => status === "open").length,
28
- working: statuses.filter((status) => status === "working").length,
29
- delegated: statuses.filter((status) => status === "delegated").length,
30
- done: statuses.filter((status) => status === "done").length,
31
- dropped: statuses.filter((status) => status === "dropped").length,
32
- terminal: statuses.filter(isTerminalStatus).length,
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
- statuses.length === 0
39
+ counts.total === 0
36
40
  ? "open"
37
- : statuses.every((value) => value === "done")
41
+ : counts.done === counts.total
38
42
  ? "done"
39
- : statuses.every(isTerminalStatus)
43
+ : counts.terminal === counts.total
40
44
  ? "dropped"
41
- : statuses.includes("working")
45
+ : counts.working > 0
42
46
  ? "working"
43
- : statuses.includes("delegated")
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
- return "phases" in task.data
305
- ? task.data.phases.map(
306
- (item) =>
307
- phases.get(`${taskId}/${item.id}`)?.data.status ?? "open",
308
- )
309
- : [task.data.status]
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 taskStatuses = new Map<string, WorkStatus>()
317
+ const taskStatusCache = new Map<string, WorkStatus>()
312
318
  const taskStatus = (taskId: string) => {
313
- const cached = taskStatuses.get(taskId)
319
+ const cached = taskStatusCache.get(taskId)
314
320
  if (cached) return cached
315
321
  const status = aggregateProgress(taskLeafStatuses(taskId)).status
316
- taskStatuses.set(taskId, status)
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