@markjaquith/agency 2.71.27 → 2.73.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 -9
- package/index.ts +1 -1
- package/package.json +3 -3
- package/schemas/{agency-kickoff-v1.schema.json → agency-execution-v1.schema.json} +66 -42
- package/src/commands/repo.test.ts +11 -1
- package/src/commands/repo.ts +17 -1
- package/src/commands/task.ts +1 -1
- package/src/commands/work.test.ts +9 -6
- package/src/commands/work.ts +5 -6
- package/src/services/IntegrationService.test.ts +42 -14
- package/src/services/RepositoryService.test.ts +150 -1
- package/src/services/RepositoryService.ts +290 -1
- package/src/workbase/AGENTS.md +74 -38
- package/src/workbase/{kickoff-contract.test.ts → execution-contract.test.ts} +45 -29
- package/src/workbase/{kickoff-contract.ts → execution-contract.ts} +46 -85
- package/src/workbase/opencode-file.ts +7 -2
package/README.md
CHANGED
|
@@ -441,7 +441,7 @@ variables. A worker must verify either signal against `agency context . --json`
|
|
|
441
441
|
before acting; a matching worker performs the task directly and must not invoke
|
|
442
442
|
`agency work` for the same target. Managed guidance also fails safe for older
|
|
443
443
|
generated prompts when their document paths, current directory, and active valid
|
|
444
|
-
context all agree.
|
|
444
|
+
context all agree. External session state is not part of this identity contract.
|
|
445
445
|
The managed OpenCode plugin validates the marker against Agency context, binds it
|
|
446
446
|
to the receiving OpenCode session, injects an explicit active-worker system
|
|
447
447
|
instruction, and restores Agency identity for that session's shell environment.
|
|
@@ -752,6 +752,7 @@ agency integration sync [--json]
|
|
|
752
752
|
agency repo setup [--dry-run | --apply] [--json]
|
|
753
753
|
agency repo add <alias> <remote> [--json]
|
|
754
754
|
agency repo link <alias> <path> [--json]
|
|
755
|
+
agency repo materialize <alias> [--json]
|
|
755
756
|
agency repo list [--json]
|
|
756
757
|
agency repo show <alias> [--json]
|
|
757
758
|
agency repo fetch <alias> [--json]
|
|
@@ -784,7 +785,11 @@ Each registration has a stable ID and may have a unique name. A default workbase
|
|
|
784
785
|
is used when the current directory is outside every workbase. `prune` removes
|
|
785
786
|
registrations whose workbase configuration no longer exists.
|
|
786
787
|
`repo add` creates a bare clone. `repo link` creates a symlink to an existing Git
|
|
787
|
-
repository.
|
|
788
|
+
repository. For Git workbases, `repo materialize` replaces a linked alias with a
|
|
789
|
+
managed bare clone while migrating its registered worktrees in place; active
|
|
790
|
+
Agency references continue to use the same alias. The command refuses remote
|
|
791
|
+
drift and stale worktree registrations. Alias names are then used by all
|
|
792
|
+
documents and commands. Remove,
|
|
788
793
|
unlink, and rename refuse aliases referenced by active work or backed by linked
|
|
789
794
|
worktrees, and report each blocker.
|
|
790
795
|
|
|
@@ -855,13 +860,13 @@ includes `selector`, absolute `documentPath`, the document `revision`, full
|
|
|
855
860
|
`validation`, normalized `recalledContext`, and `evidence`. Evidence version 1 is
|
|
856
861
|
an auditable local payload containing the canonical workbase root, target and
|
|
857
862
|
document identities, aggregate workbase revision, configuration revision,
|
|
858
|
-
repository-mapping revision,
|
|
863
|
+
repository-mapping revision, execution-contract version, validity result, recalled
|
|
859
864
|
context, and a digest over those fields. It is not a signature or an authority
|
|
860
865
|
grant. A different workbase, target, document revision, document set,
|
|
861
866
|
configuration, repository mapping, contract version, or payload digest
|
|
862
867
|
invalidates reuse. Older creation output without evidence remains compatible;
|
|
863
868
|
preflight simply validates again. The published machine schema is
|
|
864
|
-
`schemas/agency-
|
|
869
|
+
`schemas/agency-execution-v1.schema.json`.
|
|
865
870
|
|
|
866
871
|
Create investigation-only work with the existing task architecture:
|
|
867
872
|
|
|
@@ -1123,12 +1128,18 @@ promptless by default; use `--auto` to send Agency's generated context prompt.
|
|
|
1123
1128
|
`agency work prepare` resolves an execution unit and creates or reuses its
|
|
1124
1129
|
writable and reference worktrees, or its single pinned review checkout, without
|
|
1125
1130
|
launching an agent or changing status.
|
|
1131
|
+
Managed workbase guidance provides exact fast paths for 15 common intents:
|
|
1132
|
+
single-phase creation; create-and-start; materialization; remote PR sync; phase
|
|
1133
|
+
conversion; archive; review creation and start; status inspection; drop;
|
|
1134
|
+
continuation; publication and PR creation; non-PR completion; initial multi-phase
|
|
1135
|
+
setup; investigation handoff; and review refresh.
|
|
1126
1136
|
Its JSON result includes the workspace, validation result, whether supplied
|
|
1127
1137
|
evidence was `reused` or `refreshed` with stable reason strings, refreshed
|
|
1128
|
-
evidence, and a versioned `agency-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1138
|
+
evidence, and a versioned `agency-execution-v1` contract. The contract reports
|
|
1139
|
+
whether preparation was previewed or applied, a stable target identity, prepared
|
|
1140
|
+
workspace and document paths, and canonical Agency-native commands for starting
|
|
1141
|
+
work and reading context. It does not prescribe how callers present or execute
|
|
1142
|
+
prepared work.
|
|
1132
1143
|
The evidence argument may be an evidence object, task-creation JSON, or a path to
|
|
1133
1144
|
either. Use `--dry-run` to report planned fetch, branch, and worktree changes
|
|
1134
1145
|
without applying them. Validation reuse never skips readiness, active-claim,
|
|
@@ -1137,7 +1148,7 @@ checks.
|
|
|
1137
1148
|
|
|
1138
1149
|
The authoritative implementation locations for this contract are
|
|
1139
1150
|
`src/commands/task.ts` (creation output),
|
|
1140
|
-
`src/workbase/
|
|
1151
|
+
`src/workbase/execution-contract.ts` (evidence and execution schemas),
|
|
1141
1152
|
`src/commands/work.ts` (launch preflight),
|
|
1142
1153
|
`src/services/WorktreeService.ts` (workspace safety), and
|
|
1143
1154
|
`src/workbase/AGENTS.md` (generated OpenCode guidance). These paths are the
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.73.0",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"./schemas/agency-graph-v1.json": {
|
|
47
47
|
"default": "./schemas/agency-graph-v1.schema.json"
|
|
48
48
|
},
|
|
49
|
-
"./schemas/agency-
|
|
50
|
-
"default": "./schemas/agency-
|
|
49
|
+
"./schemas/agency-execution-v1.json": {
|
|
50
|
+
"default": "./schemas/agency-execution-v1.schema.json"
|
|
51
51
|
},
|
|
52
52
|
"./fixtures/protocol/success.json": {
|
|
53
53
|
"default": "./fixtures/protocol/success.json"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://github.com/markjaquith/agency/schemas/agency-
|
|
4
|
-
"title": "Agency
|
|
3
|
+
"$id": "https://github.com/markjaquith/agency/schemas/agency-execution-v1.schema.json",
|
|
4
|
+
"title": "Agency execution contract v1",
|
|
5
5
|
"oneOf": [
|
|
6
6
|
{ "$ref": "#/$defs/validationEvidence" },
|
|
7
|
-
{ "$ref": "#/$defs/
|
|
7
|
+
{ "$ref": "#/$defs/executionContract" }
|
|
8
8
|
],
|
|
9
9
|
"$defs": {
|
|
10
10
|
"recalledContext": {
|
|
@@ -54,69 +54,93 @@
|
|
|
54
54
|
},
|
|
55
55
|
"additionalProperties": false
|
|
56
56
|
},
|
|
57
|
-
"
|
|
57
|
+
"command": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"required": ["cwd", "argv"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"cwd": { "type": "string" },
|
|
62
|
+
"argv": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"minItems": 1,
|
|
65
|
+
"items": { "type": "string" }
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"additionalProperties": false
|
|
69
|
+
},
|
|
70
|
+
"executionContract": {
|
|
58
71
|
"type": "object",
|
|
59
72
|
"required": [
|
|
60
73
|
"version",
|
|
61
|
-
"
|
|
74
|
+
"capability",
|
|
75
|
+
"mode",
|
|
76
|
+
"executionIdentity",
|
|
62
77
|
"workbaseRoot",
|
|
63
|
-
"target",
|
|
64
|
-
"documentRevision",
|
|
65
78
|
"sourceLocations",
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"phaseDocument",
|
|
69
|
-
"preparedCheckout",
|
|
70
|
-
"orchestrator",
|
|
71
|
-
"steps",
|
|
72
|
-
"successFields"
|
|
79
|
+
"workspace",
|
|
80
|
+
"commands"
|
|
73
81
|
],
|
|
74
82
|
"properties": {
|
|
75
83
|
"version": { "const": 1 },
|
|
76
|
-
"
|
|
84
|
+
"capability": { "const": "agency-execution-v1" },
|
|
85
|
+
"mode": { "enum": ["preview", "applied"] },
|
|
86
|
+
"executionIdentity": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"required": ["key", "target", "documentRevision"],
|
|
89
|
+
"properties": {
|
|
90
|
+
"key": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
91
|
+
"target": { "type": "string" },
|
|
92
|
+
"documentRevision": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"additionalProperties": false
|
|
98
|
+
},
|
|
77
99
|
"workbaseRoot": { "type": "string" },
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
"phaseDocument": { "type": ["string", "null"] },
|
|
84
|
-
"preparedCheckout": { "type": ["string", "null"] },
|
|
85
|
-
"orchestrator": {
|
|
100
|
+
"sourceLocations": {
|
|
101
|
+
"type": "array",
|
|
102
|
+
"items": { "type": "string" }
|
|
103
|
+
},
|
|
104
|
+
"workspace": {
|
|
86
105
|
"type": "object",
|
|
87
106
|
"required": [
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
107
|
+
"state",
|
|
108
|
+
"executionDirectory",
|
|
109
|
+
"taskDocument",
|
|
110
|
+
"phaseDocument",
|
|
111
|
+
"checkoutPath"
|
|
91
112
|
],
|
|
92
113
|
"properties": {
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
114
|
+
"state": { "enum": ["planned", "materialized"] },
|
|
115
|
+
"executionDirectory": { "type": "string" },
|
|
116
|
+
"taskDocument": { "type": "string" },
|
|
117
|
+
"phaseDocument": { "type": ["string", "null"] },
|
|
118
|
+
"checkoutPath": { "type": ["string", "null"] }
|
|
96
119
|
},
|
|
97
120
|
"additionalProperties": false
|
|
98
121
|
},
|
|
99
|
-
"
|
|
122
|
+
"plannedActions": {
|
|
100
123
|
"type": "array",
|
|
101
|
-
"minItems": 6,
|
|
102
124
|
"items": {
|
|
103
125
|
"type": "object",
|
|
104
|
-
"required": ["
|
|
126
|
+
"required": ["kind", "target", "checkoutPath"],
|
|
105
127
|
"properties": {
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"action": { "type": "string" },
|
|
110
|
-
"path": { "type": "string" },
|
|
111
|
-
"retry": { "type": "string" },
|
|
112
|
-
"recovery": { "type": "string" },
|
|
113
|
-
"idempotencyKey": { "type": "string" },
|
|
114
|
-
"exactlyOnce": { "type": "boolean" }
|
|
128
|
+
"kind": { "const": "workspace-materialization" },
|
|
129
|
+
"target": { "type": "string" },
|
|
130
|
+
"checkoutPath": { "type": ["string", "null"] }
|
|
115
131
|
},
|
|
116
132
|
"additionalProperties": false
|
|
117
133
|
}
|
|
118
134
|
},
|
|
119
|
-
"
|
|
135
|
+
"commands": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"required": ["work", "context"],
|
|
138
|
+
"properties": {
|
|
139
|
+
"work": { "$ref": "#/$defs/command" },
|
|
140
|
+
"context": { "$ref": "#/$defs/command" }
|
|
141
|
+
},
|
|
142
|
+
"additionalProperties": false
|
|
143
|
+
}
|
|
120
144
|
},
|
|
121
145
|
"additionalProperties": false
|
|
122
146
|
}
|
|
@@ -23,7 +23,9 @@ describe("repo command", () => {
|
|
|
23
23
|
test("requires a subcommand", async () => {
|
|
24
24
|
await expect(
|
|
25
25
|
runTestEffect(repo({ args: [], silent: true })),
|
|
26
|
-
).rejects.toThrow(
|
|
26
|
+
).rejects.toThrow(
|
|
27
|
+
"Available subcommands: setup, add, link, materialize, list",
|
|
28
|
+
)
|
|
27
29
|
})
|
|
28
30
|
|
|
29
31
|
test("requires add arguments", async () => {
|
|
@@ -34,6 +36,14 @@ describe("repo command", () => {
|
|
|
34
36
|
).rejects.toThrow("Usage: agency repo add")
|
|
35
37
|
})
|
|
36
38
|
|
|
39
|
+
test("requires a materialize alias", async () => {
|
|
40
|
+
await expect(
|
|
41
|
+
runTestEffect(
|
|
42
|
+
repo({ subcommand: "materialize", args: [], silent: true }),
|
|
43
|
+
),
|
|
44
|
+
).rejects.toThrow("Usage: agency repo materialize <alias>")
|
|
45
|
+
})
|
|
46
|
+
|
|
37
47
|
test("lists repository metadata as JSON", async () => {
|
|
38
48
|
const logs = await captureLogs(() =>
|
|
39
49
|
runTestEffect(
|
package/src/commands/repo.ts
CHANGED
|
@@ -74,6 +74,21 @@ export const repo = (options: RepoOptions) =>
|
|
|
74
74
|
return
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
case "materialize": {
|
|
78
|
+
const alias = yield* requireArg(
|
|
79
|
+
options.args,
|
|
80
|
+
0,
|
|
81
|
+
"Usage: agency repo materialize <alias>",
|
|
82
|
+
)
|
|
83
|
+
const item = yield* repositories.materialize(alias, cwd)
|
|
84
|
+
log(
|
|
85
|
+
options.json
|
|
86
|
+
? JSON.stringify(item, null, 2)
|
|
87
|
+
: `Materialized repository '${alias}'`,
|
|
88
|
+
)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
case "list": {
|
|
78
93
|
const items = yield* repositories.list(cwd)
|
|
79
94
|
if (options.json) {
|
|
@@ -187,7 +202,7 @@ export const repo = (options: RepoOptions) =>
|
|
|
187
202
|
default:
|
|
188
203
|
return yield* Effect.fail(
|
|
189
204
|
new Error(
|
|
190
|
-
"Subcommand is required. Available subcommands: setup, add, link, list, show, fetch, remove, unlink, rename, remote, verify",
|
|
205
|
+
"Subcommand is required. Available subcommands: setup, add, link, materialize, list, show, fetch, remove, unlink, rename, remote, verify",
|
|
191
206
|
),
|
|
192
207
|
)
|
|
193
208
|
}
|
|
@@ -200,6 +215,7 @@ Subcommands:
|
|
|
200
215
|
setup Plan or apply portable repository setup
|
|
201
216
|
add <alias> <remote> Create a bare clone
|
|
202
217
|
link <alias> <path> Link an existing Git repository
|
|
218
|
+
materialize <alias> Replace a linked alias with a managed bare clone
|
|
203
219
|
list List repository aliases
|
|
204
220
|
show <alias> Show a repository alias
|
|
205
221
|
fetch <alias> Fetch and prune a repository
|
package/src/commands/task.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { ReviewService } from "../services/ReviewService"
|
|
|
15
15
|
import {
|
|
16
16
|
buildValidationEvidence,
|
|
17
17
|
normalizeRecalledContext,
|
|
18
|
-
} from "../workbase/
|
|
18
|
+
} from "../workbase/execution-contract"
|
|
19
19
|
|
|
20
20
|
interface TaskOptions extends BaseCommandOptions {
|
|
21
21
|
readonly subcommand?: string
|
|
@@ -532,12 +532,15 @@ describe("work command", () => {
|
|
|
532
532
|
const result = JSON.parse(output!)
|
|
533
533
|
expect(result.validationEvidence.status).toBe("refreshed")
|
|
534
534
|
expect(result.validationEvidence.reasons).toEqual(["not-supplied"])
|
|
535
|
-
expect(result.
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
535
|
+
expect(result.execution).toMatchObject({
|
|
536
|
+
capability: "agency-execution-v1",
|
|
537
|
+
mode: "preview",
|
|
538
|
+
executionIdentity: { target: "execution-unit:task/example" },
|
|
539
|
+
workspace: { state: "planned" },
|
|
540
|
+
commands: {
|
|
541
|
+
work: { argv: ["agency", "work", ".", "--auto"] },
|
|
542
|
+
},
|
|
543
|
+
})
|
|
541
544
|
|
|
542
545
|
const [reusedOutput] = await captureLogs(() =>
|
|
543
546
|
harness.runPrepare({
|
package/src/commands/work.ts
CHANGED
|
@@ -31,11 +31,11 @@ import {
|
|
|
31
31
|
} from "../workbase/agent-command"
|
|
32
32
|
import {
|
|
33
33
|
assessValidationEvidence,
|
|
34
|
-
|
|
34
|
+
buildExecutionContract,
|
|
35
35
|
buildValidationEvidence,
|
|
36
36
|
normalizeRecalledContext,
|
|
37
37
|
readValidationEvidence,
|
|
38
|
-
} from "../workbase/
|
|
38
|
+
} from "../workbase/execution-contract"
|
|
39
39
|
|
|
40
40
|
export interface WorkOptions extends BaseCommandOptions {
|
|
41
41
|
readonly directory?: string
|
|
@@ -556,22 +556,21 @@ export const workPrepare = (options: WorkOptions = {}) =>
|
|
|
556
556
|
workspace,
|
|
557
557
|
validation,
|
|
558
558
|
validationEvidence: { ...assessment.disposition, evidence },
|
|
559
|
-
|
|
559
|
+
execution: buildExecutionContract({
|
|
560
560
|
workbaseRoot: root,
|
|
561
561
|
target,
|
|
562
|
-
taskId,
|
|
563
|
-
phaseId: phase?.id,
|
|
564
562
|
taskPath: task.path,
|
|
565
563
|
phasePath: phase?.path,
|
|
566
564
|
checkoutPath: workspace.writablePath ?? workspace.reviewPath,
|
|
567
565
|
documentRevision: document.revision,
|
|
566
|
+
dryRun: options.dryRun === true,
|
|
568
567
|
}),
|
|
569
568
|
}
|
|
570
569
|
if (options.json) {
|
|
571
570
|
log(JSON.stringify(result, null, 2))
|
|
572
571
|
} else {
|
|
573
572
|
log(
|
|
574
|
-
`${workspace.dryRun ? "
|
|
573
|
+
`${workspace.dryRun ? "Workspace plan" : "Workspace ready"}: ${workspace.writablePath ?? workspace.reviewPath}`,
|
|
575
574
|
)
|
|
576
575
|
}
|
|
577
576
|
})
|
|
@@ -156,19 +156,46 @@ describe("IntegrationService", () => {
|
|
|
156
156
|
])
|
|
157
157
|
})
|
|
158
158
|
|
|
159
|
-
test("generates the
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
test("generates the complete Agency command fast paths with precedence", () => {
|
|
160
|
+
const body = managedBody(managedWorkbaseAgents)
|
|
161
|
+
|
|
162
|
+
expect(body.indexOf("## Command Fast Paths")).toBeLessThan(
|
|
163
|
+
body.indexOf("## Bootstrap"),
|
|
162
164
|
)
|
|
163
165
|
expect(managedWorkbaseAgents).toContain(
|
|
166
|
+
"take precedence over separately installed Agency skill guidance",
|
|
167
|
+
)
|
|
168
|
+
for (const recipe of [
|
|
169
|
+
"Create a single-phase task only",
|
|
164
170
|
"agency work prepare <slug> --evidence",
|
|
171
|
+
"Materialize an existing execution unit without starting it",
|
|
172
|
+
"agency sync <task> [phase] --json",
|
|
173
|
+
"agency phase create <task> <new-phase> --first-phase <existing-phase>",
|
|
174
|
+
"agency archive task <task> --dry-run --json",
|
|
175
|
+
"agency task create <slug> --review <alias>",
|
|
176
|
+
"agency status --json",
|
|
177
|
+
"agency task status <task> dropped --if-revision <revision> --json",
|
|
178
|
+
"Continue already materialized work",
|
|
179
|
+
"agency pr create <task> [phase]",
|
|
180
|
+
"agency finish <task> [phase] --session-id <id>",
|
|
181
|
+
"agency task create <slug> --multi-phase",
|
|
182
|
+
"agency task handoff <investigation-task> <new-task>",
|
|
183
|
+
"agency review refresh <task> --if-revision <revision> --json",
|
|
184
|
+
]) {
|
|
185
|
+
expect(managedWorkbaseAgents).toContain(recipe)
|
|
186
|
+
}
|
|
187
|
+
expect(managedWorkbaseAgents).toContain("agency push --json")
|
|
188
|
+
expect(managedWorkbaseAgents).toContain("agency-execution-v1")
|
|
189
|
+
expect(managedWorkbaseAgents).toContain(
|
|
190
|
+
"Never pass `--work` or `--auto` to `agency task create`",
|
|
165
191
|
)
|
|
166
|
-
expect(managedWorkbaseAgents).toContain("agency-kickoff-v1")
|
|
167
192
|
expect(managedWorkbaseAgents).toContain(
|
|
168
|
-
"
|
|
193
|
+
"Use `agency <command> --help` only as a recovery",
|
|
194
|
+
)
|
|
195
|
+
expect(managedWorkbaseAgents).not.toContain("`agency --help`")
|
|
196
|
+
expect(managedWorkbaseAgents).not.toContain(
|
|
197
|
+
"`agency worktree prepare <task>",
|
|
169
198
|
)
|
|
170
|
-
expect(managedWorkbaseAgents).toContain("exactly one final")
|
|
171
|
-
expect(managedWorkbaseAgents).toContain("leave the agent in the")
|
|
172
199
|
})
|
|
173
200
|
|
|
174
201
|
test("generates a dynamic workbase plugin", () => {
|
|
@@ -597,8 +624,10 @@ describe("IntegrationService", () => {
|
|
|
597
624
|
expect(body).toContain("Agency worker launch target: <target>.")
|
|
598
625
|
expect(body).toContain("environment variables and a generated")
|
|
599
626
|
expect(body).toContain("the initial instruction is a generated")
|
|
600
|
-
expect(body).toContain(
|
|
601
|
-
|
|
627
|
+
expect(body).toContain(
|
|
628
|
+
"External session state is never part of worker identity",
|
|
629
|
+
)
|
|
630
|
+
expect(body).toMatch(/If\s+the prompt\s+and context disagree/)
|
|
602
631
|
expect(body).toContain("marks execution work")
|
|
603
632
|
expect(body).toContain("without creating a claim")
|
|
604
633
|
expect(body).toContain("formatting, type checks, build, dead-code checks")
|
|
@@ -657,7 +686,9 @@ describe("IntegrationService", () => {
|
|
|
657
686
|
description:
|
|
658
687
|
"Handles Agency workbase orchestration and workflow operations with the Agency CLI",
|
|
659
688
|
mode: "subagent",
|
|
660
|
-
prompt: expect.
|
|
689
|
+
prompt: expect.stringMatching(
|
|
690
|
+
/agency context \. --json[\s\S]+agency work prepare[\s\S]+never pass `--work` or `--auto`/,
|
|
691
|
+
),
|
|
661
692
|
},
|
|
662
693
|
plan: {
|
|
663
694
|
disable: true,
|
|
@@ -689,10 +720,7 @@ describe("IntegrationService", () => {
|
|
|
689
720
|
expect(config.agent.agency.hidden).toBeUndefined()
|
|
690
721
|
expect(config.agent.agency.steps).toBeUndefined()
|
|
691
722
|
expect(config.agent.agency.prompt).toContain(
|
|
692
|
-
"
|
|
693
|
-
)
|
|
694
|
-
expect(config.agent.agency.prompt).toContain(
|
|
695
|
-
"return without waiting for the task to finish",
|
|
723
|
+
"Return the prepared execution contract to the caller",
|
|
696
724
|
)
|
|
697
725
|
expect(config.agent["agency-plan"].prompt).toContain(
|
|
698
726
|
"Start with `agency context . --json`",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { mkdir, stat } from "node:fs/promises"
|
|
3
|
+
import { mkdir, realpath, stat } from "node:fs/promises"
|
|
4
4
|
import { dirname, join } from "node:path"
|
|
5
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
6
|
import { RepositoryService } from "./RepositoryService"
|
|
@@ -16,6 +16,18 @@ const runGit = async (args: string[]) => {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
const gitOutput = async (args: string[]) => {
|
|
20
|
+
const process = Bun.spawn(["git", ...args], {
|
|
21
|
+
stdout: "pipe",
|
|
22
|
+
stderr: "pipe",
|
|
23
|
+
})
|
|
24
|
+
await process.exited
|
|
25
|
+
if (process.exitCode !== 0) {
|
|
26
|
+
throw new Error(await new Response(process.stderr).text())
|
|
27
|
+
}
|
|
28
|
+
return (await new Response(process.stdout).text()).trim()
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
const portableRemote = (name: string) =>
|
|
20
32
|
`https://example.com/agency-tests/${name}.git`
|
|
21
33
|
|
|
@@ -159,6 +171,143 @@ describe("RepositoryService", () => {
|
|
|
159
171
|
})
|
|
160
172
|
})
|
|
161
173
|
|
|
174
|
+
test("materializes a linked alias without invalidating active worktrees", async () => {
|
|
175
|
+
const target = join(root, "linked-repository")
|
|
176
|
+
const checkout = join(root, "tasks/active/code/linked")
|
|
177
|
+
await mkdir(target, { recursive: true })
|
|
178
|
+
await runGit(["init", "--initial-branch=main", target])
|
|
179
|
+
await runGit(["-C", target, "config", "user.email", "test@example.com"])
|
|
180
|
+
await runGit(["-C", target, "config", "user.name", "Test"])
|
|
181
|
+
await Bun.write(join(target, "README.md"), "linked\n")
|
|
182
|
+
await runGit(["-C", target, "add", "README.md"])
|
|
183
|
+
await runGit(["-C", target, "commit", "-m", "initial"])
|
|
184
|
+
await setPortableOrigin(target, "materialized")
|
|
185
|
+
await runTestEffect(
|
|
186
|
+
RepositoryService.pipe(
|
|
187
|
+
Effect.flatMap((service) => service.link("linked", target, root)),
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
await write(
|
|
191
|
+
root,
|
|
192
|
+
"tasks/active/TASK.md",
|
|
193
|
+
`---
|
|
194
|
+
ticketUrl: null
|
|
195
|
+
repo: linked
|
|
196
|
+
branch: task/active
|
|
197
|
+
base: main
|
|
198
|
+
pr: null
|
|
199
|
+
status: working
|
|
200
|
+
---
|
|
201
|
+
`,
|
|
202
|
+
)
|
|
203
|
+
await mkdir(dirname(checkout), { recursive: true })
|
|
204
|
+
await runGit([
|
|
205
|
+
"-C",
|
|
206
|
+
target,
|
|
207
|
+
"worktree",
|
|
208
|
+
"add",
|
|
209
|
+
"-b",
|
|
210
|
+
"task/active",
|
|
211
|
+
checkout,
|
|
212
|
+
"main",
|
|
213
|
+
])
|
|
214
|
+
await Bun.write(join(checkout, "dirty.txt"), "preserved\n")
|
|
215
|
+
|
|
216
|
+
const result = await runTestEffect(
|
|
217
|
+
RepositoryService.pipe(
|
|
218
|
+
Effect.flatMap((service) => service.materialize("linked", root)),
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
expect(result.kind).toBe("bare")
|
|
223
|
+
expect(result.target).toBeNull()
|
|
224
|
+
expect(result.states).toEqual(["declared", "materialized"])
|
|
225
|
+
expect(await Bun.file(join(checkout, "dirty.txt")).text()).toBe(
|
|
226
|
+
"preserved\n",
|
|
227
|
+
)
|
|
228
|
+
expect(await gitOutput(["-C", checkout, "branch", "--show-current"])).toBe(
|
|
229
|
+
"task/active",
|
|
230
|
+
)
|
|
231
|
+
expect(
|
|
232
|
+
await gitOutput(["-C", checkout, "rev-parse", "--git-common-dir"]),
|
|
233
|
+
).toBe(await realpath(join(root, "repos/linked")))
|
|
234
|
+
expect(
|
|
235
|
+
await gitOutput(["-C", target, "worktree", "list", "--porcelain"]),
|
|
236
|
+
).not.toContain(checkout)
|
|
237
|
+
expect(await Bun.file(join(target, ".git/HEAD")).exists()).toBe(true)
|
|
238
|
+
expect(await Bun.file(join(root, "tasks/active/TASK.md")).text()).toContain(
|
|
239
|
+
"repo: linked",
|
|
240
|
+
)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test("materializes an alias linked through an existing worktree", async () => {
|
|
244
|
+
const primary = join(root, "primary-repository")
|
|
245
|
+
const linkedWorktree = join(root, "linked-worktree")
|
|
246
|
+
await mkdir(primary, { recursive: true })
|
|
247
|
+
await runGit(["init", "--initial-branch=main", primary])
|
|
248
|
+
await runGit(["-C", primary, "config", "user.email", "test@example.com"])
|
|
249
|
+
await runGit(["-C", primary, "config", "user.name", "Test"])
|
|
250
|
+
await Bun.write(join(primary, "README.md"), "linked worktree\n")
|
|
251
|
+
await runGit(["-C", primary, "add", "README.md"])
|
|
252
|
+
await runGit(["-C", primary, "commit", "-m", "initial"])
|
|
253
|
+
await setPortableOrigin(primary, "linked-worktree")
|
|
254
|
+
await runGit([
|
|
255
|
+
"-C",
|
|
256
|
+
primary,
|
|
257
|
+
"worktree",
|
|
258
|
+
"add",
|
|
259
|
+
"-b",
|
|
260
|
+
"linked-branch",
|
|
261
|
+
linkedWorktree,
|
|
262
|
+
"main",
|
|
263
|
+
])
|
|
264
|
+
await runTestEffect(
|
|
265
|
+
RepositoryService.pipe(
|
|
266
|
+
Effect.flatMap((service) =>
|
|
267
|
+
service.link("linked", linkedWorktree, root),
|
|
268
|
+
),
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
await runTestEffect(
|
|
273
|
+
RepositoryService.pipe(
|
|
274
|
+
Effect.flatMap((service) => service.materialize("linked", root)),
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
expect(
|
|
279
|
+
await gitOutput(["-C", linkedWorktree, "rev-parse", "--git-common-dir"]),
|
|
280
|
+
).toBe(await realpath(join(root, "repos/linked")))
|
|
281
|
+
expect(
|
|
282
|
+
await gitOutput(["-C", primary, "worktree", "list", "--porcelain"]),
|
|
283
|
+
).not.toContain(linkedWorktree)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test("refuses to materialize linked aliases in jj workbases", async () => {
|
|
287
|
+
if (!Bun.which("jj")) return
|
|
288
|
+
await Bun.write(
|
|
289
|
+
join(root, "agency.json"),
|
|
290
|
+
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
291
|
+
)
|
|
292
|
+
const target = join(root, "linked-jj-repository")
|
|
293
|
+
await mkdir(target, { recursive: true })
|
|
294
|
+
await runGit(["init", "--initial-branch=main", target])
|
|
295
|
+
await setPortableOrigin(target, "linked-jj")
|
|
296
|
+
await runTestEffect(
|
|
297
|
+
RepositoryService.pipe(
|
|
298
|
+
Effect.flatMap((service) => service.link("linked", target, root)),
|
|
299
|
+
),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
await expect(
|
|
303
|
+
runTestEffect(
|
|
304
|
+
RepositoryService.pipe(
|
|
305
|
+
Effect.flatMap((service) => service.materialize("linked", root)),
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
).rejects.toThrow("only supported for Git workbases")
|
|
309
|
+
})
|
|
310
|
+
|
|
162
311
|
test("rejects invalid and duplicate aliases", async () => {
|
|
163
312
|
const source = join(root, "source.git")
|
|
164
313
|
await runGit(["init", "--bare", "--initial-branch=main", source])
|