@markjaquith/agency 2.1.0 → 2.3.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 +6 -0
- package/package.json +1 -1
- package/src/commands/init.test.ts +3 -0
- package/src/commands/work.test.ts +1 -1
- package/src/markdown.d.ts +4 -0
- package/src/services/WorkbaseService.test.ts +52 -0
- package/src/services/WorkbaseService.ts +24 -0
- package/src/workbase/AGENTS.md +32 -0
- package/src/workbase/agents-file.ts +24 -0
- package/src/workbase/work-target.test.ts +7 -7
- package/src/workbase/work-target.ts +14 -2
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ frontmatter; prose below it supplies human and agent context.
|
|
|
42
42
|
|
|
43
43
|
```text
|
|
44
44
|
workbase/
|
|
45
|
+
AGENTS.md # managed workbase instructions
|
|
45
46
|
agency.json
|
|
46
47
|
repos/
|
|
47
48
|
frontend/ # bare Git repository or symlink
|
|
@@ -68,6 +69,11 @@ workbase/
|
|
|
68
69
|
backend/
|
|
69
70
|
```
|
|
70
71
|
|
|
72
|
+
Agency creates `AGENTS.md` during initialization and ensures it exists whenever
|
|
73
|
+
the workbase is discovered. A checksum in the generated file lets newer Agency
|
|
74
|
+
versions refresh unmodified instructions while preserving custom or edited
|
|
75
|
+
files.
|
|
76
|
+
|
|
71
77
|
Repository metadata comes directly from Git under `repos/{alias}`. Workbase
|
|
72
78
|
configuration may provide a custom writable-worktree creation command.
|
|
73
79
|
|
package/package.json
CHANGED
|
@@ -33,6 +33,9 @@ describe("init command", () => {
|
|
|
33
33
|
expect(await Bun.file(join(root, ".gitignore")).text()).toBe(
|
|
34
34
|
"/repos/\n/tasks/*/code/\n/tasks/*/phases/*/code/\n",
|
|
35
35
|
)
|
|
36
|
+
expect(await Bun.file(join(root, "AGENTS.md")).text()).toContain(
|
|
37
|
+
"# Agency Workbase",
|
|
38
|
+
)
|
|
36
39
|
})
|
|
37
40
|
|
|
38
41
|
test("preserves existing gitignore entries", async () => {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
+
import { createHash } from "node:crypto"
|
|
3
4
|
import { mkdir } from "node:fs/promises"
|
|
4
5
|
import { dirname, join } from "node:path"
|
|
5
6
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
7
|
+
import { managedWorkbaseAgents } from "../workbase/agents-file"
|
|
6
8
|
import { WorkbaseService } from "./WorkbaseService"
|
|
7
9
|
|
|
8
10
|
const write = async (root: string, path: string, content: string) => {
|
|
@@ -11,6 +13,11 @@ const write = async (root: string, path: string, content: string) => {
|
|
|
11
13
|
await Bun.write(fullPath, content)
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
const managedAgents = (body: string) => {
|
|
17
|
+
const checksum = createHash("sha256").update(body).digest("hex")
|
|
18
|
+
return `<!-- agency-managed: sha256=${checksum} -->\n\n${body}`
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
describe("WorkbaseService", () => {
|
|
15
22
|
let root: string
|
|
16
23
|
|
|
@@ -35,6 +42,51 @@ describe("WorkbaseService", () => {
|
|
|
35
42
|
)
|
|
36
43
|
|
|
37
44
|
expect(discovered).toBe(root)
|
|
45
|
+
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
46
|
+
managedWorkbaseAgents,
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("preserves an unmanaged workbase AGENTS.md", async () => {
|
|
51
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
52
|
+
await write(root, "AGENTS.md", "# Custom instructions\n")
|
|
53
|
+
|
|
54
|
+
await runTestEffect(
|
|
55
|
+
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
59
|
+
"# Custom instructions\n",
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test("updates an unmodified managed workbase AGENTS.md", async () => {
|
|
64
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
65
|
+
await write(
|
|
66
|
+
root,
|
|
67
|
+
"AGENTS.md",
|
|
68
|
+
managedAgents("# Previous Agency instructions\n"),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
await runTestEffect(
|
|
72
|
+
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
76
|
+
managedWorkbaseAgents,
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("preserves a modified managed workbase AGENTS.md", async () => {
|
|
81
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
82
|
+
const content = `${managedAgents("# Previous Agency instructions\n")}\nUser edit\n`
|
|
83
|
+
await write(root, "AGENTS.md", content)
|
|
84
|
+
|
|
85
|
+
await runTestEffect(
|
|
86
|
+
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(content)
|
|
38
90
|
})
|
|
39
91
|
|
|
40
92
|
test("rejects an invalid worktree command template", async () => {
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
type TaskFrontmatter as TaskData,
|
|
16
16
|
} from "../workbase/schemas"
|
|
17
17
|
import { validateWorktreeCreateCommand } from "../workbase/worktree-command"
|
|
18
|
+
import {
|
|
19
|
+
canUpdateManagedWorkbaseAgents,
|
|
20
|
+
managedWorkbaseAgents,
|
|
21
|
+
} from "../workbase/agents-file"
|
|
18
22
|
|
|
19
23
|
class WorkbaseNotFoundError extends Data.TaggedError("WorkbaseNotFoundError")<{
|
|
20
24
|
readonly message: string
|
|
@@ -64,6 +68,24 @@ const decode = <S extends Schema.Schema.AnyNoContext>(
|
|
|
64
68
|
: { success: true, value: result.right }
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
const ensureWorkbaseAgents = (root: string) =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const fs = yield* FileSystemService
|
|
74
|
+
const path = join(root, "AGENTS.md")
|
|
75
|
+
if (!(yield* fs.exists(path))) {
|
|
76
|
+
yield* fs.writeFile(path, managedWorkbaseAgents)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const content = yield* fs.readFile(path)
|
|
81
|
+
if (
|
|
82
|
+
content !== managedWorkbaseAgents &&
|
|
83
|
+
canUpdateManagedWorkbaseAgents(content)
|
|
84
|
+
) {
|
|
85
|
+
yield* fs.writeFile(path, managedWorkbaseAgents)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
67
89
|
const findCycles = (nodes: readonly Dependency[]): readonly string[] => {
|
|
68
90
|
const dependencies = new Map(
|
|
69
91
|
nodes.map((node) => [node.id, [...(node.dependsOn ?? [])]]),
|
|
@@ -142,6 +164,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
142
164
|
`${existing}${prefix}${missing.join("\n")}\n`,
|
|
143
165
|
)
|
|
144
166
|
}
|
|
167
|
+
yield* ensureWorkbaseAgents(root)
|
|
145
168
|
|
|
146
169
|
return root
|
|
147
170
|
}),
|
|
@@ -198,6 +221,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
198
221
|
})
|
|
199
222
|
}
|
|
200
223
|
}
|
|
224
|
+
yield* ensureWorkbaseAgents(current)
|
|
201
225
|
return current
|
|
202
226
|
}
|
|
203
227
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Agency Workbase
|
|
2
|
+
|
|
3
|
+
This directory is an Agency workbase. Epics, tasks, and phases are durable
|
|
4
|
+
Markdown documents; repository aliases and generated Git worktrees provide code
|
|
5
|
+
access.
|
|
6
|
+
|
|
7
|
+
## Session Context
|
|
8
|
+
|
|
9
|
+
Before doing work, identify the current entity from the working directory and
|
|
10
|
+
read its context:
|
|
11
|
+
|
|
12
|
+
- In `epics/<epic>/`, read `EPIC.md`. Coordinate the epic's tasks without
|
|
13
|
+
writing implementation code.
|
|
14
|
+
- In `tasks/<task>/`, read `TASK.md`. A task with `phases` is an orchestration
|
|
15
|
+
session; a task without `phases` is a single execution unit.
|
|
16
|
+
- In `tasks/<task>/phases/<phase>/`, read both `../../TASK.md` and `PHASE.md`.
|
|
17
|
+
The phase is the execution unit.
|
|
18
|
+
|
|
19
|
+
For execution units, writable and reference checkouts live under `code/` when
|
|
20
|
+
materialized. Write only through the checkout named by the singular `repo`
|
|
21
|
+
field. Repositories listed in plural `repos` are read-only references.
|
|
22
|
+
|
|
23
|
+
## Safety
|
|
24
|
+
|
|
25
|
+
- Keep task-level decisions in `TASK.md` and phase-specific delivery context in
|
|
26
|
+
`PHASE.md`.
|
|
27
|
+
- Do not manually create, move, or remove worktrees under `code/`.
|
|
28
|
+
- Do not edit bare repositories or repository symlinks under `repos/`.
|
|
29
|
+
- Do not run `agency work` from an active agent session unless the user
|
|
30
|
+
explicitly asks to launch another agent.
|
|
31
|
+
- Run `agency validate` before worktree or pull-request operations.
|
|
32
|
+
- Create a pull request only when the user explicitly requests it.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import agentsTemplate from "./AGENTS.md" with { type: "text" }
|
|
3
|
+
|
|
4
|
+
const managedHeaderPattern =
|
|
5
|
+
/^<!-- agency-managed: sha256=([a-f0-9]{64}) -->\r?\n\r?\n/
|
|
6
|
+
|
|
7
|
+
const checksum = (content: string) =>
|
|
8
|
+
createHash("sha256").update(content).digest("hex")
|
|
9
|
+
|
|
10
|
+
const canonicalBody = agentsTemplate.endsWith("\n")
|
|
11
|
+
? agentsTemplate
|
|
12
|
+
: `${agentsTemplate}\n`
|
|
13
|
+
|
|
14
|
+
const renderManagedWorkbaseAgents = (body: string = canonicalBody) =>
|
|
15
|
+
`<!-- agency-managed: sha256=${checksum(body)} -->\n\n${body}`
|
|
16
|
+
|
|
17
|
+
export const managedWorkbaseAgents = renderManagedWorkbaseAgents()
|
|
18
|
+
|
|
19
|
+
export const canUpdateManagedWorkbaseAgents = (content: string) => {
|
|
20
|
+
const match = content.match(managedHeaderPattern)
|
|
21
|
+
if (!match?.[1]) return false
|
|
22
|
+
|
|
23
|
+
return checksum(content.slice(match[0].length)) === match[1]
|
|
24
|
+
}
|
|
@@ -56,13 +56,13 @@ describe("work target choices", () => {
|
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
expect(choices.map((choice) => choice.label)).toEqual([
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
59
|
+
"\x1b[35m\x1b[0m delivery\x1b[2m - Ship the release\x1b[0m",
|
|
60
|
+
" \x1b[36m\x1b[0m multi",
|
|
61
|
+
" \x1b[33m\x1b[0m build",
|
|
62
|
+
" \x1b[33m\x1b[0m verify",
|
|
63
|
+
" \x1b[33m\x1b[0m unlisted",
|
|
64
|
+
" \x1b[36m\x1b[0m single",
|
|
65
|
+
"\x1b[36m\x1b[0m standalone\x1b[2m - Independent work\x1b[0m",
|
|
66
66
|
])
|
|
67
67
|
expect(choices.map((choice) => choice.target.kind)).toEqual([
|
|
68
68
|
"epic",
|
|
@@ -57,7 +57,13 @@ const label = (
|
|
|
57
57
|
id: string,
|
|
58
58
|
description?: string,
|
|
59
59
|
) =>
|
|
60
|
-
`${indent}${
|
|
60
|
+
`${indent}${
|
|
61
|
+
{
|
|
62
|
+
epic: "\x1b[35m\x1b[0m",
|
|
63
|
+
task: "\x1b[36m\x1b[0m",
|
|
64
|
+
phase: "\x1b[33m\x1b[0m",
|
|
65
|
+
}[kind]
|
|
66
|
+
} ${id}${description === undefined ? "" : `\x1b[2m - ${description}\x1b[0m`}`
|
|
61
67
|
|
|
62
68
|
const taskChoices = (
|
|
63
69
|
task: TaskRecord,
|
|
@@ -164,7 +170,13 @@ export const pickWorkTarget: PickWorkTarget = (choices) =>
|
|
|
164
170
|
.map((choice, index) => `${index}\t${choice.label}`)
|
|
165
171
|
.join("\n")
|
|
166
172
|
const process = Bun.spawn(
|
|
167
|
-
[
|
|
173
|
+
[
|
|
174
|
+
"fzf",
|
|
175
|
+
"--ansi",
|
|
176
|
+
"--delimiter=\t",
|
|
177
|
+
"--with-nth=2..",
|
|
178
|
+
"--prompt=Work on> ",
|
|
179
|
+
],
|
|
168
180
|
{ stdin: new Blob([input]), stdout: "pipe", stderr: "inherit" },
|
|
169
181
|
)
|
|
170
182
|
const [exitCode, output] = await Promise.all([
|