@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.
- package/README.md +20 -3
- package/cli-main.ts +1 -0
- package/package.json +7 -2
- package/src/cli-parser.ts +13 -0
- package/src/commands/work.ts +70 -2
- package/src/services/ReadinessService.ts +20 -0
- package/src/workbase/execution-contract.ts +10 -1
- package/src/workbase/schemas.ts +12 -0
- package/fixtures/protocol/orchestration-recipes.json +0 -53
- package/pi-extensions/agency.test.ts +0 -117
- package/src/check-commits.test.ts +0 -58
- package/src/cli-parser.test.ts +0 -945
- package/src/cli.test.ts +0 -1889
- package/src/commands/act.test.ts +0 -418
- package/src/commands/archive.test.ts +0 -236
- package/src/commands/context.test.ts +0 -711
- package/src/commands/description.test.ts +0 -103
- package/src/commands/doctor.test.ts +0 -185
- package/src/commands/epic.test.ts +0 -196
- package/src/commands/init.test.ts +0 -117
- package/src/commands/integration.test.ts +0 -165
- package/src/commands/pr.test.ts +0 -248
- package/src/commands/push.test.ts +0 -56
- package/src/commands/read-only.test.ts +0 -161
- package/src/commands/repo.test.ts +0 -199
- package/src/commands/restore.test.ts +0 -106
- package/src/commands/status.test.ts +0 -113
- package/src/commands/sync.test.ts +0 -200
- package/src/commands/task-phase.test.ts +0 -410
- package/src/commands/task.test.ts +0 -281
- package/src/commands/validate.test.ts +0 -186
- package/src/commands/work.test.ts +0 -1375
- package/src/commands/workbase.test.ts +0 -170
- package/src/graph-schema.test.ts +0 -124
- package/src/protocol.test.ts +0 -163
- package/src/readiness.test.ts +0 -105
- package/src/services/ArchiveBulkService.test.ts +0 -384
- package/src/services/ArchiveService.test.ts +0 -1092
- package/src/services/EpicService.test.ts +0 -74
- package/src/services/FileSystemService.test.ts +0 -81
- package/src/services/GraphMutationService.test.ts +0 -486
- package/src/services/GraphService.test.ts +0 -494
- package/src/services/IntegrationService.test.ts +0 -1091
- package/src/services/LifecycleTransaction.test.ts +0 -145
- package/src/services/PullRequestService.test.ts +0 -716
- package/src/services/PushService.test.ts +0 -408
- package/src/services/ReadinessService.test.ts +0 -290
- package/src/services/RepositoryService.test.ts +0 -789
- package/src/services/ReviewService.test.ts +0 -408
- package/src/services/SyncService.test.ts +0 -1377
- package/src/services/TaskPhaseService.test.ts +0 -852
- package/src/services/VersionControlService.test.ts +0 -62
- package/src/services/WorkbaseService.test.ts +0 -910
- package/src/services/WorktreeLock.test.ts +0 -205
- package/src/services/WorktreePerformance.test.ts +0 -71
- package/src/services/WorktreeService.test.ts +0 -2292
- package/src/test-utils.ts +0 -110
- package/src/usage-log.test.ts +0 -188
- package/src/utils/chooser.test.ts +0 -192
- package/src/utils/effect.test.ts +0 -30
- package/src/utils/interactive.pty.test.ts +0 -128
- package/src/utils/interactive.test.tsx +0 -860
- package/src/utils/process.test.ts +0 -132
- package/src/utils/progress.test.ts +0 -37
- package/src/work-view.test.ts +0 -151
- package/src/workbase/agent-command.test.ts +0 -128
- package/src/workbase/checkout-command.test.ts +0 -62
- package/src/workbase/delivery-command.test.ts +0 -180
- package/src/workbase/dependency-graph.test.ts +0 -50
- package/src/workbase/execution-contract.test.ts +0 -161
- package/src/workbase/frontmatter.test.ts +0 -67
- package/src/workbase/repository-reference.test.ts +0 -25
- package/src/workbase/schemas.test.ts +0 -543
- package/src/workbase/work-target.test.ts +0 -108
- 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.
|
|
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.
|
|
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 &&
|
package/src/commands/work.ts
CHANGED
|
@@ -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
|
-
|
|
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: [
|
|
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,
|
package/src/workbase/schemas.ts
CHANGED
|
@@ -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
|
-
})
|