@markjaquith/agency 2.54.4 → 2.56.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
@@ -235,6 +235,56 @@ Supplemental read-only repositories remain detached Git worktrees at their
235
235
  declared refs so they do not acquire writable branches. Jj workbases always use
236
236
  jj workspaces and ignore this Git-specific customization.
237
237
 
238
+ ### Post-checkout Commands
239
+
240
+ Each repository declaration may provide a VCS-neutral `postCheckoutCommand` argv
241
+ template for repository-specific setup. Agency invokes it directly, without a
242
+ shell, with the new checkout as its working directory:
243
+
244
+ ```json
245
+ {
246
+ "version": 2,
247
+ "repositories": {
248
+ "frontend": {
249
+ "remote": "git@example.com:team/frontend.git",
250
+ "postCheckoutCommand": ["bun", "install", "--frozen-lockfile"]
251
+ }
252
+ }
253
+ }
254
+ ```
255
+
256
+ The hook runs for each newly created managed checkout, including writable and
257
+ reference checkouts, after Git worktree or jj workspace creation has completed
258
+ and Agency has validated the checkout. It does not run for a reused checkout or
259
+ for inspection-only commands. A custom `worktreeCreateCommand` completes and is
260
+ validated before this hook runs.
261
+
262
+ Available placeholders and matching environment variables are:
263
+
264
+ | Placeholder | Environment | Value |
265
+ | ------------------ | ------------------------ | ---------------------------------------------- |
266
+ | `{repoAlias}` | `AGENCY_REPO_ALIAS` | Repository alias |
267
+ | `{repositoryPath}` | `AGENCY_REPOSITORY_PATH` | Absolute source repository path under `repos/` |
268
+ | `{checkoutPath}` | `AGENCY_CHECKOUT_PATH` | Absolute managed checkout path |
269
+ | `{checkoutKind}` | `AGENCY_CHECKOUT_KIND` | `writable` or `reference` |
270
+ | `{requestedRef}` | `AGENCY_REQUESTED_REF` | Requested branch, reference, or review commit |
271
+ | `{base}` | `AGENCY_BASE` | Configured execution base |
272
+ | `{vcs}` | `AGENCY_VCS` | `git` or `jj` |
273
+ | `{workbaseRoot}` | `AGENCY_WORKBASE_ROOT` | Absolute workbase root |
274
+ | `{taskId}` | `AGENCY_TASK_ID` | Task ID |
275
+ | `{phaseId}` | `AGENCY_PHASE_ID` | Phase ID |
276
+
277
+ `{base}` and `{phaseId}` and their environment variables are empty strings when
278
+ they do not apply. Dry runs report a planned `post-checkout` operation but never
279
+ execute it. Verbose output identifies the repository and expanded command.
280
+
281
+ Hook success is part of checkout creation. A non-zero exit or failure to start
282
+ rolls back the checkout and any branch created by the same operation; if cleanup
283
+ also fails, Agency reports the exact manual recovery action. A later command
284
+ retries checkout creation and the hook rather than reusing an uninitialized
285
+ checkout. Hook commands should be idempotent so a retry is safe after any
286
+ external effects the failed invocation may have completed.
287
+
238
288
  ### Agent Runners
239
289
 
240
290
  OpenCode and Claude Code are built-in runner presets. Select either preset or a
@@ -812,12 +862,13 @@ restore preflight all destinations and graph references before changing files.
812
862
  Versioned lifecycle provenance preserves parent declarations and dependency edges
813
863
  for restoration. Archived IDs are reserved until restored.
814
864
 
815
- ### Work and Pull Requests
865
+ ### Work, Publication, and Pull Requests
816
866
 
817
867
  ```text
818
868
  agency work [<directory> | --epic <epic-id>] [--runner <name>] [--auto] [--print-command]
819
869
  agency work prepare [target] [--dry-run] [--json]
820
870
  agency worktree <list|inspect|prepare|remove|rebuild|repair>
871
+ agency push [--json]
821
872
  agency pr create <task-id> [phase-id] [--draft] [--force] [--json]
822
873
  agency pr [args...]
823
874
  ```
@@ -865,6 +916,22 @@ Agency resolves the ref to a commit and creates a detached worktree. Existing
865
916
  reference worktrees are reused only while their commit still matches the declared
866
917
  ref; use a commit SHA as `ref` when reproducibility matters.
867
918
 
919
+ `agency push` publishes the execution unit identified by current Agency context
920
+ without creating a pull request. It requires a valid, registered writable
921
+ checkout in `working` state, fetches the configured delivery remote, verifies
922
+ that the declared base is in the publication history, validates every outgoing
923
+ commit's description, author, and conflict state, and refuses non-fast-forward
924
+ updates.
925
+
926
+ For Git, YAML `branch` must exactly match the checked-out local branch, the
927
+ worktree must be clean, and `HEAD` is pushed with upstream tracking. For jj, YAML
928
+ `branch` is the authoritative delivery bookmark and need not exist before
929
+ publication. Agency publishes `@` unless it is the canonical empty, undescribed
930
+ post-commit working copy, in which case it publishes `@-`; described empty
931
+ changes remain intentional publication tips. Missing descriptions or authors
932
+ stop with exact change IDs and remediation commands. Agency creates or safely
933
+ advances only the declared bookmark and never invents a `push-*` bookmark.
934
+
868
935
  Task-aware `agency pr create <task-id> [phase-id]` uses Agency's delivery flow,
869
936
  including readiness checks and durable PR recording. Other `agency pr`
870
937
  invocations forward every argument to `gh pr`. From an execution task or phase
package/cli-main.ts CHANGED
@@ -6,6 +6,7 @@ import { parseCli } from "./src/cli-parser"
6
6
  import { init, help as initHelp } from "./src/commands/init"
7
7
  import { task, help as taskHelp } from "./src/commands/task"
8
8
  import { pr, prCreate, help as prHelp } from "./src/commands/pr"
9
+ import { push, help as pushHelp } from "./src/commands/push"
9
10
  import { work, workPrepare, help as workHelp } from "./src/commands/work"
10
11
  import { worktree, help as worktreeHelp } from "./src/commands/worktree"
11
12
  import { status, help as statusHelp } from "./src/commands/status"
@@ -34,6 +35,7 @@ import { TaskService } from "./src/services/TaskService"
34
35
  import { PhaseService } from "./src/services/PhaseService"
35
36
  import { WorktreeService } from "./src/services/WorktreeService"
36
37
  import { PullRequestService } from "./src/services/PullRequestService"
38
+ import { PushService } from "./src/services/PushService"
37
39
  import { ArchiveService } from "./src/services/ArchiveService"
38
40
  import { IntegrationService } from "./src/services/IntegrationService"
39
41
  import { ContextService } from "./src/services/ContextService"
@@ -79,6 +81,7 @@ const CliLayer = Layer.mergeAll(
79
81
  PhaseService.Default,
80
82
  WorktreeService.Default,
81
83
  PullRequestService.Default,
84
+ PushService.Default,
82
85
  ArchiveService.Default,
83
86
  IntegrationService.Default,
84
87
  ContextService.Default,
@@ -305,6 +308,22 @@ const commands: Record<string, Command> = {
305
308
  )
306
309
  },
307
310
  },
311
+ push: {
312
+ run: async (_args: string[], options: Record<string, any>) => {
313
+ if (options.help) {
314
+ console.log(pushHelp)
315
+ return
316
+ }
317
+ await runCommand(
318
+ push({
319
+ json: options.json,
320
+ silent: options.silent,
321
+ verbose: options.verbose,
322
+ cwd: options.cwd,
323
+ }),
324
+ )
325
+ },
326
+ },
308
327
  phase: {
309
328
  run: async (args: string[], options: Record<string, any>) => {
310
329
  if (options.help) return console.log(phaseHelp)
@@ -742,6 +761,7 @@ Commands:
742
761
  vcs <subcommand> Inspect or migrate the version-control backend
743
762
  next List or select ready execution units
744
763
  pr create / pr [...] Create an Agency PR or run gh pr with repository focus
764
+ push Validate and publish the current execution unit
745
765
  review refresh Explicitly refresh a pinned review task
746
766
  repo <subcommand> Manage workbase repositories
747
767
  status Show status for the current workbase
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.54.4",
3
+ "version": "2.56.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -616,6 +616,15 @@ describe("strict CLI parsing", () => {
616
616
  ).toThrow("cannot be combined")
617
617
  })
618
618
 
619
+ test("parses push without accepting an alternate target", () => {
620
+ expect(parseCli(["push", "--json"])).toMatchObject({
621
+ commandName: "push",
622
+ args: [],
623
+ values: { json: true },
624
+ })
625
+ expectUsageError(["push", "example"], "agency push")
626
+ })
627
+
619
628
  test("accepts runner selection and command inspection for work", () => {
620
629
  expect(
621
630
  parseCli([
package/src/cli-parser.ts CHANGED
@@ -913,6 +913,16 @@ const commands = {
913
913
  },
914
914
  },
915
915
  },
916
+ push: {
917
+ usage: "agency push [--json]",
918
+ options: outputOptions,
919
+ command: {
920
+ usage: "agency push [--json]",
921
+ minArgs: 0,
922
+ maxArgs: 0,
923
+ options: ["json"],
924
+ },
925
+ },
916
926
  next: {
917
927
  usage: "agency next [--select] [--json]",
918
928
  options: {
package/src/cli.test.ts CHANGED
@@ -502,6 +502,7 @@ describe("CLI", () => {
502
502
  ["archive", "Usage: agency archive"],
503
503
  ["restore", "Usage: agency restore"],
504
504
  ["work", "Usage: agency work"],
505
+ ["push", "Usage: agency push"],
505
506
  ["pr", "Work with GitHub pull requests."],
506
507
  ["status", "Usage: agency status"],
507
508
  ["validate", "Usage: agency validate"],
@@ -0,0 +1,25 @@
1
+ import { Effect } from "effect"
2
+ import { PushService } from "../services/PushService"
3
+ import type { BaseCommandOptions } from "../utils/command"
4
+ import { createLoggers } from "../utils/effect"
5
+
6
+ export const push = (options: BaseCommandOptions = {}) =>
7
+ Effect.gen(function* () {
8
+ const publications = yield* PushService
9
+ const { log } = createLoggers(options)
10
+ const result = yield* publications.publish(options.cwd ?? process.cwd())
11
+ log(
12
+ options.json
13
+ ? JSON.stringify(result, null, 2)
14
+ : `Published ${result.vcs} ${result.branch} at ${result.tip} to ${result.remote}`,
15
+ )
16
+ })
17
+
18
+ export const help = `
19
+ Usage: agency push [--json]
20
+
21
+ Validate and publish the current Agency execution unit without creating a pull
22
+ request. The command uses the durable base and branch declarations, requires the
23
+ managed writable checkout and working status, refreshes remote state, validates
24
+ every outgoing commit, and rejects non-fast-forward publication.
25
+ `
package/src/protocol.ts CHANGED
@@ -99,6 +99,7 @@ const errorMetadata: Readonly<Record<string, ErrorMetadata>> = {
99
99
  },
100
100
  ArchiveError: { code: "ARCHIVE_ERROR", retryable: false },
101
101
  WorktreeError: { code: "WORKTREE_ERROR", retryable: false },
102
+ PushError: { code: "PUSH_ERROR", retryable: false },
102
103
  PullRequestError: { code: "PULL_REQUEST_ERROR", retryable: false },
103
104
  ReviewError: { code: "REVIEW_ERROR", retryable: false },
104
105
  ContextError: {
@@ -173,6 +173,18 @@ export class DoctorService extends Effect.Service<DoctorService>()(
173
173
  ] as const,
174
174
  ]
175
175
  : []),
176
+ ...Object.entries(config.repositories ?? {}).flatMap(
177
+ ([alias, repository]) =>
178
+ repository.postCheckoutCommand
179
+ ? [
180
+ [
181
+ `integration.repository.${alias}.post-checkout`,
182
+ repository.postCheckoutCommand,
183
+ `Repository '${alias}' post-checkout hook`,
184
+ ] as const,
185
+ ]
186
+ : [],
187
+ ),
176
188
  ...Object.entries(config.runners ?? {}).map(
177
189
  ([name, runner]) =>
178
190
  [
@@ -422,6 +422,8 @@ describe("IntegrationService", () => {
422
422
  expect(body).toContain("without creating a claim")
423
423
  expect(body).toContain("formatting, type checks, build, dead-code checks")
424
424
  expect(body).toContain("Review and commit the diff")
425
+ expect(body).toContain("Use `agency push`")
426
+ expect(body).toContain("never authors semantic commit descriptions")
425
427
  expect(body).toContain("Run `agency validate`")
426
428
  expect(body).toContain("only with explicit user intent")
427
429
  expect(body).toContain("An execution unit remains `working`")