@pmelab/gtd 57.0.0 → 58.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 (46) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +9 -8
  3. package/dist/chunks/{reserved-X2RkNHin.mjs → reserved-BzAFXuz_.mjs} +26 -19
  4. package/dist/chunks/{review-v7sh5GF4.mjs → review-CVqRS0mc.mjs} +2 -2
  5. package/dist/chunks/{tail-S04KEewi.mjs → tail-JQzB21Q3.mjs} +1 -1
  6. package/dist/workflows/cleanup.mjs +2 -2
  7. package/dist/workflows/feature.mjs +52 -13
  8. package/dist/workflows/fix.mjs +2 -2
  9. package/dist/workflows/review.mjs +3 -3
  10. package/dist/workflows/rollout.mjs +1 -1
  11. package/dist/workflows.mjs +74 -28
  12. package/package.json +1 -1
  13. package/src/workflows/feature/acceptance-criteria.feature +32 -0
  14. package/src/workflows/feature/criteria-coverage.feature +0 -1
  15. package/src/workflows/feature/criteria.ts +10 -4
  16. package/src/workflows/feature/fast-path.feature +4 -0
  17. package/src/workflows/feature/feature.test.ts +3 -39
  18. package/src/workflows/feature/feature.ts +3 -4
  19. package/src/workflows/feature/lens-models.feature +0 -1
  20. package/src/workflows/feature/lens-review.feature +0 -1
  21. package/src/workflows/feature/naive-acceptance.feature +4 -0
  22. package/src/workflows/feature/naive-test.feature +11 -6
  23. package/src/workflows/feature/one-step-land.feature +4 -0
  24. package/src/workflows/feature/planning.ts +2 -0
  25. package/src/workflows/feature/refactor-absorbs.feature +4 -0
  26. package/src/workflows/feature/refactor-first.feature +4 -0
  27. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  28. package/src/workflows/feature/refactor-rebuild.feature +4 -0
  29. package/src/workflows/feature/refactor-review.feature +20 -0
  30. package/src/workflows/feature/release-flag.feature +167 -0
  31. package/src/workflows/feature/release-flag.test.ts +49 -0
  32. package/src/workflows/feature/release-flag.ts +46 -0
  33. package/src/workflows/feature/review-gate.feature +0 -1
  34. package/src/workflows/feature/reviewer-walkthrough.feature +0 -1
  35. package/src/workflows/feature/risk-tier.feature +32 -0
  36. package/src/workflows/feature/seam-rejection.feature +8 -0
  37. package/src/workflows/feature/seam.feature +8 -0
  38. package/src/workflows/feature/seam.test.ts +20 -7
  39. package/src/workflows/feature/seam.ts +7 -7
  40. package/src/workflows/feature/standards.feature +0 -1
  41. package/src/workflows/shared/options.test.ts +3 -10
  42. package/src/workflows/shared/options.ts +0 -9
  43. package/src/workflows/shared/text.test.ts +36 -28
  44. package/src/workflows/shared/text.ts +37 -20
  45. package/src/workflows/shared/vars.ts +0 -1
  46. package/src/workflows/feature/flag-mode.feature +0 -88
@@ -912,45 +912,53 @@ describe("the seam phase's texts", () => {
912
912
  expect(prompt).not.toContain("rejected")
913
913
  })
914
914
 
915
- it("cites the reference as reading material only and asks for a new release flag", () => {
915
+ it("cites the reference as reading material only", () => {
916
916
  const prompt = renderText(() => implementBuildingPrompt("abc123"))
917
917
  expect(prompt).toContain("git show abc123")
918
918
  expect(prompt).toContain("never apply")
919
- expect(prompt).toContain("release flag")
920
- })
921
-
922
- it("high and undefined tiers carry the flag bullet; low says to add none", () => {
923
- const base = renderText(() => implementBuildingPrompt("abc123"))
924
- expect(renderText(() => implementBuildingPrompt("abc123", "high"))).toBe(base)
925
- expect(base).toContain("new release flag")
926
- const low = renderText(() => implementBuildingPrompt("abc123", "low"))
927
- expect(low).toContain("Add no release flag — this change is low risk tier")
928
- expect(low).not.toContain("new release flag")
929
- expect(low).toContain("git show abc123")
930
919
  })
931
920
 
932
921
  it("drops the git show line when there is no reference", () => {
933
- const prompt = renderText(() => implementBuildingPrompt(undefined, "low"))
922
+ const prompt = renderText(() => implementBuildingPrompt(undefined))
934
923
  expect(prompt).not.toContain("git show")
935
924
  expect(prompt).not.toContain("Reference only")
936
- expect(prompt).toContain("Add no release flag")
937
- expect(prompt).toContain("Implement them properly")
938
925
  })
939
926
 
940
- it("flag mode off asks for the naive version and no release flag, whatever the tier or lap", () => {
941
- for (const [tier, amend] of [
942
- ["high", false],
943
- ["low", false],
944
- ["high", true],
945
- ] as const) {
946
- const prompt = renderText(() => implementBuildingPrompt(undefined, tier, amend, false, false))
947
- expect(prompt).toContain("Add no release flag — this project ships without release flags")
927
+ it("asks for no flag and the naive version when the requirements ask for none, on any lap", () => {
928
+ for (const amend of [false, true]) {
929
+ const prompt = renderText(() => implementBuildingPrompt(undefined, amend))
930
+ expect(prompt).toContain("Add no release flag — the requirements ask for none")
948
931
  expect(prompt).not.toContain("new release flag")
949
- expect(prompt).not.toContain("low risk tier")
950
932
  }
951
- expect(
952
- renderText(() => implementBuildingPrompt(undefined, "high", false, false, false)),
953
- ).toContain("the quickest way that works")
933
+ expect(renderText(() => implementBuildingPrompt(undefined))).toContain(
934
+ "the quickest way that works",
935
+ )
936
+ })
937
+
938
+ it("asks for a new flag, kept on a later lap, when the requirements ask for one", () => {
939
+ const first = renderText(() => implementBuildingPrompt("abc123", false, false, { kind: "new" }))
940
+ expect(first).toContain("behind a new release flag, as the requirements ask")
941
+ expect(first).toContain("Implement them properly")
942
+ const amend = renderText(() => implementBuildingPrompt(undefined, true, false, { kind: "new" }))
943
+ expect(amend).toContain("Keep the release flag the earlier lap added")
944
+ })
945
+
946
+ it("names the existing flag the requirements ask for, and asks for no new one", () => {
947
+ const prompt = renderText(() =>
948
+ implementBuildingPrompt(undefined, false, false, { kind: "existing", name: "GTD_FLAG_BETA" }),
949
+ )
950
+ expect(prompt).toContain("behind the existing flag `GTD_FLAG_BETA`")
951
+ expect(prompt).toContain("add no new flag")
952
+ expect(prompt).toContain("Implement them properly")
953
+ })
954
+
955
+ it("explains the Release Flag section at triage, at the impact turn and at the design gate", () => {
956
+ for (const prompt of [() => designTriagePrompt("base"), () => designImpactPrompt()]) {
957
+ const text = renderText(prompt)
958
+ expect(text).toContain("`## Release Flag`")
959
+ expect(text).toContain("Never add one on your own judgement")
960
+ }
961
+ expect(renderText(() => designGateAnswerMessage())).toContain("**Ask for a release flag**")
954
962
  })
955
963
 
956
964
  it("keeps a fix turn to the feedback file's rules", () => {
@@ -992,7 +1000,7 @@ describe("the seam phase's texts", () => {
992
1000
  expect(message).not.toContain("outside the expected set")
993
1001
  })
994
1002
 
995
- it("promises no judge on an overrule with flagMode off", () => {
1003
+ it("promises no judge on an overrule when no release flag is asked for", () => {
996
1004
  for (const retry of [{ red: "abc123" }, { fault: "acceptance-internals: x", hash: "abc123" }]) {
997
1005
  const message = renderText(() => seamSteerMessage(retry, false))
998
1006
  expect(message).not.toContain("seam judge")
@@ -348,6 +348,19 @@ none is declared, fall back to directories and packages. Landing is
348
348
  refused while the section is missing.
349
349
  `
350
350
 
351
+ const releaseFlagBlock = `
352
+ ## Release flag
353
+
354
+ Only when the sketch asks outright for the change to ship behind a
355
+ feature or release flag, \`.gtd/REQUIREMENTS.md\` carries a
356
+ \`## Release Flag\` section whose first line is \`new\` (a new
357
+ flag) or \`existing: <NAME>\` (behind a flag that already exists,
358
+ named as the sketch names it). Never add one on your own judgement:
359
+ without the section the change ships unflagged. A later lap keeps the
360
+ section exactly as the last settled file had it, unless the human
361
+ changed it. Landing is refused while its first line is anything else.
362
+ `
363
+
351
364
  const riskTierBlock = `
352
365
  ## Risk tier and production signals
353
366
 
@@ -389,7 +402,7 @@ ${footnoteFoldIn}${codeThreadReplies()}
389
402
  or both
390
403
  - Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
391
404
  uncommitted and finish once it reflects this lap's own work
392
- ${acceptanceCriteriaBlock}${impactBlock}
405
+ ${acceptanceCriteriaBlock}${impactBlock}${releaseFlagBlock}
393
406
  Every lap re-explores and leaves \`## Impact\` present. On a return
394
407
  lap or review loop-back the human may have edited \`## Impact\` (or
395
408
  footnoted it): read that edit from history (\`git log -1 -p --
@@ -461,11 +474,12 @@ ${stateFileRules}
461
474
  - The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
462
475
  — no other files for notes or output
463
476
  - \`.gtd/REQUIREMENTS.md\` already holds the settled acceptance
464
- criteria. Add a \`## Impact\` section and change nothing else — a
465
- turn that touches anything outside it is refused
477
+ criteria. Add a \`## Impact\` section — and a \`## Release Flag\`
478
+ section when the sketch asks for one — and change nothing else: a
479
+ turn that touches anything outside them is refused
466
480
  - No human reviews this before the build: put any surprise in the
467
481
  Impact prose, never as a question
468
- ${impactBlock}
482
+ ${impactBlock}${releaseFlagBlock}
469
483
  - Leave \`.gtd/REQUIREMENTS.md\` uncommitted and finish once it holds \`## Impact\`
470
484
  `
471
485
 
@@ -543,6 +557,7 @@ What each change does next (then run \`gtd land\`):
543
557
  - **Accept as-is** — change nothing and re-run; the plan stands as written. Refused while a thread is open.
544
558
  - **Revise answers** — tick exactly one option per open question (replace \`_your answer_\` for your own) to send it back for the agent to fold your answers in, or delete a question to skip it.
545
559
  - **Discuss** — start or continue a thread (\`- H:\`); the agent replies and this gate stops again.
560
+ - **Ask for a release flag** — add \`## Release Flag\` with \`new\` or \`existing: <NAME>\` as its first line, or delete the section to ship unflagged.
546
561
  ${riskTier ? RISK_TIER_GATE_LINE : ""}`
547
562
 
548
563
  export const builderSystem = (): string =>
@@ -1532,26 +1547,28 @@ const afterRefactorLine = (rebuild: Rebuild): string =>
1532
1547
  rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
1533
1548
  `
1534
1549
 
1535
- const flagLine = (
1536
- flagMode: boolean,
1537
- tier: "low" | "high" | undefined,
1538
- amend: boolean | undefined,
1539
- ): string =>
1540
- !flagMode
1541
- ? "- Add no release flag — this project ships without release flags"
1542
- : tier === "low"
1543
- ? "- Add no release flag — this change is low risk tier"
1550
+ /** What \`## Release Flag\` in \`.gtd/REQUIREMENTS.md\` asks for; no section is \`none\`. */
1551
+ export type ReleaseFlag =
1552
+ | { readonly kind: "none" }
1553
+ | { readonly kind: "new" }
1554
+ | { readonly kind: "existing"; readonly name: string }
1555
+
1556
+ const flagLine = (flag: ReleaseFlag, amend: boolean | undefined): string =>
1557
+ flag.kind === "none"
1558
+ ? "- Add no release flag — the requirements ask for none"
1559
+ : flag.kind === "existing"
1560
+ ? `- Put the new behaviour behind the existing flag \`${flag.name}\`, as the
1561
+ requirements ask — add no new flag`
1544
1562
  : amend === true
1545
1563
  ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag"
1546
- : `- Put the new behaviour behind a new release flag, following the
1547
- repository's flag conventions`
1564
+ : `- Put the new behaviour behind a new release flag, as the requirements ask,
1565
+ following the repository's flag conventions`
1548
1566
 
1549
1567
  export const implementBuildingPrompt = (
1550
1568
  reference: string | undefined,
1551
- tier?: "low" | "high",
1552
1569
  amend?: boolean,
1553
1570
  afterRefactor?: boolean,
1554
- flagMode = true,
1571
+ flag: ReleaseFlag = { kind: "none" },
1555
1572
  rebuild: Rebuild = {},
1556
1573
  ): string =>
1557
1574
  `${stateFileRules}
@@ -1563,7 +1580,7 @@ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${
1563
1580
  ? `- The change already exists in the tree from an earlier lap; amend it to meet
1564
1581
  the re-derived requirements instead of rewriting it
1565
1582
  `
1566
- : flagMode
1583
+ : flag.kind !== "none"
1567
1584
  ? `- Implement them properly, fitting the codebase's seams and conventions
1568
1585
  `
1569
1586
  : `- Implement them the quickest way that works — the naive version. Do not
@@ -1576,7 +1593,7 @@ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${
1576
1593
  Read it for ideas; never apply, cherry-pick or check it out — write
1577
1594
  the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
1578
1595
  `
1579
- }${rebuildLines(rebuild)}${flagLine(flagMode, tier, amend)}
1596
+ }${rebuildLines(rebuild)}${flagLine(flag, amend)}
1580
1597
  - Write the tests first, then the code that makes them pass
1581
1598
  ${behaviourTests}
1582
1599
  - Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
@@ -1599,7 +1616,7 @@ An edit together with an approval counts as steer. Only
1599
1616
  branch unjudged.
1600
1617
  `
1601
1618
 
1602
- /** `judged`: whether an overruled pass still meets the seam judge (`flagMode` on). */
1619
+ /** `judged`: whether an overruled pass still meets the seam judge (a release flag asked for). */
1603
1620
  export const seamSteerMessage = (retry: NaiveRetry, judged = true): string =>
1604
1621
  isFault(retry)
1605
1622
  ? `The naive pass failed three times and the retries are spent. The last
@@ -28,7 +28,6 @@ export const defaults: Readonly<Record<string, string>> = {
28
28
  standardMinP: "0.8",
29
29
  refactorFlagPattern: "\\bGTD_FLAG_\\w+",
30
30
  seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
31
- flagMode: "true",
32
31
  // A person tries every green naive pass at `seam.try` before anything builds on it.
33
32
  naiveTest: "false",
34
33
  // GTD_FLAG_RISK_TIER, recorded at process start; only "1" is on.
@@ -1,88 +0,0 @@
1
- @inmem
2
- Feature: flag mode off lands the naive pass with no release flag, seam judge or refactor
3
-
4
- `flagMode` is a factory option of the shipped workflows, on by default. Off,
5
- the project ships without feature flags: no seam judge and no refactor run,
6
- and the naive pass itself lands, straight through the health loop and the
7
- quality lap.
8
-
9
- Background:
10
- Given a gtd config with:
11
- """
12
- import { feature } from "@pmelab/gtd/workflows/feature"
13
- export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
14
- export default feature({ flagMode: false })
15
- """
16
- And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
17
- And a file "src/calc.ts" with:
18
- """
19
- export const add = (a: number, b: number) => a + b
20
- """
21
- And the working tree is committed as "feat: add calculator"
22
- And a file "TODO.md" with:
23
- """
24
- # Add subtract
25
-
26
- subtract(a, b) returns the difference.
27
- """
28
- And step "unwind" changes nothing
29
- And step "start-gate.check" changes nothing
30
- And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
31
- """
32
- # Requirements
33
-
34
- ## Concerns
35
-
36
- ### Subtract
37
-
38
- - `subtract(a, b)` returns `a - b`.
39
-
40
- ## Impact
41
-
42
- - the greeting module
43
-
44
- ## Acceptance Criteria
45
-
46
- - AC-1: the change behaves as described.
47
- """
48
- And step "design.gate.answer" changes nothing
49
-
50
- Scenario: flag mode off lands the naive pass unjudged, and asks for no flag
51
- When the process is driven
52
- Then the process rests at "seam.naive"
53
- And the resting step's content contains "the quickest way that works"
54
- And the resting step's content does not contain "release flag"
55
- Given step "seam.naive" writes "src/calc.test.ts" with:
56
- """
57
- it("[AC-1] subtracts", () => {})
58
- """
59
- And step "seam.naive" writes "src/calc.ts" with:
60
- """
61
- export const add = (a: number, b: number) => a + b
62
- export const subtract = (a: number, b: number) => a - b
63
- """
64
- And step "seam.check" changes nothing
65
- And step "implement.applying" runs its script
66
- And step "implement.health.check" changes nothing
67
- And step "implement.closing" runs its script
68
- And step "build.quality.correctness.reviewing" changes nothing
69
- When the process is driven
70
- Then the process rests at "build.review.reviewing"
71
- And "src/calc.ts" contains "subtract"
72
- And "src/calc.test.ts" contains "[AC-1] subtracts"
73
- And the commit subjects end with:
74
- """
75
- chore: initial commit
76
- feat: add calculator
77
- gtd(human): idle → unwind
78
- gtd(check): unwind → start-gate.check
79
- gtd(check): start-gate.check → design.triage
80
- gtd(agent): design.triage → design.gate.answer
81
- gtd(human): design.gate.answer → seam.naive
82
- gtd(agent): seam.naive → seam.check
83
- gtd(check): seam.check → implement.applying
84
- gtd(check): implement.applying → implement.health.check
85
- gtd(check): implement.health.check → implement.closing
86
- gtd(check): implement.closing → build.quality.correctness.reviewing
87
- gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
88
- """