@markjaquith/agency 1.9.5 → 1.10.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/package.json +1 -1
- package/src/commands/emit.test.ts +53 -0
- package/src/commands/emit.ts +40 -0
- package/src/commands/push.test.ts +49 -4
- package/src/commands/push.ts +15 -0
- package/templates/AGENCY.md +2 -0
package/package.json
CHANGED
|
@@ -78,6 +78,29 @@ describe("emit command", () => {
|
|
|
78
78
|
})
|
|
79
79
|
|
|
80
80
|
describe("basic functionality", () => {
|
|
81
|
+
test("fails when there are uncommitted changes", async () => {
|
|
82
|
+
// Create uncommitted changes
|
|
83
|
+
await Bun.write(join(tempDir, "uncommitted.txt"), "uncommitted\n")
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
runTestEffect(emit({ silent: true, skipFilter: true })),
|
|
87
|
+
).rejects.toThrow(/uncommitted changes/)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test("fails when there are staged but uncommitted changes", async () => {
|
|
91
|
+
// Create a staged but uncommitted change
|
|
92
|
+
await Bun.write(join(tempDir, "staged.txt"), "staged\n")
|
|
93
|
+
await Bun.spawn(["git", "add", "staged.txt"], {
|
|
94
|
+
cwd: tempDir,
|
|
95
|
+
stdout: "pipe",
|
|
96
|
+
stderr: "pipe",
|
|
97
|
+
}).exited
|
|
98
|
+
|
|
99
|
+
expect(
|
|
100
|
+
runTestEffect(emit({ silent: true, skipFilter: true })),
|
|
101
|
+
).rejects.toThrow(/uncommitted changes/)
|
|
102
|
+
})
|
|
103
|
+
|
|
81
104
|
test("throws error when git-filter-repo is not installed", async () => {
|
|
82
105
|
const hasGitFilterRepo = await checkGitFilterRepo()
|
|
83
106
|
if (hasGitFilterRepo) {
|
|
@@ -326,6 +349,36 @@ describe("emit command", () => {
|
|
|
326
349
|
clearCapturedFilterRepoCalls()
|
|
327
350
|
})
|
|
328
351
|
|
|
352
|
+
test("throws when emitted CLAUDE.md references AGENCY.md", async () => {
|
|
353
|
+
await checkoutBranch(tempDir, "main")
|
|
354
|
+
await createBranch(tempDir, "agency--claude-reference-test")
|
|
355
|
+
|
|
356
|
+
await Bun.write(
|
|
357
|
+
join(tempDir, "agency.json"),
|
|
358
|
+
JSON.stringify({
|
|
359
|
+
version: 1,
|
|
360
|
+
injectedFiles: ["AGENTS.md"],
|
|
361
|
+
template: "test",
|
|
362
|
+
createdAt: new Date().toISOString(),
|
|
363
|
+
}),
|
|
364
|
+
)
|
|
365
|
+
await Bun.write(
|
|
366
|
+
join(tempDir, "CLAUDE.md"),
|
|
367
|
+
"# Instructions\n\n@AGENCY.md\n",
|
|
368
|
+
)
|
|
369
|
+
await addAndCommit(
|
|
370
|
+
tempDir,
|
|
371
|
+
["agency.json", "CLAUDE.md"],
|
|
372
|
+
"Add Claude agency reference",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
await expect(
|
|
376
|
+
runTestEffectWithMockFilterRepo(emit({ silent: true })),
|
|
377
|
+
).rejects.toThrow(
|
|
378
|
+
"CLAUDE.md on emitted branch claude-reference-test still references @AGENCY.md",
|
|
379
|
+
)
|
|
380
|
+
})
|
|
381
|
+
|
|
329
382
|
test("constructs correct filter-repo arguments", async () => {
|
|
330
383
|
// Set up fresh branch with agency.json
|
|
331
384
|
await checkoutBranch(tempDir, "main")
|
package/src/commands/emit.ts
CHANGED
|
@@ -54,6 +54,21 @@ export const emitCore = (gitRoot: string, options: EmitOptions) =>
|
|
|
54
54
|
const fs = yield* FileSystemService
|
|
55
55
|
const filterRepo = yield* FilterRepoService
|
|
56
56
|
|
|
57
|
+
// Check for uncommitted changes
|
|
58
|
+
const statusOutput = yield* git.getStatus(gitRoot)
|
|
59
|
+
|
|
60
|
+
if (statusOutput && statusOutput.trim().length > 0) {
|
|
61
|
+
return yield* Effect.fail(
|
|
62
|
+
new Error(
|
|
63
|
+
`You have uncommitted changes. Please commit or stash them before emitting.\n` +
|
|
64
|
+
`The emit command performs internal checkouts that would destroy uncommitted work.\n` +
|
|
65
|
+
`Run 'git status' to see the changes.`,
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
verboseLog(`Working directory is clean`)
|
|
71
|
+
|
|
57
72
|
// Check if git-filter-repo is installed
|
|
58
73
|
const hasFilterRepo = yield* filterRepo.isInstalled()
|
|
59
74
|
if (!hasFilterRepo) {
|
|
@@ -248,9 +263,34 @@ export const emitCore = (gitRoot: string, options: EmitOptions) =>
|
|
|
248
263
|
enabled: !options.silent && !verbose,
|
|
249
264
|
})
|
|
250
265
|
|
|
266
|
+
yield* validateClaudeDoesNotReferenceAgency(gitRoot, emitBranchName)
|
|
267
|
+
|
|
251
268
|
log(done(`Emitted ${highlight.branch(emitBranchName)}`))
|
|
252
269
|
})
|
|
253
270
|
|
|
271
|
+
const validateClaudeDoesNotReferenceAgency = (
|
|
272
|
+
gitRoot: string,
|
|
273
|
+
emitBranchName: string,
|
|
274
|
+
) =>
|
|
275
|
+
Effect.gen(function* () {
|
|
276
|
+
const git = yield* GitService
|
|
277
|
+
const claudeContents = yield* git.getFileAtRef(
|
|
278
|
+
gitRoot,
|
|
279
|
+
emitBranchName,
|
|
280
|
+
"CLAUDE.md",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if (!claudeContents?.includes("@AGENCY.md")) {
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return yield* Effect.fail(
|
|
288
|
+
new Error(
|
|
289
|
+
`CLAUDE.md on emitted branch ${emitBranchName} still references @AGENCY.md. Remove the agency file reference before emitting.`,
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
})
|
|
293
|
+
|
|
254
294
|
const logFilterPreflight = (
|
|
255
295
|
gitRoot: string,
|
|
256
296
|
forkPoint: string,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
addAndCommit,
|
|
14
14
|
renameBranch,
|
|
15
15
|
runTestEffect,
|
|
16
|
+
runTestEffectWithMockFilterRepo,
|
|
16
17
|
} from "../test-utils"
|
|
17
18
|
|
|
18
19
|
async function setupAgencyJson(gitRoot: string): Promise<void> {
|
|
@@ -29,9 +30,9 @@ async function setupAgencyJson(gitRoot: string): Promise<void> {
|
|
|
29
30
|
await addAndCommit(gitRoot, "agency.json", "Add agency.json")
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
async function setupBareRemote(
|
|
33
|
-
// Create a bare repository to
|
|
34
|
-
const remoteDir =
|
|
33
|
+
async function setupBareRemote(): Promise<string> {
|
|
34
|
+
// Create a bare repository outside the working tree to avoid polluting git status
|
|
35
|
+
const remoteDir = await createTempDir()
|
|
35
36
|
await Bun.spawn(["git", "init", "--bare", remoteDir], {
|
|
36
37
|
stdout: "pipe",
|
|
37
38
|
stderr: "pipe",
|
|
@@ -63,7 +64,7 @@ describe("push command", () => {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// Setup bare remote and push main
|
|
66
|
-
remoteDir = await setupBareRemote(
|
|
67
|
+
remoteDir = await setupBareRemote()
|
|
67
68
|
await Bun.spawn(["git", "remote", "add", "origin", remoteDir], {
|
|
68
69
|
cwd: tempDir,
|
|
69
70
|
stdout: "pipe",
|
|
@@ -87,9 +88,37 @@ describe("push command", () => {
|
|
|
87
88
|
process.chdir(originalCwd)
|
|
88
89
|
delete process.env.AGENCY_CONFIG_PATH
|
|
89
90
|
await cleanupTempDir(tempDir)
|
|
91
|
+
await cleanupTempDir(remoteDir)
|
|
90
92
|
})
|
|
91
93
|
|
|
92
94
|
describe("basic functionality", () => {
|
|
95
|
+
test("fails when there are uncommitted changes", async () => {
|
|
96
|
+
// Create uncommitted changes
|
|
97
|
+
await Bun.write(join(tempDir, "uncommitted.txt"), "uncommitted\n")
|
|
98
|
+
|
|
99
|
+
expect(
|
|
100
|
+
runTestEffect(
|
|
101
|
+
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
102
|
+
),
|
|
103
|
+
).rejects.toThrow(/uncommitted changes/)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("fails when there are staged but uncommitted changes", async () => {
|
|
107
|
+
// Create a staged but uncommitted change
|
|
108
|
+
await Bun.write(join(tempDir, "staged.txt"), "staged\n")
|
|
109
|
+
await Bun.spawn(["git", "add", "staged.txt"], {
|
|
110
|
+
cwd: tempDir,
|
|
111
|
+
stdout: "pipe",
|
|
112
|
+
stderr: "pipe",
|
|
113
|
+
}).exited
|
|
114
|
+
|
|
115
|
+
expect(
|
|
116
|
+
runTestEffect(
|
|
117
|
+
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
118
|
+
),
|
|
119
|
+
).rejects.toThrow(/uncommitted changes/)
|
|
120
|
+
})
|
|
121
|
+
|
|
93
122
|
test("creates emit branch, pushes it, and returns to source", async () => {
|
|
94
123
|
// We're on agency--feature branch (source)
|
|
95
124
|
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
@@ -187,6 +216,22 @@ describe("push command", () => {
|
|
|
187
216
|
})
|
|
188
217
|
|
|
189
218
|
describe("error handling", () => {
|
|
219
|
+
test("throws when emitted CLAUDE.md references AGENCY.md", async () => {
|
|
220
|
+
await Bun.write(
|
|
221
|
+
join(tempDir, "CLAUDE.md"),
|
|
222
|
+
"# Instructions\n\n@AGENCY.md\n",
|
|
223
|
+
)
|
|
224
|
+
await addAndCommit(tempDir, "CLAUDE.md", "Add Claude agency reference")
|
|
225
|
+
|
|
226
|
+
await expect(
|
|
227
|
+
runTestEffectWithMockFilterRepo(
|
|
228
|
+
push({ baseBranch: "main", silent: true }),
|
|
229
|
+
),
|
|
230
|
+
).rejects.toThrow(
|
|
231
|
+
"Failed to create emit branch: CLAUDE.md on emitted branch feature still references @AGENCY.md",
|
|
232
|
+
)
|
|
233
|
+
})
|
|
234
|
+
|
|
190
235
|
test("switches to source branch when run from emit branch", async () => {
|
|
191
236
|
// First create the emit branch from source branch
|
|
192
237
|
await runTestEffect(
|
package/src/commands/push.ts
CHANGED
|
@@ -91,6 +91,21 @@ const pushCore = (gitRoot: string, options: PushOptions) =>
|
|
|
91
91
|
const git = yield* GitService
|
|
92
92
|
const configService = yield* ConfigService
|
|
93
93
|
|
|
94
|
+
// Check for uncommitted changes
|
|
95
|
+
const statusOutput = yield* git.getStatus(gitRoot)
|
|
96
|
+
|
|
97
|
+
if (statusOutput && statusOutput.trim().length > 0) {
|
|
98
|
+
return yield* Effect.fail(
|
|
99
|
+
new Error(
|
|
100
|
+
`You have uncommitted changes. Please commit or stash them before pushing.\n` +
|
|
101
|
+
`The push command performs internal checkouts that would destroy uncommitted work.\n` +
|
|
102
|
+
`Run 'git status' to see the changes.`,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
verboseLog(`Working directory is clean`)
|
|
108
|
+
|
|
94
109
|
// Load config to check emit branch pattern
|
|
95
110
|
const config = yield* configService.loadConfig()
|
|
96
111
|
|
package/templates/AGENCY.md
CHANGED
|
@@ -18,3 +18,5 @@ All work on this repository should begin by reading and understanding `TASK.md`.
|
|
|
18
18
|
When creating commit messages, do not reference changes to `TASK.md`, `AGENTS.md`, or any files tracked in `agency.json` (such as `opencode.json`). These are project management and configuration files that should not be mentioned in commit messages. Focus commit messages on actual code changes, features, fixes, and refactoring.
|
|
19
19
|
|
|
20
20
|
**Important:** Even when the only changes in a commit are to tracked files like `TASK.md`, you should still commit those changes. These updates should be co-located with the code changes they describe. Simply omit mentioning the tracked files in the commit message and focus the message on the actual code changes being made.
|
|
21
|
+
|
|
22
|
+
If a commit only updates `TASK.md`, use `--no-verify` to skip precommit hooks. Do not use `--no-verify` if the commit includes changes to any other file.
|