@markjaquith/agency 2.58.2 → 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 +55 -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/ArchiveService.ts +9 -0
- package/src/services/IntegrationService.test.ts +37 -0
- package/src/services/ReviewService.ts +2 -0
- package/src/services/WorktreeService.test.ts +279 -1
- package/src/services/WorktreeService.ts +418 -30
- 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`
|
|
@@ -925,6 +967,15 @@ conflicting worktrees. Repair is deliberately conservative: it repairs safe Git
|
|
|
925
967
|
registration issues and materializes missing checkouts, but never switches a
|
|
926
968
|
branch, resets a commit, or discards uncommitted work.
|
|
927
969
|
|
|
970
|
+
For jj, explicit removal is a suspend operation. Agency records each workspace's
|
|
971
|
+
exact `@` commit and stable change ID in `.agency-jj-resume.json`, roots that
|
|
972
|
+
commit with an internal local bookmark, and only then forgets the workspace.
|
|
973
|
+
The next `work prepare` validates all three identities, restores the workspace by
|
|
974
|
+
editing the recorded commit, and consumes the resume metadata and bookmarks.
|
|
975
|
+
Missing, ambiguous, or conflicting resume state stops instead of falling back to
|
|
976
|
+
the declared delivery bookmark, `@-`, or the base. Unknown checkout cleanliness
|
|
977
|
+
also stops removal. Git worktree removal and preparation are unchanged.
|
|
978
|
+
|
|
928
979
|
Agency launches every agent beside its epic or task document. Single-phase tasks
|
|
929
980
|
and phases first fetch repositories and create or reuse worktrees under `code/`,
|
|
930
981
|
then launch the execution agent from the task directory with absolute context
|
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:
|