@pmelab/gtd 23.1.0 → 24.0.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.
Files changed (50) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +13 -6
  3. package/dist/gtd.bundle.mjs +795 -1051
  4. package/dist/workflows.mjs +1952 -1725
  5. package/package.json +2 -1
  6. package/skills/authoring/SKILL.md +5 -4
  7. package/src/flows/factory.ts +5 -1
  8. package/src/flows/runtime.ts +3 -0
  9. package/src/workflow-kit/health.test.ts +30 -0
  10. package/src/workflow-kit/health.ts +86 -0
  11. package/src/workflow-kit/index.ts +7 -0
  12. package/src/workflow-kit/quality.test.ts +17 -0
  13. package/src/workflow-kit/quality.ts +45 -0
  14. package/src/workflow-kit/review.ts +324 -0
  15. package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
  16. package/src/workflows/feature/feature.test.ts +38 -0
  17. package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
  18. package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
  19. package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
  20. package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
  21. package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
  22. package/src/workflows/feature/review.ts +106 -0
  23. package/src/workflows/feature/steps.test.ts +82 -0
  24. package/src/workflows/feature/steps.ts +92 -0
  25. package/src/workflows/fix/fix.test.ts +94 -0
  26. package/src/workflows/fix/fix.ts +52 -0
  27. package/src/workflows/index.ts +44 -2
  28. package/src/workflows/review/review.test.ts +43 -0
  29. package/src/workflows/review/review.ts +31 -0
  30. package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
  31. package/src/workflows/{access.ts → shared/access.ts} +3 -5
  32. package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
  33. package/src/workflows/{health.ts → shared/health.ts} +38 -55
  34. package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
  35. package/src/workflows/{options.ts → shared/options.ts} +14 -2
  36. package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
  37. package/src/workflows/shared/scenarios.ts +35 -0
  38. package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
  39. package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
  40. package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
  41. package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
  42. package/src/workflows/shared/tail.test.ts +205 -0
  43. package/src/workflows/shared/tail.ts +135 -0
  44. package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
  45. package/src/workflows/{text.ts → shared/text.ts} +48 -7
  46. package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
  47. package/src/workflows/review.ts +0 -362
  48. /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
  49. /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
  50. /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
@@ -1,4 +1,46 @@
1
1
  // The `@pmelab/gtd/workflows` entry, built on its own: core never imports it,
2
2
  // a repository's `gtd.config.ts` does.
3
- export * from "./bundled.js"
4
- export { default } from "./bundled.js"
3
+ import type { Doors, Summary } from "../flows/index.js"
4
+ import { feature } from "./feature/feature.js"
5
+ import * as t from "./shared/text.js"
6
+ import { ARCHITECTURE, REQUIREMENTS, REVIEW } from "./shared/steps.js"
7
+
8
+ export { threads, type ThreadInfo } from "../flows/index.js"
9
+ export { defaults, envDefaults } from "./shared/vars.js"
10
+ export { ShippedOptions } from "./shared/options.js"
11
+ export { skills } from "./shared/skills.js"
12
+ export { access } from "./shared/access.js"
13
+ export { agentWithSkills } from "./shared/text.js"
14
+ export * from "./shared/steps.js"
15
+ export * from "./shared/health.js"
16
+ export * from "./shared/scenarios.js"
17
+ export {
18
+ closeReview,
19
+ fixQualityFindings,
20
+ handOffReview,
21
+ handOffTail,
22
+ qualityLap,
23
+ REVIEW_ORIG,
24
+ tail,
25
+ type TailPlan,
26
+ } from "./shared/tail.js"
27
+ export { lensesOf } from "../workflow-kit/index.js"
28
+ export * from "./feature/steps.js"
29
+ export * from "./feature/planning.js"
30
+ export * from "./feature/packages.js"
31
+ export { buildTail, type ReviewOutcome } from "./feature/review.js"
32
+ export * from "./feature/feature.js"
33
+ export { base, review, reviewFlow } from "./review/review.js"
34
+ export { fix, fixFlow, fixReviewNotes } from "./fix/fix.js"
35
+
36
+ export default feature
37
+
38
+ export const summary: Summary = t.summaryPrompt
39
+
40
+ export const steering = { [REQUIREMENTS]: "qa", [ARCHITECTURE]: "qa", [REVIEW]: "review" }
41
+
42
+ /**
43
+ * @deprecated Empty: each shipped workflow builds its own doors, so a config
44
+ * gets `fix` and `review [base]` by exporting `fix` and `review`.
45
+ */
46
+ export const doors: Doors = {}
@@ -0,0 +1,43 @@
1
+ import { afterEach, describe, expect, it } from "vitest"
2
+ import { installContext, type StepRequest } from "../../flows/index.js"
3
+ import { fixtureContext } from "../shared/text.fixture.js"
4
+ import { base, reviewFlow } from "./review.js"
5
+
6
+ afterEach(() => installContext(undefined))
7
+
8
+ describe("the review workflow", () => {
9
+ it("refuses a pending hand-off before requesting any step, the gate's check included", async () => {
10
+ const requested: string[] = []
11
+ const files = new Map([
12
+ [".gtd/REVIEW.md", "notes"],
13
+ [".gtd/REVIEW.orig.md", "orig"],
14
+ ])
15
+ installContext(
16
+ fixtureContext(
17
+ {},
18
+ {
19
+ step: (request: StepRequest) => {
20
+ requested.push(request.kind)
21
+ return Promise.resolve()
22
+ },
23
+ refuse: (message: string): never => {
24
+ throw new Error(message)
25
+ },
26
+ pushScope: () => undefined,
27
+ popScope: () => undefined,
28
+ read: (path) => files.get(path),
29
+ },
30
+ ),
31
+ )
32
+ await expect(reviewFlow()).rejects.toThrow(".gtd/REVIEW.orig.md is a pending hand-off")
33
+ expect(requested).toEqual([])
34
+ })
35
+ })
36
+
37
+ describe("the deprecated shipped base", () => {
38
+ it("answers for a workflow named review only, mapping reviewBase", () => {
39
+ expect(base("review", {})).toBe("")
40
+ expect(base("review", { reviewBase: "v1" })).toBe("v1")
41
+ expect(base("audit", { reviewBase: "v1" })).toBeUndefined()
42
+ })
43
+ })
@@ -0,0 +1,31 @@
1
+ import { start, type WorkflowBase } from "../../flows/index.js"
2
+ import { gate } from "../shared/health.js"
3
+ import { shipped } from "../shared/options.js"
4
+ import { handOffTail, refusePendingHandOff } from "../shared/tail.js"
5
+ import * as t from "../shared/text.js"
6
+
7
+ /**
8
+ * Pure review of everything since `reviewBase`: a green gate, the quality
9
+ * lap, then the human review. A note ends the process with `.gtd/REVIEW.md`
10
+ * left for `fix`; it never re-plans.
11
+ */
12
+ export const reviewFlow = async (): Promise<void> => {
13
+ refusePendingHandOff()
14
+ await gate("review-gate", t.reviewGateBlockedMessage())
15
+ await handOffTail(false, start())
16
+ }
17
+
18
+ export const review = shipped(
19
+ reviewFlow,
20
+ {
21
+ review: {
22
+ args: [{ name: "base", optional: true }],
23
+ vars: ({ base }) => (base === undefined ? {} : { reviewBase: base }),
24
+ },
25
+ },
26
+ (_workflow, vars) => vars.reviewBase || "",
27
+ )
28
+
29
+ /** @deprecated The `review` factory carries its own base; kept so a hand-written `review` re-exporting it still maps `reviewBase`. */
30
+ export const base: WorkflowBase = (workflow, vars) =>
31
+ workflow === "review" ? (vars.reviewBase ?? "") : undefined
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest"
2
2
  import { access } from "./access.js"
3
- import * as workflow from "./index.js"
3
+ import * as workflow from "../index.js"
4
4
 
5
5
  const { defaults } = workflow
6
6
 
@@ -1,5 +1,5 @@
1
- import type { ScopeAccess } from "../flows/index.js"
2
- import { lensesOf } from "./review.js"
1
+ import type { ScopeAccess } from "../../flows/index.js"
2
+ import { lensesOf, lensScopes } from "../../workflow-kit/index.js"
3
3
 
4
4
  // The bundled workflow's default file access, keyed by scope full name — the
5
5
  // name a `config.access` entry addresses. Only planning and review scopes
@@ -16,7 +16,5 @@ export const access = (
16
16
  // `{}` reopens what `build.review` restricted: the fixes edit code.
17
17
  "build.review.fix.nits": {},
18
18
  "build.review.fix.risks": {},
19
- ...Object.fromEntries(
20
- lensesOf(vars.qualityReviews ?? "").map((lens) => [`build.quality.${lens}`, { write: [] }]),
21
- ),
19
+ ...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] })),
22
20
  })
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { toRequest } from "../judges/index.js"
2
+ import { toRequest } from "../../judges/index.js"
3
3
  import { fastSuiteGuard, retryQuestion } from "./health.js"
4
4
  import { renderText } from "./text.fixture.js"
5
5
 
@@ -8,10 +8,10 @@ import {
8
8
  numeric,
9
9
  quote,
10
10
  read,
11
- scope,
12
11
  vars,
13
12
  type JudgeQuestion,
14
- } from "../flows/index.js"
13
+ } from "../../flows/index.js"
14
+ import * as kit from "../../workflow-kit/index.js"
15
15
  import { describeEscalation, escalate, escalationExhausted, ESCALATION, FEEDBACK } from "./steps.js"
16
16
  import * as t from "./text.js"
17
17
 
@@ -35,21 +35,23 @@ export const fastSuiteGuard = (): string[] =>
35
35
  /** Fix turns a run of red checks gets before it escalates. */
36
36
  export const FIX_CAP = 3
37
37
 
38
+ const unconfigured = (): boolean => read(SETUP) !== undefined
39
+
38
40
  /** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
39
- export const baseline = async (name: string, label = "Checking the baseline"): Promise<boolean> => {
40
- for (;;) {
41
- const green = await check(name, env.testCommand ?? "", {
42
- report: FEEDBACK,
43
- label,
44
- preamble: fastSuiteGuard(),
45
- })
46
- if (read(SETUP) === undefined) return green
47
- }
48
- }
41
+ export const baseline = (name: string, label = "Checking the baseline"): Promise<boolean> =>
42
+ kit.untilSettled(
43
+ () =>
44
+ check(name, env.testCommand ?? "", { report: FEEDBACK, label, preamble: fastSuiteGuard() }),
45
+ unconfigured,
46
+ )
49
47
 
50
- /** How many escalation rounds a run of red checks has spent — reset once the suite goes green. */
51
- export interface EscalationCount {
52
- rounds: number
48
+ export type { EscalationCount } from "../../workflow-kit/index.js"
49
+
50
+ const escalationSteps: kit.EscalationSteps = {
51
+ limit: 2,
52
+ describe: describeEscalation,
53
+ stop: escalate,
54
+ exhausted: escalationExhausted,
53
55
  }
54
56
 
55
57
  /**
@@ -57,15 +59,8 @@ export interface EscalationCount {
57
59
  * agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
58
60
  * on, the human is told the budget is spent instead.
59
61
  */
60
- export const escalation = async (count: EscalationCount): Promise<void> => {
61
- if (count.rounds >= 2) return escalationExhausted()
62
- count.rounds++
63
- await describeEscalation()
64
- await escalate()
65
- }
66
-
67
- const STAMP = /\n<!-- gtd check [0-9a-f]+ -->\n?$/
68
- const stripStamp = (text: string): string => text.replace(STAMP, "\n")
62
+ export const escalation = (count: kit.EscalationCount): Promise<void> =>
63
+ kit.escalation(count, escalationSteps)
69
64
 
70
65
  /** "Is this red round the same failure as the last one?" — asked of two reports, or of nothing when one is empty. */
71
66
  export const retryQuestion = (comparable: boolean): JudgeQuestion =>
@@ -107,7 +102,7 @@ export interface HealthOptions {
107
102
  /** Fix turns already spent before this call. */
108
103
  readonly fixesSoFar?: number
109
104
  /** Escalation rounds shared with other callers in the same run of red checks. */
110
- readonly escalations?: EscalationCount
105
+ readonly escalations?: kit.EscalationCount
111
106
  /** `fast` runs `fastTestCommand` (default `full`: `testCommand`). */
112
107
  readonly suite?: "full" | "fast"
113
108
  /** Paths removed on a green run. */
@@ -130,37 +125,25 @@ const runSuite = (options: HealthOptions): Promise<boolean> => {
130
125
  * first once `FIX_CAP` fixes are spent, or a judge calls the failure
131
126
  * identical to the previous round's.
132
127
  */
133
- export const healthy = async (
134
- fix: () => Promise<void>,
135
- options: HealthOptions = {},
136
- ): Promise<void> => {
137
- const escalations = options.escalations ?? { rounds: 0 }
138
- let fixes = options.fixesSoFar ?? 0
139
- let previous: string | undefined
140
- for (;;) {
141
- const green = await runSuite(options)
128
+ export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}): Promise<void> =>
129
+ kit.healthy(fix, {
142
130
  // Unconfigured: straight back to the check, no fix turn or count.
143
- if (options.suite === "fast" && read(SETUP) !== undefined) continue
144
- if (green) {
145
- escalations.rounds = 0
146
- return
147
- }
148
- const current = stripStamp(read(FEEDBACK) ?? "")
149
- const stuck = previous !== undefined && (await sameFailure(previous, current))
150
- previous = current
151
- if (stuck || fixes >= FIX_CAP) {
152
- await escalation(escalations)
153
- fixes = 0
154
- }
155
- fixes++
156
- await fix()
157
- }
158
- }
131
+ check:
132
+ options.suite === "fast"
133
+ ? () => kit.untilSettled(() => runSuite(options), unconfigured)
134
+ : () => runSuite(options),
135
+ report: FEEDBACK,
136
+ fixCap: FIX_CAP,
137
+ fixesSoFar: options.fixesSoFar ?? 0,
138
+ sameFailure,
139
+ escalations: options.escalations ?? { rounds: 0 },
140
+ escalation: escalationSteps,
141
+ })
159
142
 
160
143
  /** Hold the process at `<name>.blocked` until the suite is green. */
161
144
  export const gate = (name: string, message: string): Promise<void> =>
162
- scope(name, async () => {
163
- while (!(await baseline("check"))) {
164
- await human("blocked", { message, label: "Baseline is red", file: FEEDBACK })
165
- }
166
- })
145
+ kit.gate(
146
+ name,
147
+ () => baseline("check"),
148
+ () => human("blocked", { message, label: "Baseline is red", file: FEEDBACK }),
149
+ )
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { configure } from "../flows/index.js"
3
- import * as workflows from "./index.js"
2
+ import { configure } from "../../flows/index.js"
3
+ import * as workflows from "../index.js"
4
4
 
5
5
  const { defaults, envDefaults, feature, fix, review, ShippedOptions } = workflows
6
6
 
@@ -1,5 +1,11 @@
1
1
  import { Schema } from "effect"
2
- import { defineWorkflow, type Flow, type WorkflowFactory } from "../flows/index.js"
2
+ import {
3
+ defineWorkflow,
4
+ type Flow,
5
+ type WorkflowBase,
6
+ type WorkflowDoors,
7
+ type WorkflowFactory,
8
+ } from "../../flows/index.js"
3
9
  import { defaults, envDefaults } from "./vars.js"
4
10
 
5
11
  const setting = Schema.optionalWith(Schema.String, { exact: true })
@@ -27,12 +33,18 @@ const over = (
27
33
  })
28
34
 
29
35
  /** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
30
- export const shipped = (flow: Flow): WorkflowFactory<ShippedOptions> =>
36
+ export const shipped = (
37
+ flow: Flow,
38
+ doors?: WorkflowDoors,
39
+ base?: WorkflowBase,
40
+ ): WorkflowFactory<ShippedOptions> =>
31
41
  defineWorkflow(
32
42
  Schema.standardSchemaV1(ShippedOptions, { onExcessProperty: "error", errors: "all" }),
33
43
  (options) => ({
34
44
  flow,
35
45
  defaults: over(defaults, options),
36
46
  envDefaults: over(envDefaults, options),
47
+ doors,
48
+ base,
37
49
  }),
38
50
  )
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it } from "vitest"
2
- import { installContext, type StepRequest } from "../flows/index.js"
2
+ import { installContext, type StepRequest } from "../../flows/index.js"
3
3
  import {
4
4
  driftedScenarios,
5
5
  freezeScenarios,
@@ -0,0 +1,35 @@
1
+ import { human, scope } from "../../flows/index.js"
2
+ import * as kit from "../../workflow-kit/index.js"
3
+ import * as t from "./text.js"
4
+
5
+ export {
6
+ driftedScenarios,
7
+ freezeScenarios,
8
+ scenarioText,
9
+ wordingDrift,
10
+ type FrozenScenarios,
11
+ } from "../../workflow-kit/index.js"
12
+
13
+ /** After a turn: stop at `scenario-wording` while wording drifted; accept moves the snapshot. */
14
+ export const holdWording = (frozen: kit.FrozenScenarios | undefined): Promise<void> =>
15
+ kit.holdWording(frozen, (at, drifted) =>
16
+ human("scenario-wording", {
17
+ message: t.scenarioWordingMessage(at, drifted),
18
+ label: "Scenario wording changed",
19
+ acceptClean: true,
20
+ base: at,
21
+ }),
22
+ )
23
+
24
+ /** `turn`, then `holdWording`; `within` names the scope the gate rests in. */
25
+ export const guarded =
26
+ (
27
+ frozen: kit.FrozenScenarios | undefined,
28
+ turn: () => Promise<void>,
29
+ within?: string,
30
+ ): (() => Promise<void>) =>
31
+ async () => {
32
+ await turn()
33
+ if (within === undefined) await holdWording(frozen)
34
+ else await scope(within, () => holdWording(frozen))
35
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest"
2
2
  import { skills } from "./skills.js"
3
- import * as workflow from "./index.js"
3
+ import * as workflow from "../index.js"
4
4
 
5
5
  const { defaults } = workflow
6
6
 
@@ -25,6 +25,7 @@ describe("the bundled workflow's skills export", () => {
25
25
  "build.review",
26
26
  "build.review.fix.nits",
27
27
  "build.review.fix.risks",
28
+ "notes",
28
29
  "build.quality.correctness",
29
30
  "build.quality.owasp-security",
30
31
  "build.quality.ponytail-review",
@@ -55,6 +56,7 @@ describe("the bundled workflow's skills export", () => {
55
56
  "build.review": ["code-review-and-quality"],
56
57
  "build.review.fix.nits": ["incremental-implementation", "code-simplification"],
57
58
  "build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
59
+ notes: ["incremental-implementation", "code-simplification"],
58
60
  })
59
61
  })
60
62
 
@@ -1,5 +1,5 @@
1
1
  import { builtInLenses } from "./steps.js"
2
- import { lensesOf } from "./review.js"
2
+ import { lensesOf, lensScopes } from "../../workflow-kit/index.js"
3
3
 
4
4
  // The bundled workflow's default skills, one list per SCOPE, keyed by the
5
5
  // scope's full name — the same name a `config.skills` entry addresses. A
@@ -23,10 +23,11 @@ export const skills = (
23
23
  "build.review": ["code-review-and-quality"],
24
24
  "build.review.fix.nits": ["incremental-implementation", "code-simplification"],
25
25
  "build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
26
- ...Object.fromEntries(
27
- lensesOf(vars.qualityReviews ?? "").map((lens) => [
28
- `build.quality.${lens}`,
29
- builtInLenses[lens]?.skills ?? [lens],
30
- ]),
26
+ // `fix`'s turn over the notes a review handed on.
27
+ notes: ["incremental-implementation", "code-simplification"],
28
+ ...lensScopes(
29
+ "build.quality",
30
+ lensesOf(vars.qualityReviews ?? ""),
31
+ (lens) => builtInLenses[lens]?.skills ?? [lens],
31
32
  ),
32
33
  })
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import type { StepRequest } from "../flows/index.js"
2
+ import type { StepRequest } from "../../flows/index.js"
3
3
  import { captureStep } from "./text.fixture.js"
4
4
  import * as steps from "./steps.js"
5
5
 
@@ -15,31 +15,6 @@ const agentPrompt = (request: StepRequest): string => {
15
15
  }
16
16
 
17
17
  describe("the bundled workflow's steps declare skills — a bundled step's rendered prompt carries its bundled skills.ts preamble", () => {
18
- it("triage carries design.triage's bundled skills", async () => {
19
- const prompt = agentPrompt(await capture(() => steps.triage("base"), "design"))
20
- expect(prompt).toContain("spec-driven-development, planning-and-task-breakdown")
21
- })
22
-
23
- it("author carries architecture.author's bundled skills", async () => {
24
- const prompt = agentPrompt(await capture(() => steps.author(), "architecture"))
25
- expect(prompt).toContain("api-and-interface-design, documentation-and-adrs, ponytail")
26
- })
27
-
28
- it("decompose carries architecture.decompose's bundled skills", async () => {
29
- const prompt = agentPrompt(await capture(() => steps.decompose(), "architecture"))
30
- expect(prompt).toContain("incremental-implementation, planning-and-task-breakdown")
31
- })
32
-
33
- it("build carries packages.item.building's bundled skills", async () => {
34
- const prompt = agentPrompt(await capture(() => steps.build("pkg"), "packages.item"))
35
- expect(prompt).toContain("test-driven-development, incremental-implementation")
36
- })
37
-
38
- it("fixSuite carries packages.item.fix.suite's bundled skills", async () => {
39
- const prompt = agentPrompt(await capture(() => steps.fixSuite(), "packages.item"))
40
- expect(prompt).toContain("debugging-and-error-recovery")
41
- })
42
-
43
18
  it("fix carries build.fix's bundled skills", async () => {
44
19
  const prompt = agentPrompt(await capture(() => steps.fixCheck(), "build"))
45
20
  expect(prompt).toContain("debugging-and-error-recovery")
@@ -104,52 +79,12 @@ describe("the bundled workflow's steps declare skills — a bundled step's rende
104
79
  expect(prompt).toContain("code-review-and-quality")
105
80
  })
106
81
 
107
- it("collecting carries build.review.collecting's bundled skills", async () => {
108
- const prompt = agentPrompt(await capture(() => steps.collecting("capture"), "build"))
109
- expect(prompt).toContain("code-review-and-quality")
110
- })
111
-
112
82
  it("an empty configured list leaves that step's skills empty, and its rendered prompt bare", async () => {
113
83
  const request = await capture(() => steps.fixCheck(), "build", { build: [] })
114
84
  if (request.kind !== "agent") throw new Error("unreachable")
115
85
  expect(request.prompt).not.toContain("Load whatever's listed here")
116
86
  })
117
87
 
118
- it("answerReviewQuestions answers inline in .gtd/REVIEW.md as the reviewer, with its bundled skills", async () => {
119
- const request = await capture(
120
- () =>
121
- steps.answerReviewQuestions([{ id: "note-1", anchor: "calc ./a.ts#1-1", text: "why?" }]),
122
- "build",
123
- )
124
- if (request.kind !== "agent") throw new Error("unreachable")
125
- expect(request.name).toBe("review.answer-review-questions")
126
- expect(request.options).toMatchObject({
127
- file: ".gtd/REVIEW.md",
128
- mode: "review",
129
- })
130
- expect(request.prompt).toContain("code-review-and-quality")
131
- expect(request.prompt).toContain("note-1 — calc ./a.ts#1-1")
132
- expect(request.prompt).toContain("`A: ` line")
133
- expect(request.prompt).toContain("gtd check review .gtd/REVIEW.md")
134
- })
135
-
136
- it("fixNits fixes every nit in one turn and leaves .gtd/REVIEW.md alone", async () => {
137
- const request = await capture(
138
- () =>
139
- steps.fixNits([
140
- { id: "note-1", anchor: "calc ./a.ts#1-1", text: "typo" },
141
- { id: "note-2", anchor: "calc ./a.ts#2-2", text: "semicolon" },
142
- ]),
143
- "build",
144
- )
145
- if (request.kind !== "agent") throw new Error("unreachable")
146
- expect(request.name).toBe("review.fix.nits.fixing")
147
- expect(request.prompt).toContain("incremental-implementation, code-simplification")
148
- expect(request.prompt).toContain("typo")
149
- expect(request.prompt).toContain("semicolon")
150
- expect(request.prompt).toContain("Leave `.gtd/REVIEW.md` untouched")
151
- })
152
-
153
88
  it("fixRisks fixes every risk, tolerates an empty turn, and leaves .gtd/REVIEW.md alone", async () => {
154
89
  const request = await capture(
155
90
  () => steps.fixRisks([{ id: "risk-1", anchor: "calc ./a.ts#1-1", text: "Risk: drops it" }]),
@@ -1,4 +1,4 @@
1
- import { env, human } from "../flows/index.js"
1
+ import { env, human } from "../../flows/index.js"
2
2
  import * as t from "./text.js"
3
3
 
4
4
  // The bundled workflow's single steps. A step's name is relative to the
@@ -13,71 +13,8 @@ export const ARCHITECTURE = ".gtd/ARCHITECTURE.md"
13
13
  export const REVIEW = ".gtd/REVIEW.md"
14
14
  export const QUALITY = ".gtd/QUALITY.md"
15
15
 
16
- const planner = (): string => env.plannerModel ?? ""
17
- const coder = (): string => env.coderModel ?? ""
18
-
19
- // ── Planning ────────────────────────────────────────────────────────────────
20
-
21
- /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
22
- export const triage = (base: string): Promise<void> =>
23
- t.agentWithSkills("triage", t.designTriagePrompt(base), {
24
- label: "Triaging the change",
25
- file: REQUIREMENTS,
26
- mode: "qa",
27
- model: planner(),
28
- system: t.designSystem(),
29
- })
30
-
31
- export const author = (): Promise<void> =>
32
- t.agentWithSkills("author", t.architectureAuthorPrompt(), {
33
- label: "Refining the technical plan",
34
- file: ARCHITECTURE,
35
- mode: "qa",
36
- model: planner(),
37
- system: t.architectSystem(),
38
- })
39
-
40
- export const decompose = (): Promise<void> =>
41
- t.agentWithSkills("decompose.decomposing", t.architectureDecomposePrompt(), {
42
- label: "Decomposing into packages",
43
- model: planner(),
44
- system: t.architectSystem(),
45
- })
46
-
47
- export const answerProductQuestions = (): Promise<void> =>
48
- human("gate.answer", {
49
- message: t.designGateAnswerMessage(),
50
- label: "Awaiting your product answers",
51
- file: REQUIREMENTS,
52
- mode: "qa",
53
- acceptClean: true,
54
- })
55
-
56
- export const answerTechnicalQuestions = (): Promise<void> =>
57
- human("gate.answer", {
58
- message: t.architectureGateAnswerMessage(),
59
- label: "Awaiting your technical answers",
60
- file: ARCHITECTURE,
61
- mode: "qa",
62
- acceptClean: true,
63
- })
64
-
65
- // ── Packages ────────────────────────────────────────────────────────────────
66
-
67
- export const build = (pkg: string): Promise<void> =>
68
- t.agentWithSkills("building", t.packagesItemBuildingPrompt(pkg), {
69
- label: "Building",
70
- model: coder(),
71
- system: t.builderSystem(),
72
- })
73
-
74
- export const fixSuite = (): Promise<void> =>
75
- t.agentWithSkills("fix.suite.fixing", t.packagesItemFixSuitePrompt(), {
76
- label: "Fixing the check",
77
- file: FEEDBACK,
78
- model: coder(),
79
- system: t.builderSystem(),
80
- })
16
+ export const planner = (): string => env.plannerModel ?? ""
17
+ export const coder = (): string => env.coderModel ?? ""
81
18
 
82
19
  // ── Keeping the suite green ─────────────────────────────────────────────────
83
20
 
@@ -162,25 +99,6 @@ export const reviewing = (base: string, carry?: string): Promise<void> =>
162
99
  base,
163
100
  })
164
101
 
165
- /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
166
- export const answerReviewQuestions = (notes: readonly t.NoteInput[]): Promise<void> =>
167
- t.agentWithSkills("review.answer-review-questions", t.buildReviewAnswerQuestionsPrompt(notes), {
168
- label: "Answering your questions",
169
- file: REVIEW,
170
- mode: "review",
171
- model: planner(),
172
- system: t.reviewerSystem(),
173
- })
174
-
175
- /** Fix every `nit` note in one turn. */
176
- export const fixNits = (notes: readonly t.NoteInput[]): Promise<void> =>
177
- t.agentWithSkills("review.fix.nits.fixing", t.buildReviewFixNitsPrompt(notes), {
178
- label: "Fixing your nits",
179
- file: REVIEW,
180
- model: planner(),
181
- system: t.reviewerSystem(),
182
- })
183
-
184
102
  /** Fix the `Risk:`-marked notes the reviewer named; an empty turn means the risk was judged false. */
185
103
  export const fixRisks = (notes: readonly t.NoteInput[]): Promise<void> =>
186
104
  t.agentWithSkills("review.fix.risks.fixing", t.buildReviewFixRisksPrompt(notes), {
@@ -191,9 +109,12 @@ export const fixRisks = (notes: readonly t.NoteInput[]): Promise<void> =>
191
109
  allowEmpty: true,
192
110
  })
193
111
 
194
- export const awaitReview = (base: string): Promise<void> =>
112
+ export const awaitReview = (
113
+ base: string,
114
+ message = t.buildReviewAwaitReviewMessage(base),
115
+ ): Promise<void> =>
195
116
  human("review.await-review", {
196
- message: t.buildReviewAwaitReviewMessage(base),
117
+ message,
197
118
  label: "Awaiting your review",
198
119
  file: REVIEW,
199
120
  mode: "review",
@@ -206,14 +127,3 @@ export const reviewMissing = (round: string): Promise<void> =>
206
127
  message: t.buildReviewReviewMissingMessage(round),
207
128
  label: "Nothing to review",
208
129
  })
209
-
210
- /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
211
- export const collecting = (capture: string): Promise<void> =>
212
- t.agentWithSkills("review.collecting", t.buildReviewCollectingPrompt(capture), {
213
- label: "Collecting your feedback",
214
- file: REQUIREMENTS,
215
- mode: "qa",
216
- model: planner(),
217
- system: t.reviewerSystem(),
218
- allowEmpty: true,
219
- })