@pmelab/gtd 30.0.0 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "30.0.0",
3
+ "version": "30.1.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
package/README.md CHANGED
@@ -422,14 +422,15 @@ noted in steps 3 and 4 below.
422
422
 
423
423
  3. **You wait.** The agent first implements the requirements the quickest way
424
424
  that works — the naive pass — and a judge reads where it changed code (see
425
- [the naive pass](#the-naive-pass) below). Then the change is built
426
- test-first, checked against the full suite (`testCommand`) and fixed until it
427
- passes — so keep that suite fast; gtd has no lighter one. Scenario wording is
428
- not frozen: you sign the scenarios off once, in the review's `## Scenarios`
429
- chunk, where any assertion an existing scenario lost is marked `Weakened:`.
430
- After the build the quality lap runs. Three points along the process are
431
- judged rather than always asking you outright — each stops and hands you a
432
- verdict to make (`gtd judge answer`, or land with a clean tree to accept the
425
+ [the naive pass](#the-naive-pass) below; with [flag mode](#the-naive-pass)
426
+ off, neither runs). Then the change is built test-first, checked against the
427
+ full suite (`testCommand`) and fixed until it passes — so keep that suite
428
+ fast; gtd has no lighter one. Scenario wording is not frozen: you sign the
429
+ scenarios off once, in the review's `## Scenarios` chunk, where any assertion
430
+ an existing scenario lost is marked `Weakened:`. After the build the quality
431
+ lap runs. Three points along the process (two with flag mode off) are judged
432
+ rather than always asking you outright — each stops and hands you a verdict
433
+ to make (`gtd judge answer`, or land with a clean tree to accept the
433
434
  conservative default, which never skips work;
434
435
  `gtd judge run --provider fixed --answers <path>` — or the
435
436
  `GTD_JUDGE_ANSWERS` env var, inline JSON — answers one from a file, piped
@@ -595,6 +596,14 @@ out is `gtd abandon`. A process from 27.5.0 or later whose judge ruled
595
596
  that slice. Details in
596
597
  [the naive-pass path](docs/configuration.md#the-naive-pass-path).
597
598
 
599
+ **Flag mode (`flagMode`, on by default).** On, the implementation is asked for a
600
+ new release flag (none at a `low` risk tier) and the naive pass and its judge
601
+ run as above. A project that is not publicly released turns it off explicitly —
602
+ `feature({ flagMode: false })` in `gtd.config.ts`, it is never detected: then no
603
+ flag is asked for, no seam judge or refactor runs, and the implementation is the
604
+ naive version, straight through the health loop and the quality lap. See
605
+ [`flagMode`](docs/configuration.md#the-bundled-workflows-settings).
606
+
598
607
  **Risk tier (flag `GTD_FLAG_RISK_TIER=1`, off by default).** Triage proposes a
599
608
  risk tier, `low` or `high`, and the production signals to watch after merge.
600
609
  Edit the first word under `## Risk Tier` at the design gate to change it; a
@@ -993,6 +993,11 @@ export const register: Register = (on) => {
993
993
  return next(e)
994
994
  })
995
995
 
996
+ // A worker's tool call hands settings hooks the MAIN session's
997
+ // `transcript_path`, marked only by `agent_id`/`agent_type`; its own
998
+ // transcript (`agent_transcript_path`) is named only at SubagentStop, too
999
+ // late to measure a running worker's context.
1000
+ //
996
1001
  // The loop lands a turn when it ends; work left running in the background
997
1002
  // would still be writing the tree gtd is committing.
998
1003
  on("tool.call", async ($, e, next) => {
@@ -1,6 +1,6 @@
1
1
  import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, displayPath, env, head, human, judge, numeric, quote, read, refuse, scope, skillsFor, start, vars } from "../flows.mjs";
2
2
  import { escalation as escalation$1, gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
3
- import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct } from "./Schema-DO5ttJaI.mjs";
3
+ import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct, n as Boolean$ } from "./Schema-DO5ttJaI.mjs";
4
4
  //#region src/workflows/shared/registry.ts
5
5
  const FILL = "";
6
6
  const commentLength = (rest) => {
@@ -132,6 +132,7 @@ const defaults = {
132
132
  reviewSeverityMinP: "0.7",
133
133
  refactorFlagPattern: "\\bGTD_FLAG_\\w+",
134
134
  seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
135
+ flagMode: "true",
135
136
  flag_risk_tier: "0",
136
137
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
137
138
  refactorQualityReviews: "correctness, ponytail-review, conventions"
@@ -1172,18 +1173,20 @@ re-implement. It changed these files outside the expected set:
1172
1173
  ${listed(rejection.strays)}
1173
1174
  Start over and choose a different approach; do not rebuild the same diff.
1174
1175
  `}`;
1175
- const implementBuildingPrompt = (reference, tier, amend, afterRefactor) => `${stateFileRules}
1176
+ const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
1176
1177
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1177
1178
  ${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
1178
1179
  though never a release flag. Build only what is still missing, held to every
1179
1180
  rule below; if nothing is, change nothing and finish your turn
1180
1181
  ` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
1181
1182
  the re-derived requirements instead of rewriting it
1182
- ` : `- Implement them properly, fitting the codebase's seams and conventions
1183
+ ` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
1184
+ ` : `- Implement them the quickest way that works — the naive version. Do not
1185
+ restructure, abstract or refactor around the change
1183
1186
  `}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
1184
1187
  Read it for ideas; never apply, cherry-pick or check it out — write
1185
1188
  the change fresh
1186
- `}${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
1189
+ `}${!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
1187
1190
  repository's flag conventions`}
1188
1191
  - Write the tests first, then the code that makes them pass
1189
1192
  - Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
@@ -1420,12 +1423,19 @@ const ShippedOptions = Struct({
1420
1423
  refactorQualityReviews: setting,
1421
1424
  testCommand: setting,
1422
1425
  plannerModel: setting,
1423
- coderModel: setting
1426
+ coderModel: setting,
1427
+ flagMode: optionalWith(Boolean$, { exact: true })
1424
1428
  });
1425
1429
  const over = (base, options) => ({
1426
1430
  ...base,
1427
- ...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)))
1431
+ ...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)).map(([name, value]) => [name, String(value)]))
1428
1432
  });
1433
+ /** The `flagMode` setting, pinned at process start. Anything but true/false fails rather than guess a side. */
1434
+ const flagModeOn = () => {
1435
+ const value = vars.flagMode ?? defaults.flagMode;
1436
+ if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
1437
+ return value === "true";
1438
+ };
1429
1439
  /** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
1430
1440
  const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
1431
1441
  onExcessProperty: "error",
@@ -1509,4 +1519,4 @@ const steering = {
1509
1519
  [REVIEW]: "review"
1510
1520
  };
1511
1521
  //#endregion
1512
- export { seamByHandMessage as $, buildReviewFixNitsPrompt as A, idleMessage as B, reviewMissing as C, buildReviewAnswerQuestionsPrompt as D, agentWithSkills as E, designSystem as F, refactorDroppedRefusal as G, implementFixPrompt as H, designTriagePrompt as I, reviewEditNotesCapture as J, refactorFixPrompt as K, finisherSystem as L, builderSystem as M, deferNotesScript as N, buildReviewAwaitAfterDeferMessage as O, designGateAnswerMessage as P, reviewerSystem as Q, fixReviewNotesPrompt as R, planner as S, reviewing as T, naivePassPrompt as U, implementBuildingPrompt as V, refactorBuildingPrompt as W, reviewGateBlockedMessage as X, reviewEditsCapture as Y, reviewNotesCapture as Z, awaitReview as _, flagShipped as a, envDefaults as at, fixQuality as b, escalation as c, flagReferences as ct, healthy as d, requireFlag as dt, seamSteerMessage as et, FEEDBACK as f, REVIEW as g, REQUIREMENTS as h, skills as i, defaults as it, buildReviewTriageMessage as j, buildReviewCollectingPrompt as k, gate as l, flipProblems as lt, REFACTOR as m, summary as n, unwindFailedMessage as nt, shipped as o, testFileGlobs as ot, QUALITY as p, refactorRules as q, access as r, unwindFailure as rt, baseline as s, flagMentions as st, steering as t, startGateBlockedMessage as tt, guarded as u, registryEntry as ut, coder as v, reviewQuality as w, fixRisks as x, fixCheck as y, handOffAwaitReviewMessage as z };
1522
+ export { reviewerSystem as $, buildReviewCollectingPrompt as A, handOffAwaitReviewMessage as B, planner as C, agentWithSkills as D, reviewing as E, designGateAnswerMessage as F, refactorBuildingPrompt as G, implementBuildingPrompt as H, designSystem as I, refactorRules as J, refactorDroppedRefusal as K, designTriagePrompt as L, buildReviewTriageMessage as M, builderSystem as N, buildReviewAnswerQuestionsPrompt as O, deferNotesScript as P, reviewNotesCapture as Q, finisherSystem as R, fixRisks as S, reviewQuality as T, implementFixPrompt as U, idleMessage as V, naivePassPrompt as W, reviewEditsCapture as X, reviewEditNotesCapture as Y, reviewGateBlockedMessage as Z, REVIEW as _, flagModeOn as a, defaults as at, fixCheck as b, baseline as c, flagMentions as ct, guarded as d, registryEntry as dt, seamByHandMessage as et, healthy as f, requireFlag as ft, REQUIREMENTS as g, REFACTOR as h, skills as i, unwindFailure as it, buildReviewFixNitsPrompt as j, buildReviewAwaitAfterDeferMessage as k, escalation as l, flagReferences as lt, QUALITY as m, summary as n, startGateBlockedMessage as nt, flagShipped as o, envDefaults as ot, FEEDBACK as p, refactorFixPrompt as q, access as r, unwindFailedMessage as rt, shipped as s, testFileGlobs as st, steering as t, seamSteerMessage as tt, gate as u, flipProblems as ut, awaitReview as v, reviewMissing as w, fixQuality as x, coder as y, fixReviewNotesPrompt as z };
@@ -1,6 +1,6 @@
1
1
  import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
2
2
  import { fixFindings, lensesOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
3
- import { C as reviewMissing, T as reviewing, _ as awaitReview, b as fixQuality, c as escalation, d as healthy, g as REVIEW, p as QUALITY, u as guarded, w as reviewQuality, x as fixRisks, y as fixCheck, z as handOffAwaitReviewMessage } from "./reserved-iG86IR3I.mjs";
3
+ import { B as handOffAwaitReviewMessage, E as reviewing, S as fixRisks, T as reviewQuality, _ as REVIEW, b as fixCheck, d as guarded, f as healthy, l as escalation, m as QUALITY, v as awaitReview, w as reviewMissing, x as fixQuality } from "./reserved-DDiiAZ8U.mjs";
4
4
  //#region src/workflows/shared/tail.ts
5
5
  /** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
6
6
  const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { a as flagShipped, at as envDefaults, ct as flagReferences, dt as requireFlag, i as skills, it as defaults, n as summary, r as access, t as steering, ut as registryEntry, v as coder } from "../chunks/reserved-iG86IR3I.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-DA2UuJNP.mjs";
2
+ import { at as defaults, dt as registryEntry, ft as requireFlag, i as skills, lt as flagReferences, n as summary, o as flagShipped, ot as envDefaults, r as access, t as steering, y as coder } from "../chunks/reserved-DDiiAZ8U.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-ILPlELuy.mjs";
4
4
  //#region src/workflows/cleanup/cleanup.ts
5
5
  const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
6
6
  const cleanupFlow = async () => {
@@ -1,7 +1,7 @@
1
1
  import { answered, changes, changesSince, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars } from "../flows.mjs";
2
2
  import { lensesOf, review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as seamByHandMessage, A as buildReviewFixNitsPrompt, B as idleMessage, C as reviewMissing, D as buildReviewAnswerQuestionsPrompt, E as agentWithSkills, F as designSystem, G as refactorDroppedRefusal, H as implementFixPrompt, I as designTriagePrompt, J as reviewEditNotesCapture, K as refactorFixPrompt, M as builderSystem, N as deferNotesScript, O as buildReviewAwaitAfterDeferMessage, P as designGateAnswerMessage, Q as reviewerSystem, S as planner, T as reviewing, U as naivePassPrompt, V as implementBuildingPrompt, W as refactorBuildingPrompt, Y as reviewEditsCapture, Z as reviewNotesCapture, _ as awaitReview, at as envDefaults, d as healthy, et as seamSteerMessage, f as FEEDBACK, g as REVIEW, h as REQUIREMENTS, i as skills, it as defaults, j as buildReviewTriageMessage, k as buildReviewCollectingPrompt, l as gate, m as REFACTOR, n as summary, nt as unwindFailedMessage, o as shipped, ot as testFileGlobs, q as refactorRules, r as access, rt as unwindFailure, t as steering, tt as startGateBlockedMessage, u as guarded, v as coder, x as fixRisks, y as fixCheck } from "../chunks/reserved-iG86IR3I.mjs";
4
- import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-DA2UuJNP.mjs";
3
+ import { $ as reviewerSystem, A as buildReviewCollectingPrompt, C as planner, D as agentWithSkills, E as reviewing, F as designGateAnswerMessage, G as refactorBuildingPrompt, H as implementBuildingPrompt, I as designSystem, J as refactorRules, K as refactorDroppedRefusal, L as designTriagePrompt, M as buildReviewTriageMessage, N as builderSystem, O as buildReviewAnswerQuestionsPrompt, P as deferNotesScript, Q as reviewNotesCapture, S as fixRisks, U as implementFixPrompt, V as idleMessage, W as naivePassPrompt, X as reviewEditsCapture, Y as reviewEditNotesCapture, _ as REVIEW, a as flagModeOn, at as defaults, b as fixCheck, d as guarded, et as seamByHandMessage, f as healthy, g as REQUIREMENTS, h as REFACTOR, i as skills, it as unwindFailure, j as buildReviewFixNitsPrompt, k as buildReviewAwaitAfterDeferMessage, n as summary, nt as startGateBlockedMessage, ot as envDefaults, p as FEEDBACK, q as refactorFixPrompt, r as access, rt as unwindFailedMessage, s as shipped, st as testFileGlobs, t as steering, tt as seamSteerMessage, u as gate, v as awaitReview, w as reviewMissing, y as coder } from "../chunks/reserved-DDiiAZ8U.mjs";
4
+ import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-ILPlELuy.mjs";
5
5
  //#region src/workflows/shared/fences.ts
6
6
  const markFenced = (text) => {
7
7
  let fence;
@@ -625,7 +625,7 @@ function implement(first, tier, amend) {
625
625
  amend
626
626
  };
627
627
  return scope("implement", async () => {
628
- await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor), {
628
+ await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn()), {
629
629
  label: "Implementing",
630
630
  model: coder(),
631
631
  system: builderSystem(),
@@ -706,10 +706,10 @@ const unwind = () => {
706
706
  const commit = head();
707
707
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
708
708
  };
709
- /** A judged naive pass, then implement and review until a review round signs off; design must have run since `firstBase`. */
709
+ /** A judged naive pass (none with `flagMode` off), then implement and review until a review round signs off; design must have run since `firstBase`. */
710
710
  const buildFeature = async (firstBase) => {
711
711
  const tier = riskTierOn() ? recordRisk() : void 0;
712
- const seam = await seamBuild(firstBase);
712
+ const seam = flagModeOn() ? await seamBuild(firstBase) : { base: firstBase };
713
713
  await slice("feature", async () => {
714
714
  await implement({
715
715
  reference: seam.reference,
@@ -754,6 +754,7 @@ const ordinaryStart = async () => {
754
754
  file: ".gtd/TODO.md"
755
755
  });
756
756
  refusePendingHandOff();
757
+ flagModeOn();
757
758
  await unwind();
758
759
  if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
759
760
  message: unwindFailedMessage(),
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
2
  import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
3
- import { E as agentWithSkills, L as finisherSystem, R as fixReviewNotesPrompt, at as envDefaults, g as REVIEW, i as skills, it as defaults, n as summary, o as shipped, r as access, s as baseline, t as steering, v as coder } from "../chunks/reserved-iG86IR3I.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-DA2UuJNP.mjs";
3
+ import { D as agentWithSkills, R as finisherSystem, _ as REVIEW, at as defaults, c as baseline, i as skills, n as summary, ot as envDefaults, r as access, s as shipped, t as steering, y as coder, z as fixReviewNotesPrompt } from "../chunks/reserved-DDiiAZ8U.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-ILPlELuy.mjs";
5
5
  //#region src/workflows/fix/fix.ts
6
6
  /**
7
7
  * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
@@ -1,6 +1,6 @@
1
1
  import { start } from "../flows.mjs";
2
- import { X as reviewGateBlockedMessage, at as envDefaults, i as skills, it as defaults, l as gate, n as summary, o as shipped, r as access, t as steering } from "../chunks/reserved-iG86IR3I.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-DA2UuJNP.mjs";
2
+ import { Z as reviewGateBlockedMessage, at as defaults, i as skills, n as summary, ot as envDefaults, r as access, s as shipped, t as steering, u as gate } from "../chunks/reserved-DDiiAZ8U.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-ILPlELuy.mjs";
4
4
  //#region src/workflows/review/review.ts
5
5
  /**
6
6
  * Pure review of everything since `reviewBase`: a green gate, the quality
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { a as flagShipped, at as envDefaults, dt as requireFlag, f as FEEDBACK, i as skills, it as defaults, l as gate, lt as flipProblems, n as summary, r as access, st as flagMentions, t as steering, v as coder } from "../chunks/reserved-iG86IR3I.mjs";
2
+ import { at as defaults, ct as flagMentions, ft as requireFlag, i as skills, n as summary, o as flagShipped, ot as envDefaults, p as FEEDBACK, r as access, t as steering, u as gate, ut as flipProblems, y as coder } from "../chunks/reserved-DDiiAZ8U.mjs";
3
3
  //#region src/workflows/rollout/rollout.ts
4
4
  const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
5
5
  const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
@@ -16916,6 +16916,7 @@ const defaults = {
16916
16916
  reviewSeverityMinP: "0.7",
16917
16917
  refactorFlagPattern: "\\bGTD_FLAG_\\w+",
16918
16918
  seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
16919
+ flagMode: "true",
16919
16920
  flag_risk_tier: "0",
16920
16921
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
16921
16922
  refactorQualityReviews: "correctness, ponytail-review, conventions"
@@ -17956,18 +17957,20 @@ re-implement. It changed these files outside the expected set:
17956
17957
  ${listed(rejection.strays)}
17957
17958
  Start over and choose a different approach; do not rebuild the same diff.
17958
17959
  `}`;
17959
- const implementBuildingPrompt = (reference, tier, amend, afterRefactor) => `${stateFileRules}
17960
+ const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
17960
17961
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
17961
17962
  ${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
17962
17963
  though never a release flag. Build only what is still missing, held to every
17963
17964
  rule below; if nothing is, change nothing and finish your turn
17964
17965
  ` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
17965
17966
  the re-derived requirements instead of rewriting it
17966
- ` : `- Implement them properly, fitting the codebase's seams and conventions
17967
+ ` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
17968
+ ` : `- Implement them the quickest way that works — the naive version. Do not
17969
+ restructure, abstract or refactor around the change
17967
17970
  `}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
17968
17971
  Read it for ideas; never apply, cherry-pick or check it out — write
17969
17972
  the change fresh
17970
- `}${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
17973
+ `}${!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
17971
17974
  repository's flag conventions`}
17972
17975
  - Write the tests first, then the code that makes them pass
17973
17976
  - Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
@@ -37522,12 +37525,19 @@ const ShippedOptions = Struct({
37522
37525
  refactorQualityReviews: setting,
37523
37526
  testCommand: setting,
37524
37527
  plannerModel: setting,
37525
- coderModel: setting
37528
+ coderModel: setting,
37529
+ flagMode: optionalWith(Boolean$, { exact: true })
37526
37530
  });
37527
37531
  const over = (base, options) => ({
37528
37532
  ...base,
37529
- ...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)))
37533
+ ...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)).map(([name, value]) => [name, String(value)]))
37530
37534
  });
37535
+ /** The `flagMode` setting, pinned at process start. Anything but true/false fails rather than guess a side. */
37536
+ const flagModeOn = () => {
37537
+ const value = vars.flagMode ?? defaults.flagMode;
37538
+ if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
37539
+ return value === "true";
37540
+ };
37531
37541
  /** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
37532
37542
  const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
37533
37543
  onExcessProperty: "error",
@@ -38292,7 +38302,7 @@ function implement(first, tier, amend) {
38292
38302
  amend
38293
38303
  };
38294
38304
  return scope("implement", async () => {
38295
- await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor), {
38305
+ await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn()), {
38296
38306
  label: "Implementing",
38297
38307
  model: coder(),
38298
38308
  system: builderSystem(),
@@ -38373,10 +38383,10 @@ const unwind = () => {
38373
38383
  const commit = head$1();
38374
38384
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
38375
38385
  };
38376
- /** A judged naive pass, then implement and review until a review round signs off; design must have run since `firstBase`. */
38386
+ /** A judged naive pass (none with `flagMode` off), then implement and review until a review round signs off; design must have run since `firstBase`. */
38377
38387
  const buildFeature = async (firstBase) => {
38378
38388
  const tier = riskTierOn() ? recordRisk() : void 0;
38379
- const seam = await seamBuild(firstBase);
38389
+ const seam = flagModeOn() ? await seamBuild(firstBase) : { base: firstBase };
38380
38390
  await slice("feature", async () => {
38381
38391
  await implement({
38382
38392
  reference: seam.reference,
@@ -38421,6 +38431,7 @@ const ordinaryStart = async () => {
38421
38431
  file: ".gtd/TODO.md"
38422
38432
  });
38423
38433
  refusePendingHandOff();
38434
+ flagModeOn();
38424
38435
  await unwind();
38425
38436
  if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
38426
38437
  message: unwindFailedMessage(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "30.0.0",
3
+ "version": "30.1.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": {
@@ -397,3 +397,44 @@ describe("an in-flight process from before the architecture path was removed", (
397
397
  await expect(repo.next()).rejects.toThrow("the workflow changed under this process")
398
398
  })
399
399
  })
400
+
401
+ describe("flagMode", () => {
402
+ const config = (workflow: string): string =>
403
+ [
404
+ 'import { feature } from "@pmelab/gtd/workflows/feature"',
405
+ 'export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"',
406
+ `export default ${workflow}`,
407
+ "",
408
+ ].join("\n")
409
+
410
+ const designed = async (workflow: string, env: Record<string, string> = {}) => {
411
+ const repo = testRepo({ workflow, env: { GTD_QUALITYREVIEWS: "", ...env } })
412
+ repo.write("src/calc.ts", CALC)
413
+ repo.commit("feat: add calculator")
414
+ repo.write("TODO.md", "# Add subtract\n")
415
+ return repo.drive({
416
+ unwind: () => undefined,
417
+ "start-gate.check": () => undefined,
418
+ "design.triage": (_, r) =>
419
+ r.write(".gtd/REQUIREMENTS.md", "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n"),
420
+ "design.gate.answer": () => undefined,
421
+ })
422
+ }
423
+
424
+ it("set on explicitly, runs the naive pass like the default", async () => {
425
+ expect((await designed(config("feature({ flagMode: true })"))).state).toBe("seam.naive")
426
+ })
427
+
428
+ it("off through GTD_FLAGMODE=false, skips the naive pass and the seam judge", async () => {
429
+ const rest = await designed(CONFIG_SCAFFOLD, { GTD_FLAGMODE: "false" })
430
+ expect(rest.state).toBe("implement.building")
431
+ expect(rest.content).toContain("this project ships without release flags")
432
+ })
433
+
434
+ it("refuses a value other than true or false before the process starts", async () => {
435
+ const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_FLAGMODE: "off" } })
436
+ repo.write("TODO.md", "# Add subtract\n")
437
+ await expect(repo.drive({})).rejects.toThrow('flagMode must be "true" or "false", got "off"')
438
+ expect(repo.subjects()).toEqual(["chore: initial commit"])
439
+ })
440
+ })
@@ -9,20 +9,21 @@ import {
9
9
  start,
10
10
  } from "../../flows/index.js"
11
11
  import { gate } from "../shared/health.js"
12
- import { shipped } from "../shared/options.js"
12
+ import { flagModeOn, shipped } from "../shared/options.js"
13
13
  import { FEEDBACK } from "../shared/steps.js"
14
14
  import { refusePendingHandOff } from "../shared/tail.js"
15
15
  import * as t from "../shared/text.js"
16
16
  import { design } from "./planning.js"
17
17
  import { buildTail, isCode, reUnwind, type ReviewOutcome } from "./review.js"
18
- import { implement, seamBuild } from "./seam.js"
18
+ import { implement, seamBuild, type SeamOutcome } from "./seam.js"
19
19
  import { recordRisk, riskTierOn } from "./tier.js"
20
20
 
21
21
  // The ordinary start. Any change to the tree starts a process: `idle` →
22
22
  // `unwind` reverts the sketch (its intent survives in history) → a
23
23
  // green-baseline gate → design → a judged naive pass (refactoring first when
24
- // the judge asks) → implement → the quality lap → human review, which signs
25
- // off (the episode ends back at `idle`) or sends a design → implement lap.
24
+ // the judge asks; with `flagMode` off, neither) → implement → the quality lap
25
+ // → human review, which signs off (the episode ends back at `idle`) or sends a
26
+ // design → implement lap.
26
27
 
27
28
  /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
28
29
  export const unwind = (): Promise<void> => {
@@ -36,10 +37,12 @@ export { reUnwind }
36
37
 
37
38
  // seam.naive / seam.judge run once: a feedback lap is design → implement on every verdict, so a
38
39
  // seam a re-plan turns up is built unjudged and unrefactored.
39
- /** A judged naive pass, then implement and review until a review round signs off; design must have run since `firstBase`. */
40
+ /** A judged naive pass (none with `flagMode` off), then implement and review until a review round signs off; design must have run since `firstBase`. */
40
41
  export const buildFeature = async (firstBase: string): Promise<void> => {
41
42
  const tier = riskTierOn() ? recordRisk() : undefined
42
- const seam = await seamBuild(firstBase)
43
+ const seam: Omit<SeamOutcome, "reference"> & { reference?: string } = flagModeOn()
44
+ ? await seamBuild(firstBase)
45
+ : { base: firstBase }
43
46
  await slice("feature", async () => {
44
47
  await implement({ reference: seam.reference, tier, afterRefactor: seam.refactored })
45
48
  // An empty feature slice: the refactor carried the feature, so review the whole change.
@@ -75,6 +78,8 @@ export const afterTail = (outcome: ReviewOutcome): Promise<void> => featureFeedb
75
78
  export const ordinaryStart = async (): Promise<void> => {
76
79
  await human("idle", { message: t.idleMessage(), label: "Idle", file: ".gtd/TODO.md" })
77
80
  refusePendingHandOff()
81
+ // Read right after idle, so a bad value refuses the landing that would pin it.
82
+ flagModeOn()
78
83
  await unwind()
79
84
  if (changes(FEEDBACK).some((c) => c.status !== "deleted")) {
80
85
  await human("unwind-failed", {
@@ -0,0 +1,157 @@
1
+ @inmem
2
+ Feature: flag mode off builds a feature 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 implementation goes 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
+ And step "design.gate.answer" changes nothing
41
+
42
+ Scenario: flag mode off skips the seam judge and the refactor, and asks for no flag
43
+ When the process is driven
44
+ Then the process rests at "implement.building"
45
+ And the resting step's content contains "the quickest way that works"
46
+ And the resting step's content contains "Add no release flag — this project ships without release flags"
47
+ And the resting step's content does not contain "new release flag"
48
+ Given step "implement.building" writes "src/calc.ts" with:
49
+ """
50
+ export const add = (a: number, b: number) => a + b
51
+ export const subtract = (a: number, b: number) => a - b
52
+ """
53
+ And step "implement.health.check" changes nothing
54
+ And step "implement.closing" runs its script
55
+ And step "build.quality.correctness.reviewing" changes nothing
56
+ When the process is driven
57
+ Then the process rests at "build.review.reviewing"
58
+ And "src/calc.ts" contains "subtract"
59
+ And the commit subjects end with:
60
+ """
61
+ chore: initial commit
62
+ feat: add calculator
63
+ gtd(human): idle → unwind
64
+ gtd(check): unwind → start-gate.check
65
+ gtd(check): start-gate.check → design.triage
66
+ gtd(agent): design.triage → design.gate.answer
67
+ gtd(human): design.gate.answer → implement.building
68
+ gtd(agent): implement.building → implement.health.check
69
+ gtd(check): implement.health.check → implement.closing
70
+ gtd(check): implement.closing → build.quality.correctness.reviewing
71
+ gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
72
+ """
73
+
74
+ Scenario: flag mode off asks for no flag on a feedback lap either
75
+ Given step "implement.building" writes "src/calc.ts" with:
76
+ """
77
+ export const add = (a: number, b: number) => a + b
78
+ export const subtract = (a: number, b: number) => a - b
79
+ """
80
+ And step "implement.health.check" changes nothing
81
+ And step "implement.closing" runs its script
82
+ And step "build.quality.correctness.reviewing" changes nothing
83
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
84
+ """
85
+ # Review: abc1234
86
+
87
+ <!-- base: 0000000 -->
88
+
89
+ ## calc
90
+
91
+ - [ ] ./src/calc.ts#2-2
92
+ new subtract function
93
+ """
94
+ When the process is driven
95
+ Then the process rests at "build.review.await-review"
96
+ Given a file ".gtd/REVIEW.md" with:
97
+ """
98
+ # Review: abc1234
99
+
100
+ <!-- base: 0000000 -->
101
+
102
+ ## calc
103
+
104
+ - [ ] ./src/calc.ts#2-2
105
+ new subtract function[^why]
106
+
107
+ [^why]:
108
+ - H: also export multiply
109
+ """
110
+ And step "build.review.collecting" writes ".gtd/REQUIREMENTS.md" with:
111
+ """
112
+ # Requirements
113
+
114
+ ## Concerns
115
+
116
+ ### Multiply
117
+
118
+ - Export `multiply()`.
119
+ """
120
+ And step "build.review.collecting" writes ".gtd/REVIEW.md" with:
121
+ """
122
+ # Review: abc1234
123
+
124
+ <!-- base: 0000000 -->
125
+
126
+ ## calc
127
+
128
+ - [ ] ./src/calc.ts#2-2
129
+ new subtract function[^why]
130
+
131
+ [^why]:
132
+ - H: also export multiply
133
+ - A: noted in the requirements.
134
+ """
135
+ When gtd lands
136
+ And the process is driven
137
+ Then the process rests at "build.review.await-review"
138
+ Given a file ".gtd/REVIEW.md" with:
139
+ """
140
+ # Review: abc1234
141
+
142
+ <!-- base: 0000000 -->
143
+
144
+ ## calc
145
+
146
+ - [ ] ./src/calc.ts#2-2
147
+ new subtract function
148
+ """
149
+ And step "build.review.closing" runs its script
150
+ And step "re-unwind" runs its script
151
+ And step "implement.building" is no longer scripted
152
+ When gtd lands
153
+ And the process is driven
154
+ Then the process rests at "implement.building"
155
+ And the resting step's content contains "Add no release flag — this project ships without release flags"
156
+ And the resting step's content does not contain "release flag the earlier lap added"
157
+ And no commit body contains "Gtd-Kept:"
@@ -17,6 +17,7 @@ import { coder, FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
17
17
  import * as t from "../shared/text.js"
18
18
  import { defaults } from "../shared/vars.js"
19
19
  import { refactorStrays, reviewedRefactor } from "./refactor.js"
20
+ import { flagModeOn } from "../shared/options.js"
20
21
  import type { RiskTier } from "./tier.js"
21
22
 
22
23
  /** Naive-pass retries after a `reject`; the next `reject` rests at `seam.steer`. */
@@ -71,6 +72,7 @@ export function implement(
71
72
  options.tier,
72
73
  options.amend,
73
74
  options.afterRefactor,
75
+ flagModeOn(),
74
76
  ),
75
77
  {
76
78
  label: "Implementing",
@@ -30,6 +30,18 @@ describe("the shipped workflow factories", () => {
30
30
  })
31
31
  })
32
32
 
33
+ it("default flagMode on and lay a boolean factory option over it as a string", () => {
34
+ expect(defaults.flagMode).toBe("true")
35
+ expect(configure(feature({ flagMode: false }))).toMatchObject({
36
+ ok: true,
37
+ build: { defaults: { flagMode: "false" } },
38
+ })
39
+ expect(configure(review({ flagMode: true }))).toMatchObject({
40
+ ok: true,
41
+ build: { defaults: { flagMode: "true" } },
42
+ })
43
+ })
44
+
33
45
  it("declare flag_risk_tier off by default; it is no factory option", () => {
34
46
  expect(Object.keys(ShippedOptions.fields)).not.toContain("flag_risk_tier")
35
47
  })
@@ -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,
@@ -990,6 +990,22 @@ describe("the seam phase's texts", () => {
990
990
  expect(prompt).toContain("Implement them properly")
991
991
  })
992
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
+
993
1009
  it("keeps a fix turn to the feedback file's rules", () => {
994
1010
  expect(renderText(() => implementFixPrompt())).toContain(".gtd/FEEDBACK.md")
995
1011
  })
@@ -1091,6 +1091,7 @@ export const implementBuildingPrompt = (
1091
1091
  tier?: "low" | "high",
1092
1092
  amend?: boolean,
1093
1093
  afterRefactor?: boolean,
1094
+ flagMode = true,
1094
1095
  ): string =>
1095
1096
  `${stateFileRules}
1096
1097
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
@@ -1106,7 +1107,11 @@ ${
1106
1107
  ? `- The change already exists in the tree from an earlier lap; amend it to meet
1107
1108
  the re-derived requirements instead of rewriting it
1108
1109
  `
1109
- : `- Implement them properly, fitting the codebase's seams and conventions
1110
+ : flagMode
1111
+ ? `- Implement them properly, fitting the codebase's seams and conventions
1112
+ `
1113
+ : `- Implement them the quickest way that works — the naive version. Do not
1114
+ restructure, abstract or refactor around the change
1110
1115
  `
1111
1116
  }${
1112
1117
  reference === undefined || amend === true
@@ -1116,11 +1121,13 @@ ${
1116
1121
  the change fresh
1117
1122
  `
1118
1123
  }${
1119
- tier === "low"
1120
- ? "- Add no release flag — this change is low risk tier"
1121
- : amend === true
1122
- ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag"
1123
- : `- Put the new behaviour behind a new release flag, following the
1124
+ !flagMode
1125
+ ? "- Add no release flag — this project ships without release flags"
1126
+ : tier === "low"
1127
+ ? "- Add no release flag — this change is low risk tier"
1128
+ : amend === true
1129
+ ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag"
1130
+ : `- Put the new behaviour behind a new release flag, following the
1124
1131
  repository's flag conventions`
1125
1132
  }
1126
1133
  - Write the tests first, then the code that makes them pass
@@ -25,6 +25,7 @@ export const defaults: Readonly<Record<string, string>> = {
25
25
  reviewSeverityMinP: "0.7",
26
26
  refactorFlagPattern: "\\bGTD_FLAG_\\w+",
27
27
  seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
28
+ flagMode: "true",
28
29
  // GTD_FLAG_RISK_TIER, recorded at process start; only "1" is on.
29
30
  flag_risk_tier: "0",
30
31
  // Decides the turn count: one `build.quality.<lens>` scope per entry, each