@markjaquith/agency 2.59.0 → 2.61.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 +96 -4
- package/cli-main.ts +7 -0
- package/fixtures/protocol/orchestration-recipes.json +19 -0
- package/index.ts +1 -0
- package/package.json +4 -1
- package/schemas/agency-graph-v1.schema.json +53 -0
- package/schemas/agency-kickoff-v1.schema.json +124 -0
- package/src/cli-parser.test.ts +50 -2
- package/src/cli-parser.ts +52 -6
- package/src/commands/task-phase.test.ts +49 -0
- package/src/commands/task.test.ts +77 -1
- package/src/commands/task.ts +123 -7
- package/src/commands/work.test.ts +62 -3
- package/src/commands/work.ts +106 -3
- package/src/graph-schema.test.ts +8 -0
- package/src/graph-schema.ts +7 -0
- package/src/services/ArchiveService.test.ts +18 -1
- package/src/services/GraphMutationService.test.ts +48 -0
- package/src/services/IntegrationService.test.ts +47 -0
- package/src/services/LifecycleTransaction.ts +12 -0
- package/src/services/PhaseService.ts +2 -0
- package/src/services/TaskPhaseService.test.ts +238 -0
- package/src/services/TaskService.ts +265 -6
- package/src/services/WorkbaseService.ts +30 -0
- package/src/services/WorktreeService.ts +9 -6
- package/src/workbase/AGENTS.md +80 -4
- package/src/workbase/frontmatter.test.ts +18 -1
- package/src/workbase/frontmatter.ts +29 -1
- package/src/workbase/kickoff-contract.test.ts +145 -0
- package/src/workbase/kickoff-contract.ts +358 -0
- package/src/workbase/opencode-file.ts +1 -1
- package/src/workbase/schemas.test.ts +36 -0
- package/src/workbase/schemas.ts +33 -1
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { Schema, TreeFormatter } from "@effect/schema"
|
|
2
2
|
import { Data, Effect, Either } from "effect"
|
|
3
|
+
import { readdir } from "node:fs/promises"
|
|
3
4
|
import { join } from "node:path"
|
|
4
5
|
import { FileSystemService } from "./FileSystemService"
|
|
5
|
-
import { WorkbaseService } from "./WorkbaseService"
|
|
6
|
+
import { WorkbaseService, type ValidationReport } from "./WorkbaseService"
|
|
6
7
|
import { VersionControlService } from "./VersionControlService"
|
|
7
8
|
import { EpicService, type EpicRecord } from "./EpicService"
|
|
8
9
|
import {
|
|
9
10
|
EntityId,
|
|
11
|
+
PhaseFrontmatter,
|
|
10
12
|
TaskFrontmatter,
|
|
11
13
|
type RepositoryReference,
|
|
12
14
|
type ReviewRecord,
|
|
15
|
+
type TaskHandoff,
|
|
16
|
+
type TaskPurpose,
|
|
13
17
|
type TaskFrontmatter as TaskData,
|
|
14
18
|
WorkStatus,
|
|
15
19
|
} from "../workbase/schemas"
|
|
@@ -17,6 +21,7 @@ import {
|
|
|
17
21
|
formatMarkdownDocument,
|
|
18
22
|
formatWorkDocumentBody,
|
|
19
23
|
parseFrontmatter,
|
|
24
|
+
parseFrontmatterSync,
|
|
20
25
|
} from "../workbase/frontmatter"
|
|
21
26
|
import { canTransitionStatus } from "../readiness"
|
|
22
27
|
import { documentRevision } from "../workbase/document-revision"
|
|
@@ -27,6 +32,7 @@ import {
|
|
|
27
32
|
} from "../workbase/completion"
|
|
28
33
|
import {
|
|
29
34
|
documentWriteStep,
|
|
35
|
+
pathMustNotExistStep,
|
|
30
36
|
runLifecycleTransaction,
|
|
31
37
|
type TransactionStep,
|
|
32
38
|
} from "./LifecycleTransaction"
|
|
@@ -54,6 +60,27 @@ export interface CreateTaskInput {
|
|
|
54
60
|
readonly branch?: string
|
|
55
61
|
readonly base?: string
|
|
56
62
|
readonly review?: ReviewRecord
|
|
63
|
+
readonly purpose?: TaskPurpose
|
|
64
|
+
readonly handoff?: TaskHandoff
|
|
65
|
+
readonly preconditions?: readonly {
|
|
66
|
+
readonly path: string
|
|
67
|
+
readonly revision: string
|
|
68
|
+
}[]
|
|
69
|
+
readonly transactionSteps?: readonly TransactionStep[]
|
|
70
|
+
readonly postWriteSteps?: readonly TransactionStep[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface HandoffTaskInput {
|
|
74
|
+
readonly sourceTaskId: string
|
|
75
|
+
readonly sourcePhaseId?: string
|
|
76
|
+
readonly id: string
|
|
77
|
+
readonly ticketUrl: string | null
|
|
78
|
+
readonly description?: string
|
|
79
|
+
readonly epic?: string
|
|
80
|
+
readonly repo: string
|
|
81
|
+
readonly repos?: readonly RepositoryReference[]
|
|
82
|
+
readonly branch: string
|
|
83
|
+
readonly base: string
|
|
57
84
|
}
|
|
58
85
|
|
|
59
86
|
const decodeTask = (input: unknown) => {
|
|
@@ -82,6 +109,72 @@ const decodeStatus = (status: string) => {
|
|
|
82
109
|
: Effect.succeed(result.right)
|
|
83
110
|
}
|
|
84
111
|
|
|
112
|
+
const decodePhase = (input: unknown) => {
|
|
113
|
+
const result = Schema.decodeUnknownEither(PhaseFrontmatter, {
|
|
114
|
+
errors: "all",
|
|
115
|
+
onExcessProperty: "error",
|
|
116
|
+
})(input)
|
|
117
|
+
return Either.isLeft(result)
|
|
118
|
+
? Effect.fail(
|
|
119
|
+
new TaskError({ message: TreeFormatter.formatErrorSync(result.left) }),
|
|
120
|
+
)
|
|
121
|
+
: Effect.succeed(result.right)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const branchAvailableStep = (
|
|
125
|
+
root: string,
|
|
126
|
+
repo: string,
|
|
127
|
+
branch: string,
|
|
128
|
+
destinationId: string,
|
|
129
|
+
): TransactionStep => ({
|
|
130
|
+
label: `verify branch ${repo}:${branch} is available`,
|
|
131
|
+
preflight: async () => {
|
|
132
|
+
const taskEntries = await readdir(join(root, "tasks"), {
|
|
133
|
+
withFileTypes: true,
|
|
134
|
+
}).catch(() => [])
|
|
135
|
+
for (const taskEntry of taskEntries) {
|
|
136
|
+
if (!taskEntry.isDirectory() || taskEntry.name === destinationId) continue
|
|
137
|
+
const taskPath = join(root, "tasks", taskEntry.name, "TASK.md")
|
|
138
|
+
const taskContent = await Bun.file(taskPath).text()
|
|
139
|
+
const taskData = Schema.decodeUnknownSync(TaskFrontmatter, {
|
|
140
|
+
errors: "all",
|
|
141
|
+
onExcessProperty: "error",
|
|
142
|
+
})(parseFrontmatterSync(taskContent, taskPath).data)
|
|
143
|
+
if (
|
|
144
|
+
"repo" in taskData &&
|
|
145
|
+
taskData.repo === repo &&
|
|
146
|
+
taskData.branch === branch
|
|
147
|
+
) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Writable branch '${branch}' for repository '${repo}' is already owned by task '${taskEntry.name}'`,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
if (!("phases" in taskData)) continue
|
|
153
|
+
for (const phase of taskData.phases) {
|
|
154
|
+
const phasePath = join(
|
|
155
|
+
root,
|
|
156
|
+
"tasks",
|
|
157
|
+
taskEntry.name,
|
|
158
|
+
"phases",
|
|
159
|
+
phase.id,
|
|
160
|
+
"PHASE.md",
|
|
161
|
+
)
|
|
162
|
+
const phaseContent = await Bun.file(phasePath).text()
|
|
163
|
+
const phaseData = Schema.decodeUnknownSync(PhaseFrontmatter, {
|
|
164
|
+
errors: "all",
|
|
165
|
+
onExcessProperty: "error",
|
|
166
|
+
})(parseFrontmatterSync(phaseContent, phasePath).data)
|
|
167
|
+
if (phaseData.repo === repo && phaseData.branch === branch) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Writable branch '${branch}' for repository '${repo}' is already owned by phase '${taskEntry.name}/${phase.id}'`,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
apply: async () => {},
|
|
176
|
+
})
|
|
177
|
+
|
|
85
178
|
const reviewPinRef = (taskId: string) =>
|
|
86
179
|
`refs/agency/reviews/${Buffer.from(taskId).toString("hex")}`
|
|
87
180
|
|
|
@@ -151,7 +244,22 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
151
244
|
}
|
|
152
245
|
if (yield* fs.exists(archivedTaskDirectory(root, id))) {
|
|
153
246
|
return yield* new TaskError({
|
|
154
|
-
message: `Task '${id}' is archived;
|
|
247
|
+
message: `Task '${id}' is archived; explicit creation requires a different ID`,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
const taskMetadata = {
|
|
251
|
+
...(input.purpose ? { purpose: input.purpose } : {}),
|
|
252
|
+
...(input.handoff ? { handoff: input.handoff } : {}),
|
|
253
|
+
}
|
|
254
|
+
if (input.handoff && input.purpose !== "implementation") {
|
|
255
|
+
return yield* new TaskError({
|
|
256
|
+
message:
|
|
257
|
+
"Task handoff provenance requires purpose 'implementation'",
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
if (input.purpose === "implementation" && !input.handoff) {
|
|
261
|
+
return yield* new TaskError({
|
|
262
|
+
message: "Implementation-purpose tasks require handoff provenance",
|
|
155
263
|
})
|
|
156
264
|
}
|
|
157
265
|
|
|
@@ -175,6 +283,7 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
175
283
|
? { description: input.description }
|
|
176
284
|
: {}),
|
|
177
285
|
...(input.epic ? { epic: input.epic } : {}),
|
|
286
|
+
...taskMetadata,
|
|
178
287
|
review: input.review,
|
|
179
288
|
})
|
|
180
289
|
} else if (input.multiPhase) {
|
|
@@ -184,6 +293,7 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
184
293
|
? { description: input.description }
|
|
185
294
|
: {}),
|
|
186
295
|
...(input.epic ? { epic: input.epic } : {}),
|
|
296
|
+
...taskMetadata,
|
|
187
297
|
phases: [],
|
|
188
298
|
})
|
|
189
299
|
} else {
|
|
@@ -198,6 +308,7 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
198
308
|
? { description: input.description }
|
|
199
309
|
: {}),
|
|
200
310
|
...(input.epic ? { epic: input.epic } : {}),
|
|
311
|
+
...taskMetadata,
|
|
201
312
|
repo: input.repo,
|
|
202
313
|
...(input.repos?.length ? { repos: input.repos } : {}),
|
|
203
314
|
branch: input.branch,
|
|
@@ -245,7 +356,7 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
245
356
|
.join(" ")
|
|
246
357
|
const content = formatMarkdownDocument(
|
|
247
358
|
data,
|
|
248
|
-
formatWorkDocumentBody(title, "task"),
|
|
359
|
+
formatWorkDocumentBody(title, "task", input.purpose),
|
|
249
360
|
)
|
|
250
361
|
const writes: {
|
|
251
362
|
path: string
|
|
@@ -273,14 +384,29 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
273
384
|
}
|
|
274
385
|
yield* runLifecycleTransaction({
|
|
275
386
|
root,
|
|
276
|
-
preconditions:
|
|
277
|
-
|
|
278
|
-
|
|
387
|
+
preconditions: [
|
|
388
|
+
...(input.preconditions ?? []),
|
|
389
|
+
...(parentEpic
|
|
390
|
+
? [{ path: parentEpic.path, revision: parentEpic.revision }]
|
|
391
|
+
: []),
|
|
392
|
+
],
|
|
279
393
|
steps: [
|
|
394
|
+
...(input.transactionSteps ?? []),
|
|
395
|
+
pathMustNotExistStep(
|
|
396
|
+
root,
|
|
397
|
+
directory,
|
|
398
|
+
`Task '${id}' already exists`,
|
|
399
|
+
),
|
|
400
|
+
pathMustNotExistStep(
|
|
401
|
+
root,
|
|
402
|
+
archivedTaskDirectory(root, id),
|
|
403
|
+
`Task '${id}' is archived; explicit creation requires a different ID`,
|
|
404
|
+
),
|
|
280
405
|
...(input.review
|
|
281
406
|
? [reviewPinStep(root, id, input.review, reviewEnvironment)]
|
|
282
407
|
: []),
|
|
283
408
|
documentWriteStep(root, writes),
|
|
409
|
+
...(input.postWriteSteps ?? []),
|
|
284
410
|
],
|
|
285
411
|
})
|
|
286
412
|
|
|
@@ -293,6 +419,139 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
293
419
|
} satisfies TaskRecord
|
|
294
420
|
}),
|
|
295
421
|
|
|
422
|
+
handoff: (input: HandoffTaskInput, startPath: string = process.cwd()) =>
|
|
423
|
+
Effect.gen(function* () {
|
|
424
|
+
const fs = yield* FileSystemService
|
|
425
|
+
const workbase = yield* WorkbaseService
|
|
426
|
+
const service = yield* TaskService
|
|
427
|
+
const root = yield* workbase.discover(startPath)
|
|
428
|
+
const sourceTaskId = yield* decodeId(input.sourceTaskId)
|
|
429
|
+
const sourceTask = yield* service.show(sourceTaskId, root)
|
|
430
|
+
const validation = yield* workbase.validate(root)
|
|
431
|
+
if (!validation.valid) {
|
|
432
|
+
return yield* new TaskError({
|
|
433
|
+
message: "Cannot create a handoff in an invalid workbase",
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
if (sourceTask.data.purpose !== "investigation") {
|
|
437
|
+
return yield* new TaskError({
|
|
438
|
+
message: `Task '${sourceTaskId}' is not an investigation task`,
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const preconditions = [
|
|
443
|
+
{ path: sourceTask.path, revision: sourceTask.revision },
|
|
444
|
+
]
|
|
445
|
+
let source: TaskHandoff["source"]
|
|
446
|
+
let committedValidation: ValidationReport | undefined
|
|
447
|
+
let sourcePath = sourceTask.path
|
|
448
|
+
let sourceRevision = sourceTask.revision
|
|
449
|
+
if (input.sourcePhaseId) {
|
|
450
|
+
const phaseId = yield* decodeId(input.sourcePhaseId)
|
|
451
|
+
if (
|
|
452
|
+
!("phases" in sourceTask.data) ||
|
|
453
|
+
!sourceTask.data.phases.some((phase) => phase.id === phaseId)
|
|
454
|
+
) {
|
|
455
|
+
return yield* new TaskError({
|
|
456
|
+
message: `Phase '${sourceTaskId}/${phaseId}' does not exist`,
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
sourcePath = join(
|
|
460
|
+
root,
|
|
461
|
+
"tasks",
|
|
462
|
+
sourceTaskId,
|
|
463
|
+
"phases",
|
|
464
|
+
phaseId,
|
|
465
|
+
"PHASE.md",
|
|
466
|
+
)
|
|
467
|
+
const sourceContent = yield* fs.readFile(sourcePath)
|
|
468
|
+
const parsed = yield* parseFrontmatter(sourceContent, sourcePath)
|
|
469
|
+
yield* decodePhase(parsed.data)
|
|
470
|
+
sourceRevision = documentRevision(sourceContent)
|
|
471
|
+
preconditions.push({ path: sourcePath, revision: sourceRevision })
|
|
472
|
+
source = { kind: "phase", taskId: sourceTaskId, phaseId }
|
|
473
|
+
} else {
|
|
474
|
+
source = { kind: "task", taskId: sourceTaskId }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const record = yield* service.create(
|
|
478
|
+
{
|
|
479
|
+
id: input.id,
|
|
480
|
+
ticketUrl: input.ticketUrl,
|
|
481
|
+
description: input.description,
|
|
482
|
+
epic: input.epic,
|
|
483
|
+
repo: input.repo,
|
|
484
|
+
repos: input.repos,
|
|
485
|
+
branch: input.branch,
|
|
486
|
+
base: input.base,
|
|
487
|
+
purpose: "implementation",
|
|
488
|
+
handoff: { source, sourceRevision },
|
|
489
|
+
preconditions,
|
|
490
|
+
transactionSteps: [
|
|
491
|
+
branchAvailableStep(root, input.repo, input.branch, input.id),
|
|
492
|
+
],
|
|
493
|
+
postWriteSteps: [
|
|
494
|
+
{
|
|
495
|
+
label: "validate resulting workbase",
|
|
496
|
+
apply: async () => {
|
|
497
|
+
const report = await Effect.runPromise(
|
|
498
|
+
workbase
|
|
499
|
+
.validate(root)
|
|
500
|
+
.pipe(
|
|
501
|
+
Effect.provideService(FileSystemService, fs),
|
|
502
|
+
Effect.provideService(WorkbaseService, workbase),
|
|
503
|
+
),
|
|
504
|
+
)
|
|
505
|
+
if (!report.valid) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Handoff would create an invalid workbase: ${report.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`,
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
committedValidation = report
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
},
|
|
515
|
+
root,
|
|
516
|
+
)
|
|
517
|
+
if (!("repo" in record.data)) {
|
|
518
|
+
return yield* new TaskError({
|
|
519
|
+
message: `Task '${record.id}' is not an implementation execution unit`,
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
if (!committedValidation) {
|
|
523
|
+
return yield* new TaskError({
|
|
524
|
+
message: "Handoff validation did not produce a result",
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
const taskDirectory = join(root, "tasks", record.id)
|
|
528
|
+
const sourceSelector =
|
|
529
|
+
source.kind === "phase"
|
|
530
|
+
? `phase/${source.taskId}/${source.phaseId}`
|
|
531
|
+
: `task/${source.taskId}`
|
|
532
|
+
return {
|
|
533
|
+
task: {
|
|
534
|
+
id: record.id,
|
|
535
|
+
selector: `task/${record.id}`,
|
|
536
|
+
directory: taskDirectory,
|
|
537
|
+
documentPath: record.path,
|
|
538
|
+
revision: record.revision,
|
|
539
|
+
branch: record.data.branch,
|
|
540
|
+
base: record.data.base,
|
|
541
|
+
},
|
|
542
|
+
source: {
|
|
543
|
+
selector: sourceSelector,
|
|
544
|
+
documentPath: sourcePath,
|
|
545
|
+
revision: sourceRevision,
|
|
546
|
+
},
|
|
547
|
+
validation: committedValidation,
|
|
548
|
+
worktreePrepare: {
|
|
549
|
+
target: `task/${record.id}`,
|
|
550
|
+
command: ["agency", "worktree", "prepare", record.id],
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
}),
|
|
554
|
+
|
|
296
555
|
list: (startPath: string = process.cwd()) =>
|
|
297
556
|
Effect.gen(function* () {
|
|
298
557
|
const fs = yield* FileSystemService
|
|
@@ -818,6 +818,36 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
818
818
|
validateRepositories(task)
|
|
819
819
|
validateBranchOwnership(task)
|
|
820
820
|
validateCompletion(task)
|
|
821
|
+
if (task.data.handoff && task.data.purpose !== "implementation") {
|
|
822
|
+
issue(
|
|
823
|
+
task.path,
|
|
824
|
+
"Task handoff provenance requires purpose 'implementation'",
|
|
825
|
+
)
|
|
826
|
+
}
|
|
827
|
+
if (task.data.purpose === "implementation" && !task.data.handoff) {
|
|
828
|
+
issue(
|
|
829
|
+
task.path,
|
|
830
|
+
"Implementation-purpose tasks require handoff provenance",
|
|
831
|
+
)
|
|
832
|
+
}
|
|
833
|
+
if (
|
|
834
|
+
task.data.handoff?.source.kind === "task" &&
|
|
835
|
+
task.data.handoff.source.taskId === task.id
|
|
836
|
+
) {
|
|
837
|
+
issue(
|
|
838
|
+
task.path,
|
|
839
|
+
"Task handoff provenance cannot reference itself",
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
if (
|
|
843
|
+
task.data.handoff?.source.kind === "phase" &&
|
|
844
|
+
task.data.handoff.source.taskId === task.id
|
|
845
|
+
) {
|
|
846
|
+
issue(
|
|
847
|
+
task.path,
|
|
848
|
+
"Task handoff provenance cannot reference one of its own phases",
|
|
849
|
+
)
|
|
850
|
+
}
|
|
821
851
|
if (task.data.epic) {
|
|
822
852
|
const parent = epics.get(task.data.epic)
|
|
823
853
|
if (!parent) {
|
|
@@ -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
|
|
|
@@ -46,6 +62,31 @@ agent from an active agent session; creating a pull request; archiving, restorin
|
|
|
46
62
|
dropping, reopening, or completing work without a pull request; or using `--force`
|
|
47
63
|
to override readiness.
|
|
48
64
|
|
|
65
|
+
## Investigation Handoffs
|
|
66
|
+
|
|
67
|
+
An explicit request for a new, separate, or follow-up item overrides reuse of
|
|
68
|
+
every active or archived item, even when the subject or suggested ID matches.
|
|
69
|
+
Existing work may be inspected for duplicate scope, but do not mutate or select
|
|
70
|
+
it as the destination unless the user explicitly asks to reuse it.
|
|
71
|
+
|
|
72
|
+
Use this ordered flow for investigation-to-implementation work:
|
|
73
|
+
|
|
74
|
+
1. Create investigation-only work with `agency task create <id> --purpose
|
|
75
|
+
investigation ...`.
|
|
76
|
+
2. Record its boundary, evidence, findings, recommendation, and any no-change
|
|
77
|
+
outcome in the generated task sections.
|
|
78
|
+
3. Create a distinct implementation task with `agency task handoff
|
|
79
|
+
<investigation-task> <new-task> ...`; add `--source-phase <phase>` when the
|
|
80
|
+
evidence belongs to one phase.
|
|
81
|
+
4. Verify the returned new selector, source selector and revision, validation
|
|
82
|
+
result, and then confirm authority with `agency context <new-task> --json`.
|
|
83
|
+
5. Prepare, open, or launch the new item only when separately requested.
|
|
84
|
+
|
|
85
|
+
Creation and handoff do not imply worktree preparation, status changes, agent
|
|
86
|
+
launch, or UI actions. Handoff provenance is one-way historical evidence, not a
|
|
87
|
+
dependency: source rename or content changes can make its selector unresolved or
|
|
88
|
+
revision stale, and Agency must not silently rewrite that evidence.
|
|
89
|
+
|
|
49
90
|
## Safety
|
|
50
91
|
|
|
51
92
|
- Stop on validation errors, dependency blockers, an unexpected writable
|
|
@@ -64,6 +105,41 @@ to override readiness.
|
|
|
64
105
|
|
|
65
106
|
## Execution
|
|
66
107
|
|
|
108
|
+
### Canonical create and kickoff
|
|
109
|
+
|
|
110
|
+
This Agency recipe takes precedence over generic Herdr defaults whenever the
|
|
111
|
+
request creates, opens, works, launches, starts, or kicks off an Agency item.
|
|
112
|
+
Do not rediscover commands that this recipe or a known-current
|
|
113
|
+
`agency-kickoff-v1` plan supplies.
|
|
114
|
+
|
|
115
|
+
1. Create noninteractively with explicit recalled context when available:
|
|
116
|
+
`agency task create <slug> --context-repo <alias> --context-base <base> --context-slug <slug> --authoritative-source <absolute-path-or-url> --json`.
|
|
117
|
+
Repeat `--authoritative-source` as needed. Supplied context must agree with
|
|
118
|
+
explicit task flags; Agency rejects stale or conflicting values.
|
|
119
|
+
2. For create-only intent, return the creation result and stop. For open intent,
|
|
120
|
+
prepare the task with
|
|
121
|
+
`agency work prepare <slug> --evidence <creation-json> --dry-run --json`, then
|
|
122
|
+
execute the returned plan through `task-document-split` to prepare the
|
|
123
|
+
checkout and open or reuse the background tab. Stop before `runner-start`.
|
|
124
|
+
3. For work/launch/start/kickoff intent, run that same preflight and execute its
|
|
125
|
+
ordered kickoff steps. The plan owns worktree dry-run/preparation, a
|
|
126
|
+
retry-safe background Herdr tab, the side-by-side task document,
|
|
127
|
+
`agency work . --auto`, and exactly one final
|
|
128
|
+
`agency context <document-path> --json` verification.
|
|
129
|
+
4. When the orchestrator has known-current support for the plan's
|
|
130
|
+
`agency-kickoff-v1` capability, execute the supplied actions directly. Do not
|
|
131
|
+
call Herdr help, skill, or CLI discovery. If capability/version evidence is
|
|
132
|
+
absent or stale, discovery is the compatibility path; then resume the same
|
|
133
|
+
idempotency key rather than creating another tab, checkout, or runner.
|
|
134
|
+
5. After the one final context verification succeeds, leave the runner in the
|
|
135
|
+
background and stop. Do not inspect, poll, or babysit it unless the user asks.
|
|
136
|
+
|
|
137
|
+
Validation evidence is a local, auditable optimization, not authority. Preflight
|
|
138
|
+
refreshes it after workbase, target document, configuration, repository mapping,
|
|
139
|
+
payload digest, or kickoff-contract changes. Readiness, claims, repository
|
|
140
|
+
materialization, branch ownership, reference drift, and dirty-workspace checks
|
|
141
|
+
still run on every preparation.
|
|
142
|
+
|
|
67
143
|
For implementation work, read the task and phase prose returned by context,
|
|
68
144
|
change only the writable checkout, keep durable decisions current, and run the
|
|
69
145
|
repository's formatting, type checks, build, dead-code checks, and focused tests.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { parseFrontmatter } from "./frontmatter"
|
|
3
|
+
import { formatWorkDocumentBody, parseFrontmatter } from "./frontmatter"
|
|
4
4
|
|
|
5
5
|
describe("parseFrontmatter", () => {
|
|
6
6
|
test("parses YAML 1.2 frontmatter and body", async () => {
|
|
@@ -48,3 +48,20 @@ describe("parseFrontmatter", () => {
|
|
|
48
48
|
).rejects.toThrow("must begin with YAML frontmatter")
|
|
49
49
|
})
|
|
50
50
|
})
|
|
51
|
+
|
|
52
|
+
describe("formatWorkDocumentBody", () => {
|
|
53
|
+
test("generates explicit investigation sections", () => {
|
|
54
|
+
const body = formatWorkDocumentBody(
|
|
55
|
+
"Investigate Checkout",
|
|
56
|
+
"task",
|
|
57
|
+
"investigation",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
expect(body).toContain("## Investigation Boundary")
|
|
61
|
+
expect(body).toContain("## Evidence")
|
|
62
|
+
expect(body).toContain("## Findings")
|
|
63
|
+
expect(body).toContain("## Recommendation")
|
|
64
|
+
expect(body).toContain("## Implementation Handoff")
|
|
65
|
+
expect(body).not.toContain("Describe the task outcome")
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -85,7 +85,35 @@ export const formatMarkdownDocument = (data: object, body: string) =>
|
|
|
85
85
|
export const formatWorkDocumentBody = (
|
|
86
86
|
title: string,
|
|
87
87
|
kind: "epic" | "task" | "phase",
|
|
88
|
-
|
|
88
|
+
purpose?: "investigation" | "implementation",
|
|
89
|
+
) =>
|
|
90
|
+
purpose === "investigation"
|
|
91
|
+
? `# ${title}
|
|
92
|
+
|
|
93
|
+
## Investigation Boundary
|
|
94
|
+
|
|
95
|
+
State the question, scope, and implementation excluded from this investigation.
|
|
96
|
+
|
|
97
|
+
## Evidence
|
|
98
|
+
|
|
99
|
+
Record inspected sources, commands, observations, and supporting links.
|
|
100
|
+
|
|
101
|
+
## Findings
|
|
102
|
+
|
|
103
|
+
Record findings, confidence, uncertainty, and relevant constraints.
|
|
104
|
+
|
|
105
|
+
## Recommendation
|
|
106
|
+
|
|
107
|
+
Recommend implementation, no change, or further investigation.
|
|
108
|
+
|
|
109
|
+
## Implementation Handoff
|
|
110
|
+
|
|
111
|
+
Record any distinct implementation task created from this investigation.
|
|
112
|
+
|
|
113
|
+
## Important Decisions
|
|
114
|
+
|
|
115
|
+
Record consequential decisions and their rationale.`
|
|
116
|
+
: `# ${title}
|
|
89
117
|
|
|
90
118
|
## Outcome
|
|
91
119
|
|