@pmelab/gtd 57.0.0 → 58.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 (88) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +12 -10
  3. package/dist/chunks/{Schema-C6fLlQzx.mjs → Schema-B3o6eRNU.mjs} +637 -3957
  4. package/dist/chunks/{reserved-X2RkNHin.mjs → reserved-lMYCaapY.mjs} +27 -20
  5. package/dist/chunks/{review-v7sh5GF4.mjs → review-Bfaror38.mjs} +2 -2
  6. package/dist/chunks/{tail-S04KEewi.mjs → tail-Bd7L940p.mjs} +1 -1
  7. package/dist/chunks/testkit-D1-auYau.mjs +10362 -0
  8. package/dist/gtd.bundle.mjs +8 -8
  9. package/dist/testkit/gherkin.mjs +255 -0
  10. package/dist/testkit.mjs +2 -15461
  11. package/dist/workflows/cleanup.mjs +2 -2
  12. package/dist/workflows/feature.mjs +52 -13
  13. package/dist/workflows/fix.mjs +2 -2
  14. package/dist/workflows/review.mjs +3 -3
  15. package/dist/workflows/rollout.mjs +1 -1
  16. package/dist/workflows.mjs +74 -28
  17. package/package.json +8 -1
  18. package/src/cli/Cli.test.ts +1 -1
  19. package/src/cli/History.test.ts +1 -1
  20. package/src/cli/Metrics.test.ts +1 -1
  21. package/src/cli/Program.test.ts +1 -1
  22. package/src/cli/Strays.test.ts +1 -1
  23. package/src/edge/Edge.test.ts +1 -1
  24. package/src/emit/Emit.ts +1 -1
  25. package/src/emit/GitScript.ts +1 -1
  26. package/src/emit/OutcomeScript.ts +1 -1
  27. package/src/{testkit/steps.ts → gherkin/index.ts} +20 -3
  28. package/src/gherkin/module-config.feature +24 -0
  29. package/src/gherkin/tiny.fixture.ts +8 -0
  30. package/src/platform/Git.test.ts +2 -2
  31. package/src/platform/RetainedHistory.test.ts +2 -2
  32. package/src/platform/Workspace.test.ts +2 -2
  33. package/src/platform/WorktreeState.test.ts +1 -1
  34. package/src/step/planStart.test.ts +1 -1
  35. package/src/testkit/TestRepo.ts +7 -4
  36. package/src/testkit/index.ts +15 -9
  37. package/src/{testkit → twins}/Layers.ts +2 -2
  38. package/src/twins/index.ts +8 -0
  39. package/src/workflow/discovery.test.ts +1 -1
  40. package/src/workflow/discovery.ts +3 -3
  41. package/src/workflow/load.ts +3 -3
  42. package/src/workflows/feature/acceptance-criteria.feature +32 -0
  43. package/src/workflows/feature/criteria-coverage.feature +0 -1
  44. package/src/workflows/feature/criteria.ts +10 -4
  45. package/src/workflows/feature/fast-path.feature +4 -0
  46. package/src/workflows/feature/feature.test.ts +3 -39
  47. package/src/workflows/feature/feature.ts +3 -4
  48. package/src/workflows/feature/lens-models.feature +0 -1
  49. package/src/workflows/feature/lens-review.feature +0 -1
  50. package/src/workflows/feature/naive-acceptance.feature +4 -0
  51. package/src/workflows/feature/naive-test.feature +11 -6
  52. package/src/workflows/feature/one-step-land.feature +4 -0
  53. package/src/workflows/feature/planning.ts +2 -0
  54. package/src/workflows/feature/refactor-absorbs.feature +4 -0
  55. package/src/workflows/feature/refactor-first.feature +4 -0
  56. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  57. package/src/workflows/feature/refactor-rebuild.feature +4 -0
  58. package/src/workflows/feature/refactor-review.feature +20 -0
  59. package/src/workflows/feature/release-flag.feature +167 -0
  60. package/src/workflows/feature/release-flag.test.ts +49 -0
  61. package/src/workflows/feature/release-flag.ts +46 -0
  62. package/src/workflows/feature/review-gate.feature +0 -1
  63. package/src/workflows/feature/reviewer-walkthrough.feature +0 -1
  64. package/src/workflows/feature/risk-tier.feature +32 -0
  65. package/src/workflows/feature/seam-rejection.feature +8 -0
  66. package/src/workflows/feature/seam.feature +8 -0
  67. package/src/workflows/feature/seam.test.ts +20 -7
  68. package/src/workflows/feature/seam.ts +7 -7
  69. package/src/workflows/feature/standards.feature +0 -1
  70. package/src/workflows/shared/options.test.ts +3 -10
  71. package/src/workflows/shared/options.ts +0 -9
  72. package/src/workflows/shared/text.test.ts +36 -28
  73. package/src/workflows/shared/text.ts +37 -20
  74. package/src/workflows/shared/vars.ts +0 -1
  75. package/src/workflows/feature/flag-mode.feature +0 -88
  76. /package/src/{testkit → gherkin}/testkit.feature +0 -0
  77. /package/src/{testkit → twins}/EmittedScriptRecognizer.test.ts +0 -0
  78. /package/src/{testkit → twins}/EmittedScriptRecognizer.ts +0 -0
  79. /package/src/{testkit → twins}/EmittedScripts.test.ts +0 -0
  80. /package/src/{testkit → twins}/FakeGitOperations.ts +0 -0
  81. /package/src/{testkit → twins}/FlowScripts.test.ts +0 -0
  82. /package/src/{testkit → twins}/FlowScripts.ts +0 -0
  83. /package/src/{testkit → twins}/GitTiers.test.ts +0 -0
  84. /package/src/{testkit → twins}/GitTiers.ts +0 -0
  85. /package/src/{testkit → twins}/InMemRepo.ts +0 -0
  86. /package/src/{testkit → twins}/Layers.test.ts +0 -0
  87. /package/src/{testkit → twins}/StrictGitOperations.ts +0 -0
  88. /package/src/{testkit → twins}/cliIo.ts +0 -0
@@ -11,7 +11,15 @@ import {
11
11
  } from "quickpickle"
12
12
  import assert from "node:assert"
13
13
  import type { TestContext } from "vitest"
14
- import { runsItsScript, testRepo, type Actor, type TestRepo } from "./TestRepo.js"
14
+ import { resolve } from "node:path"
15
+ import { pathToFileURL } from "node:url"
16
+ import {
17
+ runsItsScript,
18
+ testRepo,
19
+ type Actor,
20
+ type TestRepo,
21
+ type WorkflowModule,
22
+ } from "../testkit/index.js"
15
23
 
16
24
  export class TestkitWorld extends QuickPickleWorld {
17
25
  // Re-declared: under `nodenext` a subclass loses the base's members from TS's view.
@@ -21,7 +29,7 @@ export class TestkitWorld extends QuickPickleWorld {
21
29
  super(context, info)
22
30
  }
23
31
 
24
- private gtdConfig: string | undefined = undefined
32
+ private gtdConfig: WorkflowModule | undefined = undefined
25
33
  private env: Record<string, string> = {}
26
34
  /** Per step, what its scripted actor does, in the order the scenario declared it. */
27
35
  actions = new Map<string, Actor[]>()
@@ -33,7 +41,7 @@ export class TestkitWorld extends QuickPickleWorld {
33
41
  // The repository is created on first use and keeps the config and
34
42
  // environment it was created with, so a later change to either is refused
35
43
  // rather than silently ignored.
36
- configure(config: string): void {
44
+ configure(config: WorkflowModule): void {
37
45
  this.assertNotCreated("a gtd config")
38
46
  this.gtdConfig = config
39
47
  }
@@ -41,6 +49,8 @@ export class TestkitWorld extends QuickPickleWorld {
41
49
  extendConfig(tail: string): void {
42
50
  this.assertNotCreated("a gtd config")
43
51
  if (this.gtdConfig === undefined) throw new Error("no gtd config to extend")
52
+ if (typeof this.gtdConfig !== "string")
53
+ throw new Error("a gtd config from a module cannot be extended")
44
54
  this.gtdConfig += `\n${tail}`
45
55
  }
46
56
 
@@ -108,6 +118,13 @@ Given("a gtd config with:", (world: TestkitWorld, source: string) => {
108
118
  world.configure(withNewline(source))
109
119
  })
110
120
 
121
+ // Imported by the test runner itself, from the working directory: the module and
122
+ // the engine share one `@pmelab/gtd/flows`, and a config source string could
123
+ // import only files written into the in-memory repository.
124
+ Given("a gtd config from the module {string}", async (world: TestkitWorld, path: string) => {
125
+ world.configure({ ...(await import(pathToFileURL(resolve(path)).href)) })
126
+ })
127
+
111
128
  Given("the gtd config ends with:", (world: TestkitWorld, source: string) => {
112
129
  world.extendConfig(withNewline(source))
113
130
  })
@@ -0,0 +1,24 @@
1
+ @inmem
2
+ Feature: a scenario runs a workflow module from disk
3
+
4
+ The test runner imports the module, so it shares the engine's runtime and
5
+ may import anything on disk. A config written as source can import only
6
+ files inside the in-memory repository.
7
+
8
+ Scenario: the module's default export starts on a change in the tree
9
+ Given a gtd config from the module "src/gherkin/tiny.fixture.ts"
10
+ And a file "IDEA.md" with:
11
+ """
12
+ a calculator
13
+ """
14
+ And step "plan" writes "PLAN.md" with:
15
+ """
16
+ add two numbers
17
+ """
18
+ When the process is driven
19
+ Then the process rests at "sign-off"
20
+ And the commit subjects end with:
21
+ """
22
+ gtd(agent): plan → sign-off
23
+ """
24
+
@@ -0,0 +1,8 @@
1
+ import { agent, human } from "../flows/index.js"
2
+
3
+ export const tiny = async (): Promise<void> => {
4
+ await agent("plan", "Write PLAN.md.")
5
+ await human("sign-off")
6
+ }
7
+
8
+ export default tiny
@@ -7,11 +7,11 @@ import { Cause, Effect, Exit, Option } from "effect"
7
7
  import { describe, expect, it } from "vitest"
8
8
  import { GitService, InvalidRefError, resolvedRefOrCorrupted } from "./Git.js"
9
9
  import { Host } from "./index.js"
10
- import { gitTiers, runGitServiceContract, runGitScriptContract } from "../testkit/index.js"
10
+ import { gitTiers, runGitServiceContract, runGitScriptContract } from "../twins/index.js"
11
11
 
12
12
  /**
13
13
  * The `GitOperations` contract, run identically against both the Live and
14
- * InMemory tiers (`src/testkit/GitTiers.ts`) — 20 operations × 2 tiers.
14
+ * InMemory tiers (`src/twins/GitTiers.ts`) — 20 operations × 2 tiers.
15
15
  */
16
16
  for (const makeTier of gitTiers) {
17
17
  const name = makeTier().name
@@ -1,6 +1,6 @@
1
1
  import { Effect, Option } from "effect"
2
2
  import { describe, expect, it, vi } from "vitest"
3
- import { strictGitOperations as stubGit } from "../testkit/index.js"
3
+ import { strictGitOperations as stubGit } from "../twins/index.js"
4
4
  import {
5
5
  clearRetainedHistory,
6
6
  HISTORY_REF,
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  /**
15
15
  * Unit coverage for the retention edge module (`src/platform/RetainedHistory.ts`),
16
- * using `strictGitOperations` (`src/testkit/StrictGitOperations.ts`) as its
16
+ * using `strictGitOperations` (`src/twins/StrictGitOperations.ts`) as its
17
17
  * `GitOperations` double — a Proxy failing on any method a test didn't
18
18
  * override, so an unexpected call fails loudly instead of silently succeeding.
19
19
  */
@@ -15,7 +15,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"
15
15
  import { Effect, Layer } from "effect"
16
16
  import { NodeContext } from "@effect/platform-node"
17
17
  import { GitService, Host, Workspace, type WorkspaceOps } from "./index.js"
18
- import { InMemRepo, makeInMemoryWorkspaceOps } from "../testkit/index.js"
18
+ import { InMemRepo, makeInMemoryWorkspaceOps } from "../twins/index.js"
19
19
 
20
20
  // `execFileSync` is a named export of the built-in `node:child_process` ESM
21
21
  // module — not configurable, so `vi.spyOn` can't wrap it directly. `vi.mock`
@@ -31,7 +31,7 @@ vi.mock("node:child_process", async (importOriginal) => {
31
31
  })
32
32
 
33
33
  /**
34
- * One `Workspace` tier, in the shape `src/testkit/GitTiers.ts` establishes
34
+ * One `Workspace` tier, in the shape `src/twins/GitTiers.ts` establishes
35
35
  * for `GitOperations`: a `root`, a way to seed working-tree/committed
36
36
  * content, and `provide` to run an Effect against this tier's `Workspace`.
37
37
  */
@@ -16,7 +16,7 @@ import { NodeContext } from "@effect/platform-node"
16
16
  import { describe, expect, it } from "vitest"
17
17
  import { loopLogPath, worktreeGitDir } from "./WorktreeState.js"
18
18
  import { GitService, Host, Workspace } from "./index.js"
19
- import { InMemRepo, testLayers } from "../testkit/index.js"
19
+ import { InMemRepo, testLayers } from "../twins/index.js"
20
20
 
21
21
  const provide = <A>(
22
22
  eff: Effect.Effect<A, never, Host | Workspace>,
@@ -1,7 +1,7 @@
1
1
  import { Effect } from "effect"
2
2
  import { describe, expect, it } from "vitest"
3
3
  import { planStart, type StartOutcome } from "./planStart.js"
4
- import { InMemRepo, testLayers } from "../testkit/index.js"
4
+ import { InMemRepo, testLayers } from "../twins/index.js"
5
5
  import { ConfigService } from "../workflow/index.js"
6
6
 
7
7
  const WORKFLOW = `import { agent, human } from "@pmelab/gtd/flows"
@@ -2,10 +2,13 @@ import { Effect } from "effect"
2
2
  import { PassThrough } from "node:stream"
3
3
  import { runCli } from "../cli/index.js"
4
4
  import { EXIT_OK, type BeatDocument } from "../wire/index.js"
5
- import { makeCapturingCliIo, type CapturedCliResult } from "./cliIo.js"
6
- import { applyEmittedScript } from "./EmittedScriptRecognizer.js"
7
- import { applyFlowScript } from "./FlowScripts.js"
8
- import { InMemRepo } from "./InMemRepo.js"
5
+ import {
6
+ applyEmittedScript,
7
+ applyFlowScript,
8
+ InMemRepo,
9
+ makeCapturingCliIo,
10
+ type CapturedCliResult,
11
+ } from "../twins/index.js"
9
12
 
10
13
  /**
11
14
  * The repository's `gtd.config.ts`: its source, or the module itself. A module
@@ -1,9 +1,15 @@
1
- export { runsItsScript, testRepo, type Actor, type TestRepo } from "./TestRepo.js"
2
- export { InMemRepo } from "./InMemRepo.js"
3
- export { makeInMemoryWorkspaceOps, testLayers, type ScriptedCommand } from "./Layers.js"
4
- export { makeCapturingCliIo } from "./cliIo.js"
5
- export { applyEmittedScript } from "./EmittedScriptRecognizer.js"
6
- export { gitTiers, runGitServiceContract, runGitScriptContract, type GitTier } from "./GitTiers.js"
7
- export { strictGitOperations } from "./StrictGitOperations.js"
8
- export { fakeGitOperations } from "./FakeGitOperations.js"
9
- export type { TestkitWorld } from "./steps.js"
1
+ // The public testkit: runs any workflow against the real engine in an in-memory
2
+ // repository. It imports no test runner, so any runner can use it; the
3
+ // gherkin steps are the opt-in \`@pmelab/gtd/testkit/gherkin\`.
4
+ export {
5
+ runsItsScript,
6
+ testRepo,
7
+ type Actor,
8
+ type Actors,
9
+ type DriveOptions,
10
+ type TestRepo,
11
+ type TestRepoOptions,
12
+ type WorkflowModule,
13
+ } from "./TestRepo.js"
14
+ export { InMemRepo, type CapturedCliResult } from "../twins/index.js"
15
+ export type { TestkitWorld } from "../gherkin/index.js"
@@ -4,7 +4,7 @@
4
4
  // gtd itself spawns no subprocess at all any more (`gtd renders; the driver
5
5
  // executes`) — a mode's `format:`/`validate:` command is emitted text, never
6
6
  // run in-process. `ScriptedCommand` below still exists for the world's own
7
- // fake-shell interpreter (`src/testkit/EmittedScriptRecognizer.ts`), which
7
+ // fake-shell interpreter (`src/twins/EmittedScriptRecognizer.ts`), which
8
8
  // simulates a driver running gtd's emitted scripts against the in-memory
9
9
  // worktree — a wholly separate mechanism from any gtd-internal layer.
10
10
 
@@ -193,7 +193,7 @@ export interface TestWorldOptions {
193
193
  * Effect test has no `--verbose` flag of its own to gate on.
194
194
  */
195
195
  readonly narrate?: (line: string) => void
196
- /** Gates `narrate` above — defaults to `true` (fires whenever a sink is given) since these tests aren't exercising `Cli.ts`'s own `--verbose` gate. `src/testkit/cliIo.ts` overrides this with the real decoded flag. */
196
+ /** Gates `narrate` above — defaults to `true` (fires whenever a sink is given) since these tests aren't exercising `Cli.ts`'s own `--verbose` gate. `src/twins/cliIo.ts` overrides this with the real decoded flag. */
197
197
  readonly verbose?: boolean
198
198
  /** Real or fake driver and tool commands — each one left out fails when called. */
199
199
  readonly tools?: Partial<ToolCommands["Type"]>
@@ -0,0 +1,8 @@
1
+ export { InMemRepo } from "./InMemRepo.js"
2
+ export { makeInMemoryWorkspaceOps, testLayers, type ScriptedCommand } from "./Layers.js"
3
+ export { makeCapturingCliIo, type CapturedCliResult } from "./cliIo.js"
4
+ export { applyEmittedScript } from "./EmittedScriptRecognizer.js"
5
+ export { applyFlowScript } from "./FlowScripts.js"
6
+ export { gitTiers, runGitServiceContract, runGitScriptContract, type GitTier } from "./GitTiers.js"
7
+ export { strictGitOperations } from "./StrictGitOperations.js"
8
+ export { fakeGitOperations } from "./FakeGitOperations.js"
@@ -6,7 +6,7 @@ import { NodeContext } from "@effect/platform-node"
6
6
  import { Effect, Layer } from "effect"
7
7
  import { afterEach, describe, expect, it } from "vitest"
8
8
  import { GitService, Host, Narrator, Workspace } from "../platform/index.js"
9
- import { InMemRepo, testLayers } from "../testkit/index.js"
9
+ import { InMemRepo, testLayers } from "../twins/index.js"
10
10
  import { ConfigDiscovery, ConfigService } from "./index.js"
11
11
 
12
12
  // A config and the local modules it imports load the same from disk, where
@@ -38,7 +38,7 @@ export const WORKFLOW_MODULE = "gtd.config.ts"
38
38
  * The file names gtd read repository settings from before `gtd.config.ts` took
39
39
  * them over. Still looked for, only to refuse them: a leftover one would
40
40
  * otherwise be silently ignored. Shared with the in-memory counterpart in
41
- * `src/testkit/Layers.ts`, so both tiers refuse the same names.
41
+ * `src/twins/Layers.ts`, so both tiers refuse the same names.
42
42
  */
43
43
  export const LEGACY_RC_FILES = [
44
44
  ".gtdrc",
@@ -71,7 +71,7 @@ const legacyRcFileAt = (path: string): LegacyRcFile | undefined => {
71
71
  * the user's home dir (inclusive, when it is an ancestor) or after reaching the
72
72
  * filesystem root — whichever comes first. Returned innermost→outermost.
73
73
  * Exported (via the barrel) for the same reason `SEARCH_PLACES` is — one
74
- * shared implementation, not a second copy in `src/testkit/Layers.ts` that
74
+ * shared implementation, not a second copy in `src/twins/Layers.ts` that
75
75
  * could silently diverge.
76
76
  */
77
77
  export const walkUp = (from: string, home: string): ReadonlyArray<string> => {
@@ -115,7 +115,7 @@ const workflowModule = (
115
115
  * reads; discovery is effectful and environment-dependent: `.Live` reads
116
116
  * REAL disk, so an `@inmem` scenario's fake
117
117
  * `Workspace` — a pure in-memory `Map`, no real files anywhere on disk —
118
- * cannot back it. `src/testkit/Layers.ts` provides the in-memory
118
+ * cannot back it. `src/twins/Layers.ts` provides the in-memory
119
119
  * counterpart instead, mirroring how `Workspace`/`Host`/`GitService` already
120
120
  * each have a production and a fake layer behind one Tag.
121
121
  */
@@ -357,14 +357,14 @@ const shippedModules = (): Promise<ShippedModules> =>
357
357
  return { modules, missing }
358
358
  })())
359
359
 
360
- /** The file each subpath a config may import serves: all but `flows`, the engine's own, and `testkit`. */
360
+ /** The file each subpath a config may import serves: all but `flows`, the engine's own, and the testkit's. */
361
361
  const subpathFiles = (condition: string): ReadonlyMap<string, string> => {
362
362
  const { root, pkg } = ownPackage()
363
363
  const files = new Map<string, string>()
364
364
  for (const [subpath, target] of Object.entries(pkg.exports)) {
365
365
  const file = typeof target === "string" ? target : target[condition]
366
- // testkit imports the consumer's test runner, and no config runs one.
367
- if (subpath === "./flows" || subpath === "./testkit" || file === undefined) continue
366
+ // A config runs no tests, and the gherkin steps import the consumer's test runner.
367
+ if (subpath === "./flows" || subpath.startsWith("./testkit") || file === undefined) continue
368
368
  files.set(`${PACKAGE}/${subpath.replace(/^\.\//, "")}`, join(root, file))
369
369
  }
370
370
  return files
@@ -39,6 +39,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
39
39
 
40
40
  - the greeting module
41
41
 
42
+ ## Release Flag
43
+
44
+ new
45
+
42
46
  ## Acceptance Criteria
43
47
 
44
48
  - AC-1: `greeting` reads "Hello, world".
@@ -67,6 +71,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
67
71
 
68
72
  - the greeting module
69
73
 
74
+ ## Release Flag
75
+
76
+ new
77
+
70
78
  ## Acceptance Criteria
71
79
 
72
80
  - `greeting` reads "Hello, world".
@@ -82,6 +90,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
82
90
 
83
91
  - the greeting module
84
92
 
93
+ ## Release Flag
94
+
95
+ new
96
+
85
97
  ## Acceptance Criteria
86
98
 
87
99
  - AC-1: `greeting` reads "Hello, world".
@@ -98,6 +110,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
98
110
 
99
111
  - the greeting module
100
112
 
113
+ ## Release Flag
114
+
115
+ new
116
+
101
117
  ## Acceptance Criteria
102
118
 
103
119
  - AC-1: `greeting` reads "Hello, world".
@@ -131,6 +147,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
131
147
 
132
148
  - the greeting module
133
149
 
150
+ ## Release Flag
151
+
152
+ new
153
+
134
154
  ## Acceptance Criteria
135
155
 
136
156
  - AC-1: `greeting` reads "Hi".
@@ -150,6 +170,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
150
170
  """
151
171
  # Requirements
152
172
 
173
+ ## Release Flag
174
+
175
+ new
176
+
153
177
  ## Acceptance Criteria
154
178
 
155
179
  - AC-1: `greeting` reads "Hello, world".
@@ -178,6 +202,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
178
202
 
179
203
  - the greeting module
180
204
 
205
+ ## Release Flag
206
+
207
+ new
208
+
181
209
  ## Acceptance Criteria
182
210
 
183
211
  - AC-1: `greeting` reads "Hi".
@@ -226,6 +254,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
226
254
 
227
255
  - the greeting module
228
256
 
257
+ ## Release Flag
258
+
259
+ new
260
+
229
261
  ## Acceptance Criteria
230
262
 
231
263
  - AC-1: `greeting` reads "Hello, world".
@@ -8,7 +8,6 @@ Feature: the criteria-coverage lens checks the change's tests against its criter
8
8
  Background:
9
9
  Given the shipped workflows, as gtd install scaffolds them
10
10
  And an environment variable "GTD_QUALITYREVIEWS" set to "criteria-coverage"
11
- And an environment variable "GTD_FLAGMODE" set to "false"
12
11
  And a file "src/calc.ts" with:
13
12
  """
14
13
  export const add = (a: number, b: number) => a + b
@@ -13,6 +13,7 @@ import {
13
13
  import { HEADING, ITEM, markFenced } from "../shared/fences.js"
14
14
  import { planner, REQUIREMENTS } from "../shared/steps.js"
15
15
  import * as t from "../shared/text.js"
16
+ import { requireReleaseFlag } from "./release-flag.js"
16
17
 
17
18
  export interface Criterion {
18
19
  readonly id: string
@@ -137,16 +138,20 @@ export const requireCriteria = (file: string): void => {
137
138
  const isImpactHeading = (h: RegExpExecArray | null): boolean =>
138
139
  h !== null && h[1]!.length === 2 && /^impact$/i.test(h[2]!.trim())
139
140
 
141
+ // The skip path's impact turn may write both: a sketch can ask for a flag.
142
+ const isImpactTurnHeading = (h: RegExpExecArray | null): boolean =>
143
+ h !== null && h[1]!.length === 2 && /^(impact|release flag)$/i.test(h[2]!.trim())
144
+
140
145
  /** Whether `text` has an unfenced, level-2 `## Impact` heading. */
141
146
  export const hasImpact = (text: string): boolean =>
142
147
  markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)))
143
148
 
144
- /** `text` without its `## Impact` section, whitespace collapsed. */
149
+ /** `text` without its `## Impact` and `## Release Flag` sections, whitespace collapsed. */
145
150
  const withoutImpact = (text: string): string => {
146
151
  let inImpact = false
147
152
  const kept = markFenced(text).filter((l) => {
148
153
  const h = l.fenced ? null : HEADING.exec(l.line)
149
- if (h !== null && h[1]!.length <= 2) inImpact = isImpactHeading(h)
154
+ if (h !== null && h[1]!.length <= 2) inImpact = isImpactTurnHeading(h)
150
155
  return !inImpact
151
156
  })
152
157
  return kept
@@ -172,13 +177,13 @@ export const requireImpact = (file: string): void => {
172
177
  )
173
178
  }
174
179
 
175
- /** Refuse a turn that changed `file` beyond its `## Impact` section. */
180
+ /** Refuse a turn that changed `file` beyond its `## Impact` and `## Release Flag` sections. */
176
181
  export const requireImpactOnly = (file: string): void => {
177
182
  const change = changes().get(file)
178
183
  if (change === undefined) return
179
184
  if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return
180
185
  refuse(
181
- `gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`,
186
+ `gtd land: impact-only: this turn may only add "## Impact" (and "## Release Flag") to ${file} — restore everything else`,
182
187
  )
183
188
  }
184
189
 
@@ -240,6 +245,7 @@ const impact = async (): Promise<void> => {
240
245
  requireCriteriaFrozen(REQUIREMENTS)
241
246
  requireImpact(REQUIREMENTS)
242
247
  requireImpactOnly(REQUIREMENTS)
248
+ requireReleaseFlag(REQUIREMENTS)
243
249
  }
244
250
 
245
251
  /**
@@ -41,6 +41,10 @@ Feature: a trivial change takes the same few beats at either risk tier
41
41
 
42
42
  - the greeting module
43
43
 
44
+ ## Release Flag
45
+
46
+ new
47
+
44
48
  ## Acceptance Criteria
45
49
 
46
50
  - AC-1: the change behaves as described.
@@ -302,38 +302,9 @@ describe("an in-flight process from before the architecture path was removed", (
302
302
  })
303
303
  })
304
304
 
305
- describe("flagMode", () => {
306
- const config = (workflow: string): string =>
307
- [
308
- 'import { feature } from "@pmelab/gtd/workflows/feature"',
309
- 'export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"',
310
- `export default ${workflow}`,
311
- "",
312
- ].join("\n")
313
-
314
- const designed = async (workflow: string, env: Record<string, string> = {}) => {
315
- const repo = testRepo({ workflow, env: { GTD_QUALITYREVIEWS: "", ...env } })
316
- repo.write("src/calc.ts", CALC)
317
- repo.commit("feat: add calculator")
318
- repo.write("TODO.md", "# Add subtract\n")
319
- return repo.drive({
320
- unwind: () => undefined,
321
- "start-gate.check": () => undefined,
322
- "design.triage": (_, r) =>
323
- r.write(
324
- ".gtd/REQUIREMENTS.md",
325
- "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
326
- ),
327
- "design.gate.answer": () => undefined,
328
- })
329
- }
330
-
331
- it("set on explicitly, runs the naive pass like the default", async () => {
332
- expect((await designed(config("feature({ flagMode: true })"))).state).toBe("seam.naive")
333
- })
334
-
335
- it("off through GTD_FLAGMODE=false, lands the naive pass with no seam judge", async () => {
336
- const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_FLAGMODE: "false" } })
305
+ describe("release flag", () => {
306
+ it("with no Release Flag section, lands the naive pass with no seam judge", async () => {
307
+ const repo = testRepo({ workflow: CONFIG_SCAFFOLD })
337
308
  repo.write("src/calc.ts", CALC)
338
309
  repo.commit("feat: add calculator")
339
310
  repo.write("TODO.md", "# Add subtract\n")
@@ -357,13 +328,6 @@ describe("flagMode", () => {
357
328
  expect(repo.read("src/calc.ts")).toBe(`${CALC}// naive\n`)
358
329
  expect(repo.subjects().some((subject) => subject.includes("seam.judge"))).toBe(false)
359
330
  })
360
-
361
- it("refuses a value other than true or false before the process starts", async () => {
362
- const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_FLAGMODE: "off" } })
363
- repo.write("TODO.md", "# Add subtract\n")
364
- await expect(repo.drive({})).rejects.toThrow('flagMode must be "true" or "false", got "off"')
365
- expect(repo.subjects()).toEqual(["chore: initial commit"])
366
- })
367
331
  })
368
332
 
369
333
  // pins the public re-exports from the feature entry point
@@ -9,7 +9,7 @@ import {
9
9
  start,
10
10
  } from "../../flows/index.js"
11
11
  import { gate } from "../shared/health.js"
12
- import { flagModeOn, naiveTestOn, shipped } from "../shared/options.js"
12
+ import { naiveTestOn, shipped } from "../shared/options.js"
13
13
  import { FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
14
14
  import { refusePendingHandOff } from "../shared/tail.js"
15
15
  import * as t from "../shared/text.js"
@@ -29,7 +29,7 @@ import { recordRisk, riskTierOn } from "./tier.js"
29
29
  // The ordinary start. Any change to the tree starts a process: `idle` →
30
30
  // `unwind` reverts the sketch (its intent survives in history) → a
31
31
  // green-baseline gate → design → a naive pass, tried by a person with
32
- // `naiveTest` on and judged with `flagMode` on: it
32
+ // `naiveTest` on and judged when the requirements ask for a release flag: it
33
33
  // lands as the implementation, or (refactoring first when the judge asks) is
34
34
  // built fresh from → the health loop → the triaged quality lap and its finding gate → a
35
35
  // reviewer's walkthrough of the whole change for the human, which signs off
@@ -46,7 +46,7 @@ export const unwind = (): Promise<void> => {
46
46
  export { reUnwind }
47
47
 
48
48
  /**
49
- * A naive pass (judged only with `flagMode` on), then implement and the
49
+ * A naive pass (judged only when a release flag is asked for), then implement and the
50
50
  * quality lap, then the review; design must have run since `firstBase`.
51
51
  */
52
52
  export const buildFeature = async (firstBase: string): Promise<void> => {
@@ -139,7 +139,6 @@ export const ordinaryStart = async (): Promise<void> => {
139
139
  await human("idle", { message: t.idleMessage(), label: "Idle", file: ".gtd/TODO.md" })
140
140
  refusePendingHandOff()
141
141
  // Read right after idle, so a bad value refuses the landing that would pin it.
142
- flagModeOn()
143
142
  naiveTestOn()
144
143
  const stated = statedCriteria(changes(), head())
145
144
  await unwind()
@@ -11,7 +11,6 @@ Feature: each quality lens runs on its own model hint
11
11
  import { feature } from "@pmelab/gtd/workflows/feature"
12
12
  export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
13
13
  export default feature({
14
- flagMode: false,
15
14
  qualityReviews: "correctness, conventions, test-audit=opus",
16
15
  })
17
16
  """
@@ -12,7 +12,6 @@ Feature: the quality lap's findings are triaged, then decided at the finding gat
12
12
  Background:
13
13
  Given the shipped workflows, as gtd install scaffolds them
14
14
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
15
- And an environment variable "GTD_FLAGMODE" set to "false"
16
15
  And a file "src/calc.ts" with:
17
16
  """
18
17
  export const add = (a: number, b: number) => a + b
@@ -31,6 +31,10 @@ Feature: a naive pass ends only when its tests reach no file it added and the su
31
31
 
32
32
  - the greeting module
33
33
 
34
+ ## Release Flag
35
+
36
+ new
37
+
34
38
  ## Acceptance Criteria
35
39
 
36
40
  - AC-1: `subtract(5, 3)` returns 2.
@@ -4,19 +4,20 @@ Feature: a person tries the naive pass by hand before anything builds on it
4
4
  `naiveTest` is a factory option of the shipped workflows, off by default. On,
5
5
  every green naive pass is applied to the working tree and the process rests
6
6
  at `seam.try`. A clean landing signs it off and the process goes on — to the
7
- seam judge, or straight to the implementation with `flagMode` off. Notes in
7
+ seam judge when the requirements ask for a release flag, else straight to the
8
+ implementation. Notes in
8
9
  `.gtd/NAIVE-TEST.md`, a code edit or a requirements edit send it back: the
9
10
  pass and the person's code edits leave the tree again, and the next naive
10
11
  pass gets the notes and the edits. Every lap ends at the gate again.
11
12
 
12
- Rule: with flagMode off
13
+ Rule: with no release flag asked for
13
14
 
14
15
  Background:
15
16
  Given a gtd config with:
16
17
  """
17
18
  import { feature } from "@pmelab/gtd/workflows/feature"
18
19
  export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
19
- export default feature({ flagMode: false, naiveTest: true })
20
+ export default feature({ naiveTest: true })
20
21
  """
21
22
  And an environment variable "GTD_QUALITYREVIEWS" set to ""
22
23
  And a file "src/calc.ts" with:
@@ -177,7 +178,7 @@ Feature: a person tries the naive pass by hand before anything builds on it
177
178
  gtd(check): seam.applying → seam.try
178
179
  """
179
180
 
180
- Rule: with flagMode on
181
+ Rule: with a release flag asked for
181
182
 
182
183
  Background:
183
184
  Given a gtd config with:
@@ -208,7 +209,11 @@ Feature: a person tries the naive pass by hand before anything builds on it
208
209
 
209
210
  - the calculator module
210
211
 
211
- ## Acceptance Criteria
212
+ ## Release Flag
213
+
214
+ new
215
+
216
+ ## Acceptance Criteria
212
217
 
213
218
  - AC-1: subtract(a, b) returns a - b.
214
219
  """
@@ -226,7 +231,7 @@ Feature: a person tries the naive pass by hand before anything builds on it
226
231
  And step "seam.applying" runs its script
227
232
  And step "seam.restoring" runs its script
228
233
 
229
- Scenario: with flagMode on the person tries the pass before the seam judge rules
234
+ Scenario: the person tries the pass before the seam judge rules
230
235
  When the process is driven
231
236
  Then the process rests at "seam.try"
232
237
  And the resting step's content contains "the seam judge rules on its shape"
@@ -39,6 +39,10 @@ Feature: a naive pass the judge rules land is itself the implementation
39
39
 
40
40
  - the greeting module
41
41
 
42
+ ## Release Flag
43
+
44
+ new
45
+
42
46
  ## Acceptance Criteria
43
47
 
44
48
  - AC-1: the change behaves as described.