@markjaquith/agency 2.2.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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -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 () => {
@@ -0,0 +1,4 @@
1
+ declare module "*.md" {
2
+ const content: string
3
+ export default content
4
+ }
@@ -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
+ }