@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/README.md
CHANGED
|
@@ -684,6 +684,32 @@ The branch defaults to `task/<id>` and the base defaults to `main`.
|
|
|
684
684
|
`task create` is always noninteractive and requires `--repo` for a single-phase
|
|
685
685
|
task. Use it instead of `task new` in scripts and agent workflows.
|
|
686
686
|
|
|
687
|
+
For deterministic callers, creation also accepts recalled context:
|
|
688
|
+
|
|
689
|
+
```text
|
|
690
|
+
agency task create <id>
|
|
691
|
+
--context-repo <alias> --context-base <base> --context-slug <id>
|
|
692
|
+
[--authoritative-source <absolute-path-or-http-url>...] --json
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Recalled values are used instead of rediscovery but must agree with equivalent
|
|
696
|
+
explicit flags. The preferred slug must equal the created ID, repository aliases
|
|
697
|
+
are validated by normal task creation, and authoritative sources must be absolute
|
|
698
|
+
paths or HTTP(S) URLs.
|
|
699
|
+
|
|
700
|
+
Machine output adds fields without changing the version 1 protocol envelope. It
|
|
701
|
+
includes `selector`, absolute `documentPath`, the document `revision`, full
|
|
702
|
+
`validation`, normalized `recalledContext`, and `evidence`. Evidence version 1 is
|
|
703
|
+
an auditable local payload containing the canonical workbase root, target and
|
|
704
|
+
document identities, aggregate workbase revision, configuration revision,
|
|
705
|
+
repository-mapping revision, kickoff-contract version, validity result, recalled
|
|
706
|
+
context, and a digest over those fields. It is not a signature or an authority
|
|
707
|
+
grant. A different workbase, target, document revision, document set,
|
|
708
|
+
configuration, repository mapping, contract version, or payload digest
|
|
709
|
+
invalidates reuse. Older creation output without evidence remains compatible;
|
|
710
|
+
preflight simply validates again. The published machine schema is
|
|
711
|
+
`schemas/agency-kickoff-v1.schema.json`.
|
|
712
|
+
|
|
687
713
|
Create a multi-phase task container:
|
|
688
714
|
|
|
689
715
|
```text
|
|
@@ -892,7 +918,7 @@ until restored.
|
|
|
892
918
|
|
|
893
919
|
```text
|
|
894
920
|
agency work [<directory> | --epic <epic-id>] [--runner <name>] [--auto] [--print-command]
|
|
895
|
-
agency work prepare [target] [--dry-run] [--json]
|
|
921
|
+
agency work prepare [target] [--evidence <json-or-path>] [--dry-run] [--json]
|
|
896
922
|
agency worktree <list|inspect|prepare|remove|rebuild|repair>
|
|
897
923
|
agency push [--json]
|
|
898
924
|
agency pr create <task-id> [phase-id] [--draft] [--force] [--json]
|
|
@@ -912,9 +938,25 @@ their built-in presets. Launches are interactive and promptless by default; use
|
|
|
912
938
|
`agency work prepare` resolves an execution unit and creates or reuses its
|
|
913
939
|
writable and reference worktrees, or its single pinned review checkout, without
|
|
914
940
|
launching an agent or changing status.
|
|
915
|
-
Its JSON result includes
|
|
916
|
-
|
|
917
|
-
|
|
941
|
+
Its JSON result includes the workspace, validation result, whether supplied
|
|
942
|
+
evidence was `reused` or `refreshed` with stable reason strings, refreshed
|
|
943
|
+
evidence, and a versioned `agency-kickoff-v1` orchestration plan. The plan has a
|
|
944
|
+
deterministic idempotency key and ordered, retry-safe actions for worktree
|
|
945
|
+
preflight/preparation, a background tab, side-by-side task document,
|
|
946
|
+
`agency work . --auto`, and exactly one final `agency context <document> --json`.
|
|
947
|
+
The evidence argument may be an evidence object, task-creation JSON, or a path to
|
|
948
|
+
either. Use `--dry-run` to report planned fetch, branch, and worktree changes
|
|
949
|
+
without applying them. Validation reuse never skips readiness, active-claim,
|
|
950
|
+
repository, ownership, reference-drift, dirty-workspace, or worktree safety
|
|
951
|
+
checks.
|
|
952
|
+
|
|
953
|
+
The authoritative implementation locations for this contract are
|
|
954
|
+
`src/commands/task.ts` (creation output),
|
|
955
|
+
`src/workbase/kickoff-contract.ts` (evidence and orchestration schemas),
|
|
956
|
+
`src/commands/work.ts` (launch preflight),
|
|
957
|
+
`src/services/WorktreeService.ts` (workspace safety), and
|
|
958
|
+
`src/workbase/AGENTS.md` (generated OpenCode guidance). These paths are the
|
|
959
|
+
deterministic source-location fixture for compatible orchestrators.
|
|
918
960
|
|
|
919
961
|
`agency worktree list` and `inspect` report each declared checkout's expected and
|
|
920
962
|
registered path, branch, commit, Agency owner, dirtiness, and conflicts. `prepare`
|
package/cli-main.ts
CHANGED
|
@@ -494,6 +494,10 @@ const commands: Record<string, Command> = {
|
|
|
494
494
|
references: options.reference,
|
|
495
495
|
branch: options.branch,
|
|
496
496
|
base: options.base,
|
|
497
|
+
contextRepo: options["context-repo"],
|
|
498
|
+
contextBase: options["context-base"],
|
|
499
|
+
contextSlug: options["context-slug"],
|
|
500
|
+
authoritativeSources: options["authoritative-source"],
|
|
497
501
|
clearReferences: options["clear-references"],
|
|
498
502
|
prUrl: options["pr-url"],
|
|
499
503
|
clearPr: options["clear-pr"],
|
|
@@ -560,6 +564,7 @@ const commands: Record<string, Command> = {
|
|
|
560
564
|
cwd: options.cwd,
|
|
561
565
|
taskId: options.task,
|
|
562
566
|
phaseId: options.phase,
|
|
567
|
+
evidence: options.evidence,
|
|
563
568
|
}),
|
|
564
569
|
)
|
|
565
570
|
},
|
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.60.0",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
"./schemas/agency-graph-v1.json": {
|
|
45
45
|
"default": "./schemas/agency-graph-v1.schema.json"
|
|
46
46
|
},
|
|
47
|
+
"./schemas/agency-kickoff-v1.json": {
|
|
48
|
+
"default": "./schemas/agency-kickoff-v1.schema.json"
|
|
49
|
+
},
|
|
47
50
|
"./fixtures/protocol/success.json": {
|
|
48
51
|
"default": "./fixtures/protocol/success.json"
|
|
49
52
|
},
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/markjaquith/agency/schemas/agency-kickoff-v1.schema.json",
|
|
4
|
+
"title": "Agency kickoff contract v1",
|
|
5
|
+
"oneOf": [
|
|
6
|
+
{ "$ref": "#/$defs/validationEvidence" },
|
|
7
|
+
{ "$ref": "#/$defs/kickoffPlan" }
|
|
8
|
+
],
|
|
9
|
+
"$defs": {
|
|
10
|
+
"recalledContext": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["preferredSlug", "authoritativeSources"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"repo": { "type": "string", "minLength": 1 },
|
|
15
|
+
"base": { "type": "string", "minLength": 1 },
|
|
16
|
+
"preferredSlug": { "type": "string", "minLength": 1 },
|
|
17
|
+
"authoritativeSources": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": { "type": "string", "minLength": 1 }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"additionalProperties": false
|
|
23
|
+
},
|
|
24
|
+
"validationEvidence": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"required": [
|
|
27
|
+
"version",
|
|
28
|
+
"workbaseRoot",
|
|
29
|
+
"target",
|
|
30
|
+
"documentPath",
|
|
31
|
+
"documentRevision",
|
|
32
|
+
"workbaseRevision",
|
|
33
|
+
"configRevision",
|
|
34
|
+
"repositoryMappingRevision",
|
|
35
|
+
"valid",
|
|
36
|
+
"recalledContext",
|
|
37
|
+
"digest"
|
|
38
|
+
],
|
|
39
|
+
"properties": {
|
|
40
|
+
"version": { "const": 1 },
|
|
41
|
+
"workbaseRoot": { "type": "string" },
|
|
42
|
+
"target": { "type": "string" },
|
|
43
|
+
"documentPath": { "type": "string" },
|
|
44
|
+
"documentRevision": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
45
|
+
"workbaseRevision": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
46
|
+
"configRevision": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
47
|
+
"repositoryMappingRevision": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
50
|
+
},
|
|
51
|
+
"valid": { "const": true },
|
|
52
|
+
"recalledContext": { "$ref": "#/$defs/recalledContext" },
|
|
53
|
+
"digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
54
|
+
},
|
|
55
|
+
"additionalProperties": false
|
|
56
|
+
},
|
|
57
|
+
"kickoffPlan": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"required": [
|
|
60
|
+
"version",
|
|
61
|
+
"idempotencyKey",
|
|
62
|
+
"workbaseRoot",
|
|
63
|
+
"target",
|
|
64
|
+
"documentRevision",
|
|
65
|
+
"sourceLocations",
|
|
66
|
+
"taskDirectory",
|
|
67
|
+
"taskDocument",
|
|
68
|
+
"phaseDocument",
|
|
69
|
+
"preparedCheckout",
|
|
70
|
+
"orchestrator",
|
|
71
|
+
"steps",
|
|
72
|
+
"successFields"
|
|
73
|
+
],
|
|
74
|
+
"properties": {
|
|
75
|
+
"version": { "const": 1 },
|
|
76
|
+
"idempotencyKey": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
77
|
+
"workbaseRoot": { "type": "string" },
|
|
78
|
+
"target": { "type": "string" },
|
|
79
|
+
"documentRevision": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
80
|
+
"sourceLocations": { "type": "array", "items": { "type": "string" } },
|
|
81
|
+
"taskDirectory": { "type": "string" },
|
|
82
|
+
"taskDocument": { "type": "string" },
|
|
83
|
+
"phaseDocument": { "type": ["string", "null"] },
|
|
84
|
+
"preparedCheckout": { "type": ["string", "null"] },
|
|
85
|
+
"orchestrator": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"required": [
|
|
88
|
+
"capability",
|
|
89
|
+
"knownCurrentCommandsBypassDiscovery",
|
|
90
|
+
"fallback"
|
|
91
|
+
],
|
|
92
|
+
"properties": {
|
|
93
|
+
"capability": { "const": "agency-kickoff-v1" },
|
|
94
|
+
"knownCurrentCommandsBypassDiscovery": { "const": true },
|
|
95
|
+
"fallback": { "type": "string" }
|
|
96
|
+
},
|
|
97
|
+
"additionalProperties": false
|
|
98
|
+
},
|
|
99
|
+
"steps": {
|
|
100
|
+
"type": "array",
|
|
101
|
+
"minItems": 6,
|
|
102
|
+
"items": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"required": ["id"],
|
|
105
|
+
"properties": {
|
|
106
|
+
"id": { "type": "string" },
|
|
107
|
+
"argv": { "type": "array", "items": { "type": "string" } },
|
|
108
|
+
"cwd": { "type": "string" },
|
|
109
|
+
"action": { "type": "string" },
|
|
110
|
+
"path": { "type": "string" },
|
|
111
|
+
"retry": { "type": "string" },
|
|
112
|
+
"recovery": { "type": "string" },
|
|
113
|
+
"idempotencyKey": { "type": "string" },
|
|
114
|
+
"exactlyOnce": { "type": "boolean" }
|
|
115
|
+
},
|
|
116
|
+
"additionalProperties": false
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"successFields": { "type": "array", "items": { "type": "string" } }
|
|
120
|
+
},
|
|
121
|
+
"additionalProperties": false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/cli-parser.test.ts
CHANGED
|
@@ -809,7 +809,7 @@ describe("strict CLI parsing", () => {
|
|
|
809
809
|
|
|
810
810
|
test("rejects required-option omissions and explicit conflicts", () => {
|
|
811
811
|
expect(() => parseCli(["task", "create", "example"])).toThrow(
|
|
812
|
-
"--repo' is required",
|
|
812
|
+
"--repo' or '--context-repo' is required",
|
|
813
813
|
)
|
|
814
814
|
for (const option of ["repo", "reference", "branch", "base"] as const) {
|
|
815
815
|
const value = option === "reference" ? "other:main" : "value"
|
package/src/cli-parser.ts
CHANGED
|
@@ -85,6 +85,10 @@ const taskCreateOptions = {
|
|
|
85
85
|
review: { type: "string" },
|
|
86
86
|
"pull-request": { type: "string" },
|
|
87
87
|
ref: { type: "string" },
|
|
88
|
+
"context-repo": { type: "string" },
|
|
89
|
+
"context-base": { type: "string" },
|
|
90
|
+
"context-slug": { type: "string" },
|
|
91
|
+
"authoritative-source": { type: "string", multiple: true },
|
|
88
92
|
} satisfies OptionConfig
|
|
89
93
|
|
|
90
94
|
const phaseCreateOptions = {
|
|
@@ -396,7 +400,7 @@ const commands = {
|
|
|
396
400
|
},
|
|
397
401
|
create: {
|
|
398
402
|
usage:
|
|
399
|
-
"agency task create <id> (--repo <alias> | --multi-phase | --review <alias> (--pull-request <value> | --ref <remote-ref>)) [options]",
|
|
403
|
+
"agency task create <id> (--repo <alias> | --context-repo <alias> | --multi-phase | --review <alias> (--pull-request <value> | --ref <remote-ref>)) [options]",
|
|
400
404
|
minArgs: 1,
|
|
401
405
|
maxArgs: 1,
|
|
402
406
|
options: [
|
|
@@ -411,9 +415,13 @@ const commands = {
|
|
|
411
415
|
"review",
|
|
412
416
|
"pull-request",
|
|
413
417
|
"ref",
|
|
418
|
+
"context-repo",
|
|
419
|
+
"context-base",
|
|
420
|
+
"context-slug",
|
|
421
|
+
"authoritative-source",
|
|
414
422
|
"json",
|
|
415
423
|
],
|
|
416
|
-
repeatable: ["reference"],
|
|
424
|
+
repeatable: ["reference", "authoritative-source"],
|
|
417
425
|
},
|
|
418
426
|
list: {
|
|
419
427
|
usage: "agency task list [filters] [--json]",
|
|
@@ -873,6 +881,7 @@ const commands = {
|
|
|
873
881
|
opencode: { type: "boolean" },
|
|
874
882
|
claude: { type: "boolean" },
|
|
875
883
|
force: { type: "boolean" },
|
|
884
|
+
evidence: { type: "string" },
|
|
876
885
|
},
|
|
877
886
|
command: {
|
|
878
887
|
usage:
|
|
@@ -891,6 +900,7 @@ const commands = {
|
|
|
891
900
|
"opencode",
|
|
892
901
|
"claude",
|
|
893
902
|
"force",
|
|
903
|
+
"evidence",
|
|
894
904
|
],
|
|
895
905
|
conflicts: [
|
|
896
906
|
["opencode", "claude"],
|
|
@@ -1262,9 +1272,13 @@ function validateTaskCreate(
|
|
|
1262
1272
|
)
|
|
1263
1273
|
}
|
|
1264
1274
|
}
|
|
1265
|
-
} else if (
|
|
1275
|
+
} else if (
|
|
1276
|
+
requireRepo &&
|
|
1277
|
+
values.repo === undefined &&
|
|
1278
|
+
values["context-repo"] === undefined
|
|
1279
|
+
) {
|
|
1266
1280
|
throw usageError(
|
|
1267
|
-
"Option '--repo' is required unless '--multi-phase' is used.",
|
|
1281
|
+
"Option '--repo' or '--context-repo' is required unless '--multi-phase' is used.",
|
|
1268
1282
|
spec.usage,
|
|
1269
1283
|
)
|
|
1270
1284
|
}
|
|
@@ -1620,9 +1634,12 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1620
1634
|
spec.usage,
|
|
1621
1635
|
)
|
|
1622
1636
|
}
|
|
1623
|
-
if (
|
|
1637
|
+
if (
|
|
1638
|
+
!preparing &&
|
|
1639
|
+
(parsed.values.json || parsed.values["dry-run"] || parsed.values.evidence)
|
|
1640
|
+
) {
|
|
1624
1641
|
throw usageError(
|
|
1625
|
-
"Options '--json'
|
|
1642
|
+
"Options '--json', '--dry-run', and '--evidence' are only valid with 'agency work prepare'.",
|
|
1626
1643
|
spec.usage,
|
|
1627
1644
|
)
|
|
1628
1645
|
}
|
|
@@ -2,7 +2,12 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
|
2
2
|
import { Effect } from "effect"
|
|
3
3
|
import { mkdir } from "node:fs/promises"
|
|
4
4
|
import { join } from "node:path"
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
captureLogs,
|
|
7
|
+
cleanupTempDir,
|
|
8
|
+
createTempDir,
|
|
9
|
+
runTestEffect,
|
|
10
|
+
} from "../test-utils"
|
|
6
11
|
import { task, type TaskInteraction } from "./task"
|
|
7
12
|
|
|
8
13
|
describe("task creation input", () => {
|
|
@@ -125,6 +130,77 @@ describe("task creation input", () => {
|
|
|
125
130
|
expect(content).toContain("base: main")
|
|
126
131
|
})
|
|
127
132
|
|
|
133
|
+
test("returns revision-bound validation evidence and normalized recalled context", async () => {
|
|
134
|
+
const [output] = await captureLogs(() =>
|
|
135
|
+
runTestEffect(
|
|
136
|
+
task({
|
|
137
|
+
subcommand: "create",
|
|
138
|
+
args: ["scripted-context"],
|
|
139
|
+
contextRepo: "agency",
|
|
140
|
+
contextBase: "main",
|
|
141
|
+
contextSlug: "scripted-context",
|
|
142
|
+
authoritativeSources: [
|
|
143
|
+
"https://example.com/spec",
|
|
144
|
+
join(root, "SOURCE.md"),
|
|
145
|
+
],
|
|
146
|
+
cwd: root,
|
|
147
|
+
json: true,
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
const result = JSON.parse(output!)
|
|
152
|
+
expect(result.selector).toBe("execution-unit:task/scripted-context")
|
|
153
|
+
expect(result.documentPath).toBe(
|
|
154
|
+
join(root, "tasks/scripted-context/TASK.md"),
|
|
155
|
+
)
|
|
156
|
+
expect(result.validation.valid).toBe(true)
|
|
157
|
+
expect(result.evidence).toEqual(
|
|
158
|
+
expect.objectContaining({
|
|
159
|
+
version: 1,
|
|
160
|
+
target: "execution-unit:task/scripted-context",
|
|
161
|
+
documentRevision: result.revision,
|
|
162
|
+
valid: true,
|
|
163
|
+
digest: expect.any(String),
|
|
164
|
+
}),
|
|
165
|
+
)
|
|
166
|
+
expect(result.recalledContext).toEqual({
|
|
167
|
+
repo: "agency",
|
|
168
|
+
base: "main",
|
|
169
|
+
preferredSlug: "scripted-context",
|
|
170
|
+
authoritativeSources: [
|
|
171
|
+
join(root, "SOURCE.md"),
|
|
172
|
+
"https://example.com/spec",
|
|
173
|
+
],
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test("rejects stale or conflicting recalled context", async () => {
|
|
178
|
+
await expect(
|
|
179
|
+
runTestEffect(
|
|
180
|
+
task({
|
|
181
|
+
subcommand: "create",
|
|
182
|
+
args: ["conflict"],
|
|
183
|
+
repo: "agency",
|
|
184
|
+
contextRepo: "other",
|
|
185
|
+
cwd: root,
|
|
186
|
+
silent: true,
|
|
187
|
+
}),
|
|
188
|
+
),
|
|
189
|
+
).rejects.toThrow("conflicts with --repo")
|
|
190
|
+
await expect(
|
|
191
|
+
runTestEffect(
|
|
192
|
+
task({
|
|
193
|
+
subcommand: "create",
|
|
194
|
+
args: ["conflict"],
|
|
195
|
+
contextRepo: "agency",
|
|
196
|
+
contextSlug: "stale-slug",
|
|
197
|
+
cwd: root,
|
|
198
|
+
silent: true,
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
).rejects.toThrow("conflicts with task ID")
|
|
202
|
+
})
|
|
203
|
+
|
|
128
204
|
test("never prompts when scripted creation is incomplete", async () => {
|
|
129
205
|
const interaction: TaskInteraction = {
|
|
130
206
|
text: () => Effect.fail(new Error("unexpected text prompt")),
|
package/src/commands/task.ts
CHANGED
|
@@ -12,6 +12,10 @@ import { getWorkViews } from "../work-view"
|
|
|
12
12
|
import { GraphMutationService } from "../services/GraphMutationService"
|
|
13
13
|
import { work as startWork, type StartWork } from "./work"
|
|
14
14
|
import { ReviewService } from "../services/ReviewService"
|
|
15
|
+
import {
|
|
16
|
+
buildValidationEvidence,
|
|
17
|
+
normalizeRecalledContext,
|
|
18
|
+
} from "../workbase/kickoff-contract"
|
|
15
19
|
|
|
16
20
|
interface TaskOptions extends BaseCommandOptions {
|
|
17
21
|
readonly subcommand?: string
|
|
@@ -45,6 +49,10 @@ interface TaskOptions extends BaseCommandOptions {
|
|
|
45
49
|
readonly noPullRequest?: boolean
|
|
46
50
|
readonly summary?: string
|
|
47
51
|
readonly evidenceUrl?: string
|
|
52
|
+
readonly contextRepo?: string
|
|
53
|
+
readonly contextBase?: string
|
|
54
|
+
readonly contextSlug?: string
|
|
55
|
+
readonly authoritativeSources?: readonly string[]
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export interface TaskInteraction {
|
|
@@ -216,7 +224,43 @@ export const task = (
|
|
|
216
224
|
return yield* Effect.fail(new Error("Task ID is required"))
|
|
217
225
|
}
|
|
218
226
|
const multiPhase = options.multiPhase ?? false
|
|
219
|
-
if (
|
|
227
|
+
if (
|
|
228
|
+
options.repo &&
|
|
229
|
+
options.contextRepo &&
|
|
230
|
+
options.repo !== options.contextRepo
|
|
231
|
+
) {
|
|
232
|
+
return yield* Effect.fail(
|
|
233
|
+
new Error(
|
|
234
|
+
`Recalled repository '${options.contextRepo}' conflicts with --repo '${options.repo}'`,
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
if (
|
|
239
|
+
options.base &&
|
|
240
|
+
options.contextBase &&
|
|
241
|
+
options.base !== options.contextBase
|
|
242
|
+
) {
|
|
243
|
+
return yield* Effect.fail(
|
|
244
|
+
new Error(
|
|
245
|
+
`Recalled base '${options.contextBase}' conflicts with --base '${options.base}'`,
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
const repo = options.repo ?? options.contextRepo
|
|
250
|
+
const base = options.base ?? options.contextBase ?? "main"
|
|
251
|
+
const recalledContext = yield* Effect.try({
|
|
252
|
+
try: () =>
|
|
253
|
+
normalizeRecalledContext({
|
|
254
|
+
id,
|
|
255
|
+
repo,
|
|
256
|
+
base: multiPhase || options.review ? undefined : base,
|
|
257
|
+
preferredSlug: options.contextSlug,
|
|
258
|
+
authoritativeSources: options.authoritativeSources,
|
|
259
|
+
}),
|
|
260
|
+
catch: (cause) =>
|
|
261
|
+
cause instanceof Error ? cause : new Error(String(cause)),
|
|
262
|
+
})
|
|
263
|
+
if (!multiPhase && !repo && !options.review) {
|
|
220
264
|
return yield* Effect.fail(
|
|
221
265
|
new Error("Writable repository is required for task create"),
|
|
222
266
|
)
|
|
@@ -236,7 +280,7 @@ export const task = (
|
|
|
236
280
|
epic: options.epic,
|
|
237
281
|
multiPhase,
|
|
238
282
|
review,
|
|
239
|
-
repo:
|
|
283
|
+
repo: review ? undefined : repo,
|
|
240
284
|
repos: review
|
|
241
285
|
? undefined
|
|
242
286
|
: parseRepositoryReferences(options.references),
|
|
@@ -244,11 +288,32 @@ export const task = (
|
|
|
244
288
|
multiPhase || review
|
|
245
289
|
? undefined
|
|
246
290
|
: (options.branch ?? `task/${id}`),
|
|
247
|
-
base: multiPhase || review ? undefined :
|
|
291
|
+
base: multiPhase || review ? undefined : base,
|
|
248
292
|
},
|
|
249
293
|
cwd,
|
|
250
294
|
)
|
|
251
|
-
const
|
|
295
|
+
const validation = yield* workbase.validate(cwd)
|
|
296
|
+
const selector = multiPhase
|
|
297
|
+
? `task:${record.id}`
|
|
298
|
+
: `execution-unit:task/${record.id}`
|
|
299
|
+
const evidence = validation.valid
|
|
300
|
+
? yield* buildValidationEvidence({
|
|
301
|
+
startPath: cwd,
|
|
302
|
+
target: selector,
|
|
303
|
+
documentPath: record.path,
|
|
304
|
+
documentRevision: record.revision,
|
|
305
|
+
recalledContext,
|
|
306
|
+
})
|
|
307
|
+
: null
|
|
308
|
+
const { content: _, ...created } = record
|
|
309
|
+
const output = {
|
|
310
|
+
...created,
|
|
311
|
+
selector,
|
|
312
|
+
documentPath: record.path,
|
|
313
|
+
validation,
|
|
314
|
+
evidence,
|
|
315
|
+
recalledContext,
|
|
316
|
+
}
|
|
252
317
|
log(
|
|
253
318
|
options.json
|
|
254
319
|
? JSON.stringify(output, null, 2)
|
|
@@ -480,6 +545,11 @@ Create options:
|
|
|
480
545
|
Read-only repository reference; repeatable
|
|
481
546
|
--branch <name> Working branch (default: task/<id>)
|
|
482
547
|
--base <name> Base branch (default: main)
|
|
548
|
+
--context-repo <alias> Recalled repository; must agree with --repo
|
|
549
|
+
--context-base <name> Recalled base; must agree with --base
|
|
550
|
+
--context-slug <id> Recalled preferred slug; must equal the task ID
|
|
551
|
+
--authoritative-source <path-or-url>
|
|
552
|
+
Authoritative input location; repeatable
|
|
483
553
|
--multi-phase Create a task container for phases
|
|
484
554
|
--review <alias> Create a pinned read-only review task
|
|
485
555
|
--pull-request <url-or-number>
|
|
@@ -497,8 +567,8 @@ Update options:
|
|
|
497
567
|
--base <name> Replace the base branch
|
|
498
568
|
--pr-url <url> / --clear-pr
|
|
499
569
|
|
|
500
|
-
Task creation is noninteractive. Single-phase tasks require --repo
|
|
501
|
-
--multi-phase instead for a task container. Guided input is available only
|
|
570
|
+
Task creation is noninteractive. Single-phase tasks require --repo or
|
|
571
|
+
--context-repo; use --multi-phase instead for a task container. Guided input is available only
|
|
502
572
|
through task new, which fails when --no-input is set or no TTY is available.
|
|
503
573
|
|
|
504
574
|
Options:
|
|
@@ -82,6 +82,7 @@ interface HarnessOptions {
|
|
|
82
82
|
readonly phaseStatuses?: Readonly<
|
|
83
83
|
Record<string, "open" | "working" | "delegated" | "done" | "dropped">
|
|
84
84
|
>
|
|
85
|
+
readonly taskRepo?: string
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
const createHarness = (options: HarnessOptions = {}) => {
|
|
@@ -138,6 +139,16 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
138
139
|
runners: options.runners,
|
|
139
140
|
},
|
|
140
141
|
}),
|
|
142
|
+
repositoryAliases: () => Effect.succeed(["agency"]),
|
|
143
|
+
validate: () =>
|
|
144
|
+
Effect.succeed({
|
|
145
|
+
root: "/workbase",
|
|
146
|
+
issues: [],
|
|
147
|
+
epicCount: 0,
|
|
148
|
+
taskCount: 1,
|
|
149
|
+
phaseCount: 0,
|
|
150
|
+
valid: true,
|
|
151
|
+
}),
|
|
141
152
|
}
|
|
142
153
|
const epics = {
|
|
143
154
|
show: (id: string) =>
|
|
@@ -154,10 +165,11 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
154
165
|
return Effect.succeed({
|
|
155
166
|
id,
|
|
156
167
|
path: `/workbase/tasks/${id}/TASK.md`,
|
|
168
|
+
revision: "a".repeat(64),
|
|
157
169
|
data: options.multiPhaseTasks?.includes(id)
|
|
158
170
|
? { phases: [] }
|
|
159
171
|
: {
|
|
160
|
-
repo: "agency",
|
|
172
|
+
repo: options.taskRepo ?? "agency",
|
|
161
173
|
branch: `task/${id}`,
|
|
162
174
|
base: "main",
|
|
163
175
|
status: taskStatuses[id] ?? options.taskStatus ?? "open",
|
|
@@ -182,6 +194,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
182
194
|
taskId,
|
|
183
195
|
id,
|
|
184
196
|
path: `/workbase/tasks/${taskId}/phases/${id}/PHASE.md`,
|
|
197
|
+
revision: "b".repeat(64),
|
|
185
198
|
data: {
|
|
186
199
|
repo: "agency",
|
|
187
200
|
branch: `task/${id}`,
|
|
@@ -246,7 +259,16 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
246
259
|
}
|
|
247
260
|
const fs = {
|
|
248
261
|
isDirectory: (path: string) =>
|
|
249
|
-
Effect.succeed(
|
|
262
|
+
Effect.succeed(
|
|
263
|
+
path === "/workbase/epics" || path === "/workbase/tasks"
|
|
264
|
+
? false
|
|
265
|
+
: (options.existingDirectories?.includes(path) ?? true),
|
|
266
|
+
),
|
|
267
|
+
readFile: (path: string) =>
|
|
268
|
+
Effect.succeed(path.endsWith("agency.json") ? '{"version":2}\n' : ""),
|
|
269
|
+
readDirectory: () => Effect.succeed([]),
|
|
270
|
+
exists: () => Effect.succeed(false),
|
|
271
|
+
realPath: (path: string) => Effect.succeed(path),
|
|
250
272
|
runCommand: (args: readonly string[]) => {
|
|
251
273
|
const cli = args[1]!
|
|
252
274
|
events.push(`probe:${cli}`)
|
|
@@ -306,6 +328,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
306
328
|
Effect.provideService(WorkbaseService, workbase as never),
|
|
307
329
|
Effect.provideService(TaskService, tasks as never),
|
|
308
330
|
Effect.provideService(PhaseService, phases as never),
|
|
331
|
+
Effect.provideService(ReadinessService, readiness as never),
|
|
309
332
|
) as Effect.Effect<void, unknown, never>,
|
|
310
333
|
)
|
|
311
334
|
|
|
@@ -458,7 +481,7 @@ describe("work command", () => {
|
|
|
458
481
|
test("prepares without launching or changing lifecycle status", async () => {
|
|
459
482
|
const harness = createHarness({ existingDirectories: [] })
|
|
460
483
|
|
|
461
|
-
await captureLogs(() =>
|
|
484
|
+
const [output] = await captureLogs(() =>
|
|
462
485
|
harness.runPrepare({
|
|
463
486
|
cwd: "/workbase",
|
|
464
487
|
directory: "example",
|
|
@@ -473,7 +496,43 @@ describe("work command", () => {
|
|
|
473
496
|
expect(harness.materializeOptions[0]).toMatchObject({
|
|
474
497
|
json: true,
|
|
475
498
|
dryRun: true,
|
|
499
|
+
validationAlreadyPerformed: true,
|
|
500
|
+
})
|
|
501
|
+
const result = JSON.parse(output!)
|
|
502
|
+
expect(result.validationEvidence.status).toBe("refreshed")
|
|
503
|
+
expect(result.validationEvidence.reasons).toEqual(["not-supplied"])
|
|
504
|
+
expect(result.kickoff.target).toBe("execution-unit:task/example")
|
|
505
|
+
expect(
|
|
506
|
+
result.kickoff.steps.filter(
|
|
507
|
+
({ id }: { id: string }) => id === "final-context-verification",
|
|
508
|
+
),
|
|
509
|
+
).toHaveLength(1)
|
|
510
|
+
|
|
511
|
+
const [reusedOutput] = await captureLogs(() =>
|
|
512
|
+
harness.runPrepare({
|
|
513
|
+
cwd: "/workbase",
|
|
514
|
+
directory: "example",
|
|
515
|
+
json: true,
|
|
516
|
+
dryRun: true,
|
|
517
|
+
evidence: JSON.stringify(result.validationEvidence.evidence),
|
|
518
|
+
}),
|
|
519
|
+
)
|
|
520
|
+
expect(JSON.parse(reusedOutput!).validationEvidence).toEqual(
|
|
521
|
+
expect.objectContaining({ status: "reused", reasons: [] }),
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
const conflicting = createHarness({
|
|
525
|
+
existingDirectories: [],
|
|
526
|
+
taskRepo: "other",
|
|
476
527
|
})
|
|
528
|
+
await expect(
|
|
529
|
+
conflicting.runPrepare({
|
|
530
|
+
cwd: "/workbase",
|
|
531
|
+
directory: "example",
|
|
532
|
+
dryRun: true,
|
|
533
|
+
evidence: JSON.stringify(result.validationEvidence.evidence),
|
|
534
|
+
}),
|
|
535
|
+
).rejects.toThrow("Recalled repository conflicts")
|
|
477
536
|
})
|
|
478
537
|
|
|
479
538
|
test("launches an epic agent from an epic directory", async () => {
|