@markjaquith/agency 2.59.0 → 2.60.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 +46 -4
- package/cli-main.ts +5 -0
- package/index.ts +1 -0
- package/package.json +4 -1
- package/schemas/agency-kickoff-v1.schema.json +124 -0
- package/src/cli-parser.test.ts +1 -1
- package/src/cli-parser.ts +23 -6
- package/src/commands/task.test.ts +77 -1
- package/src/commands/task.ts +76 -6
- package/src/commands/work.test.ts +62 -3
- package/src/commands/work.ts +106 -3
- package/src/services/IntegrationService.test.ts +37 -0
- package/src/services/WorktreeService.ts +9 -6
- package/src/workbase/AGENTS.md +55 -4
- package/src/workbase/kickoff-contract.test.ts +145 -0
- package/src/workbase/kickoff-contract.ts +358 -0
package/src/commands/work.ts
CHANGED
|
@@ -29,6 +29,13 @@ import {
|
|
|
29
29
|
resolveRunnerCommand,
|
|
30
30
|
runnerEnvironment,
|
|
31
31
|
} from "../workbase/runner-command"
|
|
32
|
+
import {
|
|
33
|
+
assessValidationEvidence,
|
|
34
|
+
buildKickoffPlan,
|
|
35
|
+
buildValidationEvidence,
|
|
36
|
+
normalizeRecalledContext,
|
|
37
|
+
readValidationEvidence,
|
|
38
|
+
} from "../workbase/kickoff-contract"
|
|
32
39
|
|
|
33
40
|
export interface WorkOptions extends BaseCommandOptions {
|
|
34
41
|
readonly directory?: string
|
|
@@ -41,6 +48,7 @@ export interface WorkOptions extends BaseCommandOptions {
|
|
|
41
48
|
readonly printCommand?: boolean
|
|
42
49
|
readonly auto?: boolean
|
|
43
50
|
readonly force?: boolean
|
|
51
|
+
readonly evidence?: string
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
export type StartWork = (options: WorkOptions) => ReturnType<typeof work>
|
|
@@ -422,6 +430,7 @@ export const workPrepare = (options: WorkOptions = {}) =>
|
|
|
422
430
|
const tasks = yield* TaskService
|
|
423
431
|
const phases = yield* PhaseService
|
|
424
432
|
const worktrees = yield* WorktreeService
|
|
433
|
+
const readiness = yield* ReadinessService
|
|
425
434
|
const { log } = createLoggers(options)
|
|
426
435
|
const cwd = options.cwd ?? process.cwd()
|
|
427
436
|
const targetPath = options.directory ? resolve(cwd, options.directory) : cwd
|
|
@@ -461,22 +470,111 @@ export const workPrepare = (options: WorkOptions = {}) =>
|
|
|
461
470
|
)
|
|
462
471
|
}
|
|
463
472
|
|
|
473
|
+
const task = yield* tasks.show(taskId, root)
|
|
474
|
+
const phase = phaseId
|
|
475
|
+
? yield* phases.show(taskId, phaseId, root)
|
|
476
|
+
: undefined
|
|
477
|
+
if ("phases" in task.data && !phase) {
|
|
478
|
+
return yield* Effect.fail(
|
|
479
|
+
new Error(`Task '${taskId}' has multiple phases; phase ID is required`),
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
if (!("phases" in task.data) && phaseId) {
|
|
483
|
+
return yield* Effect.fail(
|
|
484
|
+
new Error(
|
|
485
|
+
`Task '${taskId}' is single-phase and does not accept a phase ID`,
|
|
486
|
+
),
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
const target = phase
|
|
490
|
+
? `execution-unit:phase/${taskId}/${phase.id}`
|
|
491
|
+
: `execution-unit:task/${taskId}`
|
|
492
|
+
const document = phase ?? task
|
|
493
|
+
const suppliedEvidence = options.evidence
|
|
494
|
+
? yield* readValidationEvidence(options.evidence, cwd)
|
|
495
|
+
: undefined
|
|
496
|
+
if (
|
|
497
|
+
suppliedEvidence?.recalledContext.repo &&
|
|
498
|
+
(!("repo" in document.data) ||
|
|
499
|
+
suppliedEvidence.recalledContext.repo !== document.data.repo)
|
|
500
|
+
) {
|
|
501
|
+
return yield* Effect.fail(
|
|
502
|
+
new Error(
|
|
503
|
+
"Recalled repository conflicts with the current execution unit",
|
|
504
|
+
),
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
if (
|
|
508
|
+
suppliedEvidence?.recalledContext.base &&
|
|
509
|
+
(!("base" in document.data) ||
|
|
510
|
+
suppliedEvidence.recalledContext.base !== document.data.base)
|
|
511
|
+
) {
|
|
512
|
+
return yield* Effect.fail(
|
|
513
|
+
new Error("Recalled base conflicts with the current execution unit"),
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
const assessment = yield* assessValidationEvidence({
|
|
517
|
+
evidence: suppliedEvidence,
|
|
518
|
+
startPath: root,
|
|
519
|
+
target,
|
|
520
|
+
documentPath: document.path,
|
|
521
|
+
documentRevision: document.revision,
|
|
522
|
+
})
|
|
523
|
+
let validation: unknown = { valid: true, source: "evidence" }
|
|
524
|
+
if (assessment.disposition.status === "refreshed") {
|
|
525
|
+
validation = yield* workbase.validate(root)
|
|
526
|
+
if (!(validation as { valid: boolean }).valid && !options.force) {
|
|
527
|
+
return yield* Effect.fail(new Error("Workbase validation failed"))
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
yield* readiness.guardWorkTarget(target, root, options.force)
|
|
464
531
|
const workspace = yield* worktrees.materialize(taskId, phaseId, root, {
|
|
465
532
|
...options,
|
|
466
533
|
dryRun: options.dryRun,
|
|
534
|
+
validationAlreadyPerformed: true,
|
|
535
|
+
})
|
|
536
|
+
const recalledContext =
|
|
537
|
+
suppliedEvidence?.recalledContext ??
|
|
538
|
+
normalizeRecalledContext({
|
|
539
|
+
id: taskId,
|
|
540
|
+
repo: "repo" in document.data ? document.data.repo : undefined,
|
|
541
|
+
base: "base" in document.data ? document.data.base : undefined,
|
|
542
|
+
})
|
|
543
|
+
const evidence = yield* buildValidationEvidence({
|
|
544
|
+
startPath: root,
|
|
545
|
+
target,
|
|
546
|
+
documentPath: document.path,
|
|
547
|
+
documentRevision: document.revision,
|
|
548
|
+
recalledContext,
|
|
467
549
|
})
|
|
550
|
+
const result = {
|
|
551
|
+
...workspace,
|
|
552
|
+
workspace,
|
|
553
|
+
validation,
|
|
554
|
+
validationEvidence: { ...assessment.disposition, evidence },
|
|
555
|
+
kickoff: buildKickoffPlan({
|
|
556
|
+
workbaseRoot: root,
|
|
557
|
+
target,
|
|
558
|
+
taskId,
|
|
559
|
+
phaseId: phase?.id,
|
|
560
|
+
taskPath: task.path,
|
|
561
|
+
phasePath: phase?.path,
|
|
562
|
+
checkoutPath: workspace.writablePath ?? workspace.reviewPath,
|
|
563
|
+
documentRevision: document.revision,
|
|
564
|
+
}),
|
|
565
|
+
}
|
|
468
566
|
if (options.json) {
|
|
469
|
-
log(JSON.stringify(
|
|
567
|
+
log(JSON.stringify(result, null, 2))
|
|
470
568
|
} else {
|
|
471
569
|
log(
|
|
472
|
-
`${workspace.dryRun ? "
|
|
570
|
+
`${workspace.dryRun ? "Kickoff plan" : "Workspace ready"}: ${workspace.writablePath ?? workspace.reviewPath}`,
|
|
473
571
|
)
|
|
474
572
|
}
|
|
475
573
|
})
|
|
476
574
|
|
|
477
575
|
export const help = `
|
|
478
576
|
Usage: agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] [--auto]
|
|
479
|
-
agency work prepare [target] [--dry-run] [--json]
|
|
577
|
+
agency work prepare [target] [--evidence <json-or-path>] [--dry-run] [--json]
|
|
480
578
|
|
|
481
579
|
Launch an agent for an epic, task, or phase. With no directory, select one
|
|
482
580
|
interactively. A positional argument resolves as a directory first, then as a task
|
|
@@ -487,6 +585,10 @@ Agency's project plugin; Agency context remains authoritative for writes.
|
|
|
487
585
|
The prepare subcommand resolves and materializes an execution workspace without
|
|
488
586
|
launching an agent or changing lifecycle status. --dry-run reports planned Git
|
|
489
587
|
changes without fetching, creating branches, or creating worktrees.
|
|
588
|
+
It emits revision-bound validation evidence and an idempotent external-orchestrator
|
|
589
|
+
contract. Evidence is reused only while the target, workbase, configuration, and
|
|
590
|
+
repository mapping remain unchanged. Dynamic readiness and workspace safety checks
|
|
591
|
+
always run.
|
|
490
592
|
|
|
491
593
|
Options:
|
|
492
594
|
--epic <id> Work on an epic
|
|
@@ -500,6 +602,7 @@ Options:
|
|
|
500
602
|
--opencode Require the OpenCode preset
|
|
501
603
|
--claude Require the Claude Code preset
|
|
502
604
|
--force Override readiness; reopen terminal execution units
|
|
605
|
+
--evidence <value> Validation evidence JSON or a path to JSON (prepare only)
|
|
503
606
|
--no-input Never open an interactive selector
|
|
504
607
|
|
|
505
608
|
Without interactive input, provide an explicit workbase or cwd and an entity
|
|
@@ -107,6 +107,21 @@ describe("IntegrationService", () => {
|
|
|
107
107
|
])
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
+
test("generates the canonical Agency kickoff recipe with precedence", () => {
|
|
111
|
+
expect(managedWorkbaseAgents).toContain(
|
|
112
|
+
"takes precedence over generic Herdr defaults",
|
|
113
|
+
)
|
|
114
|
+
expect(managedWorkbaseAgents).toContain(
|
|
115
|
+
"agency work prepare <slug> --evidence",
|
|
116
|
+
)
|
|
117
|
+
expect(managedWorkbaseAgents).toContain("agency-kickoff-v1")
|
|
118
|
+
expect(managedWorkbaseAgents).toContain(
|
|
119
|
+
"call Herdr help, skill, or CLI discovery",
|
|
120
|
+
)
|
|
121
|
+
expect(managedWorkbaseAgents).toContain("exactly one final")
|
|
122
|
+
expect(managedWorkbaseAgents).toContain("leave the runner in the")
|
|
123
|
+
})
|
|
124
|
+
|
|
110
125
|
test("generates a dynamic workbase plugin", () => {
|
|
111
126
|
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
112
127
|
"process.env.AGENCY_WRITABLE_CHECKOUT",
|
|
@@ -445,6 +460,28 @@ describe("IntegrationService", () => {
|
|
|
445
460
|
expect(body).not.toContain(".opencode/command/agency.md")
|
|
446
461
|
})
|
|
447
462
|
|
|
463
|
+
test("generates repository add and setup guidance", () => {
|
|
464
|
+
const body = managedBody(managedWorkbaseAgents)
|
|
465
|
+
|
|
466
|
+
expect(body).toContain("## Adding a Repository")
|
|
467
|
+
expect(body).toContain("agency repo add <alias> <remote> --json")
|
|
468
|
+
expect(body).toContain(
|
|
469
|
+
"`agency repo add` mutates immediately and does not accept `--apply`",
|
|
470
|
+
)
|
|
471
|
+
expect(body).toContain("agency repo setup --dry-run")
|
|
472
|
+
expect(body).toContain("agency repo setup --apply")
|
|
473
|
+
expect(body).toMatch(
|
|
474
|
+
/repositories that are already declared\s+but locally missing/,
|
|
475
|
+
)
|
|
476
|
+
expect(body).toContain("Do not edit\n`agency.json` or `repos/` manually")
|
|
477
|
+
expect(body).toMatch(
|
|
478
|
+
/agency repo verify <alias> --json\s+agency validate --json/,
|
|
479
|
+
)
|
|
480
|
+
expect(body).toMatch(
|
|
481
|
+
/run only these checks, in order, unless\s+`agency context` reports a relevant problem/,
|
|
482
|
+
)
|
|
483
|
+
})
|
|
484
|
+
|
|
448
485
|
test("configures Agency agents with complete workbase access", () => {
|
|
449
486
|
const config = JSON.parse(managedBody(managedWorkbaseOpencode))
|
|
450
487
|
|
|
@@ -272,6 +272,7 @@ interface MaterializeOptions extends BaseCommandOptions {
|
|
|
272
272
|
readonly force?: boolean
|
|
273
273
|
readonly lockHeld?: boolean
|
|
274
274
|
readonly allowReferenceDrift?: boolean
|
|
275
|
+
readonly validationAlreadyPerformed?: boolean
|
|
275
276
|
}
|
|
276
277
|
|
|
277
278
|
interface RemoveOptions extends BaseCommandOptions {
|
|
@@ -1850,12 +1851,14 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1850
1851
|
const { root, config } = yield* workbase.loadConfig(startPath)
|
|
1851
1852
|
const backend = yield* versionControl.forWorkbase(root)
|
|
1852
1853
|
const materialization = Effect.gen(function* () {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1854
|
+
if (!options.validationAlreadyPerformed) {
|
|
1855
|
+
const report = yield* workbase.validate(root)
|
|
1856
|
+
const validationIssue = report.issues[0]
|
|
1857
|
+
if (validationIssue && !options.force) {
|
|
1858
|
+
return yield* new WorktreeError({
|
|
1859
|
+
message: `${validationIssue.path}: ${validationIssue.message}`,
|
|
1860
|
+
})
|
|
1861
|
+
}
|
|
1859
1862
|
}
|
|
1860
1863
|
const task = yield* tasks.show(taskId, root)
|
|
1861
1864
|
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -20,10 +20,26 @@ At the workbase root, use `agency next --json` or `agency graph --json` to choos
|
|
|
20
20
|
work, then inspect the returned document path or explicit entity selectors. Use
|
|
21
21
|
`agency --help` and `agency <command> --help` for exact command syntax.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
## Adding a Repository
|
|
24
|
+
|
|
25
|
+
Add and materialize a new repository alias with:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
agency repo add <alias> <remote> --json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`agency repo add` mutates immediately and does not accept `--apply`. Do not edit
|
|
32
|
+
`agency.json` or `repos/` manually. `agency repo setup --dry-run` and
|
|
33
|
+
`agency repo setup --apply` are only for repositories that are already declared
|
|
34
|
+
but locally missing; obtain explicit approval before applying setup.
|
|
35
|
+
|
|
36
|
+
After adding a repository, run only these checks, in order, unless
|
|
37
|
+
`agency context` reports a relevant problem:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
agency repo verify <alias> --json
|
|
41
|
+
agency validate --json
|
|
42
|
+
```
|
|
27
43
|
|
|
28
44
|
## Authority
|
|
29
45
|
|
|
@@ -64,6 +80,41 @@ to override readiness.
|
|
|
64
80
|
|
|
65
81
|
## Execution
|
|
66
82
|
|
|
83
|
+
### Canonical create and kickoff
|
|
84
|
+
|
|
85
|
+
This Agency recipe takes precedence over generic Herdr defaults whenever the
|
|
86
|
+
request creates, opens, works, launches, starts, or kicks off an Agency item.
|
|
87
|
+
Do not rediscover commands that this recipe or a known-current
|
|
88
|
+
`agency-kickoff-v1` plan supplies.
|
|
89
|
+
|
|
90
|
+
1. Create noninteractively with explicit recalled context when available:
|
|
91
|
+
`agency task create <slug> --context-repo <alias> --context-base <base> --context-slug <slug> --authoritative-source <absolute-path-or-url> --json`.
|
|
92
|
+
Repeat `--authoritative-source` as needed. Supplied context must agree with
|
|
93
|
+
explicit task flags; Agency rejects stale or conflicting values.
|
|
94
|
+
2. For create-only intent, return the creation result and stop. For open intent,
|
|
95
|
+
prepare the task with
|
|
96
|
+
`agency work prepare <slug> --evidence <creation-json> --dry-run --json`, then
|
|
97
|
+
execute the returned plan through `task-document-split` to prepare the
|
|
98
|
+
checkout and open or reuse the background tab. Stop before `runner-start`.
|
|
99
|
+
3. For work/launch/start/kickoff intent, run that same preflight and execute its
|
|
100
|
+
ordered kickoff steps. The plan owns worktree dry-run/preparation, a
|
|
101
|
+
retry-safe background Herdr tab, the side-by-side task document,
|
|
102
|
+
`agency work . --auto`, and exactly one final
|
|
103
|
+
`agency context <document-path> --json` verification.
|
|
104
|
+
4. When the orchestrator has known-current support for the plan's
|
|
105
|
+
`agency-kickoff-v1` capability, execute the supplied actions directly. Do not
|
|
106
|
+
call Herdr help, skill, or CLI discovery. If capability/version evidence is
|
|
107
|
+
absent or stale, discovery is the compatibility path; then resume the same
|
|
108
|
+
idempotency key rather than creating another tab, checkout, or runner.
|
|
109
|
+
5. After the one final context verification succeeds, leave the runner in the
|
|
110
|
+
background and stop. Do not inspect, poll, or babysit it unless the user asks.
|
|
111
|
+
|
|
112
|
+
Validation evidence is a local, auditable optimization, not authority. Preflight
|
|
113
|
+
refreshes it after workbase, target document, configuration, repository mapping,
|
|
114
|
+
payload digest, or kickoff-contract changes. Readiness, claims, repository
|
|
115
|
+
materialization, branch ownership, reference drift, and dirty-workspace checks
|
|
116
|
+
still run on every preparation.
|
|
117
|
+
|
|
67
118
|
For implementation work, read the task and phase prose returned by context,
|
|
68
119
|
change only the writable checkout, keep durable decisions current, and run the
|
|
69
120
|
repository's formatting, type checks, build, dead-code checks, and focused tests.
|
|
@@ -0,0 +1,145 @@
|
|
|
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, runTestEffect } from "../test-utils"
|
|
5
|
+
import { documentRevision } from "./document-revision"
|
|
6
|
+
import {
|
|
7
|
+
assessValidationEvidence,
|
|
8
|
+
buildKickoffPlan,
|
|
9
|
+
buildValidationEvidence,
|
|
10
|
+
KICKOFF_SOURCE_LOCATIONS,
|
|
11
|
+
normalizeRecalledContext,
|
|
12
|
+
parseValidationEvidence,
|
|
13
|
+
readValidationEvidence,
|
|
14
|
+
} from "./kickoff-contract"
|
|
15
|
+
|
|
16
|
+
describe("kickoff contract", () => {
|
|
17
|
+
let root: string
|
|
18
|
+
let taskPath: string
|
|
19
|
+
let taskContent: string
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
root = await createTempDir()
|
|
23
|
+
taskPath = join(root, "tasks/example/TASK.md")
|
|
24
|
+
taskContent =
|
|
25
|
+
"---\nticketUrl: null\nrepo: agency\nbranch: task/example\nbase: main\npr: null\nstatus: open\n---\n\n# Example\n"
|
|
26
|
+
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
27
|
+
await mkdir(join(root, "tasks/example"), { recursive: true })
|
|
28
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
29
|
+
await Bun.write(taskPath, taskContent)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterEach(async () => cleanupTempDir(root))
|
|
33
|
+
|
|
34
|
+
const createEvidence = () =>
|
|
35
|
+
runTestEffect(
|
|
36
|
+
buildValidationEvidence({
|
|
37
|
+
startPath: root,
|
|
38
|
+
target: "execution-unit:task/example",
|
|
39
|
+
documentPath: taskPath,
|
|
40
|
+
documentRevision: documentRevision(taskContent),
|
|
41
|
+
recalledContext: normalizeRecalledContext({
|
|
42
|
+
id: "example",
|
|
43
|
+
repo: "agency",
|
|
44
|
+
base: "main",
|
|
45
|
+
}),
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
test("reuses evidence only for the same workbase and revision", async () => {
|
|
50
|
+
const evidence = await createEvidence()
|
|
51
|
+
expect(parseValidationEvidence(evidence)).toEqual(evidence)
|
|
52
|
+
const assessment = await runTestEffect(
|
|
53
|
+
assessValidationEvidence({
|
|
54
|
+
evidence,
|
|
55
|
+
startPath: root,
|
|
56
|
+
target: evidence.target,
|
|
57
|
+
documentPath: taskPath,
|
|
58
|
+
documentRevision: evidence.documentRevision,
|
|
59
|
+
}),
|
|
60
|
+
)
|
|
61
|
+
expect(assessment.disposition).toEqual({ status: "reused", reasons: [] })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("treats legacy creation output as a validation refresh", async () => {
|
|
65
|
+
expect(
|
|
66
|
+
await runTestEffect(
|
|
67
|
+
readValidationEvidence(
|
|
68
|
+
JSON.stringify({ version: 1, ok: true, result: { id: "example" } }),
|
|
69
|
+
root,
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("refreshes evidence after document, config, mapping, or payload changes", async () => {
|
|
76
|
+
const evidence = await createEvidence()
|
|
77
|
+
const changedContent = `${taskContent}\nChanged\n`
|
|
78
|
+
await Bun.write(taskPath, changedContent)
|
|
79
|
+
await mkdir(join(root, "repos/other"), { recursive: true })
|
|
80
|
+
await Bun.write(
|
|
81
|
+
join(root, "agency.json"),
|
|
82
|
+
'{"version":2,"repositories":{"other":{"remote":"https://example.com/other.git"}}}\n',
|
|
83
|
+
)
|
|
84
|
+
const assessment = await runTestEffect(
|
|
85
|
+
assessValidationEvidence({
|
|
86
|
+
evidence: { ...evidence, digest: "0".repeat(64) },
|
|
87
|
+
startPath: root,
|
|
88
|
+
target: evidence.target,
|
|
89
|
+
documentPath: taskPath,
|
|
90
|
+
documentRevision: documentRevision(changedContent),
|
|
91
|
+
}),
|
|
92
|
+
)
|
|
93
|
+
expect(assessment.disposition.status).toBe("refreshed")
|
|
94
|
+
expect(assessment.disposition.reasons).toEqual(
|
|
95
|
+
expect.arrayContaining([
|
|
96
|
+
"digest-mismatch",
|
|
97
|
+
"document-revision-changed",
|
|
98
|
+
"workbase-revision-changed",
|
|
99
|
+
"configuration-changed",
|
|
100
|
+
"repository-mapping-changed",
|
|
101
|
+
]),
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("plans retry-safe single-phase and phased launches with one verification", () => {
|
|
106
|
+
const single = buildKickoffPlan({
|
|
107
|
+
workbaseRoot: root,
|
|
108
|
+
target: "execution-unit:task/example",
|
|
109
|
+
taskId: "example",
|
|
110
|
+
taskPath,
|
|
111
|
+
checkoutPath: join(root, "tasks/example/code/agency"),
|
|
112
|
+
documentRevision: "a".repeat(64),
|
|
113
|
+
})
|
|
114
|
+
const phased = buildKickoffPlan({
|
|
115
|
+
workbaseRoot: root,
|
|
116
|
+
target: "execution-unit:phase/example/implementation",
|
|
117
|
+
taskId: "example",
|
|
118
|
+
phaseId: "implementation",
|
|
119
|
+
taskPath,
|
|
120
|
+
phasePath: join(root, "tasks/example/phases/implementation/PHASE.md"),
|
|
121
|
+
documentRevision: "b".repeat(64),
|
|
122
|
+
})
|
|
123
|
+
expect(single.steps[0]?.argv).toContain("example")
|
|
124
|
+
expect(phased.steps[0]?.argv).toEqual(
|
|
125
|
+
expect.arrayContaining(["example", "implementation"]),
|
|
126
|
+
)
|
|
127
|
+
expect(
|
|
128
|
+
single.steps.filter(({ id }) => id === "final-context-verification"),
|
|
129
|
+
).toHaveLength(1)
|
|
130
|
+
expect(single.orchestrator.knownCurrentCommandsBypassDiscovery).toBe(true)
|
|
131
|
+
expect(single.sourceLocations).toEqual(KICKOFF_SOURCE_LOCATIONS)
|
|
132
|
+
expect(
|
|
133
|
+
single.steps.find(({ id }) => id === "herdr-tab")?.recovery,
|
|
134
|
+
).toContain("never create a duplicate")
|
|
135
|
+
expect(single.idempotencyKey).toBe(
|
|
136
|
+
buildKickoffPlan({
|
|
137
|
+
workbaseRoot: root,
|
|
138
|
+
target: "execution-unit:task/example",
|
|
139
|
+
taskId: "example",
|
|
140
|
+
taskPath,
|
|
141
|
+
documentRevision: "a".repeat(64),
|
|
142
|
+
}).idempotencyKey,
|
|
143
|
+
)
|
|
144
|
+
})
|
|
145
|
+
})
|