@markjaquith/agency 3.2.14 → 3.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.
Files changed (75) hide show
  1. package/README.md +20 -3
  2. package/cli-main.ts +1 -0
  3. package/package.json +7 -2
  4. package/src/cli-parser.ts +13 -0
  5. package/src/commands/work.ts +70 -2
  6. package/src/services/ReadinessService.ts +20 -0
  7. package/src/workbase/execution-contract.ts +10 -1
  8. package/src/workbase/schemas.ts +12 -0
  9. package/fixtures/protocol/orchestration-recipes.json +0 -53
  10. package/pi-extensions/agency.test.ts +0 -117
  11. package/src/check-commits.test.ts +0 -58
  12. package/src/cli-parser.test.ts +0 -945
  13. package/src/cli.test.ts +0 -1889
  14. package/src/commands/act.test.ts +0 -418
  15. package/src/commands/archive.test.ts +0 -236
  16. package/src/commands/context.test.ts +0 -711
  17. package/src/commands/description.test.ts +0 -103
  18. package/src/commands/doctor.test.ts +0 -185
  19. package/src/commands/epic.test.ts +0 -196
  20. package/src/commands/init.test.ts +0 -117
  21. package/src/commands/integration.test.ts +0 -165
  22. package/src/commands/pr.test.ts +0 -248
  23. package/src/commands/push.test.ts +0 -56
  24. package/src/commands/read-only.test.ts +0 -161
  25. package/src/commands/repo.test.ts +0 -199
  26. package/src/commands/restore.test.ts +0 -106
  27. package/src/commands/status.test.ts +0 -113
  28. package/src/commands/sync.test.ts +0 -200
  29. package/src/commands/task-phase.test.ts +0 -410
  30. package/src/commands/task.test.ts +0 -281
  31. package/src/commands/validate.test.ts +0 -186
  32. package/src/commands/work.test.ts +0 -1375
  33. package/src/commands/workbase.test.ts +0 -170
  34. package/src/graph-schema.test.ts +0 -124
  35. package/src/protocol.test.ts +0 -163
  36. package/src/readiness.test.ts +0 -105
  37. package/src/services/ArchiveBulkService.test.ts +0 -384
  38. package/src/services/ArchiveService.test.ts +0 -1092
  39. package/src/services/EpicService.test.ts +0 -74
  40. package/src/services/FileSystemService.test.ts +0 -81
  41. package/src/services/GraphMutationService.test.ts +0 -486
  42. package/src/services/GraphService.test.ts +0 -494
  43. package/src/services/IntegrationService.test.ts +0 -1091
  44. package/src/services/LifecycleTransaction.test.ts +0 -145
  45. package/src/services/PullRequestService.test.ts +0 -716
  46. package/src/services/PushService.test.ts +0 -408
  47. package/src/services/ReadinessService.test.ts +0 -290
  48. package/src/services/RepositoryService.test.ts +0 -789
  49. package/src/services/ReviewService.test.ts +0 -408
  50. package/src/services/SyncService.test.ts +0 -1377
  51. package/src/services/TaskPhaseService.test.ts +0 -852
  52. package/src/services/VersionControlService.test.ts +0 -62
  53. package/src/services/WorkbaseService.test.ts +0 -910
  54. package/src/services/WorktreeLock.test.ts +0 -205
  55. package/src/services/WorktreePerformance.test.ts +0 -71
  56. package/src/services/WorktreeService.test.ts +0 -2292
  57. package/src/test-utils.ts +0 -110
  58. package/src/usage-log.test.ts +0 -188
  59. package/src/utils/chooser.test.ts +0 -192
  60. package/src/utils/effect.test.ts +0 -30
  61. package/src/utils/interactive.pty.test.ts +0 -128
  62. package/src/utils/interactive.test.tsx +0 -860
  63. package/src/utils/process.test.ts +0 -132
  64. package/src/utils/progress.test.ts +0 -37
  65. package/src/work-view.test.ts +0 -151
  66. package/src/workbase/agent-command.test.ts +0 -128
  67. package/src/workbase/checkout-command.test.ts +0 -62
  68. package/src/workbase/delivery-command.test.ts +0 -180
  69. package/src/workbase/dependency-graph.test.ts +0 -50
  70. package/src/workbase/execution-contract.test.ts +0 -161
  71. package/src/workbase/frontmatter.test.ts +0 -67
  72. package/src/workbase/repository-reference.test.ts +0 -25
  73. package/src/workbase/schemas.test.ts +0 -543
  74. package/src/workbase/work-target.test.ts +0 -108
  75. package/src/workbase/worktree-command.test.ts +0 -61
package/README.md CHANGED
@@ -1033,12 +1033,29 @@ work and reading context. It does not prescribe how callers present or execute
1033
1033
  prepared work.
1034
1034
  The evidence argument may be an evidence object, task-creation JSON, or a path to
1035
1035
  either. Use `--dry-run` to report planned fetch, branch, and worktree changes
1036
- without applying them. Use `--force` to prepare work blocked by readiness, such
1037
- as a follow-on phase whose dependency is still active; preparation still does
1038
- not launch an agent or change lifecycle status. Validation reuse never skips
1036
+ without applying them. Validation reuse never skips
1039
1037
  readiness, repository, ownership, reference-drift, dirty-workspace, or worktree
1040
1038
  safety checks.
1041
1039
 
1040
+ Both `work` and `work prepare` accept `--allow-working-dependencies` with an
1041
+ explicit execution-unit target (task ID, directory, or `--task` with optional
1042
+ `--phase`). This narrowly admits an `open` unit whose blockers are exclusively
1043
+ dependencies with status `working`. Open, delegated, dropped, or missing
1044
+ dependencies and mixed validation/status blockers remain blocked; terminal
1045
+ targets cannot be reopened. Ready and normally resumable working units retain
1046
+ their existing behavior. Interactive target selection, orchestration targets,
1047
+ and combining this option with `--force` are rejected. Validation,
1048
+ repository/reference checks, dirty-worktree protections, and active worktree
1049
+ locks are unchanged, including in dry-run mode.
1050
+
1051
+ Preparation never launches or changes status. Its `commands.work.argv` preserves
1052
+ the explicit opt-in, but validation evidence does not store it or authorize later
1053
+ calls: pass the flag again for each prepare/work invocation. Dependency changes
1054
+ invalidate evidence normally, and readiness and locks are checked again even
1055
+ when evidence is reused. Execution identity and evidence schemas are unchanged.
1056
+ The existing broader `--force` behavior, including lock overrides and reopening
1057
+ terminal work on launch, is unchanged for callers using it alone.
1058
+
1042
1059
  The authoritative implementation locations for this contract are
1043
1060
  `src/commands/task.ts` (creation output),
1044
1061
  `src/workbase/execution-contract.ts` (evidence and execution schemas),
package/cli-main.ts CHANGED
@@ -533,6 +533,7 @@ const commands: Record<string, Command> = {
533
533
  auto: options.auto,
534
534
  printCommand: options["print-command"],
535
535
  force: options.force,
536
+ allowWorkingDependencies: options["allow-working-dependencies"],
536
537
  inputAllowed: options.inputAllowed,
537
538
  cwd: options.cwd,
538
539
  taskId: options.task,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "3.2.14",
3
+ "version": "3.3.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -22,10 +22,15 @@
22
22
  "cli.ts",
23
23
  "cli-main.ts",
24
24
  "src",
25
+ "!src/**/*.test.ts",
26
+ "!src/**/*.test.tsx",
27
+ "!src/test-utils.ts",
25
28
  "pi-extensions",
29
+ "!pi-extensions/**/*.test.ts",
26
30
  "scripts/install-pi-extension.ts",
27
31
  "schemas",
28
- "fixtures/protocol",
32
+ "fixtures/protocol/success.json",
33
+ "fixtures/protocol/error.json",
29
34
  "README.md",
30
35
  "LICENSE"
31
36
  ],
package/src/cli-parser.ts CHANGED
@@ -845,6 +845,7 @@ const commands = {
845
845
  opencode: { type: "boolean" },
846
846
  claude: { type: "boolean" },
847
847
  force: { type: "boolean" },
848
+ "allow-working-dependencies": { type: "boolean" },
848
849
  evidence: { type: "string" },
849
850
  },
850
851
  command: {
@@ -865,12 +866,14 @@ const commands = {
865
866
  "claude",
866
867
  "force",
867
868
  "evidence",
869
+ "allow-working-dependencies",
868
870
  ],
869
871
  conflicts: [
870
872
  ["opencode", "claude"],
871
873
  ["agent", "opencode"],
872
874
  ["agent", "claude"],
873
875
  ["epic", "$positional"],
876
+ ["force", "allow-working-dependencies"],
874
877
  ],
875
878
  },
876
879
  },
@@ -1618,6 +1621,16 @@ export function parseCli(args: readonly string[]): ParsedCli {
1618
1621
  spec.usage,
1619
1622
  )
1620
1623
  }
1624
+ if (
1625
+ parsed.values["allow-working-dependencies"] &&
1626
+ (parsed.values.epic ||
1627
+ (!parsed.values.task && !commandPositionals[preparing ? 1 : 0]))
1628
+ ) {
1629
+ throw usageError(
1630
+ "Option '--allow-working-dependencies' requires an explicit execution-unit target (task ID, directory, or --task with optional --phase).",
1631
+ spec.usage,
1632
+ )
1633
+ }
1621
1634
  if (
1622
1635
  (!preparing && commandPositionals.length > 1) ||
1623
1636
  (preparing &&
@@ -49,6 +49,7 @@ export interface WorkOptions extends BaseCommandOptions {
49
49
  readonly printCommand?: boolean
50
50
  readonly auto?: boolean
51
51
  readonly force?: boolean
52
+ readonly allowWorkingDependencies?: boolean
52
53
  readonly evidence?: string
53
54
  }
54
55
 
@@ -93,6 +94,22 @@ export const work = (
93
94
  pickBase: PickWorkbase = pickWorkbase,
94
95
  ) =>
95
96
  Effect.gen(function* () {
97
+ if (options.allowWorkingDependencies && options.force) {
98
+ return yield* Effect.fail(
99
+ new Error("Cannot combine --force with --allow-working-dependencies"),
100
+ )
101
+ }
102
+ if (
103
+ options.allowWorkingDependencies &&
104
+ !options.directory &&
105
+ !options.taskId
106
+ ) {
107
+ return yield* Effect.fail(
108
+ new Error(
109
+ "--allow-working-dependencies requires an explicit execution-unit target",
110
+ ),
111
+ )
112
+ }
96
113
  if (
97
114
  (options.opencode && options.claude) ||
98
115
  (options.agent && (options.opencode || options.claude))
@@ -201,6 +218,13 @@ export const work = (
201
218
  }
202
219
 
203
220
  if (!target) {
221
+ if (options.allowWorkingDependencies) {
222
+ return yield* Effect.fail(
223
+ new Error(
224
+ "--allow-working-dependencies requires an explicit execution-unit target",
225
+ ),
226
+ )
227
+ }
204
228
  if (!inputAllowed) {
205
229
  return yield* Effect.fail(
206
230
  new Error(
@@ -237,7 +261,24 @@ export const work = (
237
261
  target = yield* pick(choices, config.chooserCommand)
238
262
  if (!target) return
239
263
  }
240
- yield* readiness.guardWorkTarget(targetNodeId(target), root, options.force)
264
+ if (
265
+ options.allowWorkingDependencies &&
266
+ (target.kind === "epic" || (target.kind === "task" && target.multiPhase))
267
+ ) {
268
+ return yield* Effect.fail(
269
+ new Error(
270
+ "--allow-working-dependencies requires an execution-unit target",
271
+ ),
272
+ )
273
+ }
274
+ yield* readiness.guardWorkTarget(
275
+ targetNodeId(target),
276
+ root,
277
+ options.force,
278
+ {
279
+ allowWorkingDependencies: options.allowWorkingDependencies,
280
+ },
281
+ )
241
282
  const validationAlreadyPerformed = !options.force
242
283
 
243
284
  const continuing =
@@ -427,6 +468,22 @@ export const work = (
427
468
 
428
469
  export const workPrepare = (options: WorkOptions = {}) =>
429
470
  Effect.gen(function* () {
471
+ if (options.allowWorkingDependencies && options.force) {
472
+ return yield* Effect.fail(
473
+ new Error("Cannot combine --force with --allow-working-dependencies"),
474
+ )
475
+ }
476
+ if (
477
+ options.allowWorkingDependencies &&
478
+ !options.directory &&
479
+ !options.taskId
480
+ ) {
481
+ return yield* Effect.fail(
482
+ new Error(
483
+ "--allow-working-dependencies requires an explicit execution-unit target",
484
+ ),
485
+ )
486
+ }
430
487
  const fs = yield* FileSystemService
431
488
  const workbase = yield* WorkbaseService
432
489
  const tasks = yield* TaskService
@@ -538,7 +595,9 @@ export const workPrepare = (options: WorkOptions = {}) =>
538
595
  })
539
596
  }
540
597
  }
541
- yield* readiness.guardWorkTarget(target, root, options.force)
598
+ yield* readiness.guardWorkTarget(target, root, options.force, {
599
+ allowWorkingDependencies: options.allowWorkingDependencies,
600
+ })
542
601
  const workspace = yield* worktrees.materialize(taskId, phaseId, root, {
543
602
  ...options,
544
603
  dryRun: options.dryRun,
@@ -571,6 +630,7 @@ export const workPrepare = (options: WorkOptions = {}) =>
571
630
  checkoutPath: workspace.writablePath ?? workspace.reviewPath,
572
631
  documentRevision: document.revision,
573
632
  dryRun: options.dryRun === true,
633
+ allowWorkingDependencies: options.allowWorkingDependencies,
574
634
  }),
575
635
  }
576
636
  if (options.json) {
@@ -602,6 +662,13 @@ repository mapping remain unchanged. Dynamic readiness and workspace safety chec
602
662
  always run. With prepare, --force overrides readiness without launching or changing
603
663
  lifecycle status.
604
664
 
665
+ For an explicit execution-unit target, --allow-working-dependencies admits open
666
+ work blocked only by dependencies whose status is working. It does not override
667
+ validation, lifecycle, or workspace safety checks and cannot be combined with
668
+ --force or interactive target selection. Ready and resumable working targets keep
669
+ their normal behavior. Preparation includes the opt-in in commands.work; validation
670
+ evidence does not grant it to later calls.
671
+
605
672
  Options:
606
673
  --epic <id> Work on an epic
607
674
  --task <id> Work on a task
@@ -614,6 +681,7 @@ Options:
614
681
  --opencode Require the OpenCode preset
615
682
  --claude Require the Claude Code preset
616
683
  --force Override readiness; launched terminal work is reopened
684
+ --allow-working-dependencies Allow only working dependency blockers
617
685
  --evidence <value> Validation evidence JSON or a path to JSON (prepare only)
618
686
  --no-input Never open an interactive selector
619
687
 
@@ -175,8 +175,16 @@ export class ReadinessService extends Effect.Service<ReadinessService>()(
175
175
  target: string,
176
176
  cwd: string = process.cwd(),
177
177
  override = false,
178
+ options: { readonly allowWorkingDependencies?: boolean } = {},
178
179
  ) =>
179
180
  Effect.gen(function* () {
181
+ if (override && options.allowWorkingDependencies) {
182
+ return yield* Effect.fail(
183
+ new Error(
184
+ "Cannot combine --force with --allow-working-dependencies",
185
+ ),
186
+ )
187
+ }
180
188
  const graphs = yield* GraphService
181
189
  const graph = yield* graphs.get({ cwd })
182
190
  const node = graph.nodes.find((candidate) => candidate.id === target)
@@ -192,6 +200,18 @@ export class ReadinessService extends Effect.Service<ReadinessService>()(
192
200
  })
193
201
  }
194
202
  if (override) return
203
+ if (
204
+ options.allowWorkingDependencies &&
205
+ node.kind === "execution-unit" &&
206
+ node.status === "open" &&
207
+ !node.readiness.terminal &&
208
+ node.readiness.blockers.length > 0 &&
209
+ node.readiness.blockers.every(
210
+ (blocker) =>
211
+ blocker.kind === "dependency" && blocker.status === "working",
212
+ )
213
+ )
214
+ return
195
215
  if (!node.readiness.ready && !isResumableWork(node)) {
196
216
  const item = {
197
217
  key: node.key,
@@ -267,6 +267,7 @@ export const buildExecutionContract = (input: {
267
267
  readonly checkoutPath?: string | null
268
268
  readonly documentRevision: string
269
269
  readonly dryRun: boolean
270
+ readonly allowWorkingDependencies?: boolean
270
271
  }) => {
271
272
  const executionDirectory = dirname(input.phasePath ?? input.taskPath)
272
273
  const key = digest({
@@ -308,7 +309,15 @@ export const buildExecutionContract = (input: {
308
309
  commands: {
309
310
  work: {
310
311
  cwd: executionDirectory,
311
- argv: ["agency", "work", ".", "--auto"],
312
+ argv: [
313
+ "agency",
314
+ "work",
315
+ ".",
316
+ "--auto",
317
+ ...(input.allowWorkingDependencies
318
+ ? ["--allow-working-dependencies"]
319
+ : []),
320
+ ],
312
321
  },
313
322
  context: {
314
323
  cwd: executionDirectory,
@@ -42,6 +42,18 @@ export const WorkStatus = Schema.Literal(
42
42
 
43
43
  const IsoTimestamp = NonEmptyString.pipe(
44
44
  Schema.pattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/),
45
+ Schema.filter(
46
+ (value) => {
47
+ const timestamp = Date.parse(value)
48
+ return (
49
+ Number.isFinite(timestamp) &&
50
+ new Date(timestamp).toISOString() === value
51
+ )
52
+ },
53
+ {
54
+ message: () => "Expected a canonical ISO-8601 timestamp",
55
+ },
56
+ ),
45
57
  )
46
58
 
47
59
  const GitCommit = Schema.String.pipe(Schema.pattern(/^[a-f0-9]{40}$/))
@@ -1,53 +0,0 @@
1
- [
2
- ["context", "--task", "checkout", "--phase", "ui", "--json"],
3
- ["worktree", "inspect", "checkout", "ui", "--json"],
4
- ["next", "--json"],
5
- ["graph", "--ready", "--json"],
6
- [
7
- "task",
8
- "create",
9
- "investigate-checkout",
10
- "--purpose",
11
- "investigation",
12
- "--repo",
13
- "agency",
14
- "--json"
15
- ],
16
- [
17
- "task",
18
- "handoff",
19
- "investigate-checkout",
20
- "implement-checkout",
21
- "--repo",
22
- "agency",
23
- "--json"
24
- ],
25
- [
26
- "work",
27
- "prepare",
28
- "--task",
29
- "checkout",
30
- "--phase",
31
- "ui",
32
- "--dry-run",
33
- "--json"
34
- ],
35
- ["work", "prepare", "--task", "checkout", "--phase", "ui", "--json"],
36
- ["work", "tasks/checkout/phases/ui", "--agent", "opencode"],
37
- ["sync", "--dry-run", "--json"],
38
- ["sync", "--json"],
39
- [
40
- "task",
41
- "status",
42
- "implement-checkout",
43
- "done",
44
- "--no-pull-request",
45
- "--summary",
46
- "Completed without a pull request",
47
- "--json"
48
- ],
49
- ["pr", "create", "checkout", "ui", "--json"],
50
- ["sync", "checkout", "ui", "--json"],
51
- ["doctor", "--json"],
52
- ["worktree", "repair", "checkout", "ui", "--dry-run", "--json"]
53
- ]
@@ -1,117 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import { mkdir } from "node:fs/promises"
3
- import { join } from "node:path"
4
- import { cleanupTempDir, createTempDir } from "../src/test-utils"
5
- import agencyExtension from "./agency"
6
-
7
- describe("global Pi extension", () => {
8
- let root: string
9
-
10
- beforeEach(async () => {
11
- root = await createTempDir()
12
- })
13
-
14
- afterEach(async () => cleanupTempDir(root))
15
-
16
- const load = (response?: object) => {
17
- const handlers = new Map<string, (...args: any[]) => any>()
18
- const pi = {
19
- exec: async () => ({
20
- code: response ? 0 : 1,
21
- stdout: response ? JSON.stringify(response) : "",
22
- stderr: "",
23
- }),
24
- on: (name: string, handler: (...args: any[]) => any) => {
25
- handlers.set(name, handler)
26
- },
27
- }
28
- agencyExtension(pi as never)
29
- return handlers
30
- }
31
-
32
- test("no-ops outside Agency workbases without invoking the CLI", async () => {
33
- let executions = 0
34
- const handlers = new Map<string, (...args: any[]) => any>()
35
- agencyExtension({
36
- exec: async () => {
37
- executions += 1
38
- return { code: 1, stdout: "", stderr: "" }
39
- },
40
- on: (name: string, handler: (...args: any[]) => any) => {
41
- handlers.set(name, handler)
42
- },
43
- } as never)
44
-
45
- expect(
46
- await handlers.get("resources_discover")?.({ cwd: root }),
47
- ).toBeUndefined()
48
- expect(
49
- await handlers.get("before_agent_start")?.(
50
- { systemPrompt: "Base" },
51
- { cwd: root },
52
- ),
53
- ).toBeUndefined()
54
- expect(executions).toBe(0)
55
- })
56
-
57
- test("uses CLI context from nested directories for skills and authority", async () => {
58
- const task = join(root, "tasks", "example")
59
- const nested = join(task, "code", "agency", "src")
60
- const checkout = join(task, "code", "agency")
61
- await Bun.write(join(root, "agency.json"), '{"version":2}\n')
62
- await mkdir(join(root, ".agency"), { recursive: true })
63
- await Bun.write(
64
- join(root, ".agency", "AGENTS.md"),
65
- "# Managed instructions\n",
66
- )
67
- await mkdir(join(checkout, ".agents", "skills"), { recursive: true })
68
- await mkdir(nested, { recursive: true })
69
-
70
- const handlers = load({
71
- ok: true,
72
- result: {
73
- workbase: { root },
74
- target: {
75
- kind: "task",
76
- taskId: "example",
77
- path: join(task, "TASK.md"),
78
- },
79
- authority: { mode: "execution", writable: { checkoutPath: checkout } },
80
- documents: { task: { data: { status: "working" } } },
81
- validation: { valid: true },
82
- },
83
- })
84
- const resources = await handlers.get("resources_discover")?.({
85
- cwd: nested,
86
- })
87
- expect(resources.skillPaths).toEqual([join(checkout, ".agents", "skills")])
88
-
89
- const prompt = await handlers.get("before_agent_start")?.(
90
- { systemPrompt: "Base" },
91
- { cwd: nested },
92
- )
93
- expect(prompt.systemPrompt).toContain("# Managed instructions")
94
- expect(prompt.systemPrompt).toContain(
95
- `${checkout} as the default implementation directory`,
96
- )
97
- })
98
-
99
- test("rejects CLI context for a different workbase", async () => {
100
- await Bun.write(join(root, "agency.json"), '{"version":2}\n')
101
- const handlers = load({
102
- ok: true,
103
- result: {
104
- workbase: { root: join(root, "other") },
105
- target: { kind: "epic", epicId: "example" },
106
- validation: { valid: true },
107
- },
108
- })
109
-
110
- expect(
111
- await handlers.get("before_agent_start")?.(
112
- { systemPrompt: "Base" },
113
- { cwd: root },
114
- ),
115
- ).toBeUndefined()
116
- })
117
- })
@@ -1,58 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import { writeFileSync } from "node:fs"
3
- import { join } from "node:path"
4
- import { cleanupTempDir, createTempDir } from "./test-utils"
5
-
6
- const checkCommits = join(import.meta.dir, "../scripts/check-commits")
7
-
8
- describe("check-commits", () => {
9
- let root: string
10
-
11
- function git(...args: string[]): void {
12
- const result = Bun.spawnSync(["git", ...args], { cwd: root })
13
- if (result.exitCode !== 0) {
14
- throw new Error(new TextDecoder().decode(result.stderr))
15
- }
16
- }
17
-
18
- function commit(subject: string, file: string): void {
19
- writeFileSync(join(root, file), `${subject}\n`)
20
- git("add", file)
21
- git("commit", "-m", subject)
22
- }
23
-
24
- beforeEach(async () => {
25
- root = await createTempDir()
26
- git("init", "--initial-branch=main")
27
- git("config", "user.email", "agency@example.com")
28
- git("config", "user.name", "Agency Test")
29
- commit("chore: initial commit", "initial.txt")
30
- git("update-ref", "refs/remotes/origin/main", "HEAD")
31
- })
32
-
33
- afterEach(async () => cleanupTempDir(root))
34
-
35
- test("allows merge commits with non-conventional subjects", () => {
36
- git("checkout", "-b", "feature")
37
- commit("feat: add feature", "feature.txt")
38
- git("checkout", "-b", "topic", "main")
39
- commit("fix: add topic", "topic.txt")
40
- git("checkout", "feature")
41
- git("merge", "--no-ff", "topic", "-m", "Combine topic into feature")
42
-
43
- const result = Bun.spawnSync([checkCommits], { cwd: root })
44
-
45
- expect(result.exitCode).toBe(0)
46
- })
47
-
48
- test("rejects non-conventional non-merge commits", () => {
49
- commit("not a conventional commit", "invalid.txt")
50
-
51
- const result = Bun.spawnSync([checkCommits], { cwd: root })
52
-
53
- expect(result.exitCode).toBe(1)
54
- expect(new TextDecoder().decode(result.stderr)).toContain(
55
- "not a conventional commit",
56
- )
57
- })
58
- })