@markjaquith/agency 0.7.3 → 1.0.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 +18 -3
- package/cli.ts +2 -2
- package/package.json +1 -1
- package/src/commands/emit.test.ts +1 -1
- package/src/commands/merge.test.ts +1 -1
- package/src/commands/task-edit.test.ts +94 -0
- package/src/commands/task-main.test.ts +2 -2
- package/src/commands/task.ts +63 -41
- package/src/services/GitService.ts +16 -0
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -10,11 +10,26 @@ bun install -g @markjaquith/agency
|
|
|
10
10
|
|
|
11
11
|
## Primary Commands
|
|
12
12
|
|
|
13
|
-
### `agency task
|
|
13
|
+
### `agency task <branch-name>`
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Create a new feature branch from the latest `origin/main` and initialize `AGENTS.md` and `TASK.md` files using the template you've set for this repo. Commits smuggled files and lands you on that branch.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
**Options:**
|
|
18
|
+
|
|
19
|
+
- `--from <branch>` - Branch from a specific branch instead of `origin/main`
|
|
20
|
+
- `--from-current` - Initialize on current branch instead of creating a new one
|
|
21
|
+
- `--continue` - Continue a task by copying agency files to a new branch (after PR merge)
|
|
22
|
+
|
|
23
|
+
**Examples:**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
agency task my-feature # Create 'my-feature' from latest origin/main
|
|
27
|
+
agency task my-feature --from dev # Create 'my-feature' from 'dev' branch
|
|
28
|
+
agency task --from-current # Initialize on current branch (no new branch)
|
|
29
|
+
agency task --continue my-feature-v2 # Continue task on new branch after PR merge
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `agency edit`
|
|
18
33
|
|
|
19
34
|
Open `TASK.md` in the system editor for editing. Nice if you have to paste in large amounts of context.
|
|
20
35
|
|
package/cli.ts
CHANGED
|
@@ -426,8 +426,8 @@ Global Options:
|
|
|
426
426
|
|
|
427
427
|
Examples:
|
|
428
428
|
agency init # Initialize with template (run first)
|
|
429
|
-
agency task
|
|
430
|
-
agency task
|
|
429
|
+
agency task my-feature # Create 'my-feature' branch from origin/main
|
|
430
|
+
agency task --from-current # Initialize on current feature branch
|
|
431
431
|
agency emit # Emit a branch (prompts for base branch)
|
|
432
432
|
agency switch # Toggle between source and emitted branch
|
|
433
433
|
|
package/package.json
CHANGED
|
@@ -60,7 +60,7 @@ describe("emit command", () => {
|
|
|
60
60
|
// Initialize AGENTS.md and commit in one go
|
|
61
61
|
await initAgency(tempDir, "test")
|
|
62
62
|
|
|
63
|
-
await runTestEffect(task({ silent: true }))
|
|
63
|
+
await runTestEffect(task({ silent: true, fromCurrent: true }))
|
|
64
64
|
await addAndCommit(tempDir, "AGENTS.md", "Add AGENTS.md")
|
|
65
65
|
|
|
66
66
|
// Set up origin/main for git-filter-repo
|
|
@@ -64,7 +64,7 @@ describe("merge command", () => {
|
|
|
64
64
|
// Initialize AGENTS.md on feature branch
|
|
65
65
|
await initAgency(tempDir, "test")
|
|
66
66
|
|
|
67
|
-
await runTestEffect(task({ silent: true }))
|
|
67
|
+
await runTestEffect(task({ silent: true, fromCurrent: true }))
|
|
68
68
|
|
|
69
69
|
// Ensure agency.json has baseBranch set (task should auto-detect it, but ensure it's there)
|
|
70
70
|
const agencyJsonPath = join(tempDir, "agency.json")
|
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
initAgency,
|
|
9
9
|
readFile,
|
|
10
10
|
runTestEffect,
|
|
11
|
+
createFile,
|
|
12
|
+
runGitCommand,
|
|
11
13
|
} from "../test-utils"
|
|
14
|
+
import { chmod } from "fs/promises"
|
|
12
15
|
|
|
13
16
|
describe("edit command", () => {
|
|
14
17
|
let tempDir: string
|
|
@@ -138,4 +141,95 @@ describe("edit command", () => {
|
|
|
138
141
|
"Editor exited with code",
|
|
139
142
|
)
|
|
140
143
|
})
|
|
144
|
+
|
|
145
|
+
test("commits TASK.md with 'chore: agency edit' when file is modified", async () => {
|
|
146
|
+
await initGitRepo(tempDir)
|
|
147
|
+
process.chdir(tempDir)
|
|
148
|
+
|
|
149
|
+
// Initialize to create TASK.md
|
|
150
|
+
await initAgency(tempDir, "test-task")
|
|
151
|
+
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
152
|
+
|
|
153
|
+
// Get initial commit count
|
|
154
|
+
const result = Bun.spawnSync({
|
|
155
|
+
cmd: ["git", "rev-list", "--count", "HEAD"],
|
|
156
|
+
cwd: tempDir,
|
|
157
|
+
stdout: "pipe",
|
|
158
|
+
})
|
|
159
|
+
const initialCommits = new TextDecoder().decode(result.stdout).trim()
|
|
160
|
+
|
|
161
|
+
// Use a script that modifies TASK.md
|
|
162
|
+
const scriptPath = join(tempDir, "edit-script.sh")
|
|
163
|
+
await createFile(
|
|
164
|
+
tempDir,
|
|
165
|
+
"edit-script.sh",
|
|
166
|
+
'#!/bin/bash\necho "Updated task" >> "$1"\n',
|
|
167
|
+
)
|
|
168
|
+
await chmod(scriptPath, 0o755)
|
|
169
|
+
process.env.EDITOR = scriptPath
|
|
170
|
+
|
|
171
|
+
// Run edit command
|
|
172
|
+
await runTestEffect(taskEdit({ silent: true }))
|
|
173
|
+
|
|
174
|
+
// Check that a new commit was created
|
|
175
|
+
const finalResult = Bun.spawnSync({
|
|
176
|
+
cmd: ["git", "rev-list", "--count", "HEAD"],
|
|
177
|
+
cwd: tempDir,
|
|
178
|
+
stdout: "pipe",
|
|
179
|
+
})
|
|
180
|
+
const finalCommits = new TextDecoder().decode(finalResult.stdout).trim()
|
|
181
|
+
expect(Number.parseInt(finalCommits)).toBe(
|
|
182
|
+
Number.parseInt(initialCommits) + 1,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
// Check the commit message
|
|
186
|
+
const msgResult = Bun.spawnSync({
|
|
187
|
+
cmd: ["git", "log", "-1", "--format=%s"],
|
|
188
|
+
cwd: tempDir,
|
|
189
|
+
stdout: "pipe",
|
|
190
|
+
})
|
|
191
|
+
const commitMessage = new TextDecoder().decode(msgResult.stdout).trim()
|
|
192
|
+
expect(commitMessage).toBe("chore: agency edit")
|
|
193
|
+
|
|
194
|
+
// Check that only TASK.md was committed
|
|
195
|
+
const filesResult = Bun.spawnSync({
|
|
196
|
+
cmd: ["git", "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"],
|
|
197
|
+
cwd: tempDir,
|
|
198
|
+
stdout: "pipe",
|
|
199
|
+
})
|
|
200
|
+
const filesInCommit = new TextDecoder().decode(filesResult.stdout).trim()
|
|
201
|
+
expect(filesInCommit).toBe("TASK.md")
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test("does not commit when TASK.md is not modified", async () => {
|
|
205
|
+
await initGitRepo(tempDir)
|
|
206
|
+
process.chdir(tempDir)
|
|
207
|
+
|
|
208
|
+
// Initialize to create TASK.md
|
|
209
|
+
await initAgency(tempDir, "test-task")
|
|
210
|
+
await runTestEffect(task({ silent: true, emit: "test-feature" }))
|
|
211
|
+
|
|
212
|
+
// Get initial commit count
|
|
213
|
+
const result = Bun.spawnSync({
|
|
214
|
+
cmd: ["git", "rev-list", "--count", "HEAD"],
|
|
215
|
+
cwd: tempDir,
|
|
216
|
+
stdout: "pipe",
|
|
217
|
+
})
|
|
218
|
+
const initialCommits = new TextDecoder().decode(result.stdout).trim()
|
|
219
|
+
|
|
220
|
+
// Use a mock editor that doesn't modify the file
|
|
221
|
+
process.env.EDITOR = "true"
|
|
222
|
+
|
|
223
|
+
// Run edit command
|
|
224
|
+
await runTestEffect(taskEdit({ silent: true }))
|
|
225
|
+
|
|
226
|
+
// Check that no new commit was created
|
|
227
|
+
const finalResult = Bun.spawnSync({
|
|
228
|
+
cmd: ["git", "rev-list", "--count", "HEAD"],
|
|
229
|
+
cwd: tempDir,
|
|
230
|
+
stdout: "pipe",
|
|
231
|
+
})
|
|
232
|
+
const finalCommits = new TextDecoder().decode(finalResult.stdout).trim()
|
|
233
|
+
expect(Number.parseInt(finalCommits)).toBe(Number.parseInt(initialCommits))
|
|
234
|
+
})
|
|
141
235
|
})
|
|
@@ -676,13 +676,13 @@ describe("task command", () => {
|
|
|
676
676
|
})
|
|
677
677
|
|
|
678
678
|
describe("branch handling", () => {
|
|
679
|
-
test("fails when
|
|
679
|
+
test("fails when no branch name provided (silent mode)", async () => {
|
|
680
680
|
await initGitRepo(tempDir)
|
|
681
681
|
process.chdir(tempDir)
|
|
682
682
|
|
|
683
683
|
await initAgency(tempDir, "test")
|
|
684
684
|
await expect(runTestEffect(task({ silent: true }))).rejects.toThrow(
|
|
685
|
-
"
|
|
685
|
+
"Branch name is required",
|
|
686
686
|
)
|
|
687
687
|
})
|
|
688
688
|
|
package/src/commands/task.ts
CHANGED
|
@@ -415,7 +415,23 @@ export const task = (options: TaskOptions = {}) =>
|
|
|
415
415
|
)
|
|
416
416
|
}
|
|
417
417
|
} else {
|
|
418
|
-
// Default:
|
|
418
|
+
// Default: fetch and use latest main upstream branch
|
|
419
|
+
// First, determine the remote to fetch from
|
|
420
|
+
const remote =
|
|
421
|
+
(yield* git.getRemoteConfig(targetPath)) ||
|
|
422
|
+
(yield* git.findDefaultRemote(targetPath))
|
|
423
|
+
|
|
424
|
+
if (remote) {
|
|
425
|
+
verboseLog(`Fetching from remote: ${remote}`)
|
|
426
|
+
yield* git.fetch(targetPath, remote).pipe(
|
|
427
|
+
Effect.catchAll((err) => {
|
|
428
|
+
verboseLog(`Failed to fetch from ${remote}: ${err}`)
|
|
429
|
+
return Effect.void
|
|
430
|
+
}),
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Now resolve the main branch (preferring remote)
|
|
419
435
|
baseBranchToBranchFrom =
|
|
420
436
|
(yield* git.resolveMainBranch(targetPath)) || undefined
|
|
421
437
|
|
|
@@ -431,7 +447,8 @@ export const task = (options: TaskOptions = {}) =>
|
|
|
431
447
|
|
|
432
448
|
// Check if the base branch is an agency source branch
|
|
433
449
|
// If so, we need to emit it first and use the emit branch instead
|
|
434
|
-
if (
|
|
450
|
+
// Skip this check if using --from-current (we want to stay on current branch, not branch from it)
|
|
451
|
+
if (baseBranchToBranchFrom && !options.fromCurrent) {
|
|
435
452
|
const cleanFromBase = extractCleanBranch(
|
|
436
453
|
baseBranchToBranchFrom,
|
|
437
454
|
config.sourceBranchPattern,
|
|
@@ -473,17 +490,14 @@ export const task = (options: TaskOptions = {}) =>
|
|
|
473
490
|
// Determine branch name logic
|
|
474
491
|
let branchName = options.emit || options.branch
|
|
475
492
|
|
|
476
|
-
//
|
|
477
|
-
|
|
493
|
+
// Determine if we need a new branch name:
|
|
494
|
+
// - With --from-current on a feature branch without agency.json: can stay on current branch
|
|
495
|
+
// - All other cases: require a new branch name
|
|
496
|
+
const canStayOnCurrentBranch =
|
|
497
|
+
options.fromCurrent && isFeature && !hasAgencyJson
|
|
498
|
+
|
|
499
|
+
if (!branchName && !canStayOnCurrentBranch) {
|
|
478
500
|
if (silent) {
|
|
479
|
-
if (options.from) {
|
|
480
|
-
return yield* Effect.fail(
|
|
481
|
-
new Error(
|
|
482
|
-
`Branch name is required when using --from flag.\n` +
|
|
483
|
-
`Use: 'agency task <branch-name> --from ${options.from}'`,
|
|
484
|
-
),
|
|
485
|
-
)
|
|
486
|
-
}
|
|
487
501
|
if (hasAgencyJson) {
|
|
488
502
|
return yield* Effect.fail(
|
|
489
503
|
new Error(
|
|
@@ -495,10 +509,9 @@ export const task = (options: TaskOptions = {}) =>
|
|
|
495
509
|
}
|
|
496
510
|
return yield* Effect.fail(
|
|
497
511
|
new Error(
|
|
498
|
-
`
|
|
499
|
-
`
|
|
500
|
-
`
|
|
501
|
-
` 2. Provide a new branch name: 'agency task <branch-name>'`,
|
|
512
|
+
`Branch name is required.\n` +
|
|
513
|
+
`Use: 'agency task <branch-name>'\n` +
|
|
514
|
+
`Or use --from-current to initialize on the current branch.`,
|
|
502
515
|
),
|
|
503
516
|
)
|
|
504
517
|
}
|
|
@@ -910,6 +923,7 @@ const taskEditEffect = (options: TaskEditOptions = {}) =>
|
|
|
910
923
|
const { log, verboseLog } = createLoggers(options)
|
|
911
924
|
|
|
912
925
|
const fs = yield* FileSystemService
|
|
926
|
+
const git = yield* GitService
|
|
913
927
|
|
|
914
928
|
const gitRoot = yield* ensureGitRepo()
|
|
915
929
|
|
|
@@ -943,6 +957,26 @@ const taskEditEffect = (options: TaskEditOptions = {}) =>
|
|
|
943
957
|
}
|
|
944
958
|
|
|
945
959
|
log(done("TASK.md edited"))
|
|
960
|
+
|
|
961
|
+
// Check if TASK.md has uncommitted changes
|
|
962
|
+
const hasChanges = yield* git.hasUncommittedChanges(gitRoot, "TASK.md")
|
|
963
|
+
verboseLog(`TASK.md has uncommitted changes: ${hasChanges}`)
|
|
964
|
+
|
|
965
|
+
if (hasChanges) {
|
|
966
|
+
// Commit the changes
|
|
967
|
+
yield* Effect.gen(function* () {
|
|
968
|
+
yield* git.gitAdd(["TASK.md"], gitRoot)
|
|
969
|
+
yield* git.gitCommit("chore: agency edit", gitRoot, {
|
|
970
|
+
noVerify: true,
|
|
971
|
+
})
|
|
972
|
+
log(done("Committed TASK.md changes"))
|
|
973
|
+
}).pipe(
|
|
974
|
+
Effect.catchAll((err) => {
|
|
975
|
+
verboseLog(`Failed to commit TASK.md: ${err}`)
|
|
976
|
+
return Effect.void
|
|
977
|
+
}),
|
|
978
|
+
)
|
|
979
|
+
}
|
|
946
980
|
})
|
|
947
981
|
|
|
948
982
|
export const editHelp = `
|
|
@@ -962,28 +996,22 @@ Example:
|
|
|
962
996
|
`
|
|
963
997
|
|
|
964
998
|
export const help = `
|
|
965
|
-
Usage: agency task
|
|
999
|
+
Usage: agency task <branch-name> [options]
|
|
966
1000
|
|
|
967
1001
|
Initialize template files (AGENTS.md, TASK.md, opencode.json) in a git repository.
|
|
968
1002
|
|
|
969
1003
|
IMPORTANT:
|
|
970
1004
|
- You must run 'agency init' first to select a template
|
|
971
|
-
-
|
|
972
|
-
|
|
973
|
-
If you're on the main branch, you must either:
|
|
974
|
-
1. Switch to an existing feature branch first, then run 'agency task'
|
|
975
|
-
2. Provide a branch name: 'agency task <branch-name>'
|
|
976
|
-
|
|
977
|
-
Initializes files at the root of the current git repository.
|
|
1005
|
+
- A branch name is required (creates a new branch from the latest origin/main)
|
|
978
1006
|
|
|
979
1007
|
Arguments:
|
|
980
|
-
branch-name
|
|
1008
|
+
branch-name Name for the new feature branch (required)
|
|
981
1009
|
|
|
982
1010
|
Options:
|
|
983
1011
|
--emit Branch name to create (alternative to positional arg)
|
|
984
1012
|
--branch (Deprecated: use --emit) Branch name to create
|
|
985
1013
|
--from <branch> Branch to branch from instead of main upstream branch
|
|
986
|
-
--from-current
|
|
1014
|
+
--from-current Initialize on current branch instead of creating a new one
|
|
987
1015
|
--continue Continue a task by copying agency files to a new branch
|
|
988
1016
|
|
|
989
1017
|
Continue Mode (--continue):
|
|
@@ -999,39 +1027,33 @@ Continue Mode (--continue):
|
|
|
999
1027
|
4. The emitBranch in agency.json is updated for the new branch
|
|
1000
1028
|
|
|
1001
1029
|
Base Branch Selection:
|
|
1002
|
-
By default, 'agency task'
|
|
1003
|
-
You can override this behavior with:
|
|
1030
|
+
By default, 'agency task' fetches from the remote and branches from the latest
|
|
1031
|
+
main upstream branch (e.g., origin/main). You can override this behavior with:
|
|
1004
1032
|
|
|
1005
1033
|
- --from <branch>: Branch from a specific branch
|
|
1006
|
-
- --from-current:
|
|
1034
|
+
- --from-current: Initialize on your current branch (no new branch created)
|
|
1007
1035
|
|
|
1008
1036
|
If the base branch is an agency source branch (e.g., agency/branch-A), the command
|
|
1009
1037
|
will automatically use its emit branch instead. This allows you to layer work on top
|
|
1010
1038
|
of other feature branches while maintaining clean branch history.
|
|
1011
1039
|
|
|
1012
1040
|
Examples:
|
|
1013
|
-
agency task
|
|
1014
|
-
agency task --from agency/branch-B # Branch from agency/branch-B's emit branch
|
|
1015
|
-
agency task --from-current # Branch from current branch's emit branch
|
|
1041
|
+
agency task my-feature # Create 'my-feature' from latest origin/main
|
|
1016
1042
|
agency task my-feature --from develop # Create 'my-feature' from 'develop'
|
|
1043
|
+
agency task --from-current # Initialize on current branch (no new branch)
|
|
1017
1044
|
agency task --continue my-feature-v2 # Continue task on new branch after PR merge
|
|
1018
1045
|
|
|
1019
1046
|
Template Workflow:
|
|
1020
1047
|
1. Run 'agency init' to select template (saved to .git/config)
|
|
1021
|
-
2. Run 'agency task' to create
|
|
1048
|
+
2. Run 'agency task <branch-name>' to create feature branch with template files
|
|
1022
1049
|
3. Use 'agency template save <file>' to update template with local changes
|
|
1023
1050
|
4. Template directory only created when you save files to it
|
|
1024
1051
|
|
|
1025
1052
|
Branch Creation:
|
|
1026
1053
|
When creating a new branch without --from or --from-current:
|
|
1027
|
-
1.
|
|
1028
|
-
2.
|
|
1029
|
-
3.
|
|
1030
|
-
|
|
1031
|
-
When using --from with an agency source branch:
|
|
1032
|
-
1. Verifies the emit branch exists for the source branch
|
|
1033
|
-
2. Uses the emit branch as the actual base to avoid agency files
|
|
1034
|
-
3. Fails if emit branch doesn't exist (run 'agency emit' first)
|
|
1054
|
+
1. Fetches from the configured remote (or origin)
|
|
1055
|
+
2. Auto-detects main upstream branch (origin/main, origin/master, etc.)
|
|
1056
|
+
3. Creates new branch from the latest remote main branch
|
|
1035
1057
|
|
|
1036
1058
|
Notes:
|
|
1037
1059
|
- Files are created at the git repository root, not the current directory
|
|
@@ -850,5 +850,21 @@ export class GitService extends Effect.Service<GitService>()("GitService", {
|
|
|
850
850
|
Effect.map((result) => (result.exitCode === 0 ? result.stdout : null)),
|
|
851
851
|
Effect.catchAll(() => Effect.succeed(null)),
|
|
852
852
|
),
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Check if a file has uncommitted changes (staged or unstaged).
|
|
856
|
+
* Uses `git diff HEAD` to check for any changes to the file.
|
|
857
|
+
* @param gitRoot - The git repository root
|
|
858
|
+
* @param filePath - Path to the file relative to git root
|
|
859
|
+
* @returns true if the file has changes, false otherwise
|
|
860
|
+
*/
|
|
861
|
+
hasUncommittedChanges: (gitRoot: string, filePath: string) =>
|
|
862
|
+
pipe(
|
|
863
|
+
runGitCommand(["git", "diff", "HEAD", "--", filePath], gitRoot),
|
|
864
|
+
Effect.map(
|
|
865
|
+
(result) => result.exitCode === 0 && result.stdout.length > 0,
|
|
866
|
+
),
|
|
867
|
+
Effect.catchAll(() => Effect.succeed(false)),
|
|
868
|
+
),
|
|
853
869
|
}),
|
|
854
870
|
}) {}
|
package/src/types.ts
CHANGED
|
@@ -55,7 +55,7 @@ Agency is a CLI tool for managing \`AGENTS.md\`, \`TASK.md\`, and \`opencode.jso
|
|
|
55
55
|
|
|
56
56
|
## Key Commands
|
|
57
57
|
|
|
58
|
-
- \`agency task
|
|
58
|
+
- \`agency task <branch-name>\` - Create feature branch and initialize template files
|
|
59
59
|
- \`agency edit\` - Open TASK.md in system editor
|
|
60
60
|
- \`agency template save\` - Save current file versions back to a template
|
|
61
61
|
- \`agency template use\` - Switch to a different template
|