@markjaquith/agency 2.38.1 → 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/services/ContextService.ts +98 -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
|
|
@@ -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 {
|