@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Schema, TreeFormatter } from "@effect/schema"
|
|
2
2
|
import { Data, Effect, Either } from "effect"
|
|
3
3
|
import { join, resolve } from "node:path"
|
|
4
|
-
import { lstat, rename, rm } from "node:fs/promises"
|
|
4
|
+
import { cp, lstat, realpath, rename, rm } from "node:fs/promises"
|
|
5
5
|
import { FileSystemService } from "./FileSystemService"
|
|
6
6
|
import { GraphService } from "./GraphService"
|
|
7
7
|
import { WorkbaseService } from "./WorkbaseService"
|
|
@@ -327,6 +327,27 @@ const replaceWithMoveStep = (
|
|
|
327
327
|
manualRecovery: `Restore ${backup} to ${current}`,
|
|
328
328
|
})
|
|
329
329
|
|
|
330
|
+
const runGit = (
|
|
331
|
+
fs: Effect.Effect.Success<typeof FileSystemService>,
|
|
332
|
+
args: readonly string[],
|
|
333
|
+
label: string,
|
|
334
|
+
) =>
|
|
335
|
+
Effect.runPromise(
|
|
336
|
+
fs
|
|
337
|
+
.runCommand(["git", ...args], { captureOutput: true })
|
|
338
|
+
.pipe(
|
|
339
|
+
Effect.flatMap((result) =>
|
|
340
|
+
result.exitCode === 0
|
|
341
|
+
? Effect.void
|
|
342
|
+
: Effect.fail(
|
|
343
|
+
new Error(
|
|
344
|
+
`${label}: ${result.stderr.trim() || result.stdout.trim()}`,
|
|
345
|
+
),
|
|
346
|
+
),
|
|
347
|
+
),
|
|
348
|
+
) as Effect.Effect<void, unknown, never>,
|
|
349
|
+
)
|
|
350
|
+
|
|
330
351
|
const runTransaction = (
|
|
331
352
|
state: Effect.Effect.Success<ReturnType<typeof configState>>,
|
|
332
353
|
config: WorkbaseConfig,
|
|
@@ -501,6 +522,274 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
501
522
|
return destination
|
|
502
523
|
}),
|
|
503
524
|
|
|
525
|
+
materialize: (alias: string, startPath: string = process.cwd()) =>
|
|
526
|
+
Effect.gen(function* () {
|
|
527
|
+
const fs = yield* FileSystemService
|
|
528
|
+
const versionControl = yield* VersionControlService
|
|
529
|
+
const repository = yield* find(alias, startPath)
|
|
530
|
+
if (repository.kind !== "symlink" || !repository.target) {
|
|
531
|
+
return yield* new RepositoryError({
|
|
532
|
+
message: `Repository alias '${alias}' is not linked`,
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
if (repository.states.includes("invalid")) {
|
|
536
|
+
return yield* new RepositoryError({
|
|
537
|
+
message: `Repository alias '${alias}' has an invalid linked repository`,
|
|
538
|
+
})
|
|
539
|
+
}
|
|
540
|
+
if (repository.states.includes("remote-drifted")) {
|
|
541
|
+
return yield* new RepositoryError({
|
|
542
|
+
message: `Repository alias '${alias}' cannot be materialized while its origin differs from the portable declaration`,
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
if (!repository.declaredRemote) {
|
|
546
|
+
return yield* new RepositoryError({
|
|
547
|
+
message: `Repository alias '${alias}' has no portable remote declaration`,
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const state = yield* configState(startPath)
|
|
552
|
+
const backend = yield* versionControl.forWorkbase(state.root)
|
|
553
|
+
if (backend.kind !== "git") {
|
|
554
|
+
return yield* new RepositoryError({
|
|
555
|
+
message: `Repository alias materialization is only supported for Git workbases`,
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const source = yield* fs.realPath(repository.path)
|
|
560
|
+
const registered = yield* backend.listWorkspaces(repository.path)
|
|
561
|
+
const registeredPaths = registered
|
|
562
|
+
.map((workspace) => workspace.path)
|
|
563
|
+
.sort()
|
|
564
|
+
const worktrees: (typeof registered)[number][] = []
|
|
565
|
+
for (const workspace of registered) {
|
|
566
|
+
if (!(yield* fs.isDirectory(workspace.path))) {
|
|
567
|
+
return yield* new RepositoryError({
|
|
568
|
+
message: `Repository alias '${alias}' has a stale worktree registration: ${workspace.path}`,
|
|
569
|
+
})
|
|
570
|
+
}
|
|
571
|
+
if (
|
|
572
|
+
(yield* fs.inspectFile(join(workspace.path, ".git"))).kind ===
|
|
573
|
+
"file"
|
|
574
|
+
)
|
|
575
|
+
worktrees.push(workspace)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const commonDirectory = yield* fs
|
|
579
|
+
.runCommand(
|
|
580
|
+
[
|
|
581
|
+
"git",
|
|
582
|
+
"-C",
|
|
583
|
+
source,
|
|
584
|
+
"rev-parse",
|
|
585
|
+
"--path-format=absolute",
|
|
586
|
+
"--git-common-dir",
|
|
587
|
+
],
|
|
588
|
+
{ captureOutput: true },
|
|
589
|
+
)
|
|
590
|
+
.pipe(
|
|
591
|
+
Effect.flatMap((result) =>
|
|
592
|
+
result.exitCode === 0
|
|
593
|
+
? Effect.succeed(result.stdout.trim())
|
|
594
|
+
: Effect.fail(
|
|
595
|
+
new RepositoryError({
|
|
596
|
+
message: `Failed to locate Git metadata for repository alias '${alias}'`,
|
|
597
|
+
}),
|
|
598
|
+
),
|
|
599
|
+
),
|
|
600
|
+
)
|
|
601
|
+
const sourceWorktrees = join(commonDirectory, "worktrees")
|
|
602
|
+
const hasWorktreeMetadata = yield* fs.isDirectory(sourceWorktrees)
|
|
603
|
+
if (worktrees.length > 0 && !hasWorktreeMetadata) {
|
|
604
|
+
return yield* new RepositoryError({
|
|
605
|
+
message: `Repository alias '${alias}' is missing Git metadata for its registered worktrees`,
|
|
606
|
+
})
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const suffix = `${process.pid}-${Date.now()}`
|
|
610
|
+
const staging = join(
|
|
611
|
+
state.root,
|
|
612
|
+
"repos",
|
|
613
|
+
`.agency-materialize-${repository.alias}-${suffix}`,
|
|
614
|
+
)
|
|
615
|
+
const aliasBackup = join(
|
|
616
|
+
state.root,
|
|
617
|
+
"repos",
|
|
618
|
+
`.agency-linked-${repository.alias}-${suffix}`,
|
|
619
|
+
)
|
|
620
|
+
const metadataBackup = `${sourceWorktrees}.agency-materialize-${suffix}`
|
|
621
|
+
yield* fs.createDirectory(join(state.root, "repos"))
|
|
622
|
+
yield* backend.cloneRepository(source, staging).pipe(
|
|
623
|
+
Effect.catchAll((cause) =>
|
|
624
|
+
fs.deleteDirectory(staging).pipe(
|
|
625
|
+
Effect.ignore,
|
|
626
|
+
Effect.zipRight(
|
|
627
|
+
Effect.fail(
|
|
628
|
+
new RepositoryError({
|
|
629
|
+
message: `Failed to materialize repository '${alias}': ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
630
|
+
cause,
|
|
631
|
+
}),
|
|
632
|
+
),
|
|
633
|
+
),
|
|
634
|
+
),
|
|
635
|
+
),
|
|
636
|
+
)
|
|
637
|
+
yield* backend
|
|
638
|
+
.setRemoteUrl(staging, "origin", repository.declaredRemote)
|
|
639
|
+
.pipe(
|
|
640
|
+
Effect.catchAll((cause) =>
|
|
641
|
+
fs
|
|
642
|
+
.deleteDirectory(staging)
|
|
643
|
+
.pipe(Effect.ignore, Effect.zipRight(Effect.fail(cause))),
|
|
644
|
+
),
|
|
645
|
+
)
|
|
646
|
+
for (const workspace of worktrees) {
|
|
647
|
+
if (!workspace.commit) continue
|
|
648
|
+
const object = yield* fs.runCommand(
|
|
649
|
+
[
|
|
650
|
+
"git",
|
|
651
|
+
"--git-dir",
|
|
652
|
+
staging,
|
|
653
|
+
"cat-file",
|
|
654
|
+
"-e",
|
|
655
|
+
`${workspace.commit}^{commit}`,
|
|
656
|
+
],
|
|
657
|
+
{ captureOutput: true },
|
|
658
|
+
)
|
|
659
|
+
if (object.exitCode !== 0) {
|
|
660
|
+
yield* fs.deleteDirectory(staging).pipe(Effect.ignore)
|
|
661
|
+
return yield* new RepositoryError({
|
|
662
|
+
message: `Registered worktree commit '${workspace.commit}' is missing from the materialized repository`,
|
|
663
|
+
})
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
let metadataMoved = false
|
|
668
|
+
let aliasMoved = false
|
|
669
|
+
let cloneInstalled = false
|
|
670
|
+
const workspacePaths = worktrees.map((workspace) => workspace.path)
|
|
671
|
+
const repair = (gitDirectory: string) =>
|
|
672
|
+
workspacePaths.length === 0
|
|
673
|
+
? Promise.resolve()
|
|
674
|
+
: runGit(
|
|
675
|
+
fs,
|
|
676
|
+
[
|
|
677
|
+
"--git-dir",
|
|
678
|
+
gitDirectory,
|
|
679
|
+
"worktree",
|
|
680
|
+
"repair",
|
|
681
|
+
...workspacePaths,
|
|
682
|
+
],
|
|
683
|
+
"Failed to repair Git worktrees",
|
|
684
|
+
)
|
|
685
|
+
const rollbackMigration = async () => {
|
|
686
|
+
const errors: unknown[] = []
|
|
687
|
+
if (metadataMoved) {
|
|
688
|
+
try {
|
|
689
|
+
await rename(metadataBackup, sourceWorktrees)
|
|
690
|
+
metadataMoved = false
|
|
691
|
+
await repair(commonDirectory)
|
|
692
|
+
} catch (error) {
|
|
693
|
+
errors.push(error)
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (cloneInstalled) {
|
|
697
|
+
try {
|
|
698
|
+
await rename(repository.path, staging)
|
|
699
|
+
cloneInstalled = false
|
|
700
|
+
} catch (error) {
|
|
701
|
+
errors.push(error)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (aliasMoved) {
|
|
705
|
+
try {
|
|
706
|
+
await rename(aliasBackup, repository.path)
|
|
707
|
+
aliasMoved = false
|
|
708
|
+
} catch (error) {
|
|
709
|
+
errors.push(error)
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (errors.length > 0) throw new AggregateError(errors)
|
|
713
|
+
}
|
|
714
|
+
const migration: TransactionStep = {
|
|
715
|
+
label: `materialize linked repository ${repository.alias}`,
|
|
716
|
+
preflight: async () => {
|
|
717
|
+
const stats = await lstat(repository.path)
|
|
718
|
+
if (
|
|
719
|
+
!stats.isSymbolicLink() ||
|
|
720
|
+
(await realpath(repository.path)) !== source
|
|
721
|
+
)
|
|
722
|
+
throw new Error(
|
|
723
|
+
`Repository alias '${repository.alias}' changed during materialization`,
|
|
724
|
+
)
|
|
725
|
+
const current = await Effect.runPromise(
|
|
726
|
+
backend
|
|
727
|
+
.listWorkspaces(repository.path)
|
|
728
|
+
.pipe(
|
|
729
|
+
Effect.provideService(FileSystemService, fs),
|
|
730
|
+
) as unknown as Effect.Effect<
|
|
731
|
+
readonly { readonly path: string }[],
|
|
732
|
+
unknown,
|
|
733
|
+
never
|
|
734
|
+
>,
|
|
735
|
+
)
|
|
736
|
+
const currentPaths = current
|
|
737
|
+
.map((workspace) => workspace.path)
|
|
738
|
+
.sort()
|
|
739
|
+
if (
|
|
740
|
+
JSON.stringify(currentPaths) !== JSON.stringify(registeredPaths)
|
|
741
|
+
)
|
|
742
|
+
throw new Error(
|
|
743
|
+
`Git worktree registrations changed during materialization`,
|
|
744
|
+
)
|
|
745
|
+
},
|
|
746
|
+
apply: async () => {
|
|
747
|
+
try {
|
|
748
|
+
if (hasWorktreeMetadata) {
|
|
749
|
+
await rename(sourceWorktrees, metadataBackup)
|
|
750
|
+
metadataMoved = true
|
|
751
|
+
await cp(metadataBackup, join(staging, "worktrees"), {
|
|
752
|
+
recursive: true,
|
|
753
|
+
})
|
|
754
|
+
}
|
|
755
|
+
await rename(repository.path, aliasBackup)
|
|
756
|
+
aliasMoved = true
|
|
757
|
+
await rename(staging, repository.path)
|
|
758
|
+
cloneInstalled = true
|
|
759
|
+
await repair(repository.path)
|
|
760
|
+
} catch (cause) {
|
|
761
|
+
try {
|
|
762
|
+
await rollbackMigration()
|
|
763
|
+
} catch (rollbackCause) {
|
|
764
|
+
throw new Error(
|
|
765
|
+
`Repository materialization failed and rollback requires manual recovery: restore ${aliasBackup} to ${repository.path} and ${metadataBackup} to ${sourceWorktrees}`,
|
|
766
|
+
{ cause: new AggregateError([cause, rollbackCause]) },
|
|
767
|
+
)
|
|
768
|
+
}
|
|
769
|
+
throw cause
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
rollback: rollbackMigration,
|
|
773
|
+
finalize: async () => {
|
|
774
|
+
await rm(aliasBackup, { recursive: true, force: true })
|
|
775
|
+
await rm(metadataBackup, { recursive: true, force: true })
|
|
776
|
+
},
|
|
777
|
+
manualRecovery: `Restore ${aliasBackup} to ${repository.path} and ${metadataBackup} to ${sourceWorktrees}`,
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
yield* runLifecycleTransaction({
|
|
781
|
+
root: state.root,
|
|
782
|
+
preconditions: [{ path: state.path, revision: state.revision }],
|
|
783
|
+
steps: [migration],
|
|
784
|
+
}).pipe(
|
|
785
|
+
Effect.mapError(
|
|
786
|
+
(cause) => new RepositoryError({ message: cause.message, cause }),
|
|
787
|
+
),
|
|
788
|
+
Effect.ensuring(fs.deleteDirectory(staging).pipe(Effect.ignore)),
|
|
789
|
+
)
|
|
790
|
+
return yield* find(alias, startPath)
|
|
791
|
+
}),
|
|
792
|
+
|
|
504
793
|
list: (startPath: string = process.cwd()) =>
|
|
505
794
|
Effect.gen(function* () {
|
|
506
795
|
const fs = yield* FileSystemService
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -4,6 +4,76 @@ This directory is an Agency workbase. Epics, tasks, and phases are durable
|
|
|
4
4
|
Markdown documents; repository aliases and generated Git worktrees or jj
|
|
5
5
|
workspaces provide code access according to the workbase's `vcs` setting.
|
|
6
6
|
|
|
7
|
+
## Command Fast Paths
|
|
8
|
+
|
|
9
|
+
When a request clearly matches one of these intents, use the exact recipe without
|
|
10
|
+
probing CLI help or listing unrelated workbase state. Substitute known values,
|
|
11
|
+
retain `--if-revision` guards when shown, and do not add flags that are not shown.
|
|
12
|
+
|
|
13
|
+
1. Create a single-phase task only:
|
|
14
|
+
`agency task create <slug> --repo <alias> --base <base> --description <text> --json`.
|
|
15
|
+
Add `--authoritative-source <absolute-path-or-url>` only for already known
|
|
16
|
+
sources. Return the creation result and stop.
|
|
17
|
+
2. Create, materialize, and start a single-phase task: run the create-only command,
|
|
18
|
+
then
|
|
19
|
+
`agency work prepare <slug> --evidence <creation-json-or-path> --json`.
|
|
20
|
+
Return the applied execution contract so the caller can run `commands.work`.
|
|
21
|
+
3. Materialize an existing execution unit without starting it:
|
|
22
|
+
`agency work prepare <task-or-document> --json`. Return the applied execution
|
|
23
|
+
contract and stop. Add `--dry-run` only when the user asks for a preview.
|
|
24
|
+
4. Reconcile remote pull-request state and completion:
|
|
25
|
+
`agency sync <task> [phase] --json`.
|
|
26
|
+
5. Convert an existing single-phase task and add a phase:
|
|
27
|
+
`agency phase create <task> <new-phase> --first-phase <existing-phase> --repo <alias> --branch <branch> --base <base> [--depends-on <existing-phase>] --json`.
|
|
28
|
+
6. Archive terminal work: first run `agency archive task <task> --dry-run --json`,
|
|
29
|
+
`agency archive phase <task> <phase> --dry-run --json`, or
|
|
30
|
+
`agency archive epic <epic> --dry-run --json`; if the preflight is safe,
|
|
31
|
+
repeat the same command without `--dry-run`.
|
|
32
|
+
7. Create and start review work: run either
|
|
33
|
+
`agency task create <slug> --review <alias> --pull-request <url-or-number> --json`
|
|
34
|
+
or `agency task create <slug> --review <alias> --ref <remote-ref> --json`, then
|
|
35
|
+
run `agency work prepare <slug> --evidence <creation-json-or-path> --json` and
|
|
36
|
+
return the applied execution contract so the caller can run `commands.work`.
|
|
37
|
+
8. Inspect one item with `agency context <task-or-document> --json`; inspect the
|
|
38
|
+
whole workbase with `agency status --json`.
|
|
39
|
+
9. Drop work with the current document revision: use
|
|
40
|
+
`agency task status <task> dropped --if-revision <revision> --json` or
|
|
41
|
+
`agency phase status <task> <phase> dropped --if-revision <revision> --json`.
|
|
42
|
+
10. Continue already materialized work: run
|
|
43
|
+
`agency work prepare <task-or-document> --json` and return the applied
|
|
44
|
+
execution contract so the caller can run `commands.work`.
|
|
45
|
+
11. Publish without a pull request from the execution checkout with
|
|
46
|
+
`agency push --json`. Create and record a pull request with
|
|
47
|
+
`agency pr create <task> [phase] [--draft] [--title <title>] [--label <label>] --json`;
|
|
48
|
+
do not run a separate push first because `pr create` owns publication.
|
|
49
|
+
12. Complete genuine non-PR work. For an active claim, run
|
|
50
|
+
`agency finish <task> [phase] --session-id <id> --revision <revision> --outcome done --no-pull-request --summary <text> [--evidence-url <url>]`.
|
|
51
|
+
Without a claim, run
|
|
52
|
+
`agency task status <task> done --if-revision <revision> --no-pull-request --summary <text> [--evidence-url <url>] --json`
|
|
53
|
+
or
|
|
54
|
+
`agency phase status <task> <phase> done --if-revision <revision> --no-pull-request --summary <text> [--evidence-url <url>] --json`.
|
|
55
|
+
13. Create a multi-phase task initially with
|
|
56
|
+
`agency task create <slug> --multi-phase --description <text> --json`, then
|
|
57
|
+
create each execution phase with
|
|
58
|
+
`agency phase create <slug> <phase> --repo <alias> --branch <branch> --base <base> [--depends-on <phase>] --json`.
|
|
59
|
+
14. Hand off an investigation to distinct implementation work with
|
|
60
|
+
`agency task handoff <investigation-task> <new-task> [--source-phase <phase>] --repo <alias> --base <base> --json`, then verify the returned destination with
|
|
61
|
+
`agency context <new-task> --json`. Do not prepare or start it unless requested.
|
|
62
|
+
15. Refresh a pinned review task with the current revision:
|
|
63
|
+
`agency review refresh <task> --if-revision <revision> --json`.
|
|
64
|
+
|
|
65
|
+
Never pass `--work` or `--auto` to `agency task create`. Do not run separate
|
|
66
|
+
`agency validate`, `agency worktree prepare`, `agency graph`, `agency task list`,
|
|
67
|
+
or `agency repo list` commands before these recipes when the required parameters
|
|
68
|
+
are already known. `agency work prepare` owns validation, readiness checks,
|
|
69
|
+
workspace materialization, and the versioned `agency-execution-v1` contract.
|
|
70
|
+
|
|
71
|
+
These fast paths take precedence over separately installed Agency skill guidance.
|
|
72
|
+
Use `agency <command> --help` only as a recovery step when no recipe matches or a
|
|
73
|
+
prescribed command rejects known-current syntax. The caller owns how prepared
|
|
74
|
+
execution is presented and started; Agency returns domain facts and native
|
|
75
|
+
commands without prescribing an execution environment.
|
|
76
|
+
|
|
7
77
|
## Bootstrap
|
|
8
78
|
|
|
9
79
|
Start every session with one read-only command:
|
|
@@ -18,7 +88,8 @@ from directory names or stale prose.
|
|
|
18
88
|
|
|
19
89
|
At the workbase root, use `agency next --json` or `agency graph --json` to choose
|
|
20
90
|
work, then inspect the returned document path or explicit entity selectors. Use
|
|
21
|
-
|
|
91
|
+
the command fast path above whenever the user's intent already identifies the
|
|
92
|
+
operation and required parameters.
|
|
22
93
|
|
|
23
94
|
## Adding a Repository
|
|
24
95
|
|
|
@@ -106,41 +177,6 @@ revision stale, and Agency must not silently rewrite that evidence.
|
|
|
106
177
|
|
|
107
178
|
## Execution
|
|
108
179
|
|
|
109
|
-
### Canonical create and kickoff
|
|
110
|
-
|
|
111
|
-
This Agency recipe takes precedence over generic Herdr defaults whenever the
|
|
112
|
-
request creates, opens, works, launches, starts, or kicks off an Agency item.
|
|
113
|
-
Do not rediscover commands that this recipe or a known-current
|
|
114
|
-
`agency-kickoff-v1` plan supplies.
|
|
115
|
-
|
|
116
|
-
1. Create noninteractively with explicit recalled context when available:
|
|
117
|
-
`agency task create <slug> --context-repo <alias> --context-base <base> --context-slug <slug> --authoritative-source <absolute-path-or-url> --json`.
|
|
118
|
-
Repeat `--authoritative-source` as needed. Supplied context must agree with
|
|
119
|
-
explicit task flags; Agency rejects stale or conflicting values.
|
|
120
|
-
2. For create-only intent, return the creation result and stop. For open intent,
|
|
121
|
-
prepare the task with
|
|
122
|
-
`agency work prepare <slug> --evidence <creation-json> --dry-run --json`, then
|
|
123
|
-
execute the returned plan through `task-document-split` to prepare the
|
|
124
|
-
checkout and open or reuse the background tab. Stop before `agent-start`.
|
|
125
|
-
3. For work/launch/start/kickoff intent, run that same preflight and execute its
|
|
126
|
-
ordered kickoff steps. The plan owns worktree dry-run/preparation, a
|
|
127
|
-
retry-safe background Herdr tab, the side-by-side task document,
|
|
128
|
-
`agency work . --auto`, and exactly one final
|
|
129
|
-
`agency context <document-path> --json` verification.
|
|
130
|
-
4. When the orchestrator has known-current support for the plan's
|
|
131
|
-
`agency-kickoff-v1` capability, execute the supplied actions directly. Do not
|
|
132
|
-
call Herdr help, skill, or CLI discovery. If capability/version evidence is
|
|
133
|
-
absent or stale, discovery is the compatibility path; then resume the same
|
|
134
|
-
idempotency key rather than creating another tab, checkout, or agent.
|
|
135
|
-
5. After the one final context verification succeeds, leave the agent in the
|
|
136
|
-
background and stop. Do not inspect, poll, or babysit it unless the user asks.
|
|
137
|
-
|
|
138
|
-
Validation evidence is a local, auditable optimization, not authority. Preflight
|
|
139
|
-
refreshes it after workbase, target document, configuration, repository mapping,
|
|
140
|
-
payload digest, or kickoff-contract changes. Readiness, claims, repository
|
|
141
|
-
materialization, branch ownership, reference drift, and dirty-workspace checks
|
|
142
|
-
still run on every preparation.
|
|
143
|
-
|
|
144
180
|
For implementation work, read the task and phase prose returned by context,
|
|
145
181
|
change only the writable checkout, keep durable decisions current, and run the
|
|
146
182
|
repository's formatting, type checks, build, dead-code checks, and focused tests.
|
|
@@ -171,8 +207,8 @@ environment variables. If the variables and prompt marker are absent, fail safe
|
|
|
171
207
|
when the initial instruction is a generated `Start`, `Continue`, or `Work on`
|
|
172
208
|
prompt whose absolute document paths match the current directory and the active,
|
|
173
209
|
valid `agency context`: treat the process as the current worker and do not
|
|
174
|
-
recursively launch.
|
|
175
|
-
and context disagree, stop and ask the user rather than launching.
|
|
210
|
+
recursively launch. External session state is never part of worker identity. If
|
|
211
|
+
the prompt and context disagree, stop and ask the user rather than launching.
|
|
176
212
|
|
|
177
213
|
For OpenCode, Agency's managed plugin validates the generated marker against
|
|
178
214
|
`agency context`, binds that identity to the OpenCode session, injects an
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { mkdir } from "node:fs/promises"
|
|
3
|
-
import { join } from "node:path"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
4
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
5
5
|
import { documentRevision } from "./document-revision"
|
|
6
6
|
import {
|
|
7
7
|
assessValidationEvidence,
|
|
8
|
-
|
|
8
|
+
buildExecutionContract,
|
|
9
9
|
buildValidationEvidence,
|
|
10
|
-
|
|
10
|
+
EXECUTION_SOURCE_LOCATIONS,
|
|
11
11
|
normalizeRecalledContext,
|
|
12
12
|
parseValidationEvidence,
|
|
13
13
|
readValidationEvidence,
|
|
14
|
-
} from "./
|
|
14
|
+
} from "./execution-contract"
|
|
15
15
|
|
|
16
|
-
describe("
|
|
16
|
+
describe("execution contract", () => {
|
|
17
17
|
let root: string
|
|
18
18
|
let taskPath: string
|
|
19
19
|
let taskContent: string
|
|
@@ -102,44 +102,60 @@ describe("kickoff contract", () => {
|
|
|
102
102
|
)
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
-
test("
|
|
106
|
-
const
|
|
105
|
+
test("describes prepared execution without prescribing orchestration", () => {
|
|
106
|
+
const checkoutPath = join(root, "tasks/example/code/agency")
|
|
107
|
+
const applied = buildExecutionContract({
|
|
107
108
|
workbaseRoot: root,
|
|
108
109
|
target: "execution-unit:task/example",
|
|
109
|
-
taskId: "example",
|
|
110
110
|
taskPath,
|
|
111
|
-
checkoutPath
|
|
111
|
+
checkoutPath,
|
|
112
112
|
documentRevision: "a".repeat(64),
|
|
113
|
+
dryRun: false,
|
|
113
114
|
})
|
|
114
|
-
const
|
|
115
|
+
const phasePath = join(root, "tasks/example/phases/implementation/PHASE.md")
|
|
116
|
+
const preview = buildExecutionContract({
|
|
115
117
|
workbaseRoot: root,
|
|
116
118
|
target: "execution-unit:phase/example/implementation",
|
|
117
|
-
taskId: "example",
|
|
118
|
-
phaseId: "implementation",
|
|
119
119
|
taskPath,
|
|
120
|
-
phasePath
|
|
120
|
+
phasePath,
|
|
121
|
+
checkoutPath: join(
|
|
122
|
+
root,
|
|
123
|
+
"tasks/example/phases/implementation/code/agency",
|
|
124
|
+
),
|
|
121
125
|
documentRevision: "b".repeat(64),
|
|
126
|
+
dryRun: true,
|
|
122
127
|
})
|
|
123
|
-
expect(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
expect(applied).toMatchObject({
|
|
129
|
+
capability: "agency-execution-v1",
|
|
130
|
+
mode: "applied",
|
|
131
|
+
workspace: {
|
|
132
|
+
state: "materialized",
|
|
133
|
+
checkoutPath,
|
|
134
|
+
},
|
|
135
|
+
commands: {
|
|
136
|
+
work: {
|
|
137
|
+
cwd: dirname(taskPath),
|
|
138
|
+
argv: ["agency", "work", ".", "--auto"],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
expect(applied.sourceLocations).toEqual(EXECUTION_SOURCE_LOCATIONS)
|
|
143
|
+
expect(preview).toMatchObject({
|
|
144
|
+
mode: "preview",
|
|
145
|
+
workspace: { state: "planned" },
|
|
146
|
+
plannedActions: [{ kind: "workspace-materialization" }],
|
|
147
|
+
commands: {
|
|
148
|
+
context: { cwd: dirname(phasePath) },
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
expect(applied.executionIdentity.key).toBe(
|
|
152
|
+
buildExecutionContract({
|
|
137
153
|
workbaseRoot: root,
|
|
138
154
|
target: "execution-unit:task/example",
|
|
139
|
-
taskId: "example",
|
|
140
155
|
taskPath,
|
|
141
156
|
documentRevision: "a".repeat(64),
|
|
142
|
-
|
|
157
|
+
dryRun: true,
|
|
158
|
+
}).executionIdentity.key,
|
|
143
159
|
)
|
|
144
160
|
})
|
|
145
161
|
})
|