@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
@@ -17636,7 +17636,6 @@ const defaults = {
17636
17636
  standardMinP: "0.8",
17637
17637
  refactorFlagPattern: "\\bGTD_FLAG_\\w+",
17638
17638
  seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
17639
- flagMode: "true",
17640
17639
  naiveTest: "false",
17641
17640
  flag_risk_tier: "0",
17642
17641
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge, criteria-coverage",
@@ -18070,6 +18069,18 @@ in its docs or agent instructions (\`AGENTS.md\`, \`CLAUDE.md\`). When
18070
18069
  none is declared, fall back to directories and packages. Landing is
18071
18070
  refused while the section is missing.
18072
18071
  `;
18072
+ const releaseFlagBlock = `
18073
+ ## Release flag
18074
+
18075
+ Only when the sketch asks outright for the change to ship behind a
18076
+ feature or release flag, \`.gtd/REQUIREMENTS.md\` carries a
18077
+ \`## Release Flag\` section whose first line is \`new\` (a new
18078
+ flag) or \`existing: <NAME>\` (behind a flag that already exists,
18079
+ named as the sketch names it). Never add one on your own judgement:
18080
+ without the section the change ships unflagged. A later lap keeps the
18081
+ section exactly as the last settled file had it, unless the human
18082
+ changed it. Landing is refused while its first line is anything else.
18083
+ `;
18073
18084
  const riskTierBlock = `
18074
18085
  ## Risk tier and production signals
18075
18086
 
@@ -18109,7 +18120,7 @@ ${footnoteFoldIn}${codeThreadReplies()}
18109
18120
  or both
18110
18121
  - Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
18111
18122
  uncommitted and finish once it reflects this lap's own work
18112
- ${acceptanceCriteriaBlock}${impactBlock}
18123
+ ${acceptanceCriteriaBlock}${impactBlock}${releaseFlagBlock}
18113
18124
  Every lap re-explores and leaves \`## Impact\` present. On a return
18114
18125
  lap or review loop-back the human may have edited \`## Impact\` (or
18115
18126
  footnoted it): read that edit from history (\`git log -1 -p --
@@ -18179,11 +18190,12 @@ ${stateFileRules}
18179
18190
  - The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
18180
18191
  — no other files for notes or output
18181
18192
  - \`.gtd/REQUIREMENTS.md\` already holds the settled acceptance
18182
- criteria. Add a \`## Impact\` section and change nothing else — a
18183
- turn that touches anything outside it is refused
18193
+ criteria. Add a \`## Impact\` section — and a \`## Release Flag\`
18194
+ section when the sketch asks for one — and change nothing else: a
18195
+ turn that touches anything outside them is refused
18184
18196
  - No human reviews this before the build: put any surprise in the
18185
18197
  Impact prose, never as a question
18186
- ${impactBlock}
18198
+ ${impactBlock}${releaseFlagBlock}
18187
18199
  - Leave \`.gtd/REQUIREMENTS.md\` uncommitted and finish once it holds \`## Impact\`
18188
18200
  `;
18189
18201
  const clearCriteriaQuestion = {
@@ -18242,6 +18254,7 @@ What each change does next (then run \`gtd land\`):
18242
18254
  - **Accept as-is** — change nothing and re-run; the plan stands as written. Refused while a thread is open.
18243
18255
  - **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.
18244
18256
  - **Discuss** — start or continue a thread (\`- H:\`); the agent replies and this gate stops again.
18257
+ - **Ask for a release flag** — add \`## Release Flag\` with \`new\` or \`existing: <NAME>\` as its first line, or delete the section to ship unflagged.
18245
18258
  ${riskTier ? RISK_TIER_GATE_LINE : ""}`;
18246
18259
  const builderSystem = () => `${builderPersona}
18247
18260
 
@@ -18921,21 +18934,22 @@ const afterRefactorLine = (rebuild) => `- A reviewed refactor has just landed an
18921
18934
  though never a release flag. Build only what is still missing, held to every
18922
18935
  rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
18923
18936
  `;
18924
- const flagLine = (flagMode, tier, amend) => !flagMode ? "- Add no release flag — this project ships without release flags" : tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, following the
18925
- repository's flag conventions`;
18926
- const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}) => `${stateFileRules}
18937
+ const flagLine = (flag, amend) => flag.kind === "none" ? "- Add no release flag — the requirements ask for none" : flag.kind === "existing" ? `- Put the new behaviour behind the existing flag \`${flag.name}\`, as the
18938
+ requirements ask — add no new flag` : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, as the requirements ask,
18939
+ following the repository's flag conventions`;
18940
+ const implementBuildingPrompt = (reference, amend, afterRefactor, flag = { kind: "none" }, rebuild = {}) => `${stateFileRules}
18927
18941
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18928
18942
  ${impactHints}
18929
18943
  ${criteriaBinding(true)}
18930
18944
  ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
18931
18945
  the re-derived requirements instead of rewriting it
18932
- ` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
18946
+ ` : flag.kind !== "none" ? `- Implement them properly, fitting the codebase's seams and conventions
18933
18947
  ` : `- Implement them the quickest way that works — the naive version. Do not
18934
18948
  restructure, abstract or refactor around the change
18935
18949
  `}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
18936
18950
  Read it for ideas; never apply, cherry-pick or check it out — write
18937
18951
  the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
18938
- `}${rebuildLines(rebuild)}${flagLine(flagMode, tier, amend)}
18952
+ `}${rebuildLines(rebuild)}${flagLine(flag, amend)}
18939
18953
  - Write the tests first, then the code that makes them pass
18940
18954
  ${behaviourTests}
18941
18955
  - Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
@@ -18954,7 +18968,7 @@ An edit together with an approval counts as steer. Only
18954
18968
  \`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
18955
18969
  branch unjudged.
18956
18970
  `;
18957
- /** `judged`: whether an overruled pass still meets the seam judge (`flagMode` on). */
18971
+ /** `judged`: whether an overruled pass still meets the seam judge (a release flag asked for). */
18958
18972
  const seamSteerMessage = (retry, judged = true) => isFault(retry) ? `The naive pass failed three times and the retries are spent. The last
18959
18973
  attempt's tests (\`git show ${retry.hash}\`) fall short:
18960
18974
  ${retry.fault}
@@ -38602,19 +38616,12 @@ const ShippedOptions = Struct({
38602
38616
  testCommand: setting,
38603
38617
  plannerModel: setting,
38604
38618
  coderModel: setting,
38605
- flagMode: optionalWith(Boolean$, { exact: true }),
38606
38619
  naiveTest: optionalWith(Boolean$, { exact: true })
38607
38620
  });
38608
38621
  const over = (base, options) => ({
38609
38622
  ...base,
38610
38623
  ...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)).map(([name, value]) => [name, String(value)]))
38611
38624
  });
38612
- /** The `flagMode` setting, pinned at process start. Anything but true/false fails rather than guess a side. */
38613
- const flagModeOn = () => {
38614
- const value = vars.flagMode ?? defaults.flagMode;
38615
- if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
38616
- return value === "true";
38617
- };
38618
38625
  /** The `naiveTest` setting, pinned at process start. Anything but true/false fails rather than guess. */
38619
38626
  const naiveTestOn = () => {
38620
38627
  const value = vars.naiveTest ?? defaults.naiveTest;
@@ -39032,6 +39039,43 @@ const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
39032
39039
  /** A list item, its checkbox dropped: its text. */
39033
39040
  const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
39034
39041
  //#endregion
39042
+ //#region src/workflows/feature/release-flag.ts
39043
+ const NAME = /^existing:\s*`?([A-Za-z_][\w.-]*)`?$/i;
39044
+ const isFlagHeading = (h) => h !== null && h[1].length === 2 && /^release flag$/i.test(h[2].trim());
39045
+ /** The first line under `## Release Flag`, `undefined` without the section. */
39046
+ const requestLine = (text) => {
39047
+ const lines = markFenced(text);
39048
+ const at = lines.findIndex((l) => !l.fenced && isFlagHeading(HEADING.exec(l.line)));
39049
+ if (at === -1) return void 0;
39050
+ const body = lines.slice(at + 1);
39051
+ const end = body.findIndex((l) => !l.fenced && HEADING.test(l.line));
39052
+ return (end === -1 ? body : body.slice(0, end)).find((l) => l.line.trim() !== "")?.line.trim();
39053
+ };
39054
+ /** What `text` asks for, or why it cannot be read. No section asks for no flag. */
39055
+ const releaseFlagOf = (text) => {
39056
+ const line = requestLine(text);
39057
+ if (line === void 0 || /^none$/i.test(line)) return { kind: "none" };
39058
+ if (/^new$/i.test(line)) return { kind: "new" };
39059
+ const name = NAME.exec(line)?.[1];
39060
+ return name === void 0 ? { unreadable: line } : {
39061
+ kind: "existing",
39062
+ name
39063
+ };
39064
+ };
39065
+ /** Refuse `file` when its `## Release Flag` section is neither `none`, `new` nor `existing: <NAME>`. */
39066
+ const requireReleaseFlag = (file) => {
39067
+ const request = releaseFlagOf(read(file) ?? "");
39068
+ if (!("unreadable" in request)) return;
39069
+ refuse(`gtd land: release-flag: ${file}'s "## Release Flag" reads ${JSON.stringify(request.unreadable)} — its first line must be \`new\`, \`existing: <NAME>\` or \`none\`, or leave the section out for no flag`);
39070
+ };
39071
+ /** What `.gtd/REQUIREMENTS.md` asks for now; a person's unreadable edit is refused. */
39072
+ const requestedFlag = () => {
39073
+ requireReleaseFlag(REQUIREMENTS);
39074
+ return releaseFlagOf(read(".gtd/REQUIREMENTS.md") ?? "");
39075
+ };
39076
+ /** Whether the change goes behind a release flag: only then does the seam judge rule. */
39077
+ const flagged = () => requestedFlag().kind !== "none";
39078
+ //#endregion
39035
39079
  //#region src/workflows/feature/criteria.ts
39036
39080
  /** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
39037
39081
  const CLEAR_MIN_P = .8;
@@ -39123,14 +39167,15 @@ const requireCriteria = (file) => {
39123
39167
  refuse(`gtd land: acceptance-criteria: ${file} has ${criteria.length === 0 ? `no "## Acceptance Criteria" list` : unnamed.length > 0 ? `${unnamed.length} criterion without an ID:\n${unnamed.map((c) => ` - ${c.text}`).join("\n")}` : `duplicate IDs: ${[...new Set(twice)].join(", ")}`} — list every criterion under "## Acceptance Criteria" as "- AC-<n>: <checkable criterion>", each ID unique and never reused`);
39124
39168
  };
39125
39169
  const isImpactHeading = (h) => h !== null && h[1].length === 2 && /^impact$/i.test(h[2].trim());
39170
+ const isImpactTurnHeading = (h) => h !== null && h[1].length === 2 && /^(impact|release flag)$/i.test(h[2].trim());
39126
39171
  /** Whether `text` has an unfenced, level-2 `## Impact` heading. */
39127
39172
  const hasImpact = (text) => markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)));
39128
- /** `text` without its `## Impact` section, whitespace collapsed. */
39173
+ /** `text` without its `## Impact` and `## Release Flag` sections, whitespace collapsed. */
39129
39174
  const withoutImpact = (text) => {
39130
39175
  let inImpact = false;
39131
39176
  return markFenced(text).filter((l) => {
39132
39177
  const h = l.fenced ? null : HEADING.exec(l.line);
39133
- if (h !== null && h[1].length <= 2) inImpact = isImpactHeading(h);
39178
+ if (h !== null && h[1].length <= 2) inImpact = isImpactTurnHeading(h);
39134
39179
  return !inImpact;
39135
39180
  }).map((l) => l.line).join(" ").replace(/\s+/g, " ").trim();
39136
39181
  };
@@ -39147,12 +39192,12 @@ const requireImpact = (file) => {
39147
39192
  if (hasImpact(text)) return;
39148
39193
  refuse(`gtd land: impact: ${file} has no "## Impact" section — explore the codebase and add "## Impact" naming the units, public surfaces and surprises this change touches`);
39149
39194
  };
39150
- /** Refuse a turn that changed `file` beyond its `## Impact` section. */
39195
+ /** Refuse a turn that changed `file` beyond its `## Impact` and `## Release Flag` sections. */
39151
39196
  const requireImpactOnly = (file) => {
39152
39197
  const change = changes().get(file);
39153
39198
  if (change === void 0) return;
39154
39199
  if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return;
39155
- refuse(`gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`);
39200
+ refuse(`gtd land: impact-only: this turn may only add "## Impact" (and "## Release Flag") to ${file} — restore everything else`);
39156
39201
  };
39157
39202
  const idNumber = (id) => Number(id.slice(3));
39158
39203
  /** Why `listed` renumbers or reuses an earlier ID; `undefined` when it does neither. */
@@ -39199,6 +39244,7 @@ const impact = async () => {
39199
39244
  requireCriteriaFrozen(REQUIREMENTS);
39200
39245
  requireImpact(REQUIREMENTS);
39201
39246
  requireImpactOnly(REQUIREMENTS);
39247
+ requireReleaseFlag(REQUIREMENTS);
39202
39248
  };
39203
39249
  /**
39204
39250
  * The skip path: when the sketch stated clear, checkable criteria, write them
@@ -39302,6 +39348,7 @@ const design = (base, guard) => scope("design", async () => {
39302
39348
  requireReplies(REQUIREMENTS);
39303
39349
  requireCriteria(REQUIREMENTS);
39304
39350
  requireImpact(REQUIREMENTS);
39351
+ requireReleaseFlag(REQUIREMENTS);
39305
39352
  guard?.();
39306
39353
  } while (await noteGate(REQUIREMENTS, answerProductQuestions));
39307
39354
  });
@@ -40088,7 +40135,7 @@ const applied = async (tip) => {
40088
40135
  return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
40089
40136
  };
40090
40137
  const build = async (options) => {
40091
- await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild), {
40138
+ await agentWithSkills("building", implementBuildingPrompt(options.reference, options.amend, options.afterRefactor, requestedFlag(), options.rebuild), {
40092
40139
  label: "Implementing",
40093
40140
  model: coder(),
40094
40141
  system: builderSystem(),
@@ -40115,7 +40162,7 @@ const implementing = (options) => scope("implement", async () => {
40115
40162
  /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
40116
40163
  const steer = async (retry) => {
40117
40164
  await human("steer", {
40118
- message: seamSteerMessage(retry, flagModeOn()),
40165
+ message: seamSteerMessage(retry, flagged()),
40119
40166
  label: "Steer or overrule the naive pass",
40120
40167
  file: REQUIREMENTS,
40121
40168
  acceptClean: true
@@ -40194,7 +40241,7 @@ const tried = async (tip) => {
40194
40241
  const before = head$1();
40195
40242
  await run("applying", applyScript(tip.hash, FEEDBACK, naiveApplyFailure(tip.hash), [".gtd"]), { label: "Applying the naive pass to try it" });
40196
40243
  await human("try", {
40197
- message: naiveTryMessage(flagModeOn()),
40244
+ message: naiveTryMessage(flagged()),
40198
40245
  label: "Try the naive pass",
40199
40246
  file: NAIVE_TEST,
40200
40247
  acceptClean: true
@@ -40228,7 +40275,7 @@ const decide = () => scope("seam", async () => {
40228
40275
  continue;
40229
40276
  }
40230
40277
  }
40231
- const outcome = verdict ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
40278
+ const outcome = verdict ?? (flagged() ? await ruled(tip, vetting.again) : {
40232
40279
  reference: tip.hash,
40233
40280
  tip
40234
40281
  });
@@ -40297,7 +40344,7 @@ const unwind = () => {
40297
40344
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
40298
40345
  };
40299
40346
  /**
40300
- * A naive pass (judged only with `flagMode` on), then implement and the
40347
+ * A naive pass (judged only when a release flag is asked for), then implement and the
40301
40348
  * quality lap, then the review; design must have run since `firstBase`.
40302
40349
  */
40303
40350
  const buildFeature = async (firstBase) => {
@@ -40389,7 +40436,6 @@ const ordinaryStart = async () => {
40389
40436
  file: ".gtd/TODO.md"
40390
40437
  });
40391
40438
  refusePendingHandOff();
40392
- flagModeOn();
40393
40439
  naiveTestOn();
40394
40440
  const stated = statedCriteria(changes(), head$1());
40395
40441
  await unwind();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "57.0.0",
3
+ "version": "58.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
@@ -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.
@@ -9,6 +9,7 @@ import {
9
9
  } from "../../flows/index.js"
10
10
  import { REQUIREMENTS } from "../shared/steps.js"
11
11
  import { requireCriteria, requireImpact } from "./criteria.js"
12
+ import { requireReleaseFlag } from "./release-flag.js"
12
13
  import { answerProductQuestions, triage } from "./steps.js"
13
14
 
14
15
  /**
@@ -34,6 +35,7 @@ export const design = (base: string, guard?: () => void): Promise<void> =>
34
35
  requireReplies(REQUIREMENTS)
35
36
  requireCriteria(REQUIREMENTS)
36
37
  requireImpact(REQUIREMENTS)
38
+ requireReleaseFlag(REQUIREMENTS)
37
39
  guard?.()
38
40
  } while (await noteGate(REQUIREMENTS, answerProductQuestions))
39
41
  })
@@ -40,6 +40,10 @@ Feature: a refactor that carried the whole feature still gets a quality lap over
40
40
 
41
41
  - the greeting module
42
42
 
43
+ ## Release Flag
44
+
45
+ new
46
+
43
47
  ## Acceptance Criteria
44
48
 
45
49
  - AC-1: the change behaves as described.
@@ -39,6 +39,10 @@ Feature: a refactor-first judgment walks one fixed path to the review gate
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.