@markjaquith/agency 2.71.4 → 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 +4 -3
- package/src/commands/doctor.test.ts +0 -4
- package/src/services/DoctorService.ts +103 -52
- package/src/services/GraphService.ts +82 -16
- package/src/services/RepositoryService.ts +58 -56
- package/src/services/WorkbaseService.ts +31 -1
- 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.6",
|
|
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,7 +64,9 @@
|
|
|
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
|
+
"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",
|
|
@@ -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 {
|
|
7
|
+
import type { PhaseRecord } from "./PhaseService"
|
|
9
8
|
import { RepositoryService } from "./RepositoryService"
|
|
10
|
-
import {
|
|
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 =
|
|
104
|
+
const tool = (
|
|
109
105
|
id: string,
|
|
110
106
|
executable: string,
|
|
111
107
|
level: DoctorCheckLevel,
|
|
112
108
|
label: string,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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*
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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*
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
@@ -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
|
|
|
@@ -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: (
|
|
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
|
}),
|
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
|
-
}
|