@markjaquith/agency 2.38.0 → 2.39.0
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/README.md +14 -7
- package/cli.ts +1 -0
- package/package.json +1 -1
- package/skills/agency/references/commands.md +8 -6
- package/skills/agency/references/contracts.md +9 -5
- package/src/cli-parser.test.ts +7 -0
- package/src/cli-parser.ts +5 -3
- package/src/commands/context.test.ts +40 -5
- package/src/commands/context.ts +5 -2
- package/src/commands/status.ts +1 -0
- package/src/services/ContextService.ts +181 -40
- package/src/services/GraphService.ts +60 -28
- package/src/services/WorkbaseService.ts +57 -27
- package/src/work-view.ts +3 -0
package/README.md
CHANGED
|
@@ -358,10 +358,16 @@ agency validate
|
|
|
358
358
|
|
|
359
359
|
### Target Context
|
|
360
360
|
|
|
361
|
-
`agency context [target] --json` returns
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
paths,
|
|
361
|
+
`agency context [target] --json` returns complete bootstrap context without
|
|
362
|
+
modifying the workbase or fetching repositories. At the workbase root it returns
|
|
363
|
+
a discovery catalog of all epics, tasks, and phases, including frontmatter,
|
|
364
|
+
paths, and document revisions. Elsewhere it returns context for an epic, task,
|
|
365
|
+
or phase. The target defaults to the current directory; entity directories,
|
|
366
|
+
document paths, checkout descendants, and bare task IDs are accepted.
|
|
367
|
+
|
|
368
|
+
Root discovery is compact by default and includes a hint to run `agency context
|
|
369
|
+
. --full --json` when document prose is needed. Entity context remains complete
|
|
370
|
+
by default. `--compact` explicitly requests compact entity context.
|
|
365
371
|
|
|
366
372
|
The result includes workbase and target identity, ancestor frontmatter and prose
|
|
367
373
|
with SHA-256 hashes, dependency and readiness state, aggregate status, writable
|
|
@@ -369,9 +375,10 @@ and reference authority, local checkout and resolved-commit state, recorded PR
|
|
|
369
375
|
state, and validation warnings. Only `done` satisfies a dependency; `dropped` is
|
|
370
376
|
terminal but remains a blocker.
|
|
371
377
|
|
|
372
|
-
Complete output is the default. Pass `--compact` explicitly
|
|
373
|
-
prose and low-level Git details while retaining identity,
|
|
374
|
-
paths, graph state, materialization state, and validation
|
|
378
|
+
Complete output is the default for entity targets. Pass `--compact` explicitly
|
|
379
|
+
to omit document prose and low-level Git details while retaining identity,
|
|
380
|
+
hashes, authority, paths, graph state, materialization state, and validation
|
|
381
|
+
warnings.
|
|
375
382
|
|
|
376
383
|
### Workbase Graph
|
|
377
384
|
|
package/cli.ts
CHANGED
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ Commands that return Agency-owned data accept `--json` unless noted otherwise.
|
|
|
8
8
|
## Discovery And Health
|
|
9
9
|
|
|
10
10
|
```text
|
|
11
|
-
agency context [target] [--json] [--compact]
|
|
11
|
+
agency context [target] [--json] [--compact | --full]
|
|
12
12
|
agency graph [--json | --jsonl] [--ready | --blocked]
|
|
13
13
|
[--status <status>...] [--repository <alias>...] [--kind <kind>...]
|
|
14
14
|
[--include <bodies|workspace|git|pr>...]
|
|
@@ -21,11 +21,13 @@ agency integration status [--json]
|
|
|
21
21
|
agency integration sync [--json]
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
`context` defaults to cwd and accepts
|
|
25
|
-
checkout descendants, or a task ID.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
`context` defaults to cwd and accepts the workbase root, entity directories,
|
|
25
|
+
document paths, checkout descendants, or a task ID. Root context catalogs all
|
|
26
|
+
epics, tasks, and phases in compact form and hints that `--full` includes prose.
|
|
27
|
+
Entity context is complete by default; `--compact` intentionally omits prose and
|
|
28
|
+
low-level Git details. `graph` computes readiness before applying filters.
|
|
29
|
+
`doctor` discovers required tools, integrations, repositories, refs, worktrees,
|
|
30
|
+
permissions, drift, and optional runner capabilities. `sync` is
|
|
29
31
|
observational unless `--apply` is explicit. Integration status includes safe
|
|
30
32
|
remediation when customized OpenCode config prevents Agency from guaranteeing
|
|
31
33
|
whole-workbase access.
|
|
@@ -8,9 +8,11 @@ names; they are not duplicated in frontmatter.
|
|
|
8
8
|
`agency context . --json` returns a versioned success envelope whose result
|
|
9
9
|
contains:
|
|
10
10
|
|
|
11
|
-
- `projection`: `
|
|
11
|
+
- `projection`: `compact` for root discovery by default, otherwise `complete`
|
|
12
|
+
unless compact entity context was explicitly requested;
|
|
12
13
|
- `workbase`: root, config path, and config version;
|
|
13
|
-
- `target`: resolved epic, task, or phase identity;
|
|
14
|
+
- `target`: resolved workbase, epic, task, or phase identity;
|
|
15
|
+
- `discovery`: at the workbase root, all valid epic, task, and phase documents;
|
|
14
16
|
- `documents`: ancestor frontmatter, paths, SHA-256 revisions, and prose;
|
|
15
17
|
- `graph`: parent, dependencies, dependents, readiness blockers, and progress;
|
|
16
18
|
- `authority`: `orchestration` or `execution`, one writable checkout or none,
|
|
@@ -283,9 +285,11 @@ selection. Supply every required value or explicit entity selector. Global
|
|
|
283
285
|
`--cwd` and `--workbase` selectors apply to `next` as they do to other discovery
|
|
284
286
|
commands.
|
|
285
287
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
288
|
+
At a workbase root, context defaults to a compact discovery catalog of epics,
|
|
289
|
+
tasks, and phases, includes no writable authority, and provides a hint for
|
|
290
|
+
requesting `--full`. Entity context defaults to the `complete` projection.
|
|
291
|
+
`--compact` omits prose and low-level Git details but retains identity, document
|
|
292
|
+
hashes, authority, paths, graph state, materialization, and validation warnings.
|
|
289
293
|
|
|
290
294
|
Graph projections are opt-in with repeatable
|
|
291
295
|
`--include <bodies|workspace|git|pr>`. Filters such as `--ready`, `--blocked`,
|
package/src/cli-parser.test.ts
CHANGED
|
@@ -403,6 +403,13 @@ describe("strict CLI parsing", () => {
|
|
|
403
403
|
values: { json: true, compact: true },
|
|
404
404
|
})
|
|
405
405
|
expectUsageError(["status", "--compact"], "agency status")
|
|
406
|
+
expect(parseCli(["context", ".", "--json", "--full"])).toMatchObject({
|
|
407
|
+
commandName: "context",
|
|
408
|
+
args: ["."],
|
|
409
|
+
values: { json: true, full: true },
|
|
410
|
+
})
|
|
411
|
+
expectUsageError(["context", ".", "--compact", "--full"], "agency context")
|
|
412
|
+
expectUsageError(["status", "--full"], "agency status")
|
|
406
413
|
})
|
|
407
414
|
|
|
408
415
|
test("accepts work preparation options only for the prepare subcommand", () => {
|
package/src/cli-parser.ts
CHANGED
|
@@ -891,17 +891,19 @@ const commands = {
|
|
|
891
891
|
},
|
|
892
892
|
},
|
|
893
893
|
context: {
|
|
894
|
-
usage: "agency context [target] [--json] [--compact]",
|
|
894
|
+
usage: "agency context [target] [--json] [--compact | --full]",
|
|
895
895
|
options: {
|
|
896
896
|
...outputOptions,
|
|
897
897
|
...entitySelectorOptions,
|
|
898
898
|
compact: { type: "boolean" },
|
|
899
|
+
full: { type: "boolean" },
|
|
899
900
|
},
|
|
900
901
|
command: {
|
|
901
|
-
usage: "agency context [target] [--json] [--compact]",
|
|
902
|
+
usage: "agency context [target] [--json] [--compact | --full]",
|
|
902
903
|
minArgs: 0,
|
|
903
904
|
maxArgs: 1,
|
|
904
|
-
options: ["json", "compact", "epic", "task", "phase"],
|
|
905
|
+
options: ["json", "compact", "full", "epic", "task", "phase"],
|
|
906
|
+
conflicts: [["compact", "full"]],
|
|
905
907
|
},
|
|
906
908
|
},
|
|
907
909
|
graph: {
|
|
@@ -28,9 +28,10 @@ const readContext = async (
|
|
|
28
28
|
root: string,
|
|
29
29
|
target: string | undefined,
|
|
30
30
|
compact = false,
|
|
31
|
+
full = false,
|
|
31
32
|
) => {
|
|
32
33
|
const logs = await captureLogs(() =>
|
|
33
|
-
runTestEffect(context({ cwd: root, target, compact, json: true })),
|
|
34
|
+
runTestEffect(context({ cwd: root, target, compact, full, json: true })),
|
|
34
35
|
)
|
|
35
36
|
expect(logs).toHaveLength(1)
|
|
36
37
|
return JSON.parse(logs[0]!)
|
|
@@ -267,7 +268,7 @@ status: dropped
|
|
|
267
268
|
})
|
|
268
269
|
})
|
|
269
270
|
|
|
270
|
-
test("resolves a bare task ID and
|
|
271
|
+
test("resolves a bare task ID and returns root discovery context", async () => {
|
|
271
272
|
const task = await readContext(root, "agent-contract")
|
|
272
273
|
expect(task.target).toMatchObject({
|
|
273
274
|
kind: "task",
|
|
@@ -275,9 +276,43 @@ status: dropped
|
|
|
275
276
|
})
|
|
276
277
|
expect(task.graph.parent).toEqual({ kind: "epic", id: "contract" })
|
|
277
278
|
|
|
278
|
-
await
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
const workbase = await readContext(root, ".")
|
|
280
|
+
expect(workbase).toMatchObject({
|
|
281
|
+
projection: "compact",
|
|
282
|
+
workbase: { root, version: 2 },
|
|
283
|
+
target: { kind: "workbase", path: root },
|
|
284
|
+
hint: "Run agency context . --full --json to include document prose.",
|
|
285
|
+
authority: {
|
|
286
|
+
mode: "orchestration",
|
|
287
|
+
writable: null,
|
|
288
|
+
references: [],
|
|
289
|
+
},
|
|
290
|
+
validation: { valid: true, warnings: [] },
|
|
291
|
+
})
|
|
292
|
+
expect(
|
|
293
|
+
workbase.discovery.epics.map((item: { id: string }) => item.id),
|
|
294
|
+
).toEqual(["contract"])
|
|
295
|
+
expect(
|
|
296
|
+
workbase.discovery.tasks.map((item: { id: string }) => item.id),
|
|
297
|
+
).toEqual(["agent-contract", "foundations"])
|
|
298
|
+
expect(
|
|
299
|
+
workbase.discovery.phases.map(
|
|
300
|
+
(item: { taskId: string; id: string }) => `${item.taskId}/${item.id}`,
|
|
301
|
+
),
|
|
302
|
+
).toEqual(["agent-contract/context-command", "agent-contract/schema"])
|
|
303
|
+
expect(workbase.discovery.tasks[0].body).toBeUndefined()
|
|
304
|
+
expect(workbase.discovery.phases[0].data.status).toBe("open")
|
|
305
|
+
expect(workbase.discovery.phases[0].sha256).toMatch(/^[a-f0-9]{64}$/)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test("includes discovery prose only in full root context", async () => {
|
|
309
|
+
const workbase = await readContext(root, ".", false, true)
|
|
310
|
+
expect(workbase.projection).toBe("complete")
|
|
311
|
+
expect(workbase.hint).toBeNull()
|
|
312
|
+
expect(workbase.discovery.epics[0].body).toContain("# Contract")
|
|
313
|
+
expect(workbase.discovery.tasks[0].body).toContain("Task prose.")
|
|
314
|
+
expect(workbase.discovery.tasks[0].data.phases).toBeDefined()
|
|
315
|
+
expect(workbase.discovery.phases[0].taskId).toBe("agent-contract")
|
|
281
316
|
})
|
|
282
317
|
|
|
283
318
|
test("resolves bare task IDs from inside another target", async () => {
|
package/src/commands/context.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createLoggers } from "../utils/effect"
|
|
|
6
6
|
interface ContextOptions extends BaseCommandOptions {
|
|
7
7
|
readonly target?: string
|
|
8
8
|
readonly compact?: boolean
|
|
9
|
+
readonly full?: boolean
|
|
9
10
|
readonly json?: boolean
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -20,12 +21,14 @@ export const context = (options: ContextOptions = {}) =>
|
|
|
20
21
|
export const help = `
|
|
21
22
|
Usage: agency context [target] [options]
|
|
22
23
|
|
|
23
|
-
Return
|
|
24
|
-
defaults to the current directory and may be
|
|
24
|
+
Return complete, read-only context for a workbase, epic, task, or phase. The
|
|
25
|
+
target defaults to the current directory and may be the workbase root, an entity
|
|
26
|
+
path, or a task ID. Workbase context catalogs all epics, tasks, and phases.
|
|
25
27
|
|
|
26
28
|
Options:
|
|
27
29
|
--json Output a versioned machine result
|
|
28
30
|
--compact Omit prose bodies and low-level Git details
|
|
31
|
+
--full Include prose bodies in root workbase context
|
|
29
32
|
--epic <id> Select an epic
|
|
30
33
|
--task <id> Select a task
|
|
31
34
|
--phase <id> Select a phase together with --task
|
package/src/commands/status.ts
CHANGED
|
@@ -108,6 +108,7 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
108
108
|
readonly target?: string
|
|
109
109
|
readonly cwd?: string
|
|
110
110
|
readonly compact?: boolean
|
|
111
|
+
readonly full?: boolean
|
|
111
112
|
}) =>
|
|
112
113
|
Effect.gen(function* () {
|
|
113
114
|
const fs = yield* FileSystemService
|
|
@@ -121,6 +122,103 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
121
122
|
candidateExists ? candidate : cwd,
|
|
122
123
|
)
|
|
123
124
|
|
|
125
|
+
if (relative(root, candidate) === "") {
|
|
126
|
+
const compact = !options.full
|
|
127
|
+
const discover = <S extends Schema.Schema.AnyNoContext>(
|
|
128
|
+
id: string,
|
|
129
|
+
path: string,
|
|
130
|
+
schema: S,
|
|
131
|
+
extra: Record<string, string> = {},
|
|
132
|
+
) =>
|
|
133
|
+
Effect.gen(function* () {
|
|
134
|
+
if (!(yield* fs.exists(path))) return null
|
|
135
|
+
const content = yield* fs.readFile(path)
|
|
136
|
+
const parsed = yield* Effect.either(
|
|
137
|
+
parseFrontmatter(content, path),
|
|
138
|
+
)
|
|
139
|
+
if (Either.isLeft(parsed)) return null
|
|
140
|
+
const decoded = decode(schema, parsed.right.data)
|
|
141
|
+
if (!decoded.ok) return null
|
|
142
|
+
return {
|
|
143
|
+
...extra,
|
|
144
|
+
id,
|
|
145
|
+
path,
|
|
146
|
+
sha256: documentRevision(content),
|
|
147
|
+
data: decoded.value,
|
|
148
|
+
...(compact ? {} : { body: parsed.right.body }),
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const epics: unknown[] = []
|
|
153
|
+
const tasks: unknown[] = []
|
|
154
|
+
const phases: unknown[] = []
|
|
155
|
+
const epicRoot = join(root, "epics")
|
|
156
|
+
if (yield* fs.isDirectory(epicRoot)) {
|
|
157
|
+
for (const entry of (yield* fs.readDirectory(epicRoot))
|
|
158
|
+
.filter((item) => item.isDirectory)
|
|
159
|
+
.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
160
|
+
const document = yield* discover(
|
|
161
|
+
entry.name,
|
|
162
|
+
join(epicRoot, entry.name, "EPIC.md"),
|
|
163
|
+
EpicFrontmatter,
|
|
164
|
+
)
|
|
165
|
+
if (document) epics.push(document)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const taskRoot = join(root, "tasks")
|
|
170
|
+
if (yield* fs.isDirectory(taskRoot)) {
|
|
171
|
+
for (const entry of (yield* fs.readDirectory(taskRoot))
|
|
172
|
+
.filter((item) => item.isDirectory)
|
|
173
|
+
.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
174
|
+
const document = yield* discover(
|
|
175
|
+
entry.name,
|
|
176
|
+
join(taskRoot, entry.name, "TASK.md"),
|
|
177
|
+
TaskFrontmatter,
|
|
178
|
+
)
|
|
179
|
+
if (document) tasks.push(document)
|
|
180
|
+
|
|
181
|
+
const phaseRoot = join(taskRoot, entry.name, "phases")
|
|
182
|
+
if (!(yield* fs.isDirectory(phaseRoot))) continue
|
|
183
|
+
for (const phaseEntry of (yield* fs.readDirectory(phaseRoot))
|
|
184
|
+
.filter((item) => item.isDirectory)
|
|
185
|
+
.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
186
|
+
const phase = yield* discover(
|
|
187
|
+
phaseEntry.name,
|
|
188
|
+
join(phaseRoot, phaseEntry.name, "PHASE.md"),
|
|
189
|
+
PhaseFrontmatter,
|
|
190
|
+
{ taskId: entry.name },
|
|
191
|
+
)
|
|
192
|
+
if (phase) phases.push(phase)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const validation = yield* workbase.validate(root)
|
|
198
|
+
return {
|
|
199
|
+
projection: compact ? "compact" : "complete",
|
|
200
|
+
workbase: {
|
|
201
|
+
root,
|
|
202
|
+
configPath: join(root, "agency.json"),
|
|
203
|
+
version: config.version,
|
|
204
|
+
},
|
|
205
|
+
target: { kind: "workbase", path: root },
|
|
206
|
+
hint: compact
|
|
207
|
+
? "Run agency context . --full --json to include document prose."
|
|
208
|
+
: null,
|
|
209
|
+
discovery: { epics, tasks, phases },
|
|
210
|
+
authority: {
|
|
211
|
+
mode: "orchestration",
|
|
212
|
+
writable: null,
|
|
213
|
+
references: [],
|
|
214
|
+
},
|
|
215
|
+
validation: {
|
|
216
|
+
valid: validation.valid,
|
|
217
|
+
warnings: validation.issues,
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
124
222
|
const inferTarget = (): Target | null => {
|
|
125
223
|
if (!candidateExists && !suppliedTarget.includes(sep)) {
|
|
126
224
|
return {
|
|
@@ -264,47 +362,90 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
264
362
|
const entries = (yield* fs.readDirectory(taskRoot))
|
|
265
363
|
.filter((entry) => entry.isDirectory)
|
|
266
364
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
365
|
+
const documents = yield* Effect.all(
|
|
366
|
+
entries.map((entry) =>
|
|
367
|
+
Effect.gen(function* () {
|
|
368
|
+
const path = join(taskRoot, entry.name, "TASK.md")
|
|
369
|
+
let taskDocument: Document<TaskData> | null = null
|
|
370
|
+
if (yield* fs.exists(path)) {
|
|
371
|
+
const content = yield* fs.readFile(path)
|
|
372
|
+
const parsed = yield* Effect.either(
|
|
373
|
+
parseFrontmatter(content, path),
|
|
374
|
+
)
|
|
375
|
+
if (Either.isRight(parsed)) {
|
|
376
|
+
const decoded = decode(TaskFrontmatter, parsed.right.data)
|
|
377
|
+
if (decoded.ok) {
|
|
378
|
+
taskDocument = {
|
|
379
|
+
id: entry.name,
|
|
380
|
+
path,
|
|
381
|
+
sha256: documentRevision(content),
|
|
382
|
+
data: decoded.value,
|
|
383
|
+
body: parsed.right.body,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const phasesPath = join(taskRoot, entry.name, "phases")
|
|
390
|
+
if (!(yield* fs.isDirectory(phasesPath))) {
|
|
391
|
+
return { taskDocument, phaseDocuments: [] }
|
|
392
|
+
}
|
|
393
|
+
const phaseEntries = (yield* fs.readDirectory(phasesPath))
|
|
394
|
+
.filter((item) => item.isDirectory)
|
|
395
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
396
|
+
const childDocuments = yield* Effect.all(
|
|
397
|
+
phaseEntries.map((phaseEntry) =>
|
|
398
|
+
Effect.gen(function* () {
|
|
399
|
+
const phasePath = join(
|
|
400
|
+
phasesPath,
|
|
401
|
+
phaseEntry.name,
|
|
402
|
+
"PHASE.md",
|
|
403
|
+
)
|
|
404
|
+
if (!(yield* fs.exists(phasePath))) return null
|
|
405
|
+
const content = yield* fs.readFile(phasePath)
|
|
406
|
+
const parsed = yield* Effect.either(
|
|
407
|
+
parseFrontmatter(content, phasePath),
|
|
408
|
+
)
|
|
409
|
+
if (Either.isLeft(parsed)) return null
|
|
410
|
+
const decoded = decode(
|
|
411
|
+
PhaseFrontmatter,
|
|
412
|
+
parsed.right.data,
|
|
413
|
+
)
|
|
414
|
+
if (!decoded.ok) return null
|
|
415
|
+
return [
|
|
416
|
+
`${entry.name}/${phaseEntry.name}`,
|
|
417
|
+
{
|
|
418
|
+
id: phaseEntry.name,
|
|
419
|
+
path: phasePath,
|
|
420
|
+
sha256: documentRevision(content),
|
|
421
|
+
data: decoded.value,
|
|
422
|
+
body: parsed.right.body,
|
|
423
|
+
},
|
|
424
|
+
] as const
|
|
425
|
+
}),
|
|
426
|
+
),
|
|
427
|
+
{ concurrency: "unbounded" },
|
|
428
|
+
)
|
|
429
|
+
return {
|
|
430
|
+
taskDocument,
|
|
431
|
+
phaseDocuments: childDocuments.filter(
|
|
432
|
+
(document): document is NonNullable<typeof document> =>
|
|
433
|
+
document !== null,
|
|
434
|
+
),
|
|
435
|
+
}
|
|
436
|
+
}),
|
|
437
|
+
),
|
|
438
|
+
{ concurrency: "unbounded" },
|
|
439
|
+
)
|
|
440
|
+
for (const document of documents) {
|
|
441
|
+
if (document.taskDocument) {
|
|
442
|
+
taskDocuments.set(
|
|
443
|
+
document.taskDocument.id,
|
|
444
|
+
document.taskDocument,
|
|
299
445
|
)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
path: phasePath,
|
|
304
|
-
sha256: documentRevision(phaseContent),
|
|
305
|
-
data: phaseDecoded.value,
|
|
306
|
-
body: phaseParsed.right.body,
|
|
307
|
-
})
|
|
446
|
+
}
|
|
447
|
+
for (const [key, phaseDocument] of document.phaseDocuments) {
|
|
448
|
+
phaseDocuments.set(key, phaseDocument)
|
|
308
449
|
}
|
|
309
450
|
}
|
|
310
451
|
}
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
type WorkStatus,
|
|
34
34
|
} from "../workbase/schemas"
|
|
35
35
|
import { FileSystemService } from "./FileSystemService"
|
|
36
|
-
import { WorkbaseService } from "./WorkbaseService"
|
|
36
|
+
import { WorkbaseService, type ValidationReport } from "./WorkbaseService"
|
|
37
37
|
|
|
38
38
|
class GraphError extends Data.TaggedError("GraphError")<{
|
|
39
39
|
readonly message: string
|
|
@@ -58,6 +58,7 @@ type ExecutionData = PhaseData | Extract<TaskData, { readonly repo: string }>
|
|
|
58
58
|
|
|
59
59
|
export interface GraphOptions {
|
|
60
60
|
readonly cwd?: string
|
|
61
|
+
readonly validation?: ValidationReport
|
|
61
62
|
readonly ready?: boolean
|
|
62
63
|
readonly blocked?: boolean
|
|
63
64
|
readonly statuses?: readonly WorkStatus[]
|
|
@@ -166,34 +167,64 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
166
167
|
} satisfies Document<Schema.Schema.Type<S>>
|
|
167
168
|
})
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
const epicIds = yield* directories(join(root, "epics"))
|
|
171
|
+
const epicDocuments = yield* Effect.all(
|
|
172
|
+
epicIds.map((id) =>
|
|
173
|
+
Effect.gen(function* () {
|
|
174
|
+
const path = join(root, "epics", id, "EPIC.md")
|
|
175
|
+
return (yield* fs.exists(path))
|
|
176
|
+
? yield* readDocument(id, path, EpicFrontmatter)
|
|
177
|
+
: null
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
{ concurrency: "unbounded" },
|
|
181
|
+
)
|
|
182
|
+
for (const document of epicDocuments) {
|
|
183
|
+
if (document) epics.set(document.id, document)
|
|
174
184
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
id,
|
|
187
|
-
"phases",
|
|
188
|
-
phaseId,
|
|
189
|
-
"PHASE.md",
|
|
190
|
-
)
|
|
191
|
-
if (yield* fs.exists(phasePath)) {
|
|
192
|
-
phases.set(
|
|
193
|
-
`${id}/${phaseId}`,
|
|
194
|
-
yield* readDocument(phaseId, phasePath, PhaseFrontmatter),
|
|
185
|
+
|
|
186
|
+
const taskIds = yield* directories(join(root, "tasks"))
|
|
187
|
+
const taskDocuments = yield* Effect.all(
|
|
188
|
+
taskIds.map((id) =>
|
|
189
|
+
Effect.gen(function* () {
|
|
190
|
+
const path = join(root, "tasks", id, "TASK.md")
|
|
191
|
+
const task = (yield* fs.exists(path))
|
|
192
|
+
? yield* readDocument(id, path, TaskFrontmatter)
|
|
193
|
+
: null
|
|
194
|
+
const phaseIds = yield* directories(
|
|
195
|
+
join(root, "tasks", id, "phases"),
|
|
195
196
|
)
|
|
196
|
-
|
|
197
|
+
const taskPhases = yield* Effect.all(
|
|
198
|
+
phaseIds.map((phaseId) =>
|
|
199
|
+
Effect.gen(function* () {
|
|
200
|
+
const phasePath = join(
|
|
201
|
+
root,
|
|
202
|
+
"tasks",
|
|
203
|
+
id,
|
|
204
|
+
"phases",
|
|
205
|
+
phaseId,
|
|
206
|
+
"PHASE.md",
|
|
207
|
+
)
|
|
208
|
+
return (yield* fs.exists(phasePath))
|
|
209
|
+
? yield* readDocument(
|
|
210
|
+
phaseId,
|
|
211
|
+
phasePath,
|
|
212
|
+
PhaseFrontmatter,
|
|
213
|
+
)
|
|
214
|
+
: null
|
|
215
|
+
}),
|
|
216
|
+
),
|
|
217
|
+
{ concurrency: "unbounded" },
|
|
218
|
+
)
|
|
219
|
+
return { id, task, phases: taskPhases }
|
|
220
|
+
}),
|
|
221
|
+
),
|
|
222
|
+
{ concurrency: "unbounded" },
|
|
223
|
+
)
|
|
224
|
+
for (const documents of taskDocuments) {
|
|
225
|
+
if (documents.task) tasks.set(documents.id, documents.task)
|
|
226
|
+
for (const phase of documents.phases) {
|
|
227
|
+
if (phase) phases.set(`${documents.id}/${phase.id}`, phase)
|
|
197
228
|
}
|
|
198
229
|
}
|
|
199
230
|
|
|
@@ -262,7 +293,8 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
262
293
|
]
|
|
263
294
|
})
|
|
264
295
|
|
|
265
|
-
const validation =
|
|
296
|
+
const validation =
|
|
297
|
+
options.validation ?? (yield* workbase.validate(root))
|
|
266
298
|
const validationBlockers = (
|
|
267
299
|
paths: readonly string[],
|
|
268
300
|
): GraphBlocker[] =>
|
|
@@ -45,7 +45,7 @@ interface ValidationIssue {
|
|
|
45
45
|
readonly message: string
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
interface ValidationReport {
|
|
48
|
+
export interface ValidationReport {
|
|
49
49
|
readonly root: string
|
|
50
50
|
readonly issues: readonly ValidationIssue[]
|
|
51
51
|
readonly epicCount: number
|
|
@@ -634,34 +634,64 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
634
634
|
return decoded.value
|
|
635
635
|
})
|
|
636
636
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
637
|
+
const epicIds = yield* readDirectories(join(root, "epics"))
|
|
638
|
+
const epicDocuments = yield* Effect.all(
|
|
639
|
+
epicIds.map((id) =>
|
|
640
|
+
Effect.gen(function* () {
|
|
641
|
+
const path = join(root, "epics", id, "EPIC.md")
|
|
642
|
+
const data = yield* readDocument(path, EpicFrontmatter)
|
|
643
|
+
return data ? { id, path, data } : null
|
|
644
|
+
}),
|
|
645
|
+
),
|
|
646
|
+
{ concurrency: "unbounded" },
|
|
647
|
+
)
|
|
648
|
+
for (const document of epicDocuments) {
|
|
649
|
+
if (document) epics.set(document.id, document)
|
|
643
650
|
}
|
|
644
651
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
652
|
+
const taskIds = yield* readDirectories(join(root, "tasks"))
|
|
653
|
+
const taskDocuments = yield* Effect.all(
|
|
654
|
+
taskIds.map((id) =>
|
|
655
|
+
Effect.gen(function* () {
|
|
656
|
+
const taskPath = join(root, "tasks", id)
|
|
657
|
+
const path = join(taskPath, "TASK.md")
|
|
658
|
+
const data = yield* readDocument(path, TaskFrontmatter)
|
|
659
|
+
const phaseIds = yield* readDirectories(
|
|
660
|
+
join(taskPath, "phases"),
|
|
661
|
+
)
|
|
662
|
+
const taskPhases = yield* Effect.all(
|
|
663
|
+
phaseIds.map((phaseId) =>
|
|
664
|
+
Effect.gen(function* () {
|
|
665
|
+
const phasePath = join(
|
|
666
|
+
taskPath,
|
|
667
|
+
"phases",
|
|
668
|
+
phaseId,
|
|
669
|
+
"PHASE.md",
|
|
670
|
+
)
|
|
671
|
+
const phase = yield* readDocument(
|
|
672
|
+
phasePath,
|
|
673
|
+
PhaseFrontmatter,
|
|
674
|
+
)
|
|
675
|
+
return phase
|
|
676
|
+
? { id: phaseId, path: phasePath, data: phase }
|
|
677
|
+
: null
|
|
678
|
+
}),
|
|
679
|
+
),
|
|
680
|
+
{ concurrency: "unbounded" },
|
|
681
|
+
)
|
|
682
|
+
return {
|
|
683
|
+
id,
|
|
684
|
+
task: data ? { id, path, data } : null,
|
|
685
|
+
phases: taskPhases,
|
|
686
|
+
}
|
|
687
|
+
}),
|
|
688
|
+
),
|
|
689
|
+
{ concurrency: "unbounded" },
|
|
690
|
+
)
|
|
691
|
+
for (const documents of taskDocuments) {
|
|
692
|
+
if (documents.task) tasks.set(documents.id, documents.task)
|
|
693
|
+
for (const phase of documents.phases) {
|
|
694
|
+
if (phase) phases.set(`${documents.id}/${phase.id}`, phase)
|
|
665
695
|
}
|
|
666
696
|
}
|
|
667
697
|
|
package/src/work-view.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import type { GraphNode } from "./graph-schema"
|
|
3
3
|
import { GraphService } from "./services/GraphService"
|
|
4
|
+
import type { ValidationReport } from "./services/WorkbaseService"
|
|
4
5
|
import type { WorkStatus } from "./workbase/schemas"
|
|
5
6
|
|
|
6
7
|
type WorkNode = Extract<
|
|
@@ -12,6 +13,7 @@ type ExecutionNode = Extract<WorkNode, { readonly kind: "execution-unit" }>
|
|
|
12
13
|
|
|
13
14
|
export interface WorkViewOptions {
|
|
14
15
|
readonly cwd?: string
|
|
16
|
+
readonly validation?: ValidationReport
|
|
15
17
|
readonly statuses?: readonly string[]
|
|
16
18
|
readonly repositories?: readonly string[]
|
|
17
19
|
readonly ready?: boolean
|
|
@@ -170,6 +172,7 @@ export const getWorkViews = (options: WorkViewOptions = {}) =>
|
|
|
170
172
|
)
|
|
171
173
|
const graph = yield* graphService.get({
|
|
172
174
|
cwd: options.cwd,
|
|
175
|
+
validation: options.validation,
|
|
173
176
|
include: ["workspace"],
|
|
174
177
|
})
|
|
175
178
|
const workNodes = graph.nodes.filter(
|