@pmelab/gtd 30.1.0 → 32.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 (33) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +29 -23
  3. package/claude/hooks/ship.ts +7 -3
  4. package/dist/chunks/{reserved-DDiiAZ8U.mjs → reserved-CHTJTzpA.mjs} +52 -3
  5. package/dist/chunks/{tail-ILPlELuy.mjs → tail-CCsDBbZe.mjs} +1 -1
  6. package/dist/workflows/cleanup.mjs +2 -2
  7. package/dist/workflows/feature.mjs +137 -8
  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 +188 -10
  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 +41 -11
  19. package/src/workflows/feature/feature.ts +7 -4
  20. package/src/workflows/feature/flag-mode.feature +8 -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 +12 -7
  31. package/src/workflows/feature/tier-pin.feature +4 -0
  32. package/src/workflows/index.ts +1 -0
  33. package/src/workflows/shared/text.ts +77 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "30.1.0",
3
+ "version": "32.0.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
@@ -339,28 +339,29 @@ asks, and moves the process on only when you say so; a review you leave
339
339
  untouched signs off. While a step runs you can keep asking; requests for changes
340
340
  wait for the next rest. When the process finishes, Claude offers to squash it
341
341
  and open its pull request(s) (or run `/gtd ship`; `GTD_SHIP_MODE` picks one
342
- stacked pull request per slice or one commit per slice, see the
343
- [driver doc](docs/driver.md)); a process started for an issue ships with
344
- `Closes #N` on its top pull request; a later process on an already-stacked
345
- branch, such as review fixes, lands one commit per slice on the top pull request
346
- and keeps the stack; a closed pull request whose branch was deleted from origin
347
- is left behind and a new one opens; re-running `/gtd ship` after a lower stacked
348
- pull request merged restacks the rest (it refuses, with nothing changed, when
349
- that pull request merged anywhere but the default branch (unless into a lower
350
- slice branch before that one merged into it), a slice branch is checked out in
351
- another worktree, an unrelated local branch sits inside the rebased commits, or
352
- a slice's change is already upstream; a failed rebase is rolled back; to tear a
353
- stack down from its worktree, delete the top branch with
354
- `git push origin --delete`, since `gh pr close --delete-branch` cannot). A
355
- process can change hands at any gate: ask Claude to hand it off (or run
356
- `/gtd throw @dev`) and it opens a draft pull request assigned to them, and
357
- `/gtd catch <pr>` picks it up exactly where it waits, so whoever wrote the
358
- requirements can hand the build to someone else. Ship, hand-off and throw answer
359
- at once and run in the background, with progress on the status line; they report
360
- by waking Claude, a toast, a transcript line and a push notification, and a
361
- failed push or `gh` call reports as a failure. While a ship or hand-off runs or
362
- is queued, `/gtd catch`, the doors, `/gtd <requirements>` and bare `/gtd` wait
363
- for its report. See
342
+ commit per slice under one pull request (the default) or, set to `stack`, one
343
+ stacked pull request per slice, see the [driver doc](docs/driver.md)); a process
344
+ started for an issue ships with `Closes #N` on its top pull request; a closed
345
+ pull request whose branch was deleted from origin is left behind and a new one
346
+ opens; on a branch an earlier ship in this clone stacked (unless
347
+ `GTD_SHIP_MODE=commits`), a later process, such as review fixes, lands one
348
+ commit per slice on the top pull request and keeps the stack, and re-running
349
+ `/gtd ship` after a lower stacked pull request merged restacks the rest (it
350
+ refuses, with nothing changed, when that pull request merged anywhere but the
351
+ default branch (unless into a lower slice branch before that one merged into
352
+ it), a slice branch is checked out in another worktree, an unrelated local
353
+ branch sits inside the rebased commits, or a slice's change is already upstream;
354
+ a failed rebase is rolled back; to tear a stack down from its worktree, delete
355
+ the top branch with `git push origin --delete`, since
356
+ `gh pr close --delete-branch` cannot). A process can change hands at any gate:
357
+ ask Claude to hand it off (or run `/gtd throw @dev`) and it opens a draft pull
358
+ request assigned to them, and `/gtd catch <pr>` picks it up exactly where it
359
+ waits, so whoever wrote the requirements can hand the build to someone else.
360
+ Ship, hand-off and throw answer at once and run in the background, with progress
361
+ on the status line; they report by waking Claude, a toast, a transcript line and
362
+ a push notification, and a failed push or `gh` call reports as a failure. While
363
+ a ship or hand-off runs or is queued, `/gtd catch`, the doors,
364
+ `/gtd <requirements>` and bare `/gtd` wait for its report. See
364
365
  [Inside Claude Code](https://github.com/pmelab/gtd/blob/main/docs/driver.md#inside-claude-code-the-gtd-mod).
365
366
 
366
367
  ### Then let an agent build your own
@@ -418,7 +419,12 @@ noted in steps 3 and 4 below.
418
419
  with `- A: <reply>` and the process stops at the same gate again. Close a
419
420
  thread by replying with a conclusion or deleting it. While a thread's last
420
421
  entry is the agent's, moving on is refused. Leave the file untouched and
421
- start the loop to accept the plan as-is, unanswered questions and all.
422
+ start the loop to accept the plan as-is, unanswered questions and all. The
423
+ file lists numbered acceptance criteria (`AC-1`, `AC-2`, …) that bind the
424
+ build. Sketch your own under an `Acceptance criteria` heading and, when a
425
+ judge finds every one clear and checkable, they are recorded as they are and
426
+ this moment is skipped; one vague criterion brings it back. See
427
+ [Acceptance criteria](docs/configuration.md#acceptance-criteria).
422
428
 
423
429
  3. **You wait.** The agent first implements the requirements the quickest way
424
430
  that works — the naive pass — and a judge reads where it changed code (see
@@ -49,7 +49,7 @@ export function costTrailers(log: string) {
49
49
  export type ShipMode = "stack" | "commits"
50
50
 
51
51
  export function shipMode(raw: string | undefined): ShipMode | { refusal: string } {
52
- if (!raw) return "stack"
52
+ if (!raw) return "commits"
53
53
  if (raw === "stack" || raw === "commits") return raw
54
54
  return { refusal: `GTD_SHIP_MODE must be 'stack' or 'commits', not '${raw}'.` }
55
55
  }
@@ -209,8 +209,8 @@ async function preflight(git: Git, ok: Ok) {
209
209
  }
210
210
 
211
211
  export async function ship(io: ShipIo, isDry: boolean, rawMode?: string): Promise<Shipped> {
212
- const mode = shipMode(rawMode)
213
- if (typeof mode === "object") return fail(mode.refusal)
212
+ const valid = shipMode(rawMode)
213
+ if (typeof valid === "object") return fail(valid.refusal)
214
214
  const git = async (...args: string[]) => (await io.run(["git", ...args])).out.trim()
215
215
  const ok = async (...args: string[]) => (await io.run(["git", ...args])).code === 0
216
216
 
@@ -223,6 +223,10 @@ export async function ship(io: ShipIo, isDry: boolean, rawMode?: string): Promis
223
223
  const live = names(await git("config", "--local", "--get", STACK_KEY(branch)))
224
224
  const retired = names(await git("config", "--local", "--get", RETIRED_KEY(branch)))
225
225
  const recorded = [...live, ...retired]
226
+ // Unset keeps a live stack stacking: commits mode would retarget its top PR
227
+ // and strand the lower ones. A dead record ships as commits.
228
+ const stacked = !rawMode && live.length > 0 && (await chainsToHead(git, ok, branch, live))
229
+ const mode: ShipMode = stacked ? "stack" : valid
226
230
  const squashed = await squash(io, git, ok, branch, baseBranch, isDry, mode)
227
231
  if (!squashed.ok) return squashed
228
232
  // Until the PR step succeeds the record only moves aside: a retry must not
@@ -439,6 +439,21 @@ const codeThreadReplies = () => {
439
439
  concluded thread in and delete its lines:
440
440
  ${waiting.map((t) => ` - ${t.path}:${t.line}: ${t.first}`).join("\n")}`;
441
441
  };
442
+ const acceptanceCriteriaBlock = `
443
+ ## Acceptance criteria
444
+
445
+ Whichever lap this is, \`.gtd/REQUIREMENTS.md\` carries a
446
+ \`## Acceptance Criteria\` section: one \`- AC-<n>: <criterion>\` line
447
+ per criterion, numbered from \`AC-1\`. They bind every later step —
448
+ the build is done when each one holds — so each must be checkable: a
449
+ test or a command shows it holds or not. A later lap keeps every ID:
450
+ reword a criterion in place, append a new one with the next unused
451
+ number, delete a dropped one — never renumber, never reuse an ID. When
452
+ the file holds no list (a review loop-back rewrites it), recover the
453
+ last settled one from history (\`git log -p -- .gtd/REQUIREMENTS.md\`)
454
+ and carry its IDs over. Landing is refused while a criterion has no ID or
455
+ two share one.
456
+ `;
442
457
  const riskTierBlock = `
443
458
  ## Risk tier and production signals
444
459
 
@@ -482,7 +497,7 @@ ${footnoteFoldIn}${codeThreadReplies()}
482
497
  or both
483
498
  - Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
484
499
  uncommitted and finish once it reflects this lap's own work
485
-
500
+ ${acceptanceCriteriaBlock}
486
501
  ## First lap
487
502
 
488
503
  \`.gtd/REQUIREMENTS.md\` does not exist yet (or holds whatever the
@@ -537,6 +552,34 @@ ${questionBarReturn}
537
552
  made). If red, make that breakage the first concern, ahead of
538
553
  everything else — the build assumes the suite started green
539
554
  ${riskTier ? riskTierBlock : ""}`;
555
+ const clearCriteriaQuestion = {
556
+ id: "clear",
557
+ primitive: "noul",
558
+ 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.",
559
+ 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."
560
+ };
561
+ const clearCriteriaMessage = () => `The sketch stated its own acceptance criteria. Run \`gtd judge answer\` and
562
+ pipe \`yes\` when every one is clear and checkable, so design is skipped and
563
+ they are recorded as they are — or land without a verdict to run the
564
+ ordinary design conversation.
565
+ `;
566
+ const criterionLine = (c) => `- ${c.id}: ${c.text.replace(/\s+/g, " ")}`;
567
+ /** Writes \`.gtd/REQUIREMENTS.md\` from criteria the sketch stated, wrapped the way oxfmt writes it. */
568
+ const normaliseCriteriaScript = (stated) => {
569
+ return `mkdir -p .gtd
570
+ printf '%s\\n' ${[
571
+ "# Requirements",
572
+ "",
573
+ `Stated in \`${stated.path}\` by the sketch:`,
574
+ "",
575
+ `\`git show ${stated.commit}:${stated.path}\``,
576
+ "",
577
+ "## Acceptance Criteria",
578
+ "",
579
+ ...stated.criteria.flatMap((c) => wrapItem(criterionLine(c)))
580
+ ].map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
581
+ `;
582
+ };
540
583
  const designSystem = () => `${designPersona}
541
584
 
542
585
  ${agentConduct}`;
@@ -861,7 +904,8 @@ What each change does next (then run \`gtd land\`):
861
904
  const wrapItem = (item) => {
862
905
  const out = [];
863
906
  let line = "";
864
- for (const word of item.split(" ")) if (line !== "" && line.length + 1 + word.length > 80) {
907
+ const words = item.split(" ").reduce((acc, word) => acc.length > 0 && /^([-+*>#]|\d+[.)])$/.test(word) ? [...acc.slice(0, -1), `${acc.at(-1)} ${word}`] : [...acc, word], []);
908
+ for (const word of words) if (line !== "" && line.length + 1 + word.length > 80) {
865
909
  out.push(line);
866
910
  line = ` ${word}`;
867
911
  } else line = line === "" ? word : `${line} ${word}`;
@@ -1159,8 +1203,12 @@ Print the closing message and stop — this writes nothing itself.
1159
1203
  `;
1160
1204
  };
1161
1205
  const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
1206
+ const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
1207
+ the file, never from memory, and stop only when each one holds. They are
1208
+ settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
1162
1209
  const naivePassPrompt = (rejection) => `${stateFileRules}
1163
1210
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1211
+ ${criteriaBinding(false)}
1164
1212
  - Implement them the quickest way that works — the naive version. Do not
1165
1213
  restructure, abstract or refactor around the change; a later step judges
1166
1214
  the shape of your diff and a reviewer makes it fit
@@ -1175,6 +1223,7 @@ Start over and choose a different approach; do not rebuild the same diff.
1175
1223
  `}`;
1176
1224
  const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
1177
1225
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1226
+ ${criteriaBinding(true)}
1178
1227
  ${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
1179
1228
  though never a release flag. Build only what is still missing, held to every
1180
1229
  rule below; if nothing is, change nothing and finish your turn
@@ -1519,4 +1568,4 @@ const steering = {
1519
1568
  [REVIEW]: "review"
1520
1569
  };
1521
1570
  //#endregion
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 };
1571
+ export { reviewEditNotesCapture as $, buildReviewCollectingPrompt as A, designTriagePrompt as B, planner as C, agentWithSkills as D, reviewing as E, clearCriteriaQuestion as F, implementBuildingPrompt as G, fixReviewNotesPrompt as H, criterionLine as I, normaliseCriteriaScript as J, implementFixPrompt as K, deferNotesScript as L, buildReviewTriageMessage as M, builderSystem as N, buildReviewAnswerQuestionsPrompt as O, clearCriteriaMessage as P, refactorRules as Q, designGateAnswerMessage as R, fixRisks as S, reviewQuality as T, handOffAwaitReviewMessage as U, finisherSystem as V, idleMessage as W, refactorDroppedRefusal as X, refactorBuildingPrompt as Y, refactorFixPrompt as Z, REVIEW as _, flagModeOn as a, seamSteerMessage as at, fixCheck as b, baseline as c, unwindFailure as ct, guarded as d, testFileGlobs as dt, reviewEditsCapture as et, healthy as f, flagMentions as ft, REQUIREMENTS as g, requireFlag as gt, REFACTOR as h, registryEntry as ht, skills as i, seamByHandMessage as it, buildReviewFixNitsPrompt as j, buildReviewAwaitAfterDeferMessage as k, escalation as l, defaults as lt, QUALITY as m, flipProblems as mt, summary as n, reviewNotesCapture as nt, flagShipped as o, startGateBlockedMessage as ot, FEEDBACK as p, flagReferences as pt, naivePassPrompt as q, access as r, reviewerSystem as rt, shipped as s, unwindFailedMessage as st, steering as t, reviewGateBlockedMessage as tt, gate as u, envDefaults as ut, awaitReview as v, reviewMissing as w, fixQuality as x, coder as y, designSystem 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 { 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";
3
+ import { E as reviewing, S as fixRisks, T as reviewQuality, U as handOffAwaitReviewMessage, _ 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-CHTJTzpA.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 { 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";
2
+ import { gt as requireFlag, ht as registryEntry, i as skills, lt as defaults, n as summary, o as flagShipped, pt as flagReferences, r as access, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-CHTJTzpA.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CCsDBbZe.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 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";
3
+ import { $ as reviewEditNotesCapture, A as buildReviewCollectingPrompt, B as designTriagePrompt, C as planner, D as agentWithSkills, E as reviewing, F as clearCriteriaQuestion, G as implementBuildingPrompt, I as criterionLine, J as normaliseCriteriaScript, K as implementFixPrompt, L as deferNotesScript, M as buildReviewTriageMessage, N as builderSystem, O as buildReviewAnswerQuestionsPrompt, P as clearCriteriaMessage, Q as refactorRules, R as designGateAnswerMessage, S as fixRisks, W as idleMessage, X as refactorDroppedRefusal, Y as refactorBuildingPrompt, Z as refactorFixPrompt, _ as REVIEW, a as flagModeOn, at as seamSteerMessage, b as fixCheck, ct as unwindFailure, d as guarded, dt as testFileGlobs, et as reviewEditsCapture, f as healthy, g as REQUIREMENTS, h as REFACTOR, i as skills, it as seamByHandMessage, j as buildReviewFixNitsPrompt, k as buildReviewAwaitAfterDeferMessage, lt as defaults, n as summary, nt as reviewNotesCapture, ot as startGateBlockedMessage, p as FEEDBACK, q as naivePassPrompt, r as access, rt as reviewerSystem, s as shipped, st as unwindFailedMessage, t as steering, u as gate, ut as envDefaults, v as awaitReview, w as reviewMissing, y as coder, z as designSystem } from "../chunks/reserved-CHTJTzpA.mjs";
4
+ import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-CCsDBbZe.mjs";
5
5
  //#region src/workflows/shared/fences.ts
6
6
  const markFenced = (text) => {
7
7
  let fence;
@@ -17,6 +17,128 @@ const markFenced = (text) => {
17
17
  });
18
18
  };
19
19
  //#endregion
20
+ //#region src/workflows/feature/criteria.ts
21
+ /** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
22
+ const CLEAR_MIN_P = .8;
23
+ const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
24
+ const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
25
+ const ID = /^(AC-\d+):\s*(.*)$/;
26
+ /** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
27
+ const listUnder = (text, title, level) => {
28
+ const lines = markFenced(text);
29
+ const at = lines.findIndex((l) => {
30
+ const heading = l.fenced ? null : HEADING.exec(l.line);
31
+ return heading !== null && (level === void 0 || heading[1].length === level) && title.test(heading[2]);
32
+ });
33
+ if (at === -1) return void 0;
34
+ const items = [];
35
+ let open = false;
36
+ for (const { line, fenced } of lines.slice(at + 1)) {
37
+ if (fenced) {
38
+ open = false;
39
+ continue;
40
+ }
41
+ if (HEADING.test(line)) break;
42
+ const item = ITEM.exec(line);
43
+ if (item !== null) {
44
+ items.push(item[1].trim());
45
+ open = true;
46
+ } else if (open && line.trim() !== "") items[items.length - 1] = `${items.at(-1)} ${line.trim()}`.trim();
47
+ else open = false;
48
+ }
49
+ return items;
50
+ };
51
+ /** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
52
+ const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
53
+ const id = ID.exec(item);
54
+ return id === null ? {
55
+ id: "",
56
+ text: item
57
+ } : {
58
+ id: id[1],
59
+ text: id[2]
60
+ };
61
+ });
62
+ /** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
63
+ const numbered = (items) => {
64
+ const ids = items.map((item) => ID.exec(item));
65
+ const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
66
+ return items.map((item, i) => own ? {
67
+ id: ids[i][1],
68
+ text: ids[i][2]
69
+ } : {
70
+ id: `AC-${i + 1}`,
71
+ text: item.replace(ID, "$2")
72
+ });
73
+ };
74
+ const STATED = /^acceptance criteria:?$/i;
75
+ const statedItems = (text) => text === void 0 ? [] : (listUnder(text, STATED) ?? []).filter((item) => item !== "");
76
+ /**
77
+ * The criteria a `TODO.md` in the sketch `sketch` (the idle turn's changes)
78
+ * added or changed under an `Acceptance criteria` heading. More than one such
79
+ * file is ambiguous, and any other change in the sketch is input only triage
80
+ * folds in, so none.
81
+ */
82
+ const statedCriteria = (sketch, commit) => {
83
+ const found = sketch.flatMap((change) => {
84
+ if (!/(^|\/)TODO\.md$/.test(change.path) || change.after === void 0) return [];
85
+ const items = statedItems(change.after);
86
+ if (items.length === 0 || items.join("\n") === statedItems(change.before).join("\n")) return [];
87
+ return [{
88
+ path: change.path,
89
+ commit,
90
+ text: change.after,
91
+ criteria: numbered(items)
92
+ }];
93
+ });
94
+ return found.length === 1 && sketch.length === 1 ? found[0] : void 0;
95
+ };
96
+ const listing = (criteria) => criteria.map(criterionLine).join("\n");
97
+ /** Refuse a turn that leaves `file` without ID'd acceptance criteria. A deleted file is `requireProgress`'s call. */
98
+ const requireCriteria = (file) => {
99
+ const text = read(file);
100
+ if (text === void 0) return;
101
+ const criteria = criteriaOf(text) ?? [];
102
+ const unnamed = criteria.filter((c) => c.id === "");
103
+ const ids = criteria.map((c) => c.id).filter((id) => id !== "");
104
+ const twice = ids.filter((id, i) => ids.indexOf(id) !== i);
105
+ if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return;
106
+ 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`);
107
+ };
108
+ /** Refuse a turn that changed the acceptance criteria in `file`: after the design gate only another design lap may. */
109
+ const requireCriteriaFrozen = (file) => {
110
+ const change = changes().get(file);
111
+ if (change === void 0) return;
112
+ if (listing(criteriaOf(change.before ?? "") ?? []) === listing(criteriaOf(change.after ?? "") ?? [])) return;
113
+ refuse(`gtd land: criteria-frozen: the acceptance criteria in ${file} are settled — restore them; only a design lap changes them`);
114
+ };
115
+ /** Whether the judge trusts `stated` as clear and checkable, every criterion of it. Truncated evidence never is. */
116
+ const clear = async (stated) => {
117
+ const { answers, truncated } = await judge("clear", {
118
+ questions: [clearCriteriaQuestion],
119
+ evidence: {
120
+ criteria: listing(stated.criteria),
121
+ sketch: stated.text
122
+ },
123
+ message: clearCriteriaMessage(),
124
+ label: "Judging the stated acceptance criteria"
125
+ });
126
+ return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P);
127
+ };
128
+ /**
129
+ * The skip path: when the sketch stated clear, checkable criteria, write them
130
+ * to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
131
+ * design gate. `false` when design must run.
132
+ */
133
+ const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scope("design", async () => {
134
+ if (!await clear(stated)) return false;
135
+ await run("normalise", normaliseCriteriaScript(stated), {
136
+ label: "Recording the stated acceptance criteria",
137
+ file: REQUIREMENTS
138
+ });
139
+ return true;
140
+ });
141
+ //#endregion
20
142
  //#region src/workflows/feature/tier.ts
21
143
  /** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
22
144
  const riskTierOn = () => vars.flag_risk_tier === "1";
@@ -137,6 +259,7 @@ const design = (base) => scope("design", async () => {
137
259
  await triage(base);
138
260
  requireProgress(REQUIREMENTS);
139
261
  requireReplies(REQUIREMENTS);
262
+ requireCriteria(REQUIREMENTS);
140
263
  } while (await noteGate(REQUIREMENTS, answerProductQuestions));
141
264
  });
142
265
  //#endregion
@@ -631,12 +754,16 @@ function implement(first, tier, amend) {
631
754
  system: builderSystem(),
632
755
  ...options.afterRefactor === true ? { allowEmpty: true } : {}
633
756
  });
757
+ requireCriteriaFrozen(REQUIREMENTS);
634
758
  await healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
635
759
  label: "Fixing the check",
636
760
  file: FEEDBACK,
637
761
  model: coder(),
638
762
  system: builderSystem()
639
- }));
763
+ }), {
764
+ guard: () => requireCriteriaFrozen(REQUIREMENTS),
765
+ guardRests: false
766
+ });
640
767
  await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
641
768
  });
642
769
  }
@@ -667,6 +794,7 @@ const decide = () => scope("seam", async () => {
667
794
  acceptClean: true
668
795
  });
669
796
  if (changes().length === 0) return { reference: tip.hash };
797
+ requireCriteria(REQUIREMENTS);
670
798
  rejection = void 0;
671
799
  retries = 0;
672
800
  }
@@ -723,9 +851,9 @@ const buildFeature = async (firstBase) => {
723
851
  }), base);
724
852
  });
725
853
  };
726
- /** Design, then `buildFeature`. */
727
- const planAndBuild = async (firstBase) => {
728
- await design(firstBase);
854
+ /** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
855
+ const planAndBuild = async (firstBase, stated) => {
856
+ if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
729
857
  await buildFeature(firstBase);
730
858
  };
731
859
  const featureFeedback = async (first, lensBase) => {
@@ -755,6 +883,7 @@ const ordinaryStart = async () => {
755
883
  });
756
884
  refusePendingHandOff();
757
885
  flagModeOn();
886
+ const stated = statedCriteria(changes(), head());
758
887
  await unwind();
759
888
  if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
760
889
  message: unwindFailedMessage(),
@@ -762,11 +891,11 @@ const ordinaryStart = async () => {
762
891
  file: FEEDBACK
763
892
  });
764
893
  await gate("start-gate", startGateBlockedMessage());
765
- await planAndBuild(start());
894
+ await planAndBuild(start(), stated);
766
895
  };
767
896
  const feature = shipped(ordinaryStart);
768
897
  //#endregion
769
898
  //#region src/workflows/feature/index.ts
770
899
  var feature_default = feature;
771
900
  //#endregion
772
- export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, implement, judgeSeam, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, steering, summary, triage, unwind };
901
+ export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, criteriaOf, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, implement, judgeSeam, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, statedCriteria, steering, summary, triage, unwind };
@@ -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 { 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";
3
+ import { D as agentWithSkills, H as fixReviewNotesPrompt, V as finisherSystem, _ as REVIEW, c as baseline, i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-CHTJTzpA.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-CCsDBbZe.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 { 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";
2
+ import { i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, tt as reviewGateBlockedMessage, u as gate, ut as envDefaults } from "../chunks/reserved-CHTJTzpA.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CCsDBbZe.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 { 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";
2
+ import { ft as flagMentions, gt as requireFlag, i as skills, lt as defaults, mt as flipProblems, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, ut as envDefaults, y as coder } from "../chunks/reserved-CHTJTzpA.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.";