@markjaquith/agency 2.71.5 → 2.71.6

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.5",
3
+ "version": "2.71.6",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -66,6 +66,7 @@
66
66
  "preuninstall": "bun scripts/install-pi-extension.ts uninstall",
67
67
  "benchmark:status": "bun scripts/benchmark-status.ts",
68
68
  "benchmark:workbase": "bun scripts/benchmark-workbase.ts",
69
+ "benchmark:doctor": "bun scripts/benchmark-doctor.ts",
69
70
  "benchmark:context": "bun scripts/benchmark-context.ts",
70
71
  "benchmark:sync": "bun scripts/benchmark-sync.ts",
71
72
  "benchmark:task": "bun scripts/benchmark-task.ts",
@@ -100,10 +100,6 @@ status: open
100
100
  id: "ref.agency.main",
101
101
  status: "pass",
102
102
  }),
103
- expect.objectContaining({
104
- id: "worktree.task.example",
105
- status: "pass",
106
- }),
107
103
  ]),
108
104
  )
109
105
  for (const check of report.checks) {
@@ -2,12 +2,11 @@ import { Effect, Either } from "effect"
2
2
  import { constants } from "node:fs"
3
3
  import { access } from "node:fs/promises"
4
4
  import { isAbsolute, join, resolve } from "node:path"
5
- import { EpicService } from "./EpicService"
6
5
  import { FileSystemService } from "./FileSystemService"
7
6
  import { IntegrationService } from "./IntegrationService"
8
- import { PhaseService } from "./PhaseService"
7
+ import type { PhaseRecord } from "./PhaseService"
9
8
  import { RepositoryService } from "./RepositoryService"
10
- import { TaskService } from "./TaskService"
9
+ import type { TaskRecord } from "./TaskService"
11
10
  import { WorkbaseService } from "./WorkbaseService"
12
11
  import { WorktreeService } from "./WorktreeService"
13
12
  import { VersionControlService } from "./VersionControlService"
@@ -80,12 +79,9 @@ export class DoctorService extends Effect.Service<DoctorService>()(
80
79
  sync: () => ({
81
80
  inspect: (startPath: string = process.cwd()) =>
82
81
  Effect.gen(function* () {
83
- const epics = yield* EpicService
84
82
  const fs = yield* FileSystemService
85
83
  const integrations = yield* IntegrationService
86
- const phases = yield* PhaseService
87
84
  const repositories = yield* RepositoryService
88
- const tasks = yield* TaskService
89
85
  const workbases = yield* WorkbaseService
90
86
  const worktrees = yield* WorktreeService
91
87
  const versionControl = yield* VersionControlService
@@ -105,49 +101,60 @@ export class DoctorService extends Effect.Service<DoctorService>()(
105
101
  : null,
106
102
  })
107
103
 
108
- const tool = function* (
104
+ const tool = (
109
105
  id: string,
110
106
  executable: string,
111
107
  level: DoctorCheckLevel,
112
108
  label: string,
113
- ) {
114
- const available = yield* executableAvailable(executable, root)
115
- add({
116
- id,
117
- category: id.startsWith("tool.") ? "tool" : "integration",
118
- level,
119
- status: available ? "pass" : "fail",
120
- message: available
121
- ? `${label} executable '${executable}' is available`
122
- : `${label} executable '${executable}' is unavailable`,
123
- remediation:
124
- level === "optional"
125
- ? `Install '${executable}' to enable ${label.toLowerCase()}, or leave it unavailable if unused.`
126
- : `Install '${executable}' and ensure it is executable on PATH.`,
109
+ ) =>
110
+ Effect.gen(function* () {
111
+ const available = yield* executableAvailable(executable, root)
112
+ add({
113
+ id,
114
+ category: id.startsWith("tool.") ? "tool" : "integration",
115
+ level,
116
+ status: available ? "pass" : "fail",
117
+ message: available
118
+ ? `${label} executable '${executable}' is available`
119
+ : `${label} executable '${executable}' is unavailable`,
120
+ remediation:
121
+ level === "optional"
122
+ ? `Install '${executable}' to enable ${label.toLowerCase()}, or leave it unavailable if unused.`
123
+ : `Install '${executable}' and ensure it is executable on PATH.`,
124
+ })
125
+ return available
127
126
  })
128
- return available
129
- }
130
127
 
131
- const gitAvailable = yield* tool("tool.git", "git", "error", "Git")
132
- const jjAvailable = yield* tool(
133
- "tool.jj",
134
- "jj",
135
- config.vcs === "jj" ? "error" : "optional",
136
- "Jujutsu",
128
+ const [gitAvailable, jjAvailable] = yield* Effect.all(
129
+ [
130
+ tool("tool.git", "git", "error", "Git"),
131
+ tool(
132
+ "tool.jj",
133
+ "jj",
134
+ config.vcs === "jj" ? "error" : "optional",
135
+ "Jujutsu",
136
+ ),
137
+ ],
138
+ { concurrency: "unbounded", batching: true },
137
139
  )
138
140
  const versionControlAvailable =
139
141
  gitAvailable && (config.vcs !== "jj" || jjAvailable)
140
- yield* tool(
141
- "capability.agent.opencode",
142
- "opencode",
143
- "optional",
144
- "OpenCode agent",
145
- )
146
- yield* tool(
147
- "capability.agent.claude",
148
- "claude",
149
- "optional",
150
- "Claude agent",
142
+ yield* Effect.all(
143
+ [
144
+ tool(
145
+ "capability.agent.opencode",
146
+ "opencode",
147
+ "optional",
148
+ "OpenCode agent",
149
+ ),
150
+ tool(
151
+ "capability.agent.claude",
152
+ "claude",
153
+ "optional",
154
+ "Claude agent",
155
+ ),
156
+ ],
157
+ { concurrency: "unbounded", batching: true },
151
158
  )
152
159
 
153
160
  const configuredCommands: readonly (readonly [
@@ -250,11 +257,16 @@ export class DoctorService extends Effect.Service<DoctorService>()(
250
257
  ]
251
258
  : []),
252
259
  ]
253
- for (const [id, command, label] of configuredCommands) {
254
- yield* tool(id, command[0]!, "error", label)
255
- }
260
+ yield* Effect.all(
261
+ configuredCommands.map(([id, command, label]) =>
262
+ tool(id, command[0]!, "error", label),
263
+ ),
264
+ { concurrency: 16, batching: true },
265
+ )
256
266
 
257
- const validation = yield* workbases.validate(root)
267
+ const validation = yield* workbases.validate(root, {
268
+ includeDocuments: true,
269
+ })
258
270
  add({
259
271
  id: "workbase.validation",
260
272
  category: "workbase",
@@ -267,7 +279,7 @@ export class DoctorService extends Effect.Service<DoctorService>()(
267
279
  "Run 'agency validate' and correct every reported issue.",
268
280
  })
269
281
 
270
- for (const [id, mode, level, label, remediation] of [
282
+ const permissionChecks = [
271
283
  [
272
284
  "permission.workbase.read",
273
285
  constants.R_OK,
@@ -282,8 +294,16 @@ export class DoctorService extends Effect.Service<DoctorService>()(
282
294
  "writable",
283
295
  `Grant the current user write access to ${root} before running mutation commands.`,
284
296
  ],
285
- ] as const) {
286
- const available = yield* permissionAvailable(root, mode)
297
+ ] as const
298
+ const permissionResults = yield* Effect.all(
299
+ permissionChecks.map(([, mode]) => permissionAvailable(root, mode)),
300
+ { concurrency: "unbounded", batching: true },
301
+ )
302
+ for (const [
303
+ index,
304
+ [id, , level, label, remediation],
305
+ ] of permissionChecks.entries()) {
306
+ const available = permissionResults[index]!
287
307
  add({
288
308
  id,
289
309
  category: "permission",
@@ -321,12 +341,12 @@ export class DoctorService extends Effect.Service<DoctorService>()(
321
341
  values.add(ref)
322
342
  refs.set(repo, values)
323
343
  }
324
- if (validation.valid) {
325
- for (const epic of yield* epics.list(root)) {
344
+ if (validation.valid && validation.documents) {
345
+ for (const epic of validation.documents.epics) {
326
346
  for (const reference of epic.data.repos)
327
347
  declareRef(reference.repo, reference.ref)
328
348
  }
329
- for (const task of yield* tasks.list(root)) {
349
+ for (const task of validation.documents.tasks) {
330
350
  if ("review" in task.data) {
331
351
  declareRef(task.data.review.repo, task.data.review.commit)
332
352
  reviewSources.push({
@@ -344,7 +364,9 @@ export class DoctorService extends Effect.Service<DoctorService>()(
344
364
  for (const reference of task.data.repos ?? [])
345
365
  declareRef(reference.repo, reference.ref)
346
366
  } else {
347
- for (const phase of yield* phases.list(task.id, root)) {
367
+ for (const phase of validation.documents.phasesByTask.get(
368
+ task.id,
369
+ ) ?? []) {
348
370
  declareRef(phase.data.repo, phase.data.base)
349
371
  for (const reference of phase.data.repos ?? [])
350
372
  declareRef(reference.repo, reference.ref)
@@ -442,7 +464,36 @@ export class DoctorService extends Effect.Service<DoctorService>()(
442
464
  }
443
465
 
444
466
  if (validation.valid && versionControlAvailable) {
445
- const inspected = yield* Effect.either(worktrees.list(root))
467
+ const tasks = validation.documents?.tasks.map(
468
+ (record) =>
469
+ ({ ...record, content: "", revision: "" }) satisfies TaskRecord,
470
+ )
471
+ const phasesByTask = validation.documents
472
+ ? new Map(
473
+ [...validation.documents.phasesByTask].map(
474
+ ([taskId, records]) =>
475
+ [
476
+ taskId,
477
+ records.map(
478
+ (record) =>
479
+ ({
480
+ ...record,
481
+ taskId,
482
+ content: "",
483
+ revision: "",
484
+ }) satisfies PhaseRecord,
485
+ ),
486
+ ] as const,
487
+ ),
488
+ )
489
+ : undefined
490
+ const inspected = yield* Effect.either(
491
+ worktrees.list(root, {
492
+ materializedOnly: true,
493
+ tasks,
494
+ phasesByTask,
495
+ }),
496
+ )
446
497
  if (Either.isLeft(inspected)) {
447
498
  add({
448
499
  id: "worktree.inspection",
@@ -56,6 +56,7 @@ export interface ValidationReport {
56
56
  readonly taskCount: number
57
57
  readonly phaseCount: number
58
58
  readonly valid: boolean
59
+ readonly documents?: ValidationDocuments
59
60
  }
60
61
 
61
62
  interface DocumentRecord<T> {
@@ -64,6 +65,15 @@ interface DocumentRecord<T> {
64
65
  readonly data: T
65
66
  }
66
67
 
68
+ interface ValidationDocuments {
69
+ readonly epics: readonly DocumentRecord<EpicData>[]
70
+ readonly tasks: readonly DocumentRecord<TaskData>[]
71
+ readonly phasesByTask: ReadonlyMap<
72
+ string,
73
+ readonly DocumentRecord<PhaseData>[]
74
+ >
75
+ }
76
+
67
77
  type DecodeResult<T> =
68
78
  | { readonly success: true; readonly value: T }
69
79
  | { readonly success: false; readonly error: string }
@@ -653,7 +663,10 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
653
663
  ),
654
664
  ),
655
665
 
656
- validate: (startPath: string = process.cwd()) =>
666
+ validate: (
667
+ startPath: string = process.cwd(),
668
+ options: { readonly includeDocuments?: boolean } = {},
669
+ ) =>
657
670
  Effect.gen(function* () {
658
671
  const service = yield* WorkbaseService
659
672
  const fs = yield* FileSystemService
@@ -959,6 +972,14 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
959
972
  : a.path.localeCompare(b.path),
960
973
  )
961
974
 
975
+ const phasesByTask = new Map<string, DocumentRecord<PhaseData>[]>()
976
+ for (const [key, phase] of phases) {
977
+ const taskId = key.slice(0, key.indexOf("/"))
978
+ const records = phasesByTask.get(taskId) ?? []
979
+ records.push(phase)
980
+ phasesByTask.set(taskId, records)
981
+ }
982
+
962
983
  return {
963
984
  root,
964
985
  issues,
@@ -966,6 +987,15 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
966
987
  taskCount: tasks.size,
967
988
  phaseCount: phases.size,
968
989
  valid: issues.length === 0,
990
+ ...(options.includeDocuments
991
+ ? {
992
+ documents: {
993
+ epics: [...epics.values()],
994
+ tasks: [...tasks.values()],
995
+ phasesByTask,
996
+ },
997
+ }
998
+ : {}),
969
999
  } satisfies ValidationReport
970
1000
  }),
971
1001
  }),