@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "30.0.0",
3
+ "version": "31.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
@@ -418,18 +418,24 @@ noted in steps 3 and 4 below.
418
418
  with `- A: <reply>` and the process stops at the same gate again. Close a
419
419
  thread by replying with a conclusion or deleting it. While a thread's last
420
420
  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.
421
+ start the loop to accept the plan as-is, unanswered questions and all. The
422
+ file lists numbered acceptance criteria (`AC-1`, `AC-2`, …) that bind the
423
+ build. Sketch your own under an `Acceptance criteria` heading and, when a
424
+ judge finds every one clear and checkable, they are recorded as they are and
425
+ this moment is skipped; one vague criterion brings it back. See
426
+ [Acceptance criteria](docs/configuration.md#acceptance-criteria).
422
427
 
423
428
  3. **You wait.** The agent first implements the requirements the quickest way
424
429
  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
430
+ [the naive pass](#the-naive-pass) below; with [flag mode](#the-naive-pass)
431
+ off, neither runs). Then the change is built test-first, checked against the
432
+ full suite (`testCommand`) and fixed until it passes — so keep that suite
433
+ fast; gtd has no lighter one. Scenario wording is not frozen: you sign the
434
+ scenarios off once, in the review's `## Scenarios` chunk, where any assertion
435
+ an existing scenario lost is marked `Weakened:`. After the build the quality
436
+ lap runs. Three points along the process (two with flag mode off) are judged
437
+ rather than always asking you outright — each stops and hands you a verdict
438
+ to make (`gtd judge answer`, or land with a clean tree to accept the
433
439
  conservative default, which never skips work;
434
440
  `gtd judge run --provider fixed --answers <path>` — or the
435
441
  `GTD_JUDGE_ANSWERS` env var, inline JSON — answers one from a file, piped
@@ -595,6 +601,14 @@ out is `gtd abandon`. A process from 27.5.0 or later whose judge ruled
595
601
  that slice. Details in
596
602
  [the naive-pass path](docs/configuration.md#the-naive-pass-path).
597
603
 
604
+ **Flag mode (`flagMode`, on by default).** On, the implementation is asked for a
605
+ new release flag (none at a `low` risk tier) and the naive pass and its judge
606
+ run as above. A project that is not publicly released turns it off explicitly —
607
+ `feature({ flagMode: false })` in `gtd.config.ts`, it is never detected: then no
608
+ flag is asked for, no seam judge or refactor runs, and the implementation is the
609
+ naive version, straight through the health loop and the quality lap. See
610
+ [`flagMode`](docs/configuration.md#the-bundled-workflows-settings).
611
+
598
612
  **Risk tier (flag `GTD_FLAG_RISK_TIER=1`, off by default).** Triage proposes a
599
613
  risk tier, `low` or `high`, and the production signals to watch after merge.
600
614
  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"
@@ -438,6 +439,21 @@ const codeThreadReplies = () => {
438
439
  concluded thread in and delete its lines:
439
440
  ${waiting.map((t) => ` - ${t.path}:${t.line}: ${t.first}`).join("\n")}`;
440
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
+ `;
441
457
  const riskTierBlock = `
442
458
  ## Risk tier and production signals
443
459
 
@@ -481,7 +497,7 @@ ${footnoteFoldIn}${codeThreadReplies()}
481
497
  or both
482
498
  - Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
483
499
  uncommitted and finish once it reflects this lap's own work
484
-
500
+ ${acceptanceCriteriaBlock}
485
501
  ## First lap
486
502
 
487
503
  \`.gtd/REQUIREMENTS.md\` does not exist yet (or holds whatever the
@@ -536,6 +552,34 @@ ${questionBarReturn}
536
552
  made). If red, make that breakage the first concern, ahead of
537
553
  everything else — the build assumes the suite started green
538
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
+ };
539
583
  const designSystem = () => `${designPersona}
540
584
 
541
585
  ${agentConduct}`;
@@ -860,7 +904,8 @@ What each change does next (then run \`gtd land\`):
860
904
  const wrapItem = (item) => {
861
905
  const out = [];
862
906
  let line = "";
863
- 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) {
864
909
  out.push(line);
865
910
  line = ` ${word}`;
866
911
  } else line = line === "" ? word : `${line} ${word}`;
@@ -1158,8 +1203,12 @@ Print the closing message and stop — this writes nothing itself.
1158
1203
  `;
1159
1204
  };
1160
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"}`;
1161
1209
  const naivePassPrompt = (rejection) => `${stateFileRules}
1162
1210
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1211
+ ${criteriaBinding(false)}
1163
1212
  - Implement them the quickest way that works — the naive version. Do not
1164
1213
  restructure, abstract or refactor around the change; a later step judges
1165
1214
  the shape of your diff and a reviewer makes it fit
@@ -1172,18 +1221,21 @@ re-implement. It changed these files outside the expected set:
1172
1221
  ${listed(rejection.strays)}
1173
1222
  Start over and choose a different approach; do not rebuild the same diff.
1174
1223
  `}`;
1175
- const implementBuildingPrompt = (reference, tier, amend, afterRefactor) => `${stateFileRules}
1224
+ const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
1176
1225
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1226
+ ${criteriaBinding(true)}
1177
1227
  ${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
1178
1228
  though never a release flag. Build only what is still missing, held to every
1179
1229
  rule below; if nothing is, change nothing and finish your turn
1180
1230
  ` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
1181
1231
  the re-derived requirements instead of rewriting it
1182
- ` : `- Implement them properly, fitting the codebase's seams and conventions
1232
+ ` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
1233
+ ` : `- Implement them the quickest way that works — the naive version. Do not
1234
+ restructure, abstract or refactor around the change
1183
1235
  `}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
1184
1236
  Read it for ideas; never apply, cherry-pick or check it out — write
1185
1237
  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
1238
+ `}${!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
1239
  repository's flag conventions`}
1188
1240
  - Write the tests first, then the code that makes them pass
1189
1241
  - Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
@@ -1420,12 +1472,19 @@ const ShippedOptions = Struct({
1420
1472
  refactorQualityReviews: setting,
1421
1473
  testCommand: setting,
1422
1474
  plannerModel: setting,
1423
- coderModel: setting
1475
+ coderModel: setting,
1476
+ flagMode: optionalWith(Boolean$, { exact: true })
1424
1477
  });
1425
1478
  const over = (base, options) => ({
1426
1479
  ...base,
1427
- ...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)))
1480
+ ...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)).map(([name, value]) => [name, String(value)]))
1428
1481
  });
1482
+ /** The `flagMode` setting, pinned at process start. Anything but true/false fails rather than guess a side. */
1483
+ const flagModeOn = () => {
1484
+ const value = vars.flagMode ?? defaults.flagMode;
1485
+ if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
1486
+ return value === "true";
1487
+ };
1429
1488
  /** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
1430
1489
  const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
1431
1490
  onExcessProperty: "error",
@@ -1509,4 +1568,4 @@ const steering = {
1509
1568
  [REVIEW]: "review"
1510
1569
  };
1511
1570
  //#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 };
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 { 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 { 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 { 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 { 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 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 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
@@ -625,18 +748,22 @@ function implement(first, tier, amend) {
625
748
  amend
626
749
  };
627
750
  return scope("implement", async () => {
628
- await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor), {
751
+ await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn()), {
629
752
  label: "Implementing",
630
753
  model: coder(),
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
  }
@@ -706,10 +834,10 @@ const unwind = () => {
706
834
  const commit = head();
707
835
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
708
836
  };
709
- /** A judged naive pass, then implement and review until a review round signs off; design must have run since `firstBase`. */
837
+ /** 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
838
  const buildFeature = async (firstBase) => {
711
839
  const tier = riskTierOn() ? recordRisk() : void 0;
712
- const seam = await seamBuild(firstBase);
840
+ const seam = flagModeOn() ? await seamBuild(firstBase) : { base: firstBase };
713
841
  await slice("feature", async () => {
714
842
  await implement({
715
843
  reference: seam.reference,
@@ -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) => {
@@ -754,6 +882,8 @@ const ordinaryStart = async () => {
754
882
  file: ".gtd/TODO.md"
755
883
  });
756
884
  refusePendingHandOff();
885
+ flagModeOn();
886
+ const stated = statedCriteria(changes(), head());
757
887
  await unwind();
758
888
  if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
759
889
  message: unwindFailedMessage(),
@@ -761,11 +891,11 @@ const ordinaryStart = async () => {
761
891
  file: FEEDBACK
762
892
  });
763
893
  await gate("start-gate", startGateBlockedMessage());
764
- await planAndBuild(start());
894
+ await planAndBuild(start(), stated);
765
895
  };
766
896
  const feature = shipped(ordinaryStart);
767
897
  //#endregion
768
898
  //#region src/workflows/feature/index.ts
769
899
  var feature_default = feature;
770
900
  //#endregion
771
- 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 { 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, 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 { 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 { 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 { 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 { 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.";