@markjaquith/agency 2.21.0 → 2.23.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 +9 -0
- package/cli.ts +3 -0
- package/package.json +1 -1
- package/src/cli-parser.test.ts +25 -0
- package/src/cli-parser.ts +22 -7
- package/src/cli.test.ts +35 -0
- package/src/commands/epic.test.ts +12 -3
- package/src/commands/epic.ts +11 -1
- package/src/commands/phase.ts +13 -1
- package/src/commands/pr.ts +2 -2
- package/src/commands/task-phase.test.ts +16 -4
- package/src/commands/task.ts +13 -1
- package/src/graph-schema.ts +2 -10
- package/src/protocol.test.ts +19 -0
- package/src/services/ClaimService.ts +34 -23
- package/src/services/ContextService.ts +8 -10
- package/src/services/EpicService.ts +16 -2
- package/src/services/GraphMutationService.test.ts +31 -0
- package/src/services/GraphMutationService.ts +126 -11
- package/src/services/GraphService.ts +4 -15
- package/src/services/PhaseService.ts +26 -4
- package/src/services/PullRequestService.test.ts +91 -4
- package/src/services/PullRequestService.ts +108 -31
- package/src/services/SyncService.test.ts +95 -1
- package/src/services/SyncService.ts +150 -60
- package/src/services/TaskPhaseService.test.ts +9 -1
- package/src/services/TaskService.ts +22 -3
- package/src/services/WorkbaseService.ts +2 -0
- package/src/work-view.ts +2 -0
- package/src/workbase/delivery-command.test.ts +54 -0
- package/src/workbase/delivery-command.ts +135 -0
- package/src/workbase/document-revision.ts +16 -0
- package/src/workbase/schemas.test.ts +35 -0
- package/src/workbase/schemas.ts +23 -1
package/README.md
CHANGED
|
@@ -400,6 +400,8 @@ repository. Alias names are then used by all documents and commands.
|
|
|
400
400
|
Commands that print Agency-owned results accept `--json`, including initialization,
|
|
401
401
|
integration inspection/sync, repository mutations, entity creation/list/show,
|
|
402
402
|
status, validation, graph export, reconciliation, and PR creation.
|
|
403
|
+
Entity create, list, and show results include a stable SHA-256 `revision` of the
|
|
404
|
+
complete Markdown document.
|
|
403
405
|
|
|
404
406
|
### Epics
|
|
405
407
|
|
|
@@ -515,6 +517,13 @@ references as one rollback-capable mutation and refuse when a materialized
|
|
|
515
517
|
worktree would make the directory move unsafe. Mutation JSON includes changed
|
|
516
518
|
paths and the focused validation scope.
|
|
517
519
|
|
|
520
|
+
Epic, task, and phase update, rename, move, and dependency mutations accept
|
|
521
|
+
`--if-revision <hash>`. The option is optional for interactive human use. When
|
|
522
|
+
provided, Agency fails with a structured `REVISION_CONFLICT` containing the
|
|
523
|
+
expected and current revisions if the target changed. Multi-document mutations
|
|
524
|
+
also recheck every affected document after taking the mutation lock and before
|
|
525
|
+
writing anything.
|
|
526
|
+
|
|
518
527
|
Single-phase tasks and phases store status in YAML. New execution units start
|
|
519
528
|
`open`, and `agency work` marks the selected execution unit `working` immediately
|
|
520
529
|
before launch. Use claims for coordinated ownership and the status subcommands
|
package/cli.ts
CHANGED
|
@@ -238,6 +238,7 @@ const commands: Record<string, Command> = {
|
|
|
238
238
|
ticketUrl: options["ticket-url"],
|
|
239
239
|
description: options.description,
|
|
240
240
|
clearDescription: options["clear-description"],
|
|
241
|
+
ifRevision: options["if-revision"],
|
|
241
242
|
repos: options.repo,
|
|
242
243
|
json: options.json,
|
|
243
244
|
statuses: options.status,
|
|
@@ -289,6 +290,7 @@ const commands: Record<string, Command> = {
|
|
|
289
290
|
clearReferences: options["clear-references"],
|
|
290
291
|
prUrl: options["pr-url"],
|
|
291
292
|
clearPr: options["clear-pr"],
|
|
293
|
+
ifRevision: options["if-revision"],
|
|
292
294
|
dependsOn: options["depends-on"],
|
|
293
295
|
firstPhase: options["first-phase"],
|
|
294
296
|
json: options.json,
|
|
@@ -399,6 +401,7 @@ const commands: Record<string, Command> = {
|
|
|
399
401
|
clearReferences: options["clear-references"],
|
|
400
402
|
prUrl: options["pr-url"],
|
|
401
403
|
clearPr: options["clear-pr"],
|
|
404
|
+
ifRevision: options["if-revision"],
|
|
402
405
|
noEpic: options["no-epic"],
|
|
403
406
|
multiPhase: options["multi-phase"],
|
|
404
407
|
json: options.json,
|
package/package.json
CHANGED
package/src/cli-parser.test.ts
CHANGED
|
@@ -296,6 +296,31 @@ describe("strict CLI parsing", () => {
|
|
|
296
296
|
).toThrow("must be 'done' or 'dropped'")
|
|
297
297
|
})
|
|
298
298
|
|
|
299
|
+
test("accepts valid mutation revisions and rejects malformed hashes", () => {
|
|
300
|
+
const revision = "a".repeat(64)
|
|
301
|
+
expect(
|
|
302
|
+
parseCli([
|
|
303
|
+
"task",
|
|
304
|
+
"move",
|
|
305
|
+
"example",
|
|
306
|
+
"--no-epic",
|
|
307
|
+
"--if-revision",
|
|
308
|
+
revision,
|
|
309
|
+
]),
|
|
310
|
+
).toMatchObject({ values: { "if-revision": revision } })
|
|
311
|
+
expect(() =>
|
|
312
|
+
parseCli([
|
|
313
|
+
"phase",
|
|
314
|
+
"rename",
|
|
315
|
+
"task",
|
|
316
|
+
"phase",
|
|
317
|
+
"renamed",
|
|
318
|
+
"--if-revision",
|
|
319
|
+
"stale",
|
|
320
|
+
]),
|
|
321
|
+
).toThrow("64-character SHA-256 hash")
|
|
322
|
+
})
|
|
323
|
+
|
|
299
324
|
test("accepts context projections and keeps compact command-local", () => {
|
|
300
325
|
expect(parseCli(["context", ".", "--json", "--compact"])).toMatchObject({
|
|
301
326
|
commandName: "context",
|
package/src/cli-parser.ts
CHANGED
|
@@ -91,6 +91,7 @@ const phaseCreateOptions = {
|
|
|
91
91
|
} satisfies OptionConfig
|
|
92
92
|
|
|
93
93
|
const mutationOptions = {
|
|
94
|
+
"if-revision": { type: "string" },
|
|
94
95
|
"clear-description": { type: "boolean" },
|
|
95
96
|
"clear-ticket": { type: "boolean" },
|
|
96
97
|
"clear-references": { type: "boolean" },
|
|
@@ -249,6 +250,7 @@ const commands = {
|
|
|
249
250
|
"clear-description",
|
|
250
251
|
"ticket-url",
|
|
251
252
|
"repo",
|
|
253
|
+
"if-revision",
|
|
252
254
|
"json",
|
|
253
255
|
],
|
|
254
256
|
repeatable: ["repo"],
|
|
@@ -258,7 +260,7 @@ const commands = {
|
|
|
258
260
|
usage: "agency epic rename <id> <new-id> [--json]",
|
|
259
261
|
minArgs: 2,
|
|
260
262
|
maxArgs: 2,
|
|
261
|
-
options: ["json"],
|
|
263
|
+
options: ["if-revision", "json"],
|
|
262
264
|
},
|
|
263
265
|
},
|
|
264
266
|
},
|
|
@@ -344,6 +346,7 @@ const commands = {
|
|
|
344
346
|
"base",
|
|
345
347
|
"pr-url",
|
|
346
348
|
"clear-pr",
|
|
349
|
+
"if-revision",
|
|
347
350
|
"json",
|
|
348
351
|
],
|
|
349
352
|
repeatable: ["reference"],
|
|
@@ -358,13 +361,13 @@ const commands = {
|
|
|
358
361
|
usage: "agency task rename <id> <new-id> [--json]",
|
|
359
362
|
minArgs: 2,
|
|
360
363
|
maxArgs: 2,
|
|
361
|
-
options: ["json"],
|
|
364
|
+
options: ["if-revision", "json"],
|
|
362
365
|
},
|
|
363
366
|
move: {
|
|
364
367
|
usage: "agency task move <id> (--epic <id> | --no-epic) [--json]",
|
|
365
368
|
minArgs: 1,
|
|
366
369
|
maxArgs: 1,
|
|
367
|
-
options: ["epic", "no-epic", "json"],
|
|
370
|
+
options: ["epic", "no-epic", "if-revision", "json"],
|
|
368
371
|
conflicts: [["epic", "no-epic"]],
|
|
369
372
|
},
|
|
370
373
|
dependency: {
|
|
@@ -372,7 +375,7 @@ const commands = {
|
|
|
372
375
|
"agency task dependency <add|remove> <task-id> <dependency-id> [--json]",
|
|
373
376
|
minArgs: 3,
|
|
374
377
|
maxArgs: 3,
|
|
375
|
-
options: ["json"],
|
|
378
|
+
options: ["if-revision", "json"],
|
|
376
379
|
},
|
|
377
380
|
},
|
|
378
381
|
},
|
|
@@ -441,6 +444,7 @@ const commands = {
|
|
|
441
444
|
"base",
|
|
442
445
|
"pr-url",
|
|
443
446
|
"clear-pr",
|
|
447
|
+
"if-revision",
|
|
444
448
|
"json",
|
|
445
449
|
],
|
|
446
450
|
repeatable: ["reference"],
|
|
@@ -454,14 +458,14 @@ const commands = {
|
|
|
454
458
|
usage: "agency phase rename <task-id> <phase-id> <new-id> [--json]",
|
|
455
459
|
minArgs: 3,
|
|
456
460
|
maxArgs: 3,
|
|
457
|
-
options: ["json"],
|
|
461
|
+
options: ["if-revision", "json"],
|
|
458
462
|
},
|
|
459
463
|
dependency: {
|
|
460
464
|
usage:
|
|
461
465
|
"agency phase dependency <add|remove> <task-id> <phase-id> <dependency-id> [--json]",
|
|
462
466
|
minArgs: 4,
|
|
463
467
|
maxArgs: 4,
|
|
464
|
-
options: ["json"],
|
|
468
|
+
options: ["if-revision", "json"],
|
|
465
469
|
},
|
|
466
470
|
},
|
|
467
471
|
},
|
|
@@ -1125,11 +1129,22 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1125
1129
|
["epic", "task", "phase"].includes(commandName) &&
|
|
1126
1130
|
subcommand === "update"
|
|
1127
1131
|
) {
|
|
1128
|
-
const mutationNames = (spec.options ?? []).filter(
|
|
1132
|
+
const mutationNames = (spec.options ?? []).filter(
|
|
1133
|
+
(name) => name !== "json" && name !== "if-revision",
|
|
1134
|
+
)
|
|
1129
1135
|
if (!mutationNames.some((name) => parsed.values[name] !== undefined)) {
|
|
1130
1136
|
throw usageError("At least one update option is required.", spec.usage)
|
|
1131
1137
|
}
|
|
1132
1138
|
}
|
|
1139
|
+
if (
|
|
1140
|
+
typeof parsed.values["if-revision"] === "string" &&
|
|
1141
|
+
!/^[a-f0-9]{64}$/.test(parsed.values["if-revision"])
|
|
1142
|
+
) {
|
|
1143
|
+
throw usageError(
|
|
1144
|
+
"Option '--if-revision' must be a 64-character SHA-256 hash.",
|
|
1145
|
+
spec.usage,
|
|
1146
|
+
)
|
|
1147
|
+
}
|
|
1133
1148
|
if (commandName === "graph") {
|
|
1134
1149
|
validateGraphOptions(parsed.values, spec)
|
|
1135
1150
|
}
|
package/src/cli.test.ts
CHANGED
|
@@ -203,6 +203,41 @@ describe("CLI", () => {
|
|
|
203
203
|
),
|
|
204
204
|
)
|
|
205
205
|
}
|
|
206
|
+
const observed = parseJson(
|
|
207
|
+
await runCli(["task", "show", "second", "--json"], root),
|
|
208
|
+
)
|
|
209
|
+
const taskPath = join(root, "tasks/second/TASK.md")
|
|
210
|
+
await Bun.write(
|
|
211
|
+
taskPath,
|
|
212
|
+
`${await Bun.file(taskPath).text()}\nConcurrent edit.\n`,
|
|
213
|
+
)
|
|
214
|
+
const conflict = await runCli(
|
|
215
|
+
[
|
|
216
|
+
"task",
|
|
217
|
+
"update",
|
|
218
|
+
"second",
|
|
219
|
+
"--description",
|
|
220
|
+
"Stale update",
|
|
221
|
+
"--if-revision",
|
|
222
|
+
observed.revision,
|
|
223
|
+
"--json",
|
|
224
|
+
],
|
|
225
|
+
root,
|
|
226
|
+
)
|
|
227
|
+
expect(conflict.exitCode).toBe(1)
|
|
228
|
+
expect(JSON.parse(conflict.stdout)).toMatchObject({
|
|
229
|
+
ok: false,
|
|
230
|
+
error: {
|
|
231
|
+
code: "REVISION_CONFLICT",
|
|
232
|
+
retryable: true,
|
|
233
|
+
fields: {
|
|
234
|
+
path: "tasks/second/TASK.md",
|
|
235
|
+
expectedRevision: observed.revision,
|
|
236
|
+
currentRevision: expect.stringMatching(/^[a-f0-9]{64}$/),
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
expect(await Bun.file(taskPath).text()).toContain("Concurrent edit.")
|
|
206
241
|
|
|
207
242
|
const updated = parseJson(
|
|
208
243
|
await runCli(
|
|
@@ -40,9 +40,12 @@ describe("epic command", () => {
|
|
|
40
40
|
),
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
const created = JSON.parse(logs[0]!)
|
|
44
|
+
expect(created.revision).toMatch(/^[a-f0-9]{64}$/)
|
|
45
|
+
expect(created).toEqual({
|
|
44
46
|
id: "example",
|
|
45
47
|
path: join(root, "epics/example/EPIC.md"),
|
|
48
|
+
revision: created.revision,
|
|
46
49
|
data: {
|
|
47
50
|
ticketUrl: "https://example.com/epic",
|
|
48
51
|
repos: [{ repo: "agency", ref: "main" }],
|
|
@@ -68,10 +71,13 @@ describe("epic command", () => {
|
|
|
68
71
|
epic({ subcommand: "list", args: [], cwd: root, json: true }),
|
|
69
72
|
),
|
|
70
73
|
)
|
|
71
|
-
|
|
74
|
+
const listed = JSON.parse(listLogs[0]!)
|
|
75
|
+
expect(listed[0].revision).toMatch(/^[a-f0-9]{64}$/)
|
|
76
|
+
expect(listed).toEqual([
|
|
72
77
|
{
|
|
73
78
|
id: "example",
|
|
74
79
|
path: join(root, "epics/example/EPIC.md"),
|
|
80
|
+
revision: listed[0].revision,
|
|
75
81
|
data: {
|
|
76
82
|
ticketUrl: "https://example.com/epic",
|
|
77
83
|
repos: [{ repo: "agency", ref: "main" }],
|
|
@@ -90,9 +96,12 @@ describe("epic command", () => {
|
|
|
90
96
|
}),
|
|
91
97
|
),
|
|
92
98
|
)
|
|
93
|
-
|
|
99
|
+
const shown = JSON.parse(showLogs[0]!)
|
|
100
|
+
expect(shown.revision).toBe(listed[0].revision)
|
|
101
|
+
expect(shown).toEqual({
|
|
94
102
|
id: "example",
|
|
95
103
|
path: join(root, "epics/example/EPIC.md"),
|
|
104
|
+
revision: shown.revision,
|
|
96
105
|
data: {
|
|
97
106
|
ticketUrl: "https://example.com/epic",
|
|
98
107
|
repos: [{ repo: "agency", ref: "main" }],
|
package/src/commands/epic.ts
CHANGED
|
@@ -13,6 +13,7 @@ interface EpicOptions extends BaseCommandOptions {
|
|
|
13
13
|
readonly ticketUrl?: string
|
|
14
14
|
readonly description?: string
|
|
15
15
|
readonly clearDescription?: boolean
|
|
16
|
+
readonly ifRevision?: string
|
|
16
17
|
readonly repos?: readonly string[]
|
|
17
18
|
readonly json?: boolean
|
|
18
19
|
readonly statuses?: readonly string[]
|
|
@@ -122,6 +123,7 @@ export const epic = (options: EpicOptions) =>
|
|
|
122
123
|
: parseRepositoryReferences(options.repos),
|
|
123
124
|
},
|
|
124
125
|
cwd,
|
|
126
|
+
options.ifRevision,
|
|
125
127
|
)
|
|
126
128
|
log(
|
|
127
129
|
options.json
|
|
@@ -138,7 +140,12 @@ export const epic = (options: EpicOptions) =>
|
|
|
138
140
|
new Error("Epic ID and new ID are required"),
|
|
139
141
|
)
|
|
140
142
|
}
|
|
141
|
-
const output = yield* mutations.renameEpic(
|
|
143
|
+
const output = yield* mutations.renameEpic(
|
|
144
|
+
id,
|
|
145
|
+
newId,
|
|
146
|
+
cwd,
|
|
147
|
+
options.ifRevision,
|
|
148
|
+
)
|
|
142
149
|
log(
|
|
143
150
|
options.json
|
|
144
151
|
? JSON.stringify(output, null, 2)
|
|
@@ -177,6 +184,9 @@ Update options:
|
|
|
177
184
|
--clear-description Remove the description
|
|
178
185
|
--repo <alias>:<ref> Replace repository references; repeatable
|
|
179
186
|
|
|
187
|
+
Mutation options:
|
|
188
|
+
--if-revision <hash> Require the target's current revision
|
|
189
|
+
|
|
180
190
|
Options:
|
|
181
191
|
--json Output results as JSON
|
|
182
192
|
--status <status> Filter list by status; repeatable
|
package/src/commands/phase.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface PhaseOptions extends BaseCommandOptions {
|
|
|
19
19
|
readonly clearReferences?: boolean
|
|
20
20
|
readonly prUrl?: string
|
|
21
21
|
readonly clearPr?: boolean
|
|
22
|
+
readonly ifRevision?: string
|
|
22
23
|
readonly dependsOn?: readonly string[]
|
|
23
24
|
readonly firstPhase?: string
|
|
24
25
|
readonly json?: boolean
|
|
@@ -181,6 +182,7 @@ export const phase = (options: PhaseOptions) =>
|
|
|
181
182
|
pr: options.clearPr ? null : options.prUrl,
|
|
182
183
|
},
|
|
183
184
|
cwd,
|
|
185
|
+
options.ifRevision,
|
|
184
186
|
)
|
|
185
187
|
log(
|
|
186
188
|
options.json
|
|
@@ -196,7 +198,13 @@ export const phase = (options: PhaseOptions) =>
|
|
|
196
198
|
new Error("Task ID, phase ID, and new ID are required"),
|
|
197
199
|
)
|
|
198
200
|
}
|
|
199
|
-
const output = yield* mutations.renamePhase(
|
|
201
|
+
const output = yield* mutations.renamePhase(
|
|
202
|
+
taskId,
|
|
203
|
+
phaseId,
|
|
204
|
+
newId,
|
|
205
|
+
cwd,
|
|
206
|
+
options.ifRevision,
|
|
207
|
+
)
|
|
200
208
|
log(
|
|
201
209
|
options.json
|
|
202
210
|
? JSON.stringify(output, null, 2)
|
|
@@ -225,6 +233,7 @@ export const phase = (options: PhaseOptions) =>
|
|
|
225
233
|
dependencyPhaseId,
|
|
226
234
|
dependencyId,
|
|
227
235
|
cwd,
|
|
236
|
+
options.ifRevision,
|
|
228
237
|
)
|
|
229
238
|
log(
|
|
230
239
|
options.json
|
|
@@ -257,6 +266,9 @@ Subcommands:
|
|
|
257
266
|
dependency <operation> <task> <phase> <dependency>
|
|
258
267
|
Add or remove a phase dependency
|
|
259
268
|
|
|
269
|
+
Mutation option:
|
|
270
|
+
--if-revision <hash> Require the target's current revision
|
|
271
|
+
|
|
260
272
|
Create options:
|
|
261
273
|
--description <text> Short description of the phase
|
|
262
274
|
--repo <alias> Writable repository
|
package/src/commands/pr.ts
CHANGED
|
@@ -33,8 +33,8 @@ export const pr = (options: PrOptions) =>
|
|
|
33
33
|
export const help = `
|
|
34
34
|
Usage: agency pr create <task-id> [phase-id]
|
|
35
35
|
|
|
36
|
-
Push the execution branch, create a
|
|
37
|
-
task or phase document.
|
|
36
|
+
Push the execution branch, create a pull request with the delivery provider,
|
|
37
|
+
and update its task or phase document.
|
|
38
38
|
|
|
39
39
|
Options:
|
|
40
40
|
--draft Create a draft pull request
|
|
@@ -48,10 +48,13 @@ describe("task and phase command JSON output", () => {
|
|
|
48
48
|
task({ subcommand: "list", args: [], cwd: root, json: true }),
|
|
49
49
|
),
|
|
50
50
|
)
|
|
51
|
-
|
|
51
|
+
const listed = JSON.parse(listLogs[0]!)
|
|
52
|
+
expect(listed[0].revision).toMatch(/^[a-f0-9]{64}$/)
|
|
53
|
+
expect(listed).toEqual([
|
|
52
54
|
{
|
|
53
55
|
id: "multi",
|
|
54
56
|
path: join(root, "tasks/multi/TASK.md"),
|
|
57
|
+
revision: listed[0].revision,
|
|
55
58
|
data: {
|
|
56
59
|
ticketUrl: "https://example.com/task",
|
|
57
60
|
phases: [{ id: "first" }],
|
|
@@ -69,9 +72,12 @@ describe("task and phase command JSON output", () => {
|
|
|
69
72
|
}),
|
|
70
73
|
),
|
|
71
74
|
)
|
|
72
|
-
|
|
75
|
+
const shown = JSON.parse(showLogs[0]!)
|
|
76
|
+
expect(shown.revision).toBe(listed[0].revision)
|
|
77
|
+
expect(shown).toEqual({
|
|
73
78
|
id: "multi",
|
|
74
79
|
path: join(root, "tasks/multi/TASK.md"),
|
|
80
|
+
revision: shown.revision,
|
|
75
81
|
data: {
|
|
76
82
|
ticketUrl: "https://example.com/task",
|
|
77
83
|
phases: [{ id: "first" }],
|
|
@@ -85,11 +91,14 @@ describe("task and phase command JSON output", () => {
|
|
|
85
91
|
phase({ subcommand: "list", args: ["multi"], cwd: root, json: true }),
|
|
86
92
|
),
|
|
87
93
|
)
|
|
88
|
-
|
|
94
|
+
const listed = JSON.parse(listLogs[0]!)
|
|
95
|
+
expect(listed[0].revision).toMatch(/^[a-f0-9]{64}$/)
|
|
96
|
+
expect(listed).toEqual([
|
|
89
97
|
{
|
|
90
98
|
taskId: "multi",
|
|
91
99
|
id: "first",
|
|
92
100
|
path: join(root, "tasks/multi/phases/first/PHASE.md"),
|
|
101
|
+
revision: listed[0].revision,
|
|
93
102
|
data: {
|
|
94
103
|
repo: "agency",
|
|
95
104
|
branch: "task/first",
|
|
@@ -110,10 +119,13 @@ describe("task and phase command JSON output", () => {
|
|
|
110
119
|
}),
|
|
111
120
|
),
|
|
112
121
|
)
|
|
113
|
-
|
|
122
|
+
const shown = JSON.parse(showLogs[0]!)
|
|
123
|
+
expect(shown.revision).toBe(listed[0].revision)
|
|
124
|
+
expect(shown).toEqual({
|
|
114
125
|
taskId: "multi",
|
|
115
126
|
id: "first",
|
|
116
127
|
path: join(root, "tasks/multi/phases/first/PHASE.md"),
|
|
128
|
+
revision: shown.revision,
|
|
117
129
|
data: {
|
|
118
130
|
repo: "agency",
|
|
119
131
|
branch: "task/first",
|
package/src/commands/task.ts
CHANGED
|
@@ -27,6 +27,7 @@ interface TaskOptions extends BaseCommandOptions {
|
|
|
27
27
|
readonly clearReferences?: boolean
|
|
28
28
|
readonly prUrl?: string
|
|
29
29
|
readonly clearPr?: boolean
|
|
30
|
+
readonly ifRevision?: string
|
|
30
31
|
readonly noEpic?: boolean
|
|
31
32
|
readonly multiPhase?: boolean
|
|
32
33
|
readonly json?: boolean
|
|
@@ -318,6 +319,7 @@ export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
|
|
|
318
319
|
pr: options.clearPr ? null : options.prUrl,
|
|
319
320
|
},
|
|
320
321
|
cwd,
|
|
322
|
+
options.ifRevision,
|
|
321
323
|
)
|
|
322
324
|
log(
|
|
323
325
|
options.json
|
|
@@ -333,7 +335,12 @@ export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
|
|
|
333
335
|
new Error("Task ID and new ID are required"),
|
|
334
336
|
)
|
|
335
337
|
}
|
|
336
|
-
const output = yield* mutations.renameTask(
|
|
338
|
+
const output = yield* mutations.renameTask(
|
|
339
|
+
id,
|
|
340
|
+
newId,
|
|
341
|
+
cwd,
|
|
342
|
+
options.ifRevision,
|
|
343
|
+
)
|
|
337
344
|
log(
|
|
338
345
|
options.json
|
|
339
346
|
? JSON.stringify(output, null, 2)
|
|
@@ -348,6 +355,7 @@ export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
|
|
|
348
355
|
id,
|
|
349
356
|
options.noEpic ? null : (options.epic ?? null),
|
|
350
357
|
cwd,
|
|
358
|
+
options.ifRevision,
|
|
351
359
|
)
|
|
352
360
|
log(
|
|
353
361
|
options.json
|
|
@@ -376,6 +384,7 @@ export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
|
|
|
376
384
|
id,
|
|
377
385
|
dependencyId,
|
|
378
386
|
cwd,
|
|
387
|
+
options.ifRevision,
|
|
379
388
|
)
|
|
380
389
|
log(
|
|
381
390
|
options.json
|
|
@@ -408,6 +417,9 @@ Subcommands:
|
|
|
408
417
|
dependency <operation> <task> <dependency>
|
|
409
418
|
Add or remove a task dependency
|
|
410
419
|
|
|
420
|
+
Mutation option:
|
|
421
|
+
--if-revision <hash> Require the target's current revision
|
|
422
|
+
|
|
411
423
|
Create options:
|
|
412
424
|
--ticket-url <url> External ticket URL (optional)
|
|
413
425
|
--description <text> Short description of the task
|
package/src/graph-schema.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Schema } from "@effect/schema"
|
|
|
2
2
|
import {
|
|
3
3
|
EpicFrontmatter,
|
|
4
4
|
PhaseFrontmatter,
|
|
5
|
+
PullRequestRecord,
|
|
5
6
|
TaskFrontmatter,
|
|
6
7
|
WorkStatus,
|
|
7
8
|
} from "./workbase/schemas"
|
|
@@ -109,16 +110,7 @@ export const GraphExecutionGit = Schema.Struct({
|
|
|
109
110
|
export const GraphPr = Schema.Union(
|
|
110
111
|
Schema.Struct({ url: Schema.Null, state: Schema.Literal("none") }),
|
|
111
112
|
Schema.Struct({ url: Schema.String, state: Schema.Literal("unavailable") }),
|
|
112
|
-
|
|
113
|
-
recordedUrl: Schema.String,
|
|
114
|
-
number: Schema.Number,
|
|
115
|
-
state: Schema.String,
|
|
116
|
-
title: Schema.String,
|
|
117
|
-
isDraft: Schema.Boolean,
|
|
118
|
-
headRefName: Schema.String,
|
|
119
|
-
baseRefName: Schema.String,
|
|
120
|
-
url: Schema.String,
|
|
121
|
-
}),
|
|
113
|
+
PullRequestRecord,
|
|
122
114
|
)
|
|
123
115
|
|
|
124
116
|
const NodeIdentity = {
|
package/src/protocol.test.ts
CHANGED
|
@@ -108,5 +108,24 @@ describe("machine protocol", () => {
|
|
|
108
108
|
},
|
|
109
109
|
},
|
|
110
110
|
})
|
|
111
|
+
expect(
|
|
112
|
+
errorEnvelope({
|
|
113
|
+
_tag: "RevisionConflictError",
|
|
114
|
+
message: "revision conflict",
|
|
115
|
+
path: "tasks/example/TASK.md",
|
|
116
|
+
expectedRevision: "a".repeat(64),
|
|
117
|
+
currentRevision: "b".repeat(64),
|
|
118
|
+
}),
|
|
119
|
+
).toMatchObject({
|
|
120
|
+
error: {
|
|
121
|
+
code: "REVISION_CONFLICT",
|
|
122
|
+
retryable: true,
|
|
123
|
+
fields: {
|
|
124
|
+
path: "tasks/example/TASK.md",
|
|
125
|
+
expectedRevision: "a".repeat(64),
|
|
126
|
+
currentRevision: "b".repeat(64),
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
})
|
|
111
130
|
})
|
|
112
131
|
})
|