@pmelab/gtd 30.0.0 → 31.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 (37) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +23 -9
  3. package/claude/hooks/register.tsx +5 -0
  4. package/dist/chunks/{reserved-iG86IR3I.mjs → reserved-CHTJTzpA.mjs} +68 -9
  5. package/dist/chunks/{tail-DA2UuJNP.mjs → tail-CCsDBbZe.mjs} +1 -1
  6. package/dist/workflows/cleanup.mjs +2 -2
  7. package/dist/workflows/feature.mjs +141 -11
  8. package/dist/workflows/fix.mjs +2 -2
  9. package/dist/workflows/review.mjs +2 -2
  10. package/dist/workflows/rollout.mjs +1 -1
  11. package/dist/workflows.mjs +207 -18
  12. package/package.json +1 -1
  13. package/src/workflows/feature/acceptance-criteria-tier.feature +25 -0
  14. package/src/workflows/feature/acceptance-criteria.feature +189 -0
  15. package/src/workflows/feature/criteria.test.ts +106 -0
  16. package/src/workflows/feature/criteria.ts +168 -0
  17. package/src/workflows/feature/fast-path.feature +4 -0
  18. package/src/workflows/feature/feature.test.ts +81 -10
  19. package/src/workflows/feature/feature.ts +18 -10
  20. package/src/workflows/feature/flag-mode.feature +165 -0
  21. package/src/workflows/feature/index.ts +1 -0
  22. package/src/workflows/feature/one-step-land.feature +4 -0
  23. package/src/workflows/feature/planning.ts +2 -0
  24. package/src/workflows/feature/refactor-absorbs.feature +4 -0
  25. package/src/workflows/feature/refactor-first.feature +4 -0
  26. package/src/workflows/feature/refactor-review.feature +24 -0
  27. package/src/workflows/feature/risk-tier.feature +32 -0
  28. package/src/workflows/feature/seam-rejection.feature +8 -0
  29. package/src/workflows/feature/seam.feature +8 -0
  30. package/src/workflows/feature/seam.ts +14 -7
  31. package/src/workflows/feature/tier-pin.feature +4 -0
  32. package/src/workflows/index.ts +1 -0
  33. package/src/workflows/shared/options.test.ts +12 -0
  34. package/src/workflows/shared/options.ts +17 -2
  35. package/src/workflows/shared/text.test.ts +16 -0
  36. package/src/workflows/shared/text.ts +90 -8
  37. package/src/workflows/shared/vars.ts +1 -0
@@ -499,7 +499,7 @@ const fixFindings = async (f) => {
499
499
  const DOC_STRING = /^("""|```)/;
500
500
  const THEN = /^Then\s/;
501
501
  const CONTINUATION = /^(And|But|\*)\s/;
502
- const HEADING = /^(Feature|Rule|Background|Scenario|Scenario Outline|Scenario Template|Example):/;
502
+ const HEADING$1 = /^(Feature|Rule|Background|Scenario|Scenario Outline|Scenario Template|Example):/;
503
503
  /** Trimmed lines outside doc strings, whose content is test input, not steps. */
504
504
  const stepLines = (source) => {
505
505
  const out = [];
@@ -519,7 +519,7 @@ const assertionLines = (source) => {
519
519
  let inThen = false;
520
520
  let scenario = "";
521
521
  for (const line of stepLines(source)) {
522
- if (HEADING.test(line)) scenario = line;
522
+ if (HEADING$1.test(line)) scenario = line;
523
523
  if (THEN.test(line)) inThen = true;
524
524
  else if (!CONTINUATION.test(line) && !neutral(line)) inThen = false;
525
525
  if (inThen && !neutral(line)) out.push({
@@ -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"
@@ -17222,6 +17223,21 @@ const codeThreadReplies = () => {
17222
17223
  concluded thread in and delete its lines:
17223
17224
  ${waiting.map((t) => ` - ${t.path}:${t.line}: ${t.first}`).join("\n")}`;
17224
17225
  };
17226
+ const acceptanceCriteriaBlock = `
17227
+ ## Acceptance criteria
17228
+
17229
+ Whichever lap this is, \`.gtd/REQUIREMENTS.md\` carries a
17230
+ \`## Acceptance Criteria\` section: one \`- AC-<n>: <criterion>\` line
17231
+ per criterion, numbered from \`AC-1\`. They bind every later step —
17232
+ the build is done when each one holds — so each must be checkable: a
17233
+ test or a command shows it holds or not. A later lap keeps every ID:
17234
+ reword a criterion in place, append a new one with the next unused
17235
+ number, delete a dropped one — never renumber, never reuse an ID. When
17236
+ the file holds no list (a review loop-back rewrites it), recover the
17237
+ last settled one from history (\`git log -p -- .gtd/REQUIREMENTS.md\`)
17238
+ and carry its IDs over. Landing is refused while a criterion has no ID or
17239
+ two share one.
17240
+ `;
17225
17241
  const riskTierBlock = `
17226
17242
  ## Risk tier and production signals
17227
17243
 
@@ -17265,7 +17281,7 @@ ${footnoteFoldIn}${codeThreadReplies()}
17265
17281
  or both
17266
17282
  - Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
17267
17283
  uncommitted and finish once it reflects this lap's own work
17268
-
17284
+ ${acceptanceCriteriaBlock}
17269
17285
  ## First lap
17270
17286
 
17271
17287
  \`.gtd/REQUIREMENTS.md\` does not exist yet (or holds whatever the
@@ -17320,6 +17336,34 @@ ${questionBarReturn}
17320
17336
  made). If red, make that breakage the first concern, ahead of
17321
17337
  everything else — the build assumes the suite started green
17322
17338
  ${riskTier ? riskTierBlock : ""}`;
17339
+ const clearCriteriaQuestion = {
17340
+ id: "clear",
17341
+ primitive: "noul",
17342
+ instructions: "state.criteria lists the acceptance criteria the human's sketch (state.sketch) stated, as gtd numbered them. Decide whether they can stand as the binding definition of done, with no design conversation.",
17343
+ criteria: "yes: every criterion is clear and checkable — a test or a command can show it holds or not — and together they say what to build. no: any one criterion is vague, subjective or untestable, or the sketch leaves an open product question. Unsure means no."
17344
+ };
17345
+ const clearCriteriaMessage = () => `The sketch stated its own acceptance criteria. Run \`gtd judge answer\` and
17346
+ pipe \`yes\` when every one is clear and checkable, so design is skipped and
17347
+ they are recorded as they are — or land without a verdict to run the
17348
+ ordinary design conversation.
17349
+ `;
17350
+ const criterionLine = (c) => `- ${c.id}: ${c.text.replace(/\s+/g, " ")}`;
17351
+ /** Writes \`.gtd/REQUIREMENTS.md\` from criteria the sketch stated, wrapped the way oxfmt writes it. */
17352
+ const normaliseCriteriaScript = (stated) => {
17353
+ return `mkdir -p .gtd
17354
+ printf '%s\\n' ${[
17355
+ "# Requirements",
17356
+ "",
17357
+ `Stated in \`${stated.path}\` by the sketch:`,
17358
+ "",
17359
+ `\`git show ${stated.commit}:${stated.path}\``,
17360
+ "",
17361
+ "## Acceptance Criteria",
17362
+ "",
17363
+ ...stated.criteria.flatMap((c) => wrapItem(criterionLine(c)))
17364
+ ].map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
17365
+ `;
17366
+ };
17323
17367
  const designSystem = () => `${designPersona}
17324
17368
 
17325
17369
  ${agentConduct}`;
@@ -17644,7 +17688,8 @@ What each change does next (then run \`gtd land\`):
17644
17688
  const wrapItem = (item) => {
17645
17689
  const out = [];
17646
17690
  let line = "";
17647
- for (const word of item.split(" ")) if (line !== "" && line.length + 1 + word.length > 80) {
17691
+ const words = item.split(" ").reduce((acc, word) => acc.length > 0 && /^([-+*>#]|\d+[.)])$/.test(word) ? [...acc.slice(0, -1), `${acc.at(-1)} ${word}`] : [...acc, word], []);
17692
+ for (const word of words) if (line !== "" && line.length + 1 + word.length > 80) {
17648
17693
  out.push(line);
17649
17694
  line = ` ${word}`;
17650
17695
  } else line = line === "" ? word : `${line} ${word}`;
@@ -17942,8 +17987,12 @@ Print the closing message and stop — this writes nothing itself.
17942
17987
  `;
17943
17988
  };
17944
17989
  const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
17990
+ const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
17991
+ the file, never from memory, and stop only when each one holds. They are
17992
+ settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
17945
17993
  const naivePassPrompt = (rejection) => `${stateFileRules}
17946
17994
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
17995
+ ${criteriaBinding(false)}
17947
17996
  - Implement them the quickest way that works — the naive version. Do not
17948
17997
  restructure, abstract or refactor around the change; a later step judges
17949
17998
  the shape of your diff and a reviewer makes it fit
@@ -17956,18 +18005,21 @@ re-implement. It changed these files outside the expected set:
17956
18005
  ${listed(rejection.strays)}
17957
18006
  Start over and choose a different approach; do not rebuild the same diff.
17958
18007
  `}`;
17959
- const implementBuildingPrompt = (reference, tier, amend, afterRefactor) => `${stateFileRules}
18008
+ const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
17960
18009
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18010
+ ${criteriaBinding(true)}
17961
18011
  ${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
17962
18012
  though never a release flag. Build only what is still missing, held to every
17963
18013
  rule below; if nothing is, change nothing and finish your turn
17964
18014
  ` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
17965
18015
  the re-derived requirements instead of rewriting it
17966
- ` : `- Implement them properly, fitting the codebase's seams and conventions
18016
+ ` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
18017
+ ` : `- Implement them the quickest way that works — the naive version. Do not
18018
+ restructure, abstract or refactor around the change
17967
18019
  `}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
17968
18020
  Read it for ideas; never apply, cherry-pick or check it out — write
17969
18021
  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
18022
+ `}${!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
18023
  repository's flag conventions`}
17972
18024
  - Write the tests first, then the code that makes them pass
17973
18025
  - Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
@@ -37522,12 +37574,19 @@ const ShippedOptions = Struct({
37522
37574
  refactorQualityReviews: setting,
37523
37575
  testCommand: setting,
37524
37576
  plannerModel: setting,
37525
- coderModel: setting
37577
+ coderModel: setting,
37578
+ flagMode: optionalWith(Boolean$, { exact: true })
37526
37579
  });
37527
37580
  const over = (base, options) => ({
37528
37581
  ...base,
37529
- ...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)))
37582
+ ...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)).map(([name, value]) => [name, String(value)]))
37530
37583
  });
37584
+ /** The `flagMode` setting, pinned at process start. Anything but true/false fails rather than guess a side. */
37585
+ const flagModeOn = () => {
37586
+ const value = vars.flagMode ?? defaults.flagMode;
37587
+ if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
37588
+ return value === "true";
37589
+ };
37531
37590
  /** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
37532
37591
  const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
37533
37592
  onExcessProperty: "error",
@@ -37684,6 +37743,128 @@ const markFenced = (text) => {
37684
37743
  });
37685
37744
  };
37686
37745
  //#endregion
37746
+ //#region src/workflows/feature/criteria.ts
37747
+ /** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
37748
+ const CLEAR_MIN_P = .8;
37749
+ const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
37750
+ const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
37751
+ const ID = /^(AC-\d+):\s*(.*)$/;
37752
+ /** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
37753
+ const listUnder = (text, title, level) => {
37754
+ const lines = markFenced(text);
37755
+ const at = lines.findIndex((l) => {
37756
+ const heading = l.fenced ? null : HEADING.exec(l.line);
37757
+ return heading !== null && (level === void 0 || heading[1].length === level) && title.test(heading[2]);
37758
+ });
37759
+ if (at === -1) return void 0;
37760
+ const items = [];
37761
+ let open = false;
37762
+ for (const { line, fenced } of lines.slice(at + 1)) {
37763
+ if (fenced) {
37764
+ open = false;
37765
+ continue;
37766
+ }
37767
+ if (HEADING.test(line)) break;
37768
+ const item = ITEM.exec(line);
37769
+ if (item !== null) {
37770
+ items.push(item[1].trim());
37771
+ open = true;
37772
+ } else if (open && line.trim() !== "") items[items.length - 1] = `${items.at(-1)} ${line.trim()}`.trim();
37773
+ else open = false;
37774
+ }
37775
+ return items;
37776
+ };
37777
+ /** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
37778
+ const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
37779
+ const id = ID.exec(item);
37780
+ return id === null ? {
37781
+ id: "",
37782
+ text: item
37783
+ } : {
37784
+ id: id[1],
37785
+ text: id[2]
37786
+ };
37787
+ });
37788
+ /** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
37789
+ const numbered = (items) => {
37790
+ const ids = items.map((item) => ID.exec(item));
37791
+ const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
37792
+ return items.map((item, i) => own ? {
37793
+ id: ids[i][1],
37794
+ text: ids[i][2]
37795
+ } : {
37796
+ id: `AC-${i + 1}`,
37797
+ text: item.replace(ID, "$2")
37798
+ });
37799
+ };
37800
+ const STATED = /^acceptance criteria:?$/i;
37801
+ const statedItems = (text) => text === void 0 ? [] : (listUnder(text, STATED) ?? []).filter((item) => item !== "");
37802
+ /**
37803
+ * The criteria a `TODO.md` in the sketch `sketch` (the idle turn's changes)
37804
+ * added or changed under an `Acceptance criteria` heading. More than one such
37805
+ * file is ambiguous, and any other change in the sketch is input only triage
37806
+ * folds in, so none.
37807
+ */
37808
+ const statedCriteria = (sketch, commit) => {
37809
+ const found = sketch.flatMap((change) => {
37810
+ if (!/(^|\/)TODO\.md$/.test(change.path) || change.after === void 0) return [];
37811
+ const items = statedItems(change.after);
37812
+ if (items.length === 0 || items.join("\n") === statedItems(change.before).join("\n")) return [];
37813
+ return [{
37814
+ path: change.path,
37815
+ commit,
37816
+ text: change.after,
37817
+ criteria: numbered(items)
37818
+ }];
37819
+ });
37820
+ return found.length === 1 && sketch.length === 1 ? found[0] : void 0;
37821
+ };
37822
+ const listing = (criteria) => criteria.map(criterionLine).join("\n");
37823
+ /** Refuse a turn that leaves `file` without ID'd acceptance criteria. A deleted file is `requireProgress`'s call. */
37824
+ const requireCriteria = (file) => {
37825
+ const text = read(file);
37826
+ if (text === void 0) return;
37827
+ const criteria = criteriaOf(text) ?? [];
37828
+ const unnamed = criteria.filter((c) => c.id === "");
37829
+ const ids = criteria.map((c) => c.id).filter((id) => id !== "");
37830
+ const twice = ids.filter((id, i) => ids.indexOf(id) !== i);
37831
+ if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return;
37832
+ 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`);
37833
+ };
37834
+ /** Refuse a turn that changed the acceptance criteria in `file`: after the design gate only another design lap may. */
37835
+ const requireCriteriaFrozen = (file) => {
37836
+ const change = changes().get(file);
37837
+ if (change === void 0) return;
37838
+ if (listing(criteriaOf(change.before ?? "") ?? []) === listing(criteriaOf(change.after ?? "") ?? [])) return;
37839
+ refuse(`gtd land: criteria-frozen: the acceptance criteria in ${file} are settled — restore them; only a design lap changes them`);
37840
+ };
37841
+ /** Whether the judge trusts `stated` as clear and checkable, every criterion of it. Truncated evidence never is. */
37842
+ const clear = async (stated) => {
37843
+ const { answers, truncated } = await judge("clear", {
37844
+ questions: [clearCriteriaQuestion],
37845
+ evidence: {
37846
+ criteria: listing(stated.criteria),
37847
+ sketch: stated.text
37848
+ },
37849
+ message: clearCriteriaMessage(),
37850
+ label: "Judging the stated acceptance criteria"
37851
+ });
37852
+ return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P);
37853
+ };
37854
+ /**
37855
+ * The skip path: when the sketch stated clear, checkable criteria, write them
37856
+ * to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
37857
+ * design gate. `false` when design must run.
37858
+ */
37859
+ const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scope("design", async () => {
37860
+ if (!await clear(stated)) return false;
37861
+ await run("normalise", normaliseCriteriaScript(stated), {
37862
+ label: "Recording the stated acceptance criteria",
37863
+ file: REQUIREMENTS
37864
+ });
37865
+ return true;
37866
+ });
37867
+ //#endregion
37687
37868
  //#region src/workflows/feature/tier.ts
37688
37869
  /** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
37689
37870
  const riskTierOn = () => vars.flag_risk_tier === "1";
@@ -37804,6 +37985,7 @@ const design = (base) => scope("design", async () => {
37804
37985
  await triage(base);
37805
37986
  requireProgress(REQUIREMENTS);
37806
37987
  requireReplies(REQUIREMENTS);
37988
+ requireCriteria(REQUIREMENTS);
37807
37989
  } while (await noteGate(REQUIREMENTS, answerProductQuestions));
37808
37990
  });
37809
37991
  //#endregion
@@ -38292,18 +38474,22 @@ function implement(first, tier, amend) {
38292
38474
  amend
38293
38475
  };
38294
38476
  return scope("implement", async () => {
38295
- await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor), {
38477
+ await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn()), {
38296
38478
  label: "Implementing",
38297
38479
  model: coder(),
38298
38480
  system: builderSystem(),
38299
38481
  ...options.afterRefactor === true ? { allowEmpty: true } : {}
38300
38482
  });
38483
+ requireCriteriaFrozen(REQUIREMENTS);
38301
38484
  await healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
38302
38485
  label: "Fixing the check",
38303
38486
  file: FEEDBACK,
38304
38487
  model: coder(),
38305
38488
  system: builderSystem()
38306
- }));
38489
+ }), {
38490
+ guard: () => requireCriteriaFrozen(REQUIREMENTS),
38491
+ guardRests: false
38492
+ });
38307
38493
  await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
38308
38494
  });
38309
38495
  }
@@ -38334,6 +38520,7 @@ const decide = () => scope("seam", async () => {
38334
38520
  acceptClean: true
38335
38521
  });
38336
38522
  if (changes().length === 0) return { reference: tip.hash };
38523
+ requireCriteria(REQUIREMENTS);
38337
38524
  rejection = void 0;
38338
38525
  retries = 0;
38339
38526
  }
@@ -38373,10 +38560,10 @@ const unwind = () => {
38373
38560
  const commit = head$1();
38374
38561
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
38375
38562
  };
38376
- /** A judged naive pass, then implement and review until a review round signs off; design must have run since `firstBase`. */
38563
+ /** 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
38564
  const buildFeature = async (firstBase) => {
38378
38565
  const tier = riskTierOn() ? recordRisk() : void 0;
38379
- const seam = await seamBuild(firstBase);
38566
+ const seam = flagModeOn() ? await seamBuild(firstBase) : { base: firstBase };
38380
38567
  await slice("feature", async () => {
38381
38568
  await implement({
38382
38569
  reference: seam.reference,
@@ -38390,9 +38577,9 @@ const buildFeature = async (firstBase) => {
38390
38577
  }), base);
38391
38578
  });
38392
38579
  };
38393
- /** Design, then `buildFeature`. */
38394
- const planAndBuild = async (firstBase) => {
38395
- await design(firstBase);
38580
+ /** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
38581
+ const planAndBuild = async (firstBase, stated) => {
38582
+ if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
38396
38583
  await buildFeature(firstBase);
38397
38584
  };
38398
38585
  const featureFeedback = async (first, lensBase) => {
@@ -38421,6 +38608,8 @@ const ordinaryStart = async () => {
38421
38608
  file: ".gtd/TODO.md"
38422
38609
  });
38423
38610
  refusePendingHandOff();
38611
+ flagModeOn();
38612
+ const stated = statedCriteria(changes(), head$1());
38424
38613
  await unwind();
38425
38614
  if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
38426
38615
  message: unwindFailedMessage(),
@@ -38428,7 +38617,7 @@ const ordinaryStart = async () => {
38428
38617
  file: FEEDBACK
38429
38618
  });
38430
38619
  await gate("start-gate", startGateBlockedMessage());
38431
- await planAndBuild(start());
38620
+ await planAndBuild(start(), stated);
38432
38621
  };
38433
38622
  const feature = shipped(ordinaryStart);
38434
38623
  //#endregion
@@ -38614,4 +38803,4 @@ var workflows_default = feature;
38614
38803
  */
38615
38804
  const doors = {};
38616
38805
  //#endregion
38617
- export { ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, implement, judgeSeam, lensesOf, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, steering, summary, tail, threads, triage, unwind };
38806
+ export { ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, implement, judgeSeam, lensesOf, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unwind };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "30.0.0",
3
+ "version": "31.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": {
@@ -0,0 +1,25 @@
1
+ @inmem
2
+ Feature: with GTD_FLAG_RISK_TIER on, stated acceptance criteria never skip design
3
+
4
+ The risk tier is proposed at `design.triage` and set at the design gate, so
5
+ with the flag on a sketch that states its own criteria still runs triage and
6
+ is never judged at `design.clear`.
7
+
8
+ Scenario: stated criteria go through triage, where the tier is proposed and set
9
+ Given the shipped workflows, as gtd install scaffolds them
10
+ And an environment variable "GTD_QUALITYREVIEWS" set to ""
11
+ And an environment variable "GTD_FLAG_RISK_TIER" set to "1"
12
+ And step "unwind" changes nothing
13
+ And step "start-gate.check" changes nothing
14
+ And a file "TODO.md" with:
15
+ """
16
+ ## Acceptance criteria
17
+
18
+ - `greeting` reads "Hello, world".
19
+ """
20
+ When the process is driven
21
+ Then the process rests at "design.triage"
22
+ And the commit subjects end with:
23
+ """
24
+ gtd(check): start-gate.check → design.triage
25
+ """
@@ -0,0 +1,189 @@
1
+ @inmem
2
+ Feature: acceptance criteria carry stable IDs, and a sketch that states clear ones skips design
3
+
4
+ `.gtd/REQUIREMENTS.md` lists the change's acceptance criteria as
5
+ `- AC-<n>: …` under `## Acceptance Criteria`. On the full path
6
+ `design.triage` writes them and the design gate settles them; a sketch
7
+ that already states its criteria under an `Acceptance criteria` heading
8
+ is judged at `design.clear`, and a `yes` records them as they are — no
9
+ triage, no design gate. After design they are frozen.
10
+
11
+ Background:
12
+ Given the shipped workflows, as gtd install scaffolds them
13
+ And an environment variable "GTD_QUALITYREVIEWS" set to ""
14
+ And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
15
+ And a file "src/greeting.ts" with:
16
+ """
17
+ export const greeting = "Helo, world"
18
+ """
19
+ And the working tree is committed as "feat: add greeting"
20
+ And step "unwind" changes nothing
21
+ And step "start-gate.check" changes nothing
22
+
23
+ Rule: the full path
24
+
25
+ Background:
26
+ Given a file "TODO.md" with:
27
+ """
28
+ # Fix the typo
29
+
30
+ greeting says "Helo"; make it "Hello".
31
+ """
32
+
33
+ Scenario: triage writes ID'd criteria that reach the naive pass through the design gate
34
+ Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
35
+ """
36
+ # Requirements
37
+
38
+ ## Acceptance Criteria
39
+
40
+ - AC-1: `greeting` reads "Hello, world".
41
+ - AC-2: no other export changes.
42
+ """
43
+ And step "design.gate.answer" changes nothing
44
+ When the process is driven
45
+ Then the process rests at "seam.naive"
46
+ And ".gtd/REQUIREMENTS.md" contains "- AC-1: `greeting` reads \"Hello, world\"."
47
+ And ".gtd/REQUIREMENTS.md" contains "- AC-2: no other export changes."
48
+ And the resting step's content contains "Acceptance Criteria"
49
+ And the commit subjects end with:
50
+ """
51
+ gtd(check): start-gate.check → design.triage
52
+ gtd(agent): design.triage → design.gate.answer
53
+ gtd(human): design.gate.answer → seam.naive
54
+ """
55
+
56
+ Scenario: a triage turn whose criteria carry no ID is refused
57
+ Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
58
+ """
59
+ # Requirements
60
+
61
+ ## Acceptance Criteria
62
+
63
+ - `greeting` reads "Hello, world".
64
+ """
65
+ Then driving the process is refused with "acceptance-criteria: .gtd/REQUIREMENTS.md has 1 criterion without an ID"
66
+
67
+ Scenario: a triage turn reusing an ID is refused
68
+ Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
69
+ """
70
+ # Requirements
71
+
72
+ ## Acceptance Criteria
73
+
74
+ - AC-1: `greeting` reads "Hello, world".
75
+ - AC-1: no other export changes.
76
+ """
77
+ Then driving the process is refused with "duplicate IDs: AC-1"
78
+
79
+ Scenario: an implementation turn that edits the settled criteria is refused
80
+ Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
81
+ """
82
+ # Requirements
83
+
84
+ ## Acceptance Criteria
85
+
86
+ - AC-1: `greeting` reads "Hello, world".
87
+ """
88
+ And step "design.gate.answer" changes nothing
89
+ And step "seam.naive" writes "src/greeting.ts" with:
90
+ """
91
+ export const greeting = "Hello, world"
92
+ """
93
+ And judge step "seam.judge" answers:
94
+ """
95
+ [{"id":"shape","answer":"land","p":0.9}]
96
+ """
97
+ And step "implement.building" writes ".gtd/REQUIREMENTS.md" with:
98
+ """
99
+ # Requirements
100
+
101
+ ## Acceptance Criteria
102
+
103
+ - AC-1: `greeting` reads "Hi".
104
+ """
105
+ Then driving the process is refused with "criteria-frozen"
106
+
107
+ Rule: the skip path
108
+
109
+ Scenario: clear stated criteria are normalised into ID'd requirements with no triage and no design gate
110
+ Given a file "TODO.md" with:
111
+ """
112
+ # Fix the typo
113
+
114
+ ## Acceptance criteria
115
+
116
+ - [ ] `greeting` reads "Hello, world".
117
+ - [ ] no other export changes.
118
+ """
119
+ And judge step "design.clear" answers:
120
+ """
121
+ [{"id":"clear","answer":"yes","p":0.9}]
122
+ """
123
+ And step "design.normalise" runs its script
124
+ When the process is driven
125
+ Then the process rests at "seam.naive"
126
+ And ".gtd/REQUIREMENTS.md" contains "- AC-1: `greeting` reads \"Hello, world\"."
127
+ And ".gtd/REQUIREMENTS.md" contains "- AC-2: no other export changes."
128
+ And the commit subjects end with:
129
+ """
130
+ gtd(human): idle → unwind
131
+ gtd(check): unwind → start-gate.check
132
+ gtd(check): start-gate.check → design.clear
133
+ gtd(judge): design.clear → design.normalise
134
+ gtd(check): design.normalise → seam.naive
135
+ """
136
+
137
+ Scenario: IDs the sketch already gave its criteria are kept
138
+ Given a file "TODO.md" with:
139
+ """
140
+ ## Acceptance criteria
141
+
142
+ 1. AC-3: `greeting` reads "Hello, world".
143
+ 2. AC-7: no other export changes.
144
+ """
145
+ And judge step "design.clear" answers:
146
+ """
147
+ [{"id":"clear","answer":"yes","p":0.9}]
148
+ """
149
+ And step "design.normalise" runs its script
150
+ When the process is driven
151
+ Then the process rests at "seam.naive"
152
+ And ".gtd/REQUIREMENTS.md" contains "- AC-3: `greeting` reads \"Hello, world\"."
153
+ And ".gtd/REQUIREMENTS.md" contains "- AC-7: no other export changes."
154
+
155
+ Scenario: one criterion the judge finds uncheckable falls back to the full path
156
+ Given a file "TODO.md" with:
157
+ """
158
+ # Fix the typo
159
+
160
+ ## Acceptance criteria
161
+
162
+ - `greeting` reads "Hello, world".
163
+ - the greeting feels friendlier.
164
+ """
165
+ And judge step "design.clear" answers:
166
+ """
167
+ [{"id":"clear","answer":"no","p":0.9}]
168
+ """
169
+ When the process is driven
170
+ Then the process rests at "design.triage"
171
+ And the commit subjects end with:
172
+ """
173
+ gtd(check): start-gate.check → design.clear
174
+ gtd(judge): design.clear → design.triage
175
+ """
176
+
177
+ Scenario: a sketch with no criteria heading goes straight to triage, unjudged
178
+ Given a file "TODO.md" with:
179
+ """
180
+ # Fix the typo
181
+
182
+ - `greeting` reads "Hello, world".
183
+ """
184
+ When the process is driven
185
+ Then the process rests at "design.triage"
186
+ And the commit subjects end with:
187
+ """
188
+ gtd(check): start-gate.check → design.triage
189
+ """