@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.
@@ -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; restore it before reusing this ID`,
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: parentEpic
277
- ? [{ path: parentEpic.path, revision: parentEpic.revision }]
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
- const report = yield* workbase.validate(root)
1854
- const validationIssue = report.issues[0]
1855
- if (validationIssue && !options.force) {
1856
- return yield* new WorktreeError({
1857
- message: `${validationIssue.path}: ${validationIssue.message}`,
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
 
@@ -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
- If context or doctor reports a declared but missing repository, run
24
- `agency repo setup --dry-run` and obtain explicit approval before
25
- `agency repo setup --apply`. Missing declared aliases are setup state, not a
26
- reason to edit `agency.json` or `repos/` by hand.
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
- ) => `# ${title}
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