@markjaquith/agency 2.71.4 → 2.71.5
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 +3 -3
- package/src/services/GraphService.ts +82 -16
- package/src/services/RepositoryService.ts +58 -56
- package/src/work-view.ts +24 -17
- package/scripts/install-pi-extension.ts +0 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "2.71.
|
|
3
|
+
"version": "2.71.5",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -21,9 +21,8 @@
|
|
|
21
21
|
"index.ts",
|
|
22
22
|
"cli.ts",
|
|
23
23
|
"cli-main.ts",
|
|
24
|
-
"src",
|
|
25
24
|
"pi-extensions",
|
|
26
|
-
"
|
|
25
|
+
"src",
|
|
27
26
|
"schemas",
|
|
28
27
|
"fixtures/protocol",
|
|
29
28
|
"README.md",
|
|
@@ -65,6 +64,7 @@
|
|
|
65
64
|
"scripts": {
|
|
66
65
|
"postinstall": "bun scripts/install-pi-extension.ts install",
|
|
67
66
|
"preuninstall": "bun scripts/install-pi-extension.ts uninstall",
|
|
67
|
+
"benchmark:status": "bun scripts/benchmark-status.ts",
|
|
68
68
|
"benchmark:workbase": "bun scripts/benchmark-workbase.ts",
|
|
69
69
|
"benchmark:context": "bun scripts/benchmark-context.ts",
|
|
70
70
|
"benchmark:sync": "bun scripts/benchmark-sync.ts",
|
|
@@ -277,6 +277,13 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
const taskDeclarations = new Map<string, Dependency>()
|
|
280
|
+
const phasesByTask = new Map<string, Document<PhaseData>[]>()
|
|
281
|
+
for (const [key, phase] of phases) {
|
|
282
|
+
const taskId = key.slice(0, key.indexOf("/"))
|
|
283
|
+
const values = phasesByTask.get(taskId) ?? []
|
|
284
|
+
values.push(phase)
|
|
285
|
+
phasesByTask.set(taskId, values)
|
|
286
|
+
}
|
|
280
287
|
for (const epic of epics.values()) {
|
|
281
288
|
for (const declaration of epic.data.tasks) {
|
|
282
289
|
taskDeclarations.set(declaration.id, declaration)
|
|
@@ -294,8 +301,14 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
294
301
|
)
|
|
295
302
|
: [task.data.status]
|
|
296
303
|
}
|
|
297
|
-
const
|
|
298
|
-
|
|
304
|
+
const taskStatuses = new Map<string, WorkStatus>()
|
|
305
|
+
const taskStatus = (taskId: string) => {
|
|
306
|
+
const cached = taskStatuses.get(taskId)
|
|
307
|
+
if (cached) return cached
|
|
308
|
+
const status = aggregateProgress(taskLeafStatuses(taskId)).status
|
|
309
|
+
taskStatuses.set(taskId, status)
|
|
310
|
+
return status
|
|
311
|
+
}
|
|
299
312
|
const dependencyBlockers = (
|
|
300
313
|
dependencies: readonly string[],
|
|
301
314
|
toId: (id: string) => string,
|
|
@@ -320,16 +333,20 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
320
333
|
|
|
321
334
|
const validation =
|
|
322
335
|
options.validation ?? (yield* workbase.validate(root))
|
|
336
|
+
const validationIssuesByPath = Map.groupBy(
|
|
337
|
+
validation.issues,
|
|
338
|
+
(issue) => issue.path,
|
|
339
|
+
)
|
|
323
340
|
const validationBlockers = (
|
|
324
341
|
paths: readonly string[],
|
|
325
342
|
): GraphBlocker[] =>
|
|
326
|
-
|
|
327
|
-
.
|
|
328
|
-
.map((issue) => ({
|
|
343
|
+
paths.flatMap((path) =>
|
|
344
|
+
(validationIssuesByPath.get(path) ?? []).map((issue) => ({
|
|
329
345
|
kind: "validation" as const,
|
|
330
346
|
id: issue.path,
|
|
331
347
|
reason: issue.message,
|
|
332
|
-
}))
|
|
348
|
+
})),
|
|
349
|
+
)
|
|
333
350
|
const uniqueBlockers = (blockers: readonly GraphBlocker[]) => [
|
|
334
351
|
...new Map(
|
|
335
352
|
blockers.map((item) => [
|
|
@@ -339,7 +356,9 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
339
356
|
).values(),
|
|
340
357
|
]
|
|
341
358
|
|
|
342
|
-
|
|
359
|
+
type PhaseState = ReturnType<typeof createPhaseState>
|
|
360
|
+
const phaseStates = new Map<string, PhaseState>()
|
|
361
|
+
function createPhaseState(taskId: string, phaseId: string) {
|
|
343
362
|
const task = tasks.get(taskId)
|
|
344
363
|
const phase = phases.get(`${taskId}/${phaseId}`)
|
|
345
364
|
const parentEpic = task?.data.epic
|
|
@@ -386,8 +405,18 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
386
405
|
},
|
|
387
406
|
}
|
|
388
407
|
}
|
|
408
|
+
const phaseState = (taskId: string, phaseId: string) => {
|
|
409
|
+
const key = `${taskId}/${phaseId}`
|
|
410
|
+
const cached = phaseStates.get(key)
|
|
411
|
+
if (cached) return cached
|
|
412
|
+
const state = createPhaseState(taskId, phaseId)
|
|
413
|
+
phaseStates.set(key, state)
|
|
414
|
+
return state
|
|
415
|
+
}
|
|
389
416
|
|
|
390
|
-
|
|
417
|
+
type TaskState = ReturnType<typeof createTaskState>
|
|
418
|
+
const taskStates = new Map<string, TaskState>()
|
|
419
|
+
function createTaskState(taskId: string) {
|
|
391
420
|
const task = tasks.get(taskId)
|
|
392
421
|
const parentEpic = task?.data.epic
|
|
393
422
|
? epics.get(task.data.epic)
|
|
@@ -398,9 +427,9 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
398
427
|
? [
|
|
399
428
|
relative(root, task.path),
|
|
400
429
|
...(parentEpic ? [relative(root, parentEpic.path)] : []),
|
|
401
|
-
...[
|
|
402
|
-
|
|
403
|
-
|
|
430
|
+
...(phasesByTask.get(taskId) ?? []).map((phase) =>
|
|
431
|
+
relative(root, phase.path),
|
|
432
|
+
),
|
|
404
433
|
]
|
|
405
434
|
: []
|
|
406
435
|
const blockers = [
|
|
@@ -461,6 +490,13 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
461
490
|
},
|
|
462
491
|
}
|
|
463
492
|
}
|
|
493
|
+
const taskState = (taskId: string) => {
|
|
494
|
+
const cached = taskStates.get(taskId)
|
|
495
|
+
if (cached) return cached
|
|
496
|
+
const state = createTaskState(taskId)
|
|
497
|
+
taskStates.set(taskId, state)
|
|
498
|
+
return state
|
|
499
|
+
}
|
|
464
500
|
|
|
465
501
|
const epicState = (epicId: string) => {
|
|
466
502
|
const epic = epics.get(epicId)
|
|
@@ -475,9 +511,9 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
475
511
|
const task = tasks.get(item.id)
|
|
476
512
|
return [
|
|
477
513
|
...(task ? [relative(root, task.path)] : []),
|
|
478
|
-
...[
|
|
479
|
-
|
|
480
|
-
|
|
514
|
+
...(phasesByTask.get(item.id) ?? []).map((phase) =>
|
|
515
|
+
relative(root, phase.path),
|
|
516
|
+
),
|
|
481
517
|
]
|
|
482
518
|
}),
|
|
483
519
|
]
|
|
@@ -761,6 +797,36 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
761
797
|
return result
|
|
762
798
|
})
|
|
763
799
|
|
|
800
|
+
const executionDetailsByKey = new Map<
|
|
801
|
+
string,
|
|
802
|
+
Effect.Effect.Success<ReturnType<typeof executionDetails>>
|
|
803
|
+
>()
|
|
804
|
+
const executionDocuments: readonly (readonly [
|
|
805
|
+
string,
|
|
806
|
+
string,
|
|
807
|
+
ExecutionData,
|
|
808
|
+
])[] = [
|
|
809
|
+
...[...tasks.values()].flatMap((task) =>
|
|
810
|
+
"phases" in task.data
|
|
811
|
+
? []
|
|
812
|
+
: ([[task.id, task.path, task.data]] as const),
|
|
813
|
+
),
|
|
814
|
+
...[...phases].map(
|
|
815
|
+
([key, phase]) => [key, phase.path, phase.data] as const,
|
|
816
|
+
),
|
|
817
|
+
]
|
|
818
|
+
const inspectedExecutions = yield* Effect.all(
|
|
819
|
+
executionDocuments.map(([key, path, data]) =>
|
|
820
|
+
executionDetails(path, data).pipe(
|
|
821
|
+
Effect.map((details) => [key, details] as const),
|
|
822
|
+
),
|
|
823
|
+
),
|
|
824
|
+
{ concurrency: 16 },
|
|
825
|
+
)
|
|
826
|
+
for (const [key, details] of inspectedExecutions) {
|
|
827
|
+
executionDetailsByKey.set(key, details)
|
|
828
|
+
}
|
|
829
|
+
|
|
764
830
|
const nodes: GraphNode[] = []
|
|
765
831
|
for (const epic of epics.values()) {
|
|
766
832
|
const state = epicState(epic.id)
|
|
@@ -805,7 +871,7 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
805
871
|
dependents: dependents(taskNodeId(task.id)),
|
|
806
872
|
repositories,
|
|
807
873
|
data: { taskId: task.id, ...task.data },
|
|
808
|
-
...(
|
|
874
|
+
...executionDetailsByKey.get(task.id),
|
|
809
875
|
})
|
|
810
876
|
}
|
|
811
877
|
}
|
|
@@ -830,7 +896,7 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
830
896
|
dependents: dependents(phaseNodeId(taskId, phase.id)),
|
|
831
897
|
repositories,
|
|
832
898
|
data: { taskId, phaseId: phase.id, ...phase.data },
|
|
833
|
-
...(
|
|
899
|
+
...executionDetailsByKey.get(key),
|
|
834
900
|
})
|
|
835
901
|
}
|
|
836
902
|
for (const repository of [...repositoryRecords.values()].sort(
|
|
@@ -520,62 +520,64 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
520
520
|
...Object.keys(config.repositories ?? {}),
|
|
521
521
|
...local.keys(),
|
|
522
522
|
])
|
|
523
|
-
const repositories
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
523
|
+
const repositories = yield* Effect.all(
|
|
524
|
+
[...aliases].sort().map((alias) =>
|
|
525
|
+
Effect.gen(function* () {
|
|
526
|
+
const path = join(reposPath, alias)
|
|
527
|
+
const entry = local.get(alias)
|
|
528
|
+
const declaredRemote =
|
|
529
|
+
config.repositories?.[alias]?.remote ?? null
|
|
530
|
+
if (!entry) {
|
|
531
|
+
return {
|
|
532
|
+
alias,
|
|
533
|
+
path,
|
|
534
|
+
kind: null,
|
|
535
|
+
remote: null,
|
|
536
|
+
declaredRemote,
|
|
537
|
+
target: null,
|
|
538
|
+
states: ["declared", "missing"] as RepositoryState[],
|
|
539
|
+
} satisfies RepositoryInfo
|
|
540
|
+
}
|
|
541
|
+
if (!entry.isDirectory && !entry.isSymlink) {
|
|
542
|
+
return {
|
|
543
|
+
alias,
|
|
544
|
+
path,
|
|
545
|
+
kind: null,
|
|
546
|
+
remote: null,
|
|
547
|
+
declaredRemote,
|
|
548
|
+
target: null,
|
|
549
|
+
states: [
|
|
550
|
+
...(declaredRemote ? (["declared"] as const) : []),
|
|
551
|
+
"invalid",
|
|
552
|
+
] as RepositoryState[],
|
|
553
|
+
} satisfies RepositoryInfo
|
|
554
|
+
}
|
|
555
|
+
const target = entry.isSymlink
|
|
556
|
+
? yield* fs.readSymlinkTarget(path)
|
|
557
|
+
: null
|
|
558
|
+
const inspection = yield* backend.inspectRepository(path)
|
|
559
|
+
const remote = inspection?.remote ?? null
|
|
560
|
+
const states: RepositoryState[] = []
|
|
561
|
+
if (declaredRemote) states.push("declared")
|
|
562
|
+
states.push(entry.isSymlink ? "linked" : "materialized")
|
|
563
|
+
if (!inspection) states.push("invalid")
|
|
564
|
+
if (declaredRemote && remote !== declaredRemote)
|
|
565
|
+
states.push("remote-drifted")
|
|
566
|
+
return {
|
|
567
|
+
alias,
|
|
568
|
+
path,
|
|
569
|
+
kind: entry.isSymlink
|
|
570
|
+
? "symlink"
|
|
571
|
+
: (inspection?.kind ?? "repository"),
|
|
572
|
+
remote,
|
|
573
|
+
declaredRemote,
|
|
574
|
+
target,
|
|
575
|
+
states,
|
|
576
|
+
} satisfies RepositoryInfo
|
|
577
|
+
}),
|
|
578
|
+
),
|
|
579
|
+
{ concurrency: 4 },
|
|
580
|
+
)
|
|
579
581
|
return repositories
|
|
580
582
|
}),
|
|
581
583
|
|
package/src/work-view.ts
CHANGED
|
@@ -211,27 +211,34 @@ export const getWorkViews = (options: WorkViewOptions = {}) =>
|
|
|
211
211
|
(node): node is ExecutionNode => node.kind === "execution-unit",
|
|
212
212
|
)
|
|
213
213
|
const taskOrder = orderedTasks(epics, tasks)
|
|
214
|
-
const
|
|
214
|
+
const phasesByTask = new Map(
|
|
215
|
+
taskOrder.map((task) => [task.key, orderedPhases(task, phases)]),
|
|
216
|
+
)
|
|
217
|
+
const phaseOrder = taskOrder.flatMap(
|
|
218
|
+
(task) => phasesByTask.get(task.key) ?? [],
|
|
219
|
+
)
|
|
220
|
+
const executionsByTask = new Map<string, ExecutionNode[]>()
|
|
221
|
+
const executionsByPhase = new Map<string, ExecutionNode[]>()
|
|
222
|
+
for (const execution of executions) {
|
|
223
|
+
const taskExecutions = executionsByTask.get(execution.data.taskId) ?? []
|
|
224
|
+
taskExecutions.push(execution)
|
|
225
|
+
executionsByTask.set(execution.data.taskId, taskExecutions)
|
|
226
|
+
if ("phaseId" in execution.data) {
|
|
227
|
+
const key = `${execution.data.taskId}/${execution.data.phaseId}`
|
|
228
|
+
const phaseExecutions = executionsByPhase.get(key) ?? []
|
|
229
|
+
phaseExecutions.push(execution)
|
|
230
|
+
executionsByPhase.set(key, phaseExecutions)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
215
233
|
const executionsFor = (node: EntityNode) => {
|
|
216
234
|
if (node.kind === "phase") {
|
|
217
|
-
|
|
218
|
-
const taskId = node.key.slice(0, separator)
|
|
219
|
-
const phaseId = node.key.slice(separator + 1)
|
|
220
|
-
return executions.filter(
|
|
221
|
-
(execution) =>
|
|
222
|
-
execution.data.taskId === taskId &&
|
|
223
|
-
"phaseId" in execution.data &&
|
|
224
|
-
execution.data.phaseId === phaseId,
|
|
225
|
-
)
|
|
235
|
+
return executionsByPhase.get(node.key) ?? []
|
|
226
236
|
}
|
|
227
237
|
if (node.kind === "task") {
|
|
228
|
-
return
|
|
229
|
-
(execution) => execution.data.taskId === node.key,
|
|
230
|
-
)
|
|
238
|
+
return executionsByTask.get(node.key) ?? []
|
|
231
239
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
taskIds.has(execution.data.taskId),
|
|
240
|
+
return node.data.tasks.flatMap(
|
|
241
|
+
(item) => executionsByTask.get(item.id) ?? [],
|
|
235
242
|
)
|
|
236
243
|
}
|
|
237
244
|
const makeRows = (nodes: readonly EntityNode[]) =>
|
|
@@ -257,7 +264,7 @@ export const getWorkViews = (options: WorkViewOptions = {}) =>
|
|
|
257
264
|
const phaseRows = filterRows(makeRows(phaseOrder))
|
|
258
265
|
const executionEntities: EntityNode[] = []
|
|
259
266
|
for (const task of taskOrder) {
|
|
260
|
-
const taskPhases =
|
|
267
|
+
const taskPhases = phasesByTask.get(task.key) ?? []
|
|
261
268
|
executionEntities.push(...(taskPhases.length > 0 ? taskPhases : [task]))
|
|
262
269
|
}
|
|
263
270
|
const executionRows = filterRows(makeRows(executionEntities))
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { cp, mkdir, rm } from "node:fs/promises"
|
|
2
|
-
import { homedir } from "node:os"
|
|
3
|
-
import { dirname, join } from "node:path"
|
|
4
|
-
|
|
5
|
-
export const piExtensionPath = (home = homedir()) =>
|
|
6
|
-
join(home, ".pi", "agent", "extensions", "agency.ts")
|
|
7
|
-
|
|
8
|
-
export const installPiExtension = async (
|
|
9
|
-
source = join(import.meta.dir, "..", "pi-extensions", "agency.ts"),
|
|
10
|
-
destination = piExtensionPath(),
|
|
11
|
-
) => {
|
|
12
|
-
await mkdir(dirname(destination), { recursive: true })
|
|
13
|
-
await cp(source, destination)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const uninstallPiExtension = async (destination = piExtensionPath()) => {
|
|
17
|
-
await rm(destination, { force: true })
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (import.meta.main) {
|
|
21
|
-
const command = process.argv[2] ?? "install"
|
|
22
|
-
if (command === "install") await installPiExtension()
|
|
23
|
-
else if (command === "uninstall") await uninstallPiExtension()
|
|
24
|
-
else throw new Error(`Unknown Pi extension lifecycle command: ${command}`)
|
|
25
|
-
}
|