@pmelab/gtd 29.3.1 → 30.1.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 (54) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +64 -63
  3. package/claude/hooks/register.tsx +5 -0
  4. package/dist/chunks/{reserved-Dmpg6dAp.mjs → reserved-DDiiAZ8U.mjs} +35 -239
  5. package/dist/chunks/{tail-CLyE5VFR.mjs → tail-ILPlELuy.mjs} +1 -1
  6. package/dist/gtd.bundle.mjs +0 -5
  7. package/dist/testkit.mjs +0 -5
  8. package/dist/workflows/cleanup.mjs +2 -2
  9. package/dist/workflows/feature.mjs +17 -182
  10. package/dist/workflows/fix.mjs +2 -2
  11. package/dist/workflows/review.mjs +2 -2
  12. package/dist/workflows/rollout.mjs +1 -1
  13. package/dist/workflows.mjs +47 -416
  14. package/package.json +1 -1
  15. package/skills/authoring/SKILL.md +15 -27
  16. package/src/cli/Cli.test.ts +6 -6
  17. package/src/edge/Edge.test.ts +7 -7
  18. package/src/flags/index.test.ts +0 -4
  19. package/src/flags/index.ts +0 -1
  20. package/src/workflow/load.test.ts +1 -1
  21. package/src/workflows/feature/fast-path.feature +1 -2
  22. package/src/workflows/feature/feature.test.ts +115 -63
  23. package/src/workflows/feature/feature.ts +21 -38
  24. package/src/workflows/feature/flag-mode.feature +157 -0
  25. package/src/workflows/feature/index.ts +0 -1
  26. package/src/workflows/feature/one-step-land.feature +4 -6
  27. package/src/workflows/feature/planning.ts +2 -27
  28. package/src/workflows/feature/refactor-absorbs.feature +1 -2
  29. package/src/workflows/feature/refactor-first.feature +1 -3
  30. package/src/workflows/feature/refactor-review.feature +0 -1
  31. package/src/workflows/feature/risk-tier.feature +2 -3
  32. package/src/workflows/feature/seam-rejection.feature +1 -3
  33. package/src/workflows/feature/seam.feature +4 -6
  34. package/src/workflows/feature/seam.ts +2 -0
  35. package/src/workflows/feature/steps.test.ts +0 -20
  36. package/src/workflows/feature/steps.ts +1 -49
  37. package/src/workflows/feature/{seam-pin.feature → tier-pin.feature} +17 -11
  38. package/src/workflows/feature/tier.test.ts +3 -5
  39. package/src/workflows/feature/tier.ts +2 -2
  40. package/src/workflows/index.ts +0 -1
  41. package/src/workflows/shared/access.test.ts +0 -2
  42. package/src/workflows/shared/access.ts +0 -2
  43. package/src/workflows/shared/options.test.ts +14 -3
  44. package/src/workflows/shared/options.ts +17 -2
  45. package/src/workflows/shared/prose.ts +4 -11
  46. package/src/workflows/shared/reserved.ts +2 -2
  47. package/src/workflows/shared/skills.test.ts +0 -11
  48. package/src/workflows/shared/skills.ts +2 -9
  49. package/src/workflows/shared/steps.ts +0 -1
  50. package/src/workflows/shared/text.test.ts +20 -104
  51. package/src/workflows/shared/text.ts +28 -229
  52. package/src/workflows/shared/vars.ts +2 -3
  53. package/src/workflows/feature/packages.test.ts +0 -368
  54. package/src/workflows/feature/packages.ts +0 -173
@@ -5,6 +5,8 @@ import {
5
5
  type WorkflowBase,
6
6
  type WorkflowDoors,
7
7
  type WorkflowFactory,
8
+ refuse,
9
+ vars,
8
10
  } from "../../flows/index.js"
9
11
  import { defaultFlagRegistry, defaults, envDefaults } from "./vars.js"
10
12
 
@@ -23,18 +25,31 @@ export const ShippedOptions = Schema.Struct({
23
25
  testCommand: setting,
24
26
  plannerModel: setting,
25
27
  coderModel: setting,
28
+ flagMode: Schema.optionalWith(Schema.Boolean, { exact: true }),
26
29
  })
27
30
 
28
31
  export type ShippedOptions = Schema.Schema.Encoded<typeof ShippedOptions>
29
32
 
30
33
  const over = (
31
34
  base: Readonly<Record<string, string>>,
32
- options: Readonly<Record<string, string>>,
35
+ options: Readonly<Record<string, string | boolean>>,
33
36
  ): Record<string, string> => ({
34
37
  ...base,
35
- ...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name))),
38
+ ...Object.fromEntries(
39
+ Object.entries(options)
40
+ .filter(([name]) => Object.hasOwn(base, name))
41
+ .map(([name, value]) => [name, String(value)]),
42
+ ),
36
43
  })
37
44
 
45
+ /** The `flagMode` setting, pinned at process start. Anything but true/false fails rather than guess a side. */
46
+ export const flagModeOn = (): boolean => {
47
+ const value = vars.flagMode ?? defaults.flagMode!
48
+ if (value !== "true" && value !== "false")
49
+ refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`)
50
+ return value === "true"
51
+ }
52
+
38
53
  /** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
39
54
  export const shipped = (
40
55
  flow: Flow,
@@ -73,12 +73,6 @@ classified list of concerns, and hold the running product conversation
73
73
  with the human it depends on. You are read by that human — write
74
74
  plainly — not by a parser, except where a state says otherwise.`
75
75
 
76
- export const architectPersona = `You are the technical planning voice in gtd's build pipeline: take over
77
- once product concerns are settled, reading them cold, no carried
78
- conversation. Work out the *how* per concern — structure, data models,
79
- tech-stack choices, error handling — raise the technical open
80
- questions, then pin interfaces and tests in the architecture document.`
81
-
82
76
  export const reviewerPersona = `You are the independent reviewing mind in gtd's build pipeline —
83
77
  deliberately separate from whoever wrote the code, with no attachment
84
78
  to it. Write a structured review document grouping a diff into
@@ -88,11 +82,10 @@ and, when asked, fix the small nits the human flagged and the risks
88
82
  you marked yourself, each in one batch.
89
83
  Beyond that you never fix or build anything yourself.`
90
84
 
91
- export const builderPersona = `You are the TDD implementer in gtd's build pipeline: build one
92
- package's declared scope end to end, tests first, and come back to fix
85
+ export const builderPersona = `You are the TDD implementer in gtd's build pipeline: build the
86
+ task in front of you end to end, tests first, and come back to fix
93
87
  things when redirected — a failing check, or reviewer feedback. Stay
94
- strictly inside the package in front of you; never touch another
95
- package's files or code outside the task. Treat feedback as the
88
+ strictly inside that task; never touch code outside it. Treat feedback as the
96
89
  diagnosis to act on; discard it, with reason, only when simply wrong.`
97
90
 
98
91
  export const finisherPersona = `You are the closing identity in gtd's build pipeline — the last coder
@@ -136,7 +129,7 @@ export const questionBar = `- The goal is shared understanding, not a quota or a
136
129
  settle it (treat an explicit \`.gtd/TODO.md\` directive, a committed
137
130
  hand-edit, or — on a loop-back lap — the human's own review-round
138
131
  edit/note as settled); or it would be expensive to undo once
139
- packages are written
132
+ code is written
140
133
  - Where no gap in shared understanding exists, decide it yourself:
141
134
  record it under \`## Answered Questions\` as
142
135
  \`### <the point phrased as a question>\`
@@ -1,6 +1,6 @@
1
1
  import type { Summary } from "../../flows/index.js"
2
2
  import * as t from "./text.js"
3
- import { ARCHITECTURE, REQUIREMENTS, REVIEW } from "./steps.js"
3
+ import { REQUIREMENTS, REVIEW } from "./steps.js"
4
4
 
5
5
  export { defaults, envDefaults } from "./vars.js"
6
6
  export { skills } from "./skills.js"
@@ -8,4 +8,4 @@ export { access } from "./access.js"
8
8
 
9
9
  export const summary: Summary = t.summaryPrompt
10
10
 
11
- export const steering = { [REQUIREMENTS]: "qa", [ARCHITECTURE]: "qa", [REVIEW]: "review" }
11
+ export const steering = { [REQUIREMENTS]: "qa", [REVIEW]: "review" }
@@ -14,11 +14,6 @@ describe("the bundled workflow's skills export", () => {
14
14
  expect(Object.keys(bundled).sort()).toEqual(
15
15
  [
16
16
  "design",
17
- "architecture",
18
- "architecture.decompose",
19
- "packages.item",
20
- "packages.item.fix.suite",
21
- "packages.item.health",
22
17
  "build",
23
18
  "build.health",
24
19
  "build.fix.quality",
@@ -54,7 +49,6 @@ describe("the bundled workflow's skills export", () => {
54
49
 
55
50
  it("keys no group scope or non-agent step", () => {
56
51
  for (const group of [
57
- "packages.item.fix",
58
52
  "build.fix",
59
53
  "build.review.fix",
60
54
  "build.quality",
@@ -70,11 +64,6 @@ describe("the bundled workflow's skills export", () => {
70
64
  it("pins each scope's list", () => {
71
65
  expect(bundled).toMatchObject({
72
66
  design: ["spec-driven-development", "planning-and-task-breakdown"],
73
- architecture: ["api-and-interface-design", "documentation-and-adrs", "ponytail"],
74
- "architecture.decompose": ["incremental-implementation", "planning-and-task-breakdown"],
75
- "packages.item": ["test-driven-development", "incremental-implementation"],
76
- "packages.item.fix.suite": ["debugging-and-error-recovery"],
77
- "packages.item.health": ["debugging-and-error-recovery"],
78
67
  build: ["debugging-and-error-recovery"],
79
68
  "build.health": ["debugging-and-error-recovery"],
80
69
  "build.fix.quality": ["incremental-implementation", "code-simplification"],
@@ -4,7 +4,7 @@ import { lensesOf, lensScopes } from "../../workflow-kit/index.js"
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
6
6
  // nested scope with no entry inherits its parent's; group scopes
7
- // (`packages.item.fix`, `build.fix`, `build.review.fix`, `build.quality`, `refactor.fix`) run
7
+ // (`implement.fix`, `build.fix`, `build.review.fix`, `build.quality`, `refactor.fix`) run
8
8
  // no turn and carry no key. `build.quality.<lens>` is one key per
9
9
  // `qualityReviews` entry, so the export is a function of the vars.
10
10
  export const skills = (
@@ -20,12 +20,6 @@ export const skills = (
20
20
  }
21
21
  return {
22
22
  design: ["spec-driven-development", "planning-and-task-breakdown"],
23
- architecture: ["api-and-interface-design", "documentation-and-adrs", "ponytail"],
24
- "architecture.decompose": ["incremental-implementation", "planning-and-task-breakdown"],
25
- "packages.item": ["test-driven-development", "incremental-implementation"],
26
- "packages.item.fix.suite": ["debugging-and-error-recovery"],
27
- // `health` is shared code reached from both `packages.item` and `build`.
28
- "packages.item.health": ["debugging-and-error-recovery"],
29
23
  // `fix`'s turn over the notes a review handed on.
30
24
  notes: ["incremental-implementation", "code-simplification"],
31
25
  // The refactor's own turns load `ponytail`; `refactor.build.*` carry the `build.*` lists.
@@ -34,11 +28,10 @@ export const skills = (
34
28
  refactor: ["ponytail"],
35
29
  "refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
36
30
  "refactor.health": ["ponytail", "debugging-and-error-recovery"],
37
- // Declared whether or not `naive_pass` is on, so a config keyed on them keeps
38
- // loading when the flag flips off.
39
31
  seam: ["incremental-implementation"],
40
32
  implement: ["test-driven-development", "incremental-implementation"],
41
33
  "implement.fix.suite": ["debugging-and-error-recovery"],
34
+ // `health` is shared code reached from both `implement` and `build`.
42
35
  "implement.health": ["debugging-and-error-recovery"],
43
36
  ...lensScopes(
44
37
  "build.quality",
@@ -10,7 +10,6 @@ import * as t from "./text.js"
10
10
  export const FEEDBACK = ".gtd/FEEDBACK.md"
11
11
  export const ESCALATION = ".gtd/ESCALATION.md"
12
12
  export const REQUIREMENTS = ".gtd/REQUIREMENTS.md"
13
- export const ARCHITECTURE = ".gtd/ARCHITECTURE.md"
14
13
  export const REVIEW = ".gtd/REVIEW.md"
15
14
  export const REFACTOR = ".gtd/REFACTOR.md"
16
15
  export const QUALITY = ".gtd/QUALITY.md"
@@ -8,10 +8,6 @@ import {
8
8
  deferNotesScript,
9
9
  buildReviewReviewingPrompt,
10
10
  agentWithSkills,
11
- architectureAuthorPrompt,
12
- architectureDecomposePrompt,
13
- packagesItemBuildingPrompt,
14
- architectureGateAnswerMessage,
15
11
  buildFixPrompt,
16
12
  buildFixQualityPrompt,
17
13
  buildReviewFixNitsPrompt,
@@ -167,23 +163,6 @@ describe("the shared open-question instruction", () => {
167
163
  expect(prompt).toContain("one to four bullets, free-form")
168
164
  expect(prompt).toContain("labels, no fixed bullet count")
169
165
  })
170
-
171
- it("is the identical block, full text, in both the design gate and the architecture gate", () => {
172
- // Anchored at questionBar's own first and last lines (not a bullet
173
- // partway in) so the whole ~3.4k-char block is compared — a divergence
174
- // anywhere inside, including its opening bullets, fails this test the
175
- // moment either gate's copy drifts from the shared source.
176
- const start = "The goal is shared understanding, not a quota or an empty section"
177
- const end = "Never tick a box yourself — the human ticks exactly one per question"
178
- const sharedBlock = (prompt: string): string =>
179
- prompt.slice(prompt.indexOf(start), prompt.indexOf(end) + end.length)
180
-
181
- const designBlock = sharedBlock(renderText(() => designTriagePrompt("base")))
182
- const architectureBlock = sharedBlock(renderText(() => architectureAuthorPrompt()))
183
-
184
- expect(designBlock.length).toBeGreaterThan(3000)
185
- expect(designBlock).toBe(architectureBlock)
186
- })
187
166
  })
188
167
 
189
168
  describe("answered-entry footnote exception", () => {
@@ -196,32 +175,6 @@ describe("answered-entry footnote exception", () => {
196
175
  expect(prompt).not.toContain("never re-open a settled")
197
176
  expect(prompt).not.toContain("never re-open one")
198
177
  })
199
-
200
- it("is in the architecture return lap and keeps the first-lap settled line", () => {
201
- const prompt = renderText(() => architectureAuthorPrompt())
202
- expect(prompt).toContain(`entry stays settled, except one ${exception}`)
203
- expect(prompt).not.toContain("never re-open a settled")
204
- expect(prompt).toMatch(/treat every decision[\s\S]*as settled — never re-open it/)
205
- })
206
- })
207
-
208
- describe("architectureAuthorPrompt", () => {
209
- it("qualifies the PERMISSIVE default with a settled-requirements exception", () => {
210
- const prompt = renderText(() => architectureAuthorPrompt())
211
- expect(prompt).toContain("is an open question, not a")
212
- })
213
- it("names the four content sections in order and the entry forms", () => {
214
- const prompt = renderText(() => architectureAuthorPrompt())
215
- const at = ["## Interfaces", "## Call Stacks", "## E2E Scenarios", "## Unit Tests"].map((h) =>
216
- prompt.indexOf(h),
217
- )
218
- expect(at.every((i) => i >= 0)).toBe(true)
219
- expect([...at].sort((a, b) => a - b)).toEqual(at)
220
- expect(prompt).toContain("- unit: <path>")
221
- expect(prompt).toContain("- e2e: <path>")
222
- expect(prompt).toContain("- chore: <path>")
223
- expect(prompt).toContain("No e2e change.")
224
- })
225
178
  })
226
179
 
227
180
  describe("buildFixPrompt", () => {
@@ -688,16 +641,11 @@ Print the closing message and stop — this writes nothing itself.
688
641
  it("treats a flag-on process with no tier mark as high", () => {
689
642
  const prompt = summaryPrompt({
690
643
  ...base,
691
- vars: { flag_risk_tier: "1", flag_naive_pass: "1" },
644
+ vars: { flag_risk_tier: "1" },
692
645
  })
693
646
  expect(prompt).toContain("Risk tier: high")
694
647
  })
695
648
 
696
- it("marks no tier when the risk-tier flag is on without naive-pass", () => {
697
- const prompt = summaryPrompt({ ...base, vars: { flag_risk_tier: "1", flag_naive_pass: "0" } })
698
- expect(prompt).not.toContain("Risk tier")
699
- })
700
-
701
649
  it("says the human left no comment or edit this process when humanCommits is empty", () => {
702
650
  expect(summaryPrompt(base)).toContain(
703
651
  "The human left no comment or edit this process —\nevery commit is machine-authored.",
@@ -812,10 +760,9 @@ describe("buildReviewCollectingPrompt without a refactor", () => {
812
760
  })
813
761
 
814
762
  describe("the thread rules", () => {
815
- it("are in all three answering prompts and no longer say a footnote is human input only", () => {
763
+ it("are in both answering prompts and no longer say a footnote is human input only", () => {
816
764
  const prompts = renderText(() => [
817
765
  designTriagePrompt("base"),
818
- architectureAuthorPrompt(),
819
766
  buildReviewCollectingPrompt("capture"),
820
767
  ])
821
768
  for (const prompt of prompts) {
@@ -837,13 +784,9 @@ describe("code threads in prompts", () => {
837
784
  { path: "src/b.ts", line: 9, waitingOn: "human", first: "done", faults: [] },
838
785
  ] as const
839
786
 
840
- it("lists the code threads waiting on the agent in the three gates", () => {
787
+ it("lists the code threads waiting on the agent in both gates", () => {
841
788
  const prompts = renderText(
842
- () => [
843
- designTriagePrompt("base"),
844
- architectureAuthorPrompt(),
845
- buildReviewCollectingPrompt("capture"),
846
- ],
789
+ () => [designTriagePrompt("base"), buildReviewCollectingPrompt("capture")],
847
790
  { codeThreads: waiting },
848
791
  )
849
792
  for (const prompt of prompts) {
@@ -866,7 +809,6 @@ describe("code threads in prompts", () => {
866
809
  it("teaches the syntax once in the footnote rules", () => {
867
810
  for (const message of renderText(() => [
868
811
  designGateAnswerMessage(),
869
- architectureGateAnswerMessage(),
870
812
  buildReviewAwaitReviewMessage("base"),
871
813
  ])) {
872
814
  expect(message).toContain("gtd check --open-threads")
@@ -1048,6 +990,22 @@ describe("the seam phase's texts", () => {
1048
990
  expect(prompt).toContain("Implement them properly")
1049
991
  })
1050
992
 
993
+ it("flag mode off asks for the naive version and no release flag, whatever the tier or lap", () => {
994
+ for (const [tier, amend] of [
995
+ ["high", false],
996
+ ["low", false],
997
+ ["high", true],
998
+ ] as const) {
999
+ const prompt = renderText(() => implementBuildingPrompt(undefined, tier, amend, false, false))
1000
+ expect(prompt).toContain("Add no release flag — this project ships without release flags")
1001
+ expect(prompt).not.toContain("new release flag")
1002
+ expect(prompt).not.toContain("low risk tier")
1003
+ }
1004
+ expect(
1005
+ renderText(() => implementBuildingPrompt(undefined, "high", false, false, false)),
1006
+ ).toContain("the quickest way that works")
1007
+ })
1008
+
1051
1009
  it("keeps a fix turn to the feedback file's rules", () => {
1052
1010
  expect(renderText(() => implementFixPrompt())).toContain(".gtd/FEEDBACK.md")
1053
1011
  })
@@ -1306,45 +1264,3 @@ it("asks the refactor review, and only it, to flag a test-file hunk that is more
1306
1264
  expect(refactor).toContain("give any test-file hunk its own chunk")
1307
1265
  expect(first).not.toContain("give any test-file hunk its own chunk")
1308
1266
  })
1309
-
1310
- describe("architectureDecomposePrompt tests rule", () => {
1311
- it("states one rule: write-or-change, relied-on marked (unchanged), no other untouched", () => {
1312
- const prompt = renderText(() => architectureDecomposePrompt()).replace(/\s+/g, " ")
1313
- expect(prompt).toContain("List every test the package writes or changes")
1314
- expect(prompt).toContain("also list an existing test a criterion relies on")
1315
- expect(prompt).toContain("list no other untouched test")
1316
- expect(prompt).toContain("- unit: \\`<path>\\` (unchanged)")
1317
- expect(prompt).toContain("- e2e: \\`<path>\\` (unchanged)")
1318
- expect(prompt).toContain("only decompose places the marker")
1319
- })
1320
- })
1321
-
1322
- describe("packagesItemBuildingPrompt", () => {
1323
- const prompt = renderText(() => packagesItemBuildingPrompt(".gtd/packages/01-a.md")).replace(
1324
- /\s+/g,
1325
- " ",
1326
- )
1327
- it("carries the evidence kinds and the already-done check", () => {
1328
- for (const w of [
1329
- "evidence per criterion",
1330
- "commit, file, symbol, issue URL, or command output — and change nothing else",
1331
- "it was not already done",
1332
- "via `gh`",
1333
- ])
1334
- expect(prompt).toContain(w)
1335
- })
1336
- it("states the already-met, no-tests out-of-tree and implement-normally rules flat", () => {
1337
- expect(prompt).toContain("Every criterion already met in the tree")
1338
- expect(prompt).toContain("No `## Tests` and work outside the tree")
1339
- expect(prompt).toContain(
1340
- "Any other package**: implement normally, write no `.gtd/SATISFIED.md`",
1341
- )
1342
- })
1343
- it("says an (unchanged) line must exist and stay untouched", () => {
1344
- expect(prompt).toContain("(unchanged)")
1345
- expect(prompt).toContain("stay untouched")
1346
- expect(prompt).toContain("creating, editing or deleting it refuses the turn")
1347
- expect(prompt).toContain("must exist before the turn")
1348
- expect(prompt).toContain("stop without creating it")
1349
- })
1350
- })