@markjaquith/agency 2.7.3 → 2.9.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 +71 -6
- package/cli.ts +52 -4
- package/fixtures/protocol/error.json +14 -0
- package/fixtures/protocol/success.json +7 -0
- package/index.ts +1 -0
- package/package.json +16 -1
- package/schemas/agency-envelope-v1.schema.json +38 -0
- package/skills/agency/SKILL.md +2 -0
- package/src/cli-parser.test.ts +2 -0
- package/src/cli-parser.ts +31 -1
- package/src/cli.test.ts +90 -1
- package/src/commands/init.test.ts +3 -8
- package/src/commands/integration.test.ts +51 -0
- package/src/commands/integration.ts +62 -0
- package/src/commands/read-only.test.ts +144 -0
- package/src/commands/validate.ts +7 -0
- package/src/commands/work.test.ts +3 -3
- package/src/protocol.test.ts +87 -0
- package/src/protocol.ts +211 -0
- package/src/services/IntegrationService.test.ts +137 -0
- package/src/services/IntegrationService.ts +136 -0
- package/src/services/WorkbaseService.test.ts +4 -118
- package/src/services/WorkbaseService.ts +0 -53
- package/src/services/WorktreeService.test.ts +2 -2
- package/src/test-utils.ts +12 -3
- package/src/utils/effect.test.ts +9 -3
- package/src/utils/effect.ts +4 -2
- package/src/workbase/AGENTS.md +2 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { FileSystemService } from "./FileSystemService"
|
|
4
|
+
import { WorkbaseService } from "./WorkbaseService"
|
|
5
|
+
import {
|
|
6
|
+
canUpdateManagedWorkbaseAgents,
|
|
7
|
+
managedWorkbaseAgents,
|
|
8
|
+
} from "../workbase/agents-file"
|
|
9
|
+
import {
|
|
10
|
+
canUpdateManagedWorkbaseOpencode,
|
|
11
|
+
managedWorkbaseOpencode,
|
|
12
|
+
} from "../workbase/opencode-file"
|
|
13
|
+
|
|
14
|
+
type IntegrationFileState = "managed" | "customized" | "missing" | "drifted"
|
|
15
|
+
|
|
16
|
+
interface IntegrationFileStatus {
|
|
17
|
+
readonly name: "agents" | "opencode"
|
|
18
|
+
readonly path: string
|
|
19
|
+
readonly state: IntegrationFileState
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface IntegrationSyncFile extends IntegrationFileStatus {
|
|
23
|
+
readonly changed: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const classify = (
|
|
27
|
+
name: IntegrationFileStatus["name"],
|
|
28
|
+
path: string,
|
|
29
|
+
content: string,
|
|
30
|
+
managed: string,
|
|
31
|
+
canUpdate: (content: string) => boolean,
|
|
32
|
+
): IntegrationFileStatus => ({
|
|
33
|
+
name,
|
|
34
|
+
path,
|
|
35
|
+
state:
|
|
36
|
+
content === managed
|
|
37
|
+
? "managed"
|
|
38
|
+
: canUpdate(content)
|
|
39
|
+
? "drifted"
|
|
40
|
+
: "customized",
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const inspect = (root: string) =>
|
|
44
|
+
Effect.gen(function* () {
|
|
45
|
+
const fs = yield* FileSystemService
|
|
46
|
+
const agentsPath = join(root, "AGENTS.md")
|
|
47
|
+
const opencodeDirectory = join(root, ".opencode")
|
|
48
|
+
const opencodePath = join(opencodeDirectory, "opencode.jsonc")
|
|
49
|
+
const opencodeJsonPath = join(opencodeDirectory, "opencode.json")
|
|
50
|
+
const files: IntegrationFileStatus[] = []
|
|
51
|
+
|
|
52
|
+
files.push(
|
|
53
|
+
(yield* fs.readSymlinkTarget(agentsPath)) !== null
|
|
54
|
+
? { name: "agents", path: agentsPath, state: "customized" }
|
|
55
|
+
: (yield* fs.exists(agentsPath))
|
|
56
|
+
? classify(
|
|
57
|
+
"agents",
|
|
58
|
+
agentsPath,
|
|
59
|
+
yield* fs.readFile(agentsPath),
|
|
60
|
+
managedWorkbaseAgents,
|
|
61
|
+
canUpdateManagedWorkbaseAgents,
|
|
62
|
+
)
|
|
63
|
+
: { name: "agents", path: agentsPath, state: "missing" },
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if ((yield* fs.readSymlinkTarget(opencodePath)) !== null) {
|
|
67
|
+
files.push({
|
|
68
|
+
name: "opencode",
|
|
69
|
+
path: opencodePath,
|
|
70
|
+
state: "customized",
|
|
71
|
+
})
|
|
72
|
+
} else if (yield* fs.exists(opencodePath)) {
|
|
73
|
+
files.push(
|
|
74
|
+
classify(
|
|
75
|
+
"opencode",
|
|
76
|
+
opencodePath,
|
|
77
|
+
yield* fs.readFile(opencodePath),
|
|
78
|
+
managedWorkbaseOpencode(root),
|
|
79
|
+
canUpdateManagedWorkbaseOpencode,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
} else if (yield* fs.exists(opencodeJsonPath)) {
|
|
83
|
+
files.push({
|
|
84
|
+
name: "opencode",
|
|
85
|
+
path: opencodeJsonPath,
|
|
86
|
+
state: "customized",
|
|
87
|
+
})
|
|
88
|
+
} else {
|
|
89
|
+
files.push({ name: "opencode", path: opencodePath, state: "missing" })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return files
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
96
|
+
"IntegrationService",
|
|
97
|
+
{
|
|
98
|
+
sync: () => ({
|
|
99
|
+
status: (startPath: string = process.cwd()) =>
|
|
100
|
+
Effect.gen(function* () {
|
|
101
|
+
const workbase = yield* WorkbaseService
|
|
102
|
+
const root = yield* workbase.discover(startPath)
|
|
103
|
+
return { root, files: yield* inspect(root) }
|
|
104
|
+
}),
|
|
105
|
+
|
|
106
|
+
sync: (startPath: string = process.cwd()) =>
|
|
107
|
+
Effect.gen(function* () {
|
|
108
|
+
const fs = yield* FileSystemService
|
|
109
|
+
const workbase = yield* WorkbaseService
|
|
110
|
+
const root = yield* workbase.discover(startPath)
|
|
111
|
+
const statuses = yield* inspect(root)
|
|
112
|
+
const files: IntegrationSyncFile[] = []
|
|
113
|
+
|
|
114
|
+
for (const status of statuses) {
|
|
115
|
+
const changed =
|
|
116
|
+
status.state === "missing" || status.state === "drifted"
|
|
117
|
+
if (changed) {
|
|
118
|
+
if (status.name === "agents") {
|
|
119
|
+
yield* fs.writeFile(status.path, managedWorkbaseAgents)
|
|
120
|
+
} else {
|
|
121
|
+
yield* fs.createDirectory(join(root, ".opencode"))
|
|
122
|
+
yield* fs.writeFile(status.path, managedWorkbaseOpencode(root))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
files.push({
|
|
126
|
+
...status,
|
|
127
|
+
state: changed ? "managed" : status.state,
|
|
128
|
+
changed,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { root, files }
|
|
133
|
+
}),
|
|
134
|
+
}),
|
|
135
|
+
},
|
|
136
|
+
) {}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { createHash } from "node:crypto"
|
|
4
3
|
import { mkdir, realpath } from "node:fs/promises"
|
|
5
4
|
import { dirname, join } from "node:path"
|
|
6
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
7
|
-
import { managedWorkbaseAgents } from "../workbase/agents-file"
|
|
8
|
-
import { managedWorkbaseOpencode } from "../workbase/opencode-file"
|
|
9
6
|
import { WorkbaseService } from "./WorkbaseService"
|
|
10
7
|
|
|
11
8
|
const write = async (root: string, path: string, content: string) => {
|
|
@@ -14,16 +11,6 @@ const write = async (root: string, path: string, content: string) => {
|
|
|
14
11
|
await Bun.write(fullPath, content)
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
const managedAgents = (body: string) => {
|
|
18
|
-
const checksum = createHash("sha256").update(body).digest("hex")
|
|
19
|
-
return `<!-- agency-managed: sha256=${checksum} -->\n\n${body}`
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const managedOpencode = (body: string) => {
|
|
23
|
-
const checksum = createHash("sha256").update(body).digest("hex")
|
|
24
|
-
return `// agency-managed: sha256=${checksum}\n\n${body}`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
14
|
describe("WorkbaseService", () => {
|
|
28
15
|
let root: string
|
|
29
16
|
|
|
@@ -48,12 +35,10 @@ describe("WorkbaseService", () => {
|
|
|
48
35
|
)
|
|
49
36
|
|
|
50
37
|
expect(discovered).toBe(root)
|
|
51
|
-
expect(await Bun.file(join(root, "AGENTS.md")).
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
managedWorkbaseOpencode(root),
|
|
56
|
-
)
|
|
38
|
+
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
39
|
+
expect(
|
|
40
|
+
await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
|
|
41
|
+
).toBe(false)
|
|
57
42
|
})
|
|
58
43
|
|
|
59
44
|
test("registers canonical workbase paths without duplicates", async () => {
|
|
@@ -88,105 +73,6 @@ describe("WorkbaseService", () => {
|
|
|
88
73
|
).toEqual({ version: 1, workbases: [first] })
|
|
89
74
|
})
|
|
90
75
|
|
|
91
|
-
test("preserves an unmanaged workbase OpenCode config", async () => {
|
|
92
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
93
|
-
await write(root, ".opencode/opencode.jsonc", '{"model":"test/model"}\n')
|
|
94
|
-
|
|
95
|
-
await runTestEffect(
|
|
96
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
100
|
-
'{"model":"test/model"}\n',
|
|
101
|
-
)
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
test("does not override an existing JSON OpenCode config", async () => {
|
|
105
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
106
|
-
await write(root, ".opencode/opencode.json", '{"model":"test/model"}\n')
|
|
107
|
-
|
|
108
|
-
await runTestEffect(
|
|
109
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
expect(
|
|
113
|
-
await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
|
|
114
|
-
).toBe(false)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
test("updates an unmodified managed workbase OpenCode config", async () => {
|
|
118
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
119
|
-
await write(
|
|
120
|
-
root,
|
|
121
|
-
".opencode/opencode.jsonc",
|
|
122
|
-
managedOpencode('{"references":{}}\n'),
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
await runTestEffect(
|
|
126
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
130
|
-
managedWorkbaseOpencode(root),
|
|
131
|
-
)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
test("preserves a modified managed workbase OpenCode config", async () => {
|
|
135
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
136
|
-
const content = `${managedOpencode('{"references":{}}\n')}\n// User edit\n`
|
|
137
|
-
await write(root, ".opencode/opencode.jsonc", content)
|
|
138
|
-
|
|
139
|
-
await runTestEffect(
|
|
140
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
144
|
-
content,
|
|
145
|
-
)
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
test("preserves an unmanaged workbase AGENTS.md", async () => {
|
|
149
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
150
|
-
await write(root, "AGENTS.md", "# Custom instructions\n")
|
|
151
|
-
|
|
152
|
-
await runTestEffect(
|
|
153
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
157
|
-
"# Custom instructions\n",
|
|
158
|
-
)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
test("updates an unmodified managed workbase AGENTS.md", async () => {
|
|
162
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
163
|
-
await write(
|
|
164
|
-
root,
|
|
165
|
-
"AGENTS.md",
|
|
166
|
-
managedAgents("# Previous Agency instructions\n"),
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
await runTestEffect(
|
|
170
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
174
|
-
managedWorkbaseAgents,
|
|
175
|
-
)
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
test("preserves a modified managed workbase AGENTS.md", async () => {
|
|
179
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
180
|
-
const content = `${managedAgents("# Previous Agency instructions\n")}\nUser edit\n`
|
|
181
|
-
await write(root, "AGENTS.md", content)
|
|
182
|
-
|
|
183
|
-
await runTestEffect(
|
|
184
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(content)
|
|
188
|
-
})
|
|
189
|
-
|
|
190
76
|
test("rejects an invalid worktree command template", async () => {
|
|
191
77
|
await write(
|
|
192
78
|
root,
|
|
@@ -17,14 +17,6 @@ import {
|
|
|
17
17
|
type TaskFrontmatter as TaskData,
|
|
18
18
|
} from "../workbase/schemas"
|
|
19
19
|
import { validateWorktreeCreateCommand } from "../workbase/worktree-command"
|
|
20
|
-
import {
|
|
21
|
-
canUpdateManagedWorkbaseAgents,
|
|
22
|
-
managedWorkbaseAgents,
|
|
23
|
-
} from "../workbase/agents-file"
|
|
24
|
-
import {
|
|
25
|
-
canUpdateManagedWorkbaseOpencode,
|
|
26
|
-
managedWorkbaseOpencode,
|
|
27
|
-
} from "../workbase/opencode-file"
|
|
28
20
|
|
|
29
21
|
class WorkbaseNotFoundError extends Data.TaggedError("WorkbaseNotFoundError")<{
|
|
30
22
|
readonly message: string
|
|
@@ -119,48 +111,6 @@ const readRegistry = (configDirectory?: string) =>
|
|
|
119
111
|
return { path, registry: decoded.value }
|
|
120
112
|
})
|
|
121
113
|
|
|
122
|
-
const ensureWorkbaseAgents = (root: string) =>
|
|
123
|
-
Effect.gen(function* () {
|
|
124
|
-
const fs = yield* FileSystemService
|
|
125
|
-
const path = join(root, "AGENTS.md")
|
|
126
|
-
if (!(yield* fs.exists(path))) {
|
|
127
|
-
yield* fs.writeFile(path, managedWorkbaseAgents)
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const content = yield* fs.readFile(path)
|
|
132
|
-
if (
|
|
133
|
-
content !== managedWorkbaseAgents &&
|
|
134
|
-
canUpdateManagedWorkbaseAgents(content)
|
|
135
|
-
) {
|
|
136
|
-
yield* fs.writeFile(path, managedWorkbaseAgents)
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
const ensureWorkbaseOpencode = (root: string) =>
|
|
141
|
-
Effect.gen(function* () {
|
|
142
|
-
const fs = yield* FileSystemService
|
|
143
|
-
const directory = join(root, ".opencode")
|
|
144
|
-
const path = join(directory, "opencode.jsonc")
|
|
145
|
-
const managed = managedWorkbaseOpencode(root)
|
|
146
|
-
if (!(yield* fs.exists(path))) {
|
|
147
|
-
if (yield* fs.exists(join(directory, "opencode.json"))) return
|
|
148
|
-
yield* fs.createDirectory(directory)
|
|
149
|
-
yield* fs.writeFile(path, managed)
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const content = yield* fs.readFile(path)
|
|
154
|
-
if (content !== managed && canUpdateManagedWorkbaseOpencode(content)) {
|
|
155
|
-
yield* fs.writeFile(path, managed)
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
const ensureWorkbaseAgentFiles = (root: string) =>
|
|
160
|
-
Effect.all([ensureWorkbaseAgents(root), ensureWorkbaseOpencode(root)], {
|
|
161
|
-
concurrency: "unbounded",
|
|
162
|
-
})
|
|
163
|
-
|
|
164
114
|
const findCycles = (nodes: readonly Dependency[]): readonly string[] => {
|
|
165
115
|
const dependencies = new Map(
|
|
166
116
|
nodes.map((node) => [node.id, [...(node.dependsOn ?? [])]]),
|
|
@@ -239,8 +189,6 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
239
189
|
`${existing}${prefix}${missing.join("\n")}\n`,
|
|
240
190
|
)
|
|
241
191
|
}
|
|
242
|
-
yield* ensureWorkbaseAgentFiles(root)
|
|
243
|
-
|
|
244
192
|
return root
|
|
245
193
|
}),
|
|
246
194
|
|
|
@@ -296,7 +244,6 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
296
244
|
})
|
|
297
245
|
}
|
|
298
246
|
}
|
|
299
|
-
yield* ensureWorkbaseAgentFiles(current)
|
|
300
247
|
return current
|
|
301
248
|
}
|
|
302
249
|
}
|
|
@@ -3,7 +3,7 @@ import { Effect } from "effect"
|
|
|
3
3
|
import { mkdir, rm } from "node:fs/promises"
|
|
4
4
|
import { join } from "node:path"
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
captureErrors,
|
|
7
7
|
cleanupTempDir,
|
|
8
8
|
createTempDir,
|
|
9
9
|
runTestEffect,
|
|
@@ -287,7 +287,7 @@ describe("WorktreeService", () => {
|
|
|
287
287
|
),
|
|
288
288
|
)
|
|
289
289
|
|
|
290
|
-
const logs = await
|
|
290
|
+
const logs = await captureErrors(() =>
|
|
291
291
|
runTestEffect(
|
|
292
292
|
WorktreeService.pipe(
|
|
293
293
|
Effect.flatMap((service) =>
|
package/src/test-utils.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { PhaseService } from "./services/PhaseService"
|
|
|
12
12
|
import { WorktreeService } from "./services/WorktreeService"
|
|
13
13
|
import { PullRequestService } from "./services/PullRequestService"
|
|
14
14
|
import { ArchiveService } from "./services/ArchiveService"
|
|
15
|
+
import { IntegrationService } from "./services/IntegrationService"
|
|
15
16
|
|
|
16
17
|
export const createTempDir = () => mkdtemp(join(tmpdir(), "agency-test-"))
|
|
17
18
|
|
|
@@ -28,6 +29,7 @@ const TestLayer = Layer.mergeAll(
|
|
|
28
29
|
WorktreeService.Default,
|
|
29
30
|
PullRequestService.Default,
|
|
30
31
|
ArchiveService.Default,
|
|
32
|
+
IntegrationService.Default,
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
export async function runTestEffect<A, E>(
|
|
@@ -43,17 +45,24 @@ export async function runTestEffect<A, E>(
|
|
|
43
45
|
return Effect.runPromise(program)
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
async function captureConsole(
|
|
49
|
+
method: "log" | "error",
|
|
47
50
|
run: () => Promise<unknown>,
|
|
48
51
|
): Promise<string[]> {
|
|
49
52
|
const logs: string[] = []
|
|
50
|
-
const
|
|
53
|
+
const output = spyOn(console, method).mockImplementation((...args) => {
|
|
51
54
|
logs.push(args.join(" "))
|
|
52
55
|
})
|
|
53
56
|
try {
|
|
54
57
|
await run()
|
|
55
58
|
return logs
|
|
56
59
|
} finally {
|
|
57
|
-
|
|
60
|
+
output.mockRestore()
|
|
58
61
|
}
|
|
59
62
|
}
|
|
63
|
+
|
|
64
|
+
export const captureLogs = (run: () => Promise<unknown>) =>
|
|
65
|
+
captureConsole("log", run)
|
|
66
|
+
|
|
67
|
+
export const captureErrors = (run: () => Promise<unknown>) =>
|
|
68
|
+
captureConsole("error", run)
|
package/src/utils/effect.test.ts
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
1
|
+
import { describe, expect, spyOn, test } from "bun:test"
|
|
2
2
|
import { captureLogs } from "../test-utils"
|
|
3
3
|
import { createLoggers } from "./effect"
|
|
4
4
|
|
|
5
5
|
describe("createLoggers", () => {
|
|
6
6
|
test("keeps JSON output machine-readable when verbose is enabled", async () => {
|
|
7
|
+
const errors: string[] = []
|
|
8
|
+
const error = spyOn(console, "error").mockImplementation((...args) => {
|
|
9
|
+
errors.push(args.join(" "))
|
|
10
|
+
})
|
|
7
11
|
const logs = await captureLogs(async () => {
|
|
8
12
|
const { log, verboseLog } = createLoggers({ json: true, verbose: true })
|
|
9
13
|
verboseLog("debug")
|
|
10
14
|
log('{"ok":true}')
|
|
11
15
|
})
|
|
16
|
+
error.mockRestore()
|
|
12
17
|
|
|
13
18
|
expect(logs).toEqual(['{"ok":true}'])
|
|
19
|
+
expect(errors).toEqual(["debug"])
|
|
14
20
|
})
|
|
15
21
|
|
|
16
|
-
test("lets
|
|
22
|
+
test("lets JSON output override silent", async () => {
|
|
17
23
|
const logs = await captureLogs(async () => {
|
|
18
24
|
const { log } = createLoggers({ json: true, silent: true })
|
|
19
25
|
log('{"ok":true}')
|
|
20
26
|
})
|
|
21
27
|
|
|
22
|
-
expect(logs).toEqual([])
|
|
28
|
+
expect(logs).toEqual(['{"ok":true}'])
|
|
23
29
|
})
|
|
24
30
|
})
|
package/src/utils/effect.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { emitCommandResult } from "../protocol"
|
|
2
|
+
|
|
1
3
|
export function createLoggers(options: {
|
|
2
4
|
readonly silent?: boolean
|
|
3
5
|
readonly verbose?: boolean
|
|
@@ -5,7 +7,7 @@ export function createLoggers(options: {
|
|
|
5
7
|
}) {
|
|
6
8
|
const { silent = false, verbose = false, json = false } = options
|
|
7
9
|
return {
|
|
8
|
-
log: silent ? () => {} : console.log,
|
|
9
|
-
verboseLog: verbose && !silent
|
|
10
|
+
log: json ? emitCommandResult : silent ? () => {} : console.log,
|
|
11
|
+
verboseLog: verbose && !silent ? console.error : () => {},
|
|
10
12
|
}
|
|
11
13
|
}
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -22,6 +22,8 @@ field. Repositories listed in plural `repos` are read-only references.
|
|
|
22
22
|
|
|
23
23
|
## Safety
|
|
24
24
|
|
|
25
|
+
- Discovery and observational commands are read-only; use
|
|
26
|
+
`agency integration sync` to update managed agent files explicitly.
|
|
25
27
|
- Keep task-level decisions in `TASK.md` and phase-specific delivery context in
|
|
26
28
|
`PHASE.md`.
|
|
27
29
|
- Keep execution-unit `status` current with `agency task status` or
|