@pmelab/gtd 58.2.0 → 59.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 (133) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +115 -390
  3. package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
  4. package/dist/gtd.bundle.mjs +76 -59
  5. package/dist/testkit/gherkin.mjs +1 -1
  6. package/dist/testkit.mjs +1 -1
  7. package/package.json +3 -42
  8. package/skills/authoring/SKILL.md +31 -25
  9. package/src/cli/Cli.test.ts +15 -19
  10. package/src/cli/Cli.ts +2 -4
  11. package/src/cli/Program.test.ts +60 -29
  12. package/src/cli/Program.ts +5 -7
  13. package/src/edge/Edge.ts +3 -3
  14. package/src/flags/index.test.ts +0 -13
  15. package/src/flags/index.ts +1 -3
  16. package/src/install/Install.test.ts +14 -11
  17. package/src/install/Install.ts +38 -15
  18. package/src/install/index.ts +1 -2
  19. package/src/replay/Replay.test.ts +1 -1
  20. package/src/replay/index.ts +1 -1
  21. package/src/step/planStart.ts +6 -1
  22. package/src/twins/FlowScripts.ts +1 -1
  23. package/src/workflow/index.ts +1 -0
  24. package/src/workflow/load.test.ts +138 -113
  25. package/src/workflow/load.ts +1 -1
  26. package/src/workflow/vars.test.ts +30 -3
  27. package/src/workflow/vars.ts +34 -2
  28. package/src/workflow-kit/index.ts +1 -1
  29. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  30. package/dist/chunks/review-Bfaror38.mjs +0 -427
  31. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  32. package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
  33. package/dist/workflows/cleanup.mjs +0 -36
  34. package/dist/workflows/feature.mjs +0 -1018
  35. package/dist/workflows/fix.mjs +0 -41
  36. package/dist/workflows/review.mjs +0 -45
  37. package/dist/workflows/rollout.mjs +0 -48
  38. package/dist/workflows.mjs +0 -40651
  39. package/src/install/ConfigScaffold.ts +0 -22
  40. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  41. package/src/workflows/cleanup/cleanup.feature +0 -315
  42. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  43. package/src/workflows/cleanup/cleanup.ts +0 -52
  44. package/src/workflows/cleanup/index.ts +0 -6
  45. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  46. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  47. package/src/workflows/feature/criteria-coverage.feature +0 -102
  48. package/src/workflows/feature/criteria.test.ts +0 -147
  49. package/src/workflows/feature/criteria.ts +0 -267
  50. package/src/workflows/feature/fast-path.feature +0 -93
  51. package/src/workflows/feature/feature.test.ts +0 -338
  52. package/src/workflows/feature/feature.ts +0 -156
  53. package/src/workflows/feature/green.ts +0 -19
  54. package/src/workflows/feature/index.ts +0 -18
  55. package/src/workflows/feature/lens-models.feature +0 -69
  56. package/src/workflows/feature/lens-review.feature +0 -566
  57. package/src/workflows/feature/naive-acceptance.feature +0 -147
  58. package/src/workflows/feature/naive-test.feature +0 -263
  59. package/src/workflows/feature/one-step-land.feature +0 -93
  60. package/src/workflows/feature/planning.ts +0 -41
  61. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  62. package/src/workflows/feature/refactor-first.feature +0 -158
  63. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  64. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  65. package/src/workflows/feature/refactor-review.feature +0 -1528
  66. package/src/workflows/feature/refactor.test.ts +0 -1220
  67. package/src/workflows/feature/refactor.ts +0 -368
  68. package/src/workflows/feature/release-flag.feature +0 -167
  69. package/src/workflows/feature/release-flag.test.ts +0 -49
  70. package/src/workflows/feature/release-flag.ts +0 -46
  71. package/src/workflows/feature/review-gate.feature +0 -241
  72. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  73. package/src/workflows/feature/risk-tier.feature +0 -351
  74. package/src/workflows/feature/seam-rejection.feature +0 -146
  75. package/src/workflows/feature/seam.feature +0 -297
  76. package/src/workflows/feature/seam.test.ts +0 -527
  77. package/src/workflows/feature/seam.ts +0 -426
  78. package/src/workflows/feature/standards.feature +0 -354
  79. package/src/workflows/feature/steps.test.ts +0 -65
  80. package/src/workflows/feature/steps.ts +0 -45
  81. package/src/workflows/feature/tier-pin.feature +0 -66
  82. package/src/workflows/feature/tier.test.ts +0 -166
  83. package/src/workflows/feature/tier.ts +0 -56
  84. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  85. package/src/workflows/fix/fix-quality.feature +0 -212
  86. package/src/workflows/fix/fix.feature +0 -79
  87. package/src/workflows/fix/fix.test.ts +0 -74
  88. package/src/workflows/fix/fix.ts +0 -52
  89. package/src/workflows/fix/index.ts +0 -6
  90. package/src/workflows/index.ts +0 -47
  91. package/src/workflows/review/index.ts +0 -6
  92. package/src/workflows/review/review-door.feature +0 -199
  93. package/src/workflows/review/review.test.ts +0 -27
  94. package/src/workflows/review/review.ts +0 -60
  95. package/src/workflows/rollout/index.ts +0 -6
  96. package/src/workflows/rollout/rollout.feature +0 -510
  97. package/src/workflows/rollout/rollout.test.ts +0 -263
  98. package/src/workflows/rollout/rollout.ts +0 -82
  99. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  100. package/src/workflows/shared/access.test.ts +0 -62
  101. package/src/workflows/shared/access.ts +0 -24
  102. package/src/workflows/shared/config.fixture.ts +0 -12
  103. package/src/workflows/shared/fences.ts +0 -24
  104. package/src/workflows/shared/health.test.ts +0 -148
  105. package/src/workflows/shared/health.ts +0 -191
  106. package/src/workflows/shared/learn.test.ts +0 -140
  107. package/src/workflows/shared/learn.ts +0 -201
  108. package/src/workflows/shared/lenses.test.ts +0 -39
  109. package/src/workflows/shared/lenses.ts +0 -294
  110. package/src/workflows/shared/options.test.ts +0 -157
  111. package/src/workflows/shared/options.ts +0 -92
  112. package/src/workflows/shared/progress.feature +0 -124
  113. package/src/workflows/shared/prose.ts +0 -263
  114. package/src/workflows/shared/recheck.feature +0 -60
  115. package/src/workflows/shared/registry.test.ts +0 -345
  116. package/src/workflows/shared/registry.ts +0 -183
  117. package/src/workflows/shared/reserved.ts +0 -11
  118. package/src/workflows/shared/review.test.ts +0 -1062
  119. package/src/workflows/shared/review.ts +0 -446
  120. package/src/workflows/shared/skills.test.ts +0 -121
  121. package/src/workflows/shared/skills.ts +0 -49
  122. package/src/workflows/shared/standards.test.ts +0 -90
  123. package/src/workflows/shared/standards.ts +0 -156
  124. package/src/workflows/shared/steps.test.ts +0 -219
  125. package/src/workflows/shared/steps.ts +0 -201
  126. package/src/workflows/shared/tail.test.ts +0 -454
  127. package/src/workflows/shared/tail.ts +0 -264
  128. package/src/workflows/shared/text.fixture.ts +0 -128
  129. package/src/workflows/shared/text.test.ts +0 -1430
  130. package/src/workflows/shared/text.ts +0 -1700
  131. package/src/workflows/shared/threads.test.ts +0 -129
  132. package/src/workflows/shared/vars.ts +0 -47
  133. package/src/workflows/shared/workflows.steps.ts +0 -54
@@ -1,1018 +0,0 @@
1
- import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, restoreScript, revertScript, run, scope, start, vars, wrote } from "../flows.mjs";
2
- import { lensEntriesOf } from "../workflow-kit.mjs";
3
- import { B as designSystem, Ct as testFileGlobs, D as planner, F as clearCriteriaMessage, I as clearCriteriaQuestion, J as implementBuildingPrompt, L as criterionLine, M as buildReviewCollectingPrompt, P as builderSystem, Q as naiveTryMessage, R as designGateAnswerMessage, S as collecting, St as envDefaults, V as designTriagePrompt, X as naiveApplyFailure, Y as implementFixPrompt, Z as naivePassPrompt, _ as REQUIREMENTS, _t as unwindFailedMessage, at as refactorFixPrompt, d as healthy, et as normaliseCriteriaScript, f as FEEDBACK, g as REFACTOR_NOTE, gt as startGateBlockedMessage, h as REFACTOR, ht as seamSteerMessage, i as skills, it as refactorDroppedRefusal, j as agentWithSkills, l as gate, mt as seamByHandMessage, n as summary, o as naiveTestOn, ot as refactorNotePrompt, p as NAIVE_TEST, pt as reviewerSystem, q as idleMessage, r as access, rt as refactorBuildingPrompt, s as shipped, st as refactorRules, t as steering, vt as unwindFailure, w as fixNits, x as coder, xt as defaults, y as answerReviewQuestions, z as designImpactPrompt } from "../chunks/reserved-lMYCaapY.mjs";
4
- import { a as refusePendingHandOff } from "../chunks/tail-Bd7L940p.mjs";
5
- import { n as isCode$1, r as reUnwind, t as buildTail } from "../chunks/review-Bfaror38.mjs";
6
- import { posix } from "node:path";
7
- //#region src/workflows/shared/fences.ts
8
- const markFenced = (text) => {
9
- let fence;
10
- return text.split(/\r?\n/).map((line) => {
11
- const run = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
12
- const fenced = fence !== void 0 || run !== void 0;
13
- if (fence === void 0) fence = run;
14
- else if (run !== void 0 && run[0] === fence[0] && run.length >= fence.length && /^ {0,3}(`+|~+)[ \t]*$/.test(line)) fence = void 0;
15
- return {
16
- line,
17
- fenced
18
- };
19
- });
20
- };
21
- /** A markdown heading: its `#` run, then its text. */
22
- const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
23
- /** A list item, its checkbox dropped: its text. */
24
- const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
25
- //#endregion
26
- //#region src/workflows/feature/release-flag.ts
27
- const NAME = /^existing:\s*`?([A-Za-z_][\w.-]*)`?$/i;
28
- const isFlagHeading = (h) => h !== null && h[1].length === 2 && /^release flag$/i.test(h[2].trim());
29
- /** The first line under `## Release Flag`, `undefined` without the section. */
30
- const requestLine = (text) => {
31
- const lines = markFenced(text);
32
- const at = lines.findIndex((l) => !l.fenced && isFlagHeading(HEADING.exec(l.line)));
33
- if (at === -1) return void 0;
34
- const body = lines.slice(at + 1);
35
- const end = body.findIndex((l) => !l.fenced && HEADING.test(l.line));
36
- return (end === -1 ? body : body.slice(0, end)).find((l) => l.line.trim() !== "")?.line.trim();
37
- };
38
- /** What `text` asks for, or why it cannot be read. No section asks for no flag. */
39
- const releaseFlagOf = (text) => {
40
- const line = requestLine(text);
41
- if (line === void 0 || /^none$/i.test(line)) return { kind: "none" };
42
- if (/^new$/i.test(line)) return { kind: "new" };
43
- const name = NAME.exec(line)?.[1];
44
- return name === void 0 ? { unreadable: line } : {
45
- kind: "existing",
46
- name
47
- };
48
- };
49
- /** Refuse `file` when its `## Release Flag` section is neither `none`, `new` nor `existing: <NAME>`. */
50
- const requireReleaseFlag = (file) => {
51
- const request = releaseFlagOf(read(file) ?? "");
52
- if (!("unreadable" in request)) return;
53
- refuse(`gtd land: release-flag: ${file}'s "## Release Flag" reads ${JSON.stringify(request.unreadable)} — its first line must be \`new\`, \`existing: <NAME>\` or \`none\`, or leave the section out for no flag`);
54
- };
55
- /** What `.gtd/REQUIREMENTS.md` asks for now; a person's unreadable edit is refused. */
56
- const requestedFlag = () => {
57
- requireReleaseFlag(REQUIREMENTS);
58
- return releaseFlagOf(read(".gtd/REQUIREMENTS.md") ?? "");
59
- };
60
- /** Whether the change goes behind a release flag: only then does the seam judge rule. */
61
- const flagged = () => requestedFlag().kind !== "none";
62
- //#endregion
63
- //#region src/workflows/feature/criteria.ts
64
- /** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
65
- const CLEAR_MIN_P = .8;
66
- const ID = /^(AC-\d+):\s*(.*)$/;
67
- const criterion = (id, as = id[1]) => ({
68
- id: as,
69
- text: id[2]
70
- });
71
- /** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
72
- const listUnder = (text, title, level) => {
73
- const lines = markFenced(text);
74
- const at = lines.findIndex((l) => {
75
- const heading = l.fenced ? null : HEADING.exec(l.line);
76
- return heading !== null && (level === void 0 || heading[1].length === level) && title.test(heading[2]);
77
- });
78
- if (at === -1) return void 0;
79
- const items = [];
80
- let open = false;
81
- for (const { line, fenced } of lines.slice(at + 1)) {
82
- if (fenced) {
83
- open = false;
84
- continue;
85
- }
86
- if (HEADING.test(line)) break;
87
- const item = ITEM.exec(line);
88
- if (item !== null) {
89
- items.push(item[1].trim());
90
- open = true;
91
- } else if (open && line.trim() !== "") items[items.length - 1] = `${items.at(-1)} ${line.trim()}`.trim();
92
- else open = false;
93
- }
94
- return items;
95
- };
96
- /** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
97
- const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
98
- const id = ID.exec(item);
99
- return id === null ? {
100
- id: "",
101
- text: item
102
- } : criterion(id);
103
- });
104
- /** The ID'd criteria of `text`. */
105
- const criterionIds = (text) => (criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "");
106
- /** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
107
- const numbered = (items) => {
108
- const ids = items.map((item) => ID.exec(item));
109
- const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
110
- return items.map((item, i) => {
111
- const id = ids[i];
112
- if (own) return criterion(id);
113
- return id === null ? {
114
- id: `AC-${i + 1}`,
115
- text: item
116
- } : criterion(id, `AC-${i + 1}`);
117
- });
118
- };
119
- const STATED = /^acceptance criteria:?$/i;
120
- const statedItems = (text) => text === void 0 ? [] : (listUnder(text, STATED) ?? []).filter((item) => item !== "");
121
- /**
122
- * The criteria a `TODO.md` in the sketch `sketch` (the idle turn's changes)
123
- * added or changed under an `Acceptance criteria` heading. More than one such
124
- * file is ambiguous, and any other change in the sketch is input only triage
125
- * folds in, so none.
126
- */
127
- const statedCriteria = (sketch, commit) => {
128
- const found = sketch.flatMap((change) => {
129
- if (!/(^|\/)TODO\.md$/.test(change.path) || change.after === void 0) return [];
130
- const items = statedItems(change.after);
131
- if (items.length === 0 || items.join("\n") === statedItems(change.before).join("\n")) return [];
132
- return [{
133
- path: change.path,
134
- commit,
135
- text: change.after,
136
- criteria: numbered(items)
137
- }];
138
- });
139
- return found.length === 1 && sketch.length === 1 ? found[0] : void 0;
140
- };
141
- const listing = (criteria) => criteria.map(criterionLine).join("\n");
142
- /** Refuse a turn that leaves `file` without ID'd acceptance criteria. A deleted file is `requireProgress`'s call. */
143
- const requireCriteria = (file) => {
144
- const text = read(file);
145
- if (text === void 0) return;
146
- const criteria = criteriaOf(text) ?? [];
147
- const unnamed = criteria.filter((c) => c.id === "");
148
- const ids = criteria.map((c) => c.id).filter((id) => id !== "");
149
- const twice = ids.filter((id, i) => ids.indexOf(id) !== i);
150
- if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return;
151
- 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`);
152
- };
153
- const isImpactHeading = (h) => h !== null && h[1].length === 2 && /^impact$/i.test(h[2].trim());
154
- const isImpactTurnHeading = (h) => h !== null && h[1].length === 2 && /^(impact|release flag)$/i.test(h[2].trim());
155
- /** Whether `text` has an unfenced, level-2 `## Impact` heading. */
156
- const hasImpact = (text) => markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)));
157
- /** `text` without its `## Impact` and `## Release Flag` sections, whitespace collapsed. */
158
- const withoutImpact = (text) => {
159
- let inImpact = false;
160
- return markFenced(text).filter((l) => {
161
- const h = l.fenced ? null : HEADING.exec(l.line);
162
- if (h !== null && h[1].length <= 2) inImpact = isImpactTurnHeading(h);
163
- return !inImpact;
164
- }).map((l) => l.line).join(" ").replace(/\s+/g, " ").trim();
165
- };
166
- /**
167
- * Refuse a turn that leaves `file` without a `## Impact` section. Only the
168
- * pending turn (`head()` is `""`): a triage lap recorded before Impact existed
169
- * replays without one, and refusing there would fail every in-flight process.
170
- * A human's edit at a later gate is not checked — the human owns that file then.
171
- */
172
- const requireImpact = (file) => {
173
- if (head() !== "") return;
174
- const text = read(file);
175
- if (text === void 0) return;
176
- if (hasImpact(text)) return;
177
- refuse(`gtd land: impact: ${file} has no "## Impact" section — explore the codebase and add "## Impact" naming the units, public surfaces and surprises this change touches`);
178
- };
179
- /** Refuse a turn that changed `file` beyond its `## Impact` and `## Release Flag` sections. */
180
- const requireImpactOnly = (file) => {
181
- const change = changes().get(file);
182
- if (change === void 0) return;
183
- if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return;
184
- refuse(`gtd land: impact-only: this turn may only add "## Impact" (and "## Release Flag") to ${file} — restore everything else`);
185
- };
186
- const idNumber = (id) => Number(id.slice(3));
187
- /** Why `listed` renumbers or reuses an earlier ID; `undefined` when it does neither. */
188
- const unstableIds = (listed, ids) => {
189
- const top = Math.max(0, ...ids.used.map(idNumber));
190
- const reused = listed.filter((id) => !ids.closed.includes(id) && idNumber(id) <= top);
191
- if (reused.length === 0) return void 0;
192
- return `numbers ${reused.join(", ")} at or below AC-${top}, an ID already handed out — keep a built criterion's ID (reword it in place when the review changes it), and number new ones from AC-${top + 1}`;
193
- };
194
- /** Refuse criteria in `file` that renumber or reuse an earlier ID. */
195
- const requireStableIds = (file, ids) => {
196
- const why = unstableIds(criterionIds(read(file) ?? ""), ids);
197
- if (why !== void 0) refuse(`gtd land: criteria-ids: ${file} ${why}`);
198
- };
199
- /** Refuse a turn that changed the acceptance criteria in `file`: after the design gate only another design lap may. */
200
- const requireCriteriaFrozen = (file) => {
201
- const change = changes().get(file);
202
- if (change === void 0) return;
203
- if (listing(criteriaOf(change.before ?? "") ?? []) === listing(criteriaOf(change.after ?? "") ?? [])) return;
204
- refuse(`gtd land: criteria-frozen: the acceptance criteria in ${file} are settled — restore them; only a design lap changes them`);
205
- };
206
- /** Whether the judge trusts `stated` as clear and checkable, every criterion of it. Truncated evidence never is. */
207
- const clear = async (stated) => {
208
- const { answers, truncated } = await judge("clear", {
209
- questions: [clearCriteriaQuestion],
210
- evidence: {
211
- criteria: listing(stated.criteria),
212
- sketch: stated.text
213
- },
214
- message: clearCriteriaMessage(),
215
- label: "Judging the stated acceptance criteria"
216
- });
217
- return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P);
218
- };
219
- /** The fast path's impact-only turn: add `## Impact`, leave the criteria alone. */
220
- const impact = async () => {
221
- await agentWithSkills("impact", designImpactPrompt(), {
222
- label: "Mapping the change's impact",
223
- file: REQUIREMENTS,
224
- model: planner(),
225
- system: designSystem()
226
- });
227
- requireProgress(REQUIREMENTS);
228
- requireCriteriaFrozen(REQUIREMENTS);
229
- requireImpact(REQUIREMENTS);
230
- requireImpactOnly(REQUIREMENTS);
231
- requireReleaseFlag(REQUIREMENTS);
232
- };
233
- /**
234
- * The skip path: when the sketch stated clear, checkable criteria, write them
235
- * to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
236
- * design gate. `false` when design must run.
237
- */
238
- const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scope("design", async () => {
239
- if (!await clear(stated)) return false;
240
- await run("normalise", normaliseCriteriaScript(stated), {
241
- label: "Recording the stated acceptance criteria",
242
- file: REQUIREMENTS
243
- });
244
- await impact();
245
- return true;
246
- });
247
- //#endregion
248
- //#region src/workflows/feature/tier.ts
249
- /** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
250
- const riskTierOn = () => vars.flag_risk_tier === "1";
251
- const sectionLines = (text, title) => {
252
- if (text === void 0) return [];
253
- const heading = new RegExp(`^##[ \\t]+${title}[ \\t]*$`, "i");
254
- const lines = markFenced(text);
255
- const start = lines.findIndex((l) => !l.fenced && heading.test(l.line));
256
- if (start < 0) return [];
257
- const rest = lines.slice(start + 1);
258
- const end = rest.findIndex((l) => !l.fenced && /^#{1,2}[ \t]/.test(l.line));
259
- return end < 0 ? rest : rest.slice(0, end);
260
- };
261
- /** Anything but a legible `low` is `high`: under-tiering is the risky direction. */
262
- const readRiskTier = (requirements) => {
263
- return (sectionLines(requirements, "Risk Tier").find((l) => !l.fenced && l.line.trim() !== "")?.line)?.trim().split(/\s+/)[0]?.replace(/[`*_]/g, "").toLowerCase() === "low" ? "low" : "high";
264
- };
265
- const CONTROL = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/g;
266
- const tidy = (text) => text.replace(/\s+/g, " ").replace(CONTROL, "").trim();
267
- const productionSignals = (requirements) => {
268
- const items = [];
269
- for (const { line, fenced } of sectionLines(requirements, "Production Signals")) {
270
- if (fenced) continue;
271
- const top = /^[-*+][ \t]+(.*)$/.exec(line);
272
- if (top !== null) items.push(top[1]);
273
- }
274
- return items.map(tidy).filter((item) => item !== "");
275
- };
276
- /** The tier is always marked, first, so the last `risk-tier` mark opens the current lap's marks. */
277
- const recordRisk = () => {
278
- const requirements = read(REQUIREMENTS);
279
- const tier = readRiskTier(requirements);
280
- mark("risk-tier", tier);
281
- for (const signal of productionSignals(requirements)) mark("production-signal", signal);
282
- return tier;
283
- };
284
- //#endregion
285
- //#region src/workflows/feature/steps.ts
286
- /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
287
- const triage = (base) => agentWithSkills("triage", designTriagePrompt(base, riskTierOn()), {
288
- label: "Triaging the change",
289
- file: REQUIREMENTS,
290
- mode: "qa",
291
- model: planner(),
292
- system: designSystem()
293
- });
294
- const answerProductQuestions = () => human("gate.answer", {
295
- message: designGateAnswerMessage(riskTierOn()),
296
- label: "Awaiting your product answers",
297
- file: REQUIREMENTS,
298
- mode: "qa",
299
- acceptClean: true
300
- });
301
- /** Turn the refactor review round `capture` describes into `.gtd/REFACTOR.md`, or change nothing. */
302
- const refactorCollecting = (capture, round, strays) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, {
303
- strays,
304
- round
305
- }), {
306
- label: "Collecting your refactor feedback",
307
- file: REFACTOR,
308
- model: planner(),
309
- system: reviewerSystem(),
310
- allowEmpty: true
311
- });
312
- //#endregion
313
- //#region src/workflows/feature/planning.ts
314
- /**
315
- * Always stop for the human. Resolves `true` when the round changed anything
316
- * (the author revises), `false` on a clean re-run. Open threads refuse every
317
- * landing; unticked questions only refuse a round with no thread for the
318
- * agent to answer.
319
- */
320
- const noteGate = async (file, answer) => {
321
- await answer();
322
- requireThreadsClosed(file);
323
- if (changes().length === 0) return false;
324
- if (!hasThreadFor("agent", file)) requireAnswers(file);
325
- return true;
326
- };
327
- /** Triage the sketch since `base` into requirements, until no product question is open; `guard` holds every round's criteria. */
328
- const design = (base, guard) => scope("design", async () => {
329
- do {
330
- await triage(base);
331
- requireProgress(REQUIREMENTS);
332
- requireReplies(REQUIREMENTS);
333
- requireCriteria(REQUIREMENTS);
334
- requireImpact(REQUIREMENTS);
335
- requireReleaseFlag(REQUIREMENTS);
336
- guard?.();
337
- } while (await noteGate(REQUIREMENTS, answerProductQuestions));
338
- });
339
- //#endregion
340
- //#region src/workflows/feature/green.ts
341
- const green = () => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
342
- label: "Fixing the check",
343
- file: FEEDBACK,
344
- model: coder(),
345
- system: builderSystem()
346
- }), {
347
- guard: () => requireCriteriaFrozen(REQUIREMENTS),
348
- guardRests: false
349
- });
350
- //#endregion
351
- //#region src/workflows/feature/refactor.ts
352
- const REFACTOR_PACKAGE = REFACTOR;
353
- const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
354
- const requireWithin = (allowed) => {
355
- const outside = changes().map((c) => c.path).filter((path) => isCode(path) && !allowed.includes(path));
356
- if (outside.length === 0) return;
357
- refuse(`gtd land: refactor-scope: the refactor may change only its stray paths — revert these:\n${outside.map((path) => ` - ${path}`).join("\n")}`);
358
- };
359
- const isTestFile = (path) => testFileGlobs.some((glob) => matches(path, glob));
360
- const requireNoTestFiles = () => {
361
- const tests = changes().map((c) => c.path).filter((path) => isCode(path) && isTestFile(path));
362
- if (tests.length === 0) return;
363
- refuse(`gtd land: refactor-tests: the refactor may not change a test file — revert these:\n${tests.map((path) => ` - ${path}`).join("\n")}`);
364
- };
365
- const holdsCallSites = (path) => !path.endsWith(".feature") && !path.endsWith(".snap");
366
- const COMMENT = /(?<![*/])\/\*[\s\S]*?\*\/|(?<!:)\/\/.*$/gm;
367
- const ASSERTION = /\b(?:expect|assert\w*)\s*[.(]/g;
368
- const TEST_CASE = /(?<![.\w])x?(?:it|test|describe)\s*[.(]/g;
369
- const MODIFIER = /(?<![.\w])(?:it|test|describe)\.(skip|only|todo|fails|skipIf|runIf)\b|(?<![.\w])(x)(?:it|test|describe)\b/g;
370
- const counts = (text) => {
371
- const live = text.replace(COMMENT, "");
372
- const modifiers = [...live.matchAll(MODIFIER)].map((m) => m[1] ?? m[2]).sort();
373
- return JSON.stringify([
374
- [...live.matchAll(ASSERTION)].length,
375
- [...live.matchAll(TEST_CASE)].length,
376
- modifiers
377
- ]);
378
- };
379
- const tested = (text) => {
380
- const live = text.replace(COMMENT, "");
381
- return live.search(ASSERTION) >= 0 || live.search(TEST_CASE) >= 0;
382
- };
383
- const callSitesOnly = ({ path, status, before, after }) => status === "modified" && holdsCallSites(path) && before !== void 0 && after !== void 0 && tested(before) && counts(before) === counts(after);
384
- /**
385
- * The test-file half of the refactor guard: an existing test may follow a changed call shape,
386
- * nothing more. Returns the test files it admitted, which the scope check then allows.
387
- */
388
- const requireTestCallSitesOnly = () => {
389
- const tests = changes().filter((c) => isCode(c.path) && isTestFile(c.path));
390
- const refused = tests.filter((c) => !callSitesOnly(c)).map((c) => c.path);
391
- if (refused.length > 0) refuse(`gtd land: refactor-tests: the refactor may update an existing test file's call sites only — never add, delete, skip or comment out a test or an assertion, or touch a snapshot, a .feature file or test data; revert these:\n${refused.map((path) => ` - ${path}`).join("\n")}`);
392
- return tests.map((c) => c.path);
393
- };
394
- const flagExpression = (pattern) => {
395
- if (pattern.trim() === "") throw new Error("gtd: refactorFlagPattern must not be blank");
396
- let expression;
397
- try {
398
- expression = new RegExp(pattern, "g");
399
- } catch (error) {
400
- throw new Error(`gtd: refactorFlagPattern is not a valid regular expression: ${String(error)}`);
401
- }
402
- if (expression.test("")) throw new Error("gtd: refactorFlagPattern must not match the empty string");
403
- return expression;
404
- };
405
- const countFlags = (text, expression, into) => {
406
- let zeroLength = false;
407
- for (const [name] of text.matchAll(expression)) if (name === "") zeroLength = true;
408
- else into.set(name, (into.get(name) ?? 0) + 1);
409
- return zeroLength;
410
- };
411
- const listPaths = (paths) => paths.map((path) => ` - ${path}`).join("\n");
412
- const requireNoNewFlags = (pattern, since) => {
413
- const expression = flagExpression(pattern);
414
- const before = /* @__PURE__ */ new Map();
415
- const after = /* @__PURE__ */ new Map();
416
- const unreadable = [];
417
- const zeroLength = [];
418
- for (const { path, status, before: was, after: now } of changesSince(since)) {
419
- if (!isCode(path)) continue;
420
- const oldSide = status === "added" ? "" : was;
421
- const newSide = status === "deleted" ? "" : now;
422
- if (oldSide === void 0 || newSide === void 0) {
423
- unreadable.push(path);
424
- continue;
425
- }
426
- countFlags(oldSide, expression, before);
427
- if (countFlags(newSide, expression, after)) zeroLength.push(path);
428
- }
429
- if (unreadable.length > 0) refuse(`gtd land: refactor-flags: cannot compare flag reads in:\n${listPaths(unreadable)}`);
430
- if (zeroLength.length > 0) refuse(`gtd land: refactor-flags: refactorFlagPattern (${pattern}) produced a zero-length match in:\n${listPaths(zeroLength)}`);
431
- const raised = [...after].filter(([name, count]) => count > (before.get(name) ?? 0));
432
- if (raised.length === 0) return;
433
- refuse(`gtd land: refactor-flags: the refactor may not introduce or add reads of a flag (${pattern}):\n${raised.map(([name, count]) => ` - ${name}: ${before.get(name) ?? 0} → ${count}`).join("\n")}`);
434
- };
435
- /** A test-file stray is never refactored: the implement turn still reads it in the naive tip. */
436
- const refactorStrays = (shape) => shape.strays.filter((path) => !isTestFile(path));
437
- const packageLines = (strays, pattern, reference) => [
438
- "# Behaviour-neutral refactor",
439
- "",
440
- "## Scope (read-only)",
441
- "",
442
- ...strays.map((path) => `- \`${path}\``),
443
- "",
444
- "This list is fixed when the package is derived: editing it here widens or narrows nothing.",
445
- "",
446
- "## Rules",
447
- "",
448
- "- The suite's outcome must not change",
449
- "- Change only the paths above, plus an existing test file where a changed call shape reaches it: never add or delete a test file, and never add, remove or alter a test or an assertion",
450
- `- Introduce no flag read and add no second read of an existing one (\`${pattern}\`)`,
451
- "",
452
- "## Reference",
453
- "",
454
- ...reference === void 0 ? ["None."] : [
455
- `Reading material only, never applied: \`git show ${reference} -- ${strays.map(quote).join(" ")}\``,
456
- "",
457
- "Its changes to the existing code above are the seam this refactor exists to land, minus the feature's own behaviour."
458
- ],
459
- ""
460
- ];
461
- const demoteHeadings = (text) => {
462
- const marked = markFenced(text.trimEnd());
463
- const lines = marked.map((m) => m.line);
464
- const headings = marked.map((m) => m.fenced ? void 0 : /^(#{1,6}) /.exec(m.line)?.[1]?.length);
465
- const shift = 3 - Math.min(...headings.filter((n) => n !== void 0));
466
- return lines.map((line, i) => {
467
- const depth = headings[i];
468
- if (depth === void 0) return line;
469
- return "#".repeat(Math.min(6, depth + shift)) + line.slice(depth);
470
- });
471
- };
472
- const deriveScript = (lines) => `mkdir -p .gtd
473
- printf '%s\\n' ${lines.map(quote).join(" ")} > ${quote(REFACTOR_PACKAGE)}
474
- `;
475
- /** Undefined for every shape that is not a readable `refactor-first`; throws on a bad flag pattern. */
476
- const plan = (shape, options) => {
477
- if (shape.choice !== "refactor-first" || shape.unreadable.length > 0) return void 0;
478
- const pattern = vars.refactorFlagPattern ?? defaults.refactorFlagPattern;
479
- const expression = flagExpression(pattern);
480
- const strays = refactorStrays(shape);
481
- if (strays.length === 0) return void 0;
482
- const zeroLength = strays.filter((path) => countFlags(read(path) ?? "", expression, /* @__PURE__ */ new Map()));
483
- if (zeroLength.length > 0) throw new Error(`gtd: refactorFlagPattern (${pattern}) produces a zero-length match in:\n${listPaths(zeroLength)}`);
484
- return {
485
- pattern,
486
- strays,
487
- reference: options.reference?.trim() ? options.reference : void 0,
488
- note: options.note?.trim() ? options.note : void 0
489
- };
490
- };
491
- const guardOf = ({ pattern, strays }, entry) => () => {
492
- requireWithin([...strays, ...requireTestCallSitesOnly()]);
493
- requireNoNewFlags(pattern, entry);
494
- };
495
- /** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
496
- const refactorLap = async (p, guard, feedback) => {
497
- const { pattern, strays, reference, note } = p;
498
- await run("deriving", deriveScript([
499
- ...packageLines(strays, pattern, reference),
500
- ...note === void 0 ? [] : [
501
- "## Refactor note",
502
- "",
503
- ...demoteHeadings(note),
504
- ""
505
- ],
506
- ...feedback === void 0 ? [] : [
507
- "## Review feedback",
508
- "",
509
- ...demoteHeadings(feedback),
510
- ""
511
- ]
512
- ]), {
513
- label: "Deriving the refactor package",
514
- file: REFACTOR_PACKAGE
515
- });
516
- await agentWithSkills("building", refactorBuildingPrompt({
517
- pkg: REFACTOR_PACKAGE,
518
- strays,
519
- pattern,
520
- reference,
521
- feedback: feedback !== void 0,
522
- note: note !== void 0
523
- }), {
524
- label: "Refactoring",
525
- model: coder(),
526
- system: builderSystem()
527
- });
528
- guard();
529
- await healthy(async () => {
530
- await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
531
- label: "Fixing the check",
532
- file: FEEDBACK,
533
- model: coder(),
534
- system: builderSystem()
535
- });
536
- guard();
537
- }, {
538
- describeGuard: guard,
539
- rules: refactorRules(strays, pattern)
540
- });
541
- await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
542
- };
543
- const strayAccess = (strays) => ({ write: [
544
- ...strays,
545
- ...testFileGlobs,
546
- ".gtd/**"
547
- ] });
548
- const refactorFirst = async (shape, options = {}) => {
549
- const p = plan(shape, options);
550
- if (p === void 0) return false;
551
- return scope({
552
- name: "refactor",
553
- access: strayAccess(p.strays)
554
- }, async () => {
555
- await refactorLap(p, guardOf(p, head()));
556
- return true;
557
- });
558
- };
559
- /**
560
- * `refactorFirst` plus its review, in the one `refactor` scope so the tail's
561
- * steps are `refactor.build.*`. A review edit re-runs the refactor, never a
562
- * re-plan: the collected notes ride the re-derived package. Every round
563
- * reviews the whole refactor since `entry`, since each lap rebuilds over it.
564
- */
565
- const reviewedRefactor = async (shape, options = {}) => {
566
- const p = plan(shape, options);
567
- if (p === void 0) return false;
568
- return scope({
569
- name: "refactor",
570
- access: strayAccess(p.strays)
571
- }, async () => {
572
- const entry = head();
573
- const guard = guardOf(p, entry);
574
- const feedback = {
575
- file: REFACTOR_PACKAGE,
576
- collecting: (capture, round) => refactorCollecting(capture, round, p.strays),
577
- unmatched: "gtd land: refactor-feedback: the review feedback changed files but wrote no .gtd/REFACTOR.md — a refactor cannot act on it; revert the change, or `gtd abandon` and restart",
578
- dropped: refactorDroppedRefusal
579
- };
580
- let collected;
581
- for (;;) {
582
- await refactorLap(p, guard, collected);
583
- const outcome = await buildTail(entry, {
584
- green: true,
585
- guard,
586
- guardRests: false,
587
- lenses: lensEntriesOf(vars.refactorQualityReviews ?? ""),
588
- feedback,
589
- fixAccess: strayAccess(p.strays),
590
- lensBase: entry,
591
- rules: refactorRules(p.strays, p.pattern),
592
- lap: "refactor"
593
- });
594
- if (outcome.verdict === "signoff") return true;
595
- await reUnwind(outcome);
596
- collected = read(REFACTOR_PACKAGE);
597
- if (collected === void 0) throw new Error(`gtd: review feedback left no ${REFACTOR_PACKAGE}`);
598
- }
599
- });
600
- };
601
- //#endregion
602
- //#region src/workflows/feature/seam.ts
603
- /** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
604
- const NAIVE_RETRIES = 2;
605
- /** A change to an existing file outside the allowlist is a stray even when the task needs it: the judge decides. */
606
- const judgeSeam = (tip) => judgeDiffShape("judge", {
607
- changes: tip.changes,
608
- allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
609
- exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
610
- });
611
- const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
612
- label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
613
- keep: true,
614
- model: coder(),
615
- system: builderSystem()
616
- });
617
- const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
618
- const EXTENSION = /\.[\w-]+$/;
619
- const ALIAS = /^(?:[@~]\/|#)/;
620
- /** The specifiers an added `path` answers to: itself, without its extension, and its directory for an `index`. */
621
- const namesOf = (path) => {
622
- const bare = path.replace(EXTENSION, "");
623
- return posix.basename(bare) === "index" ? [
624
- path,
625
- bare,
626
- posix.dirname(bare)
627
- ] : [path, bare];
628
- };
629
- /**
630
- * The repository paths `test` imports, each with and without an extension: a
631
- * relative specifier resolved from its directory, an aliased one (`@/`, `~/`,
632
- * `#`) as a path tail, anything else as written. Comments are skipped; a
633
- * computed import goes unseen.
634
- */
635
- const importsOf = (test) => [...(test.after ?? "").replace(COMMENT, "").matchAll(IMPORTED)].flatMap(([, , spec]) => {
636
- const relative = /^\.\.?(\/|$)/.test(spec);
637
- const tail = ALIAS.test(spec);
638
- const path = relative ? posix.join(posix.dirname(test.path), spec) : spec.replace(ALIAS, "");
639
- return [path, path.replace(EXTENSION, "")].map((p) => ({
640
- path: p,
641
- tail
642
- }));
643
- });
644
- const reaches = (imports, added) => namesOf(added).some((name) => imports.some(({ path, tail }) => name === path || tail && name.endsWith(`/${path}`)));
645
- const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
646
- /**
647
- * Why `tip`'s tests fall short, or `undefined`: a test importing a file the
648
- * pass added — naive structure a reset and refactor replaces, which the tests
649
- * must survive.
650
- */
651
- const acceptanceFault = (tip) => {
652
- const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
653
- const reaching = acceptanceTests(tip.changes).filter((test) => holdsCallSites(test.path)).flatMap((test) => {
654
- const imports = importsOf(test);
655
- return added.filter((path) => reaches(imports, path)).map((path) => ` - ${test.path} → ${path}`);
656
- });
657
- if (reaching.length > 0) return `acceptance-internals: these tests import files the naive pass added — test through code that existed before it (a public entry point, an existing module's exports):\n${reaching.join("\n")}`;
658
- };
659
- /** What a rebuild carries over from the naive pass `tip`: every test file it added or changed. */
660
- const naiveAcceptance = (tip) => ({
661
- hash: tip.hash,
662
- tests: acceptanceTests(tip.changes).map((c) => c.path)
663
- });
664
- const restore = (path) => `if git cat-file -e ${quote(`HEAD:${path}`)} 2>/dev/null; then git checkout -q HEAD -- ${quote(path)}; else rm -f -- ${quote(path)}; fi`;
665
- /**
666
- * Run the suite with `tip`'s diff applied, then take it out again; resolves
667
- * `true` when green. The pass's paths go back to HEAD before (an interrupted
668
- * run left them applied) and after; afterwards so does anything else the run
669
- * changed, judged against what was already dirty, which is left alone.
670
- */
671
- const naiveGreen = (tip) => {
672
- const paths = tip.changes.map((c) => restore(c.path));
673
- return check("check", [
674
- ...paths,
675
- "dirty=$(git -c core.quotePath=false ls-files -m -d -o --exclude-standard)",
676
- `if ! git diff --binary --no-color --no-ext-diff --no-textconv --src-prefix=a/ --dst-prefix=b/ ${quote(`${tip.hash}^`)} ${quote(tip.hash)} -- | git apply --whitespace=nowarn; then`,
677
- " echo 'gtd: the kept naive pass no longer applies to HEAD; no test ran'",
678
- " exit 1",
679
- "fi",
680
- "(",
681
- env.testCommand ?? "",
682
- ")",
683
- "code=$?",
684
- ...paths,
685
- "git -c core.quotePath=false ls-files -m -d -o --exclude-standard | while IFS= read -r path; do",
686
- " printf '%s\\n' \"$dirty\" | grep -qxF -- \"$path\" && continue",
687
- " if git cat-file -e \"HEAD:$path\" 2>/dev/null; then git checkout -q HEAD -- \"$path\"; else rm -f -- \"$path\"; fi",
688
- "done",
689
- "exit \"$code\""
690
- ].join("\n"), {
691
- report: FEEDBACK,
692
- label: "Running the naive pass's tests"
693
- });
694
- };
695
- /** Resolves `false` when the tip no longer applies (the failure is in `.gtd/FEEDBACK.md`) or has nothing outside `.gtd/` to apply. */
696
- const applied = async (tip) => {
697
- await run("applying", applyScript(tip, FEEDBACK, naiveApplyFailure(tip), [".gtd"]), { label: "Applying the naive pass" });
698
- return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
699
- };
700
- const build = async (options) => {
701
- await agentWithSkills("building", implementBuildingPrompt(options.reference, options.amend, options.afterRefactor, requestedFlag(), options.rebuild), {
702
- label: "Implementing",
703
- model: coder(),
704
- system: builderSystem(),
705
- ...options.afterRefactor === true ? { allowEmpty: true } : {}
706
- });
707
- requireCriteriaFrozen(REQUIREMENTS);
708
- };
709
- async function implement(first, tier, amend) {
710
- await implementing(typeof first === "object" && first !== null ? first : {
711
- reference: first,
712
- tier,
713
- amend
714
- });
715
- }
716
- /** `implement`, resolving with the criterion IDs the requirements listed when it closed. */
717
- const implementing = (options) => scope("implement", async () => {
718
- if (options.naive === void 0 || !await applied(options.naive)) await build(options);
719
- else requireCriteriaFrozen(REQUIREMENTS);
720
- await green();
721
- const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
722
- await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE]), { label: "Closing out the implementation" });
723
- return ids;
724
- });
725
- /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
726
- const steer = async (retry) => {
727
- await human("steer", {
728
- message: seamSteerMessage(retry, flagged()),
729
- label: "Steer or overrule the naive pass",
730
- file: REQUIREMENTS,
731
- acceptClean: true
732
- });
733
- if (changes().length === 0) return false;
734
- requireCriteria(REQUIREMENTS);
735
- return true;
736
- };
737
- /** Rejects, test faults and red checks share one budget; a steer or an overrule refills it. */
738
- const budget = () => {
739
- let retries = 0;
740
- return async (next) => {
741
- if (retries < NAIVE_RETRIES) {
742
- retries++;
743
- return {
744
- kind: "retry",
745
- retry: next
746
- };
747
- }
748
- const steered = await steer(next);
749
- retries = 0;
750
- if (read(".gtd/FEEDBACK.md") !== void 0) await run("clear", removeScript([FEEDBACK]), { label: "Clearing the red check" });
751
- return { kind: steered ? "steered" : "overruled" };
752
- };
753
- };
754
- /** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
755
- const vetted = async (tip, vetting) => {
756
- const fault = vetting.waived ? void 0 : acceptanceFault(tip);
757
- if (fault !== void 0) {
758
- const verdict = await vetting.again({
759
- fault,
760
- hash: tip.hash
761
- });
762
- if (verdict.kind !== "overruled") return verdict;
763
- vetting.waived = true;
764
- }
765
- if (await naiveGreen(tip)) return void 0;
766
- const verdict = await vetting.again({ red: tip.hash });
767
- return verdict.kind === "overruled" ? void 0 : verdict;
768
- };
769
- /** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
770
- const ruled = async (tip, again) => {
771
- const shape = await judgeSeam(tip);
772
- if (shape.choice === "land") return {
773
- reference: tip.hash,
774
- tip
775
- };
776
- if (shape.choice === "refactor-first") return {
777
- reference: tip.hash,
778
- tip,
779
- shape
780
- };
781
- const next = await again({
782
- strays: shape.strays,
783
- p: shape.judgment?.answers.shape?.p ?? 0
784
- });
785
- return next.kind === "overruled" ? {
786
- reference: tip.hash,
787
- tip
788
- } : next;
789
- };
790
- /** Back to `before`: every code path, and the notes file. `.gtd/` edits (the requirements) stay. */
791
- const untryScript = (before) => {
792
- const code = changesSince(before).filter((c) => isCode$1(c.path));
793
- return restoreScript(head(), {
794
- restore: code.filter((c) => c.status !== "added").map((c) => c.path),
795
- remove: [...code.filter((c) => c.status === "added").map((c) => c.path), NAIVE_TEST]
796
- }, before);
797
- };
798
- /**
799
- * A person tries the green pass `tip` in the working tree. Resolves
800
- * `undefined` on a clean landing (signed off), else what they sent back;
801
- * either way the pass, their code edits and the notes file leave the tree.
802
- */
803
- const tried = async (tip) => {
804
- const before = head();
805
- await run("applying", applyScript(tip.hash, FEEDBACK, naiveApplyFailure(tip.hash), [".gtd"]), { label: "Applying the naive pass to try it" });
806
- await human("try", {
807
- message: naiveTryMessage(flagged()),
808
- label: "Try the naive pass",
809
- file: NAIVE_TEST,
810
- acceptClean: true
811
- });
812
- const landed = changes();
813
- const edits = landed.some((c) => isCode$1(c.path)) ? head() : void 0;
814
- const notes = read(".gtd/NAIVE-TEST.md")?.trim() || void 0;
815
- if (landed.some((c) => c.path === ".gtd/REQUIREMENTS.md")) requireCriteria(REQUIREMENTS);
816
- await run("restoring", untryScript(before), { label: "Taking the naive pass out again" });
817
- return landed.length === 0 ? void 0 : {
818
- tested: tip.hash,
819
- notes,
820
- edits
821
- };
822
- };
823
- const decide = () => scope("seam", async () => {
824
- const vetting = {
825
- again: budget(),
826
- waived: false
827
- };
828
- let retry;
829
- for (;;) {
830
- await naivePass(retry);
831
- const tip = kept("naive");
832
- const verdict = await vetted(tip, vetting);
833
- if (verdict === void 0 && naiveTestOn()) {
834
- const sentBack = await tried(tip);
835
- if (sentBack !== void 0) {
836
- vetting.again = budget();
837
- retry = sentBack;
838
- continue;
839
- }
840
- }
841
- const outcome = verdict ?? (flagged() ? await ruled(tip, vetting.again) : {
842
- reference: tip.hash,
843
- tip
844
- });
845
- if (!("kind" in outcome)) return outcome;
846
- retry = outcome.kind === "retry" ? outcome.retry : void 0;
847
- }
848
- });
849
- /** Write the refactor note from the whole naive pass; a turn that changes anything else is refused. */
850
- const refactorNote = async (reference, strays) => {
851
- await agent("note", refactorNotePrompt(reference, strays), {
852
- label: "Writing the refactor note",
853
- file: REFACTOR_NOTE,
854
- model: coder(),
855
- system: builderSystem()
856
- });
857
- const others = changes().map((c) => c.path).filter((path) => path !== REFACTOR_NOTE);
858
- const note = read(".gtd/REFACTOR-NOTE.md") ?? "";
859
- if (others.length > 0 || note.trim() === "") refuse(`gtd land: refactor-note: write ${REFACTOR_NOTE} and change nothing else${others.length > 0 ? ` — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}` : ""}`);
860
- return note;
861
- };
862
- const seamBuild = async (base) => {
863
- const { reference, tip, shape } = await decide();
864
- if (shape === void 0) return {
865
- reference,
866
- base,
867
- land: true
868
- };
869
- if (shape.unreadable.length > 0) {
870
- await scope("seam", () => human("by-hand", {
871
- message: seamByHandMessage(shape.strays, shape.unreadable, reference),
872
- label: "Reviewing unreadable files by hand",
873
- acceptClean: true
874
- }));
875
- return {
876
- reference,
877
- base,
878
- rebuild: { acceptance: naiveAcceptance(tip) }
879
- };
880
- }
881
- const strays = refactorStrays(shape);
882
- if (strays.length === 0) return {
883
- reference,
884
- base,
885
- rebuild: { acceptance: naiveAcceptance(tip) }
886
- };
887
- const note = await scope("seam", () => refactorNote(reference, strays));
888
- const refactored = await reviewedRefactor(shape, {
889
- reference,
890
- note
891
- });
892
- return {
893
- reference,
894
- base: head(),
895
- refactored,
896
- rebuild: {
897
- note,
898
- acceptance: naiveAcceptance(tip)
899
- }
900
- };
901
- };
902
- //#endregion
903
- //#region src/workflows/feature/feature.ts
904
- /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
905
- const unwind = () => {
906
- const commit = head();
907
- return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
908
- };
909
- /**
910
- * A naive pass (judged only when a release flag is asked for), then implement and the
911
- * quality lap, then the review; design must have run since `firstBase`.
912
- */
913
- const buildFeature = async (firstBase) => {
914
- const tier = riskTierOn() ? recordRisk() : void 0;
915
- const handedOut = new Set(listedIds());
916
- const seam = await seamBuild(firstBase);
917
- const used = gather(handedOut);
918
- const requirements = read(REQUIREMENTS);
919
- const closed = await implementing({
920
- reference: seam.reference,
921
- tier,
922
- afterRefactor: seam.refactored,
923
- naive: seam.land === true ? seam.reference : void 0,
924
- rebuild: seam.rebuild
925
- });
926
- const carried = seam.rebuild?.acceptance?.tests ?? [];
927
- const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? firstBase : seam.base;
928
- const outcome = await buildTail(base, {
929
- green: true,
930
- lensBase: base,
931
- requirements
932
- });
933
- await featureFeedback(outcome, base, {
934
- closed,
935
- used
936
- });
937
- };
938
- const listedIds = () => criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
939
- const gather = (handedOut) => {
940
- for (const id of listedIds()) handedOut.add(id);
941
- return [...handedOut];
942
- };
943
- const featureFeedback = async (first, lensBase, ids) => {
944
- let outcome = first;
945
- let closed = ids.closed;
946
- const handedOut = /* @__PURE__ */ new Set([...ids.used, ...ids.closed]);
947
- while (outcome.verdict === "feedback") {
948
- await reUnwind(outcome);
949
- const lap = {
950
- closed,
951
- used: [...handedOut]
952
- };
953
- await design(outcome.base, () => {
954
- requireStableIds(REQUIREMENTS, lap);
955
- gather(handedOut);
956
- });
957
- const tier = riskTierOn() ? recordRisk() : void 0;
958
- const requirements = read(REQUIREMENTS);
959
- closed = await implementing({
960
- tier,
961
- amend: true
962
- });
963
- outcome = await buildTail(outcome.base, {
964
- green: true,
965
- lensBase,
966
- requirements,
967
- lap: "feedback"
968
- });
969
- }
970
- };
971
- /** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
972
- const planAndBuild = async (firstBase, stated) => {
973
- if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
974
- await buildFeature(firstBase);
975
- };
976
- /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. */
977
- const afterTail = async (first) => {
978
- let outcome = first;
979
- while (outcome.verdict === "feedback") {
980
- await reUnwind(outcome);
981
- await design(outcome.base);
982
- const tier = riskTierOn() ? recordRisk() : void 0;
983
- const requirements = read(REQUIREMENTS);
984
- await implement({
985
- tier,
986
- amend: true
987
- });
988
- outcome = await buildTail(outcome.base, {
989
- green: true,
990
- requirements
991
- });
992
- }
993
- };
994
- /** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
995
- const ordinaryStart = async () => {
996
- await human("idle", {
997
- message: idleMessage(),
998
- label: "Idle",
999
- file: ".gtd/TODO.md"
1000
- });
1001
- refusePendingHandOff();
1002
- naiveTestOn();
1003
- const stated = statedCriteria(changes(), head());
1004
- await unwind();
1005
- if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
1006
- message: unwindFailedMessage(),
1007
- label: "Could not unwind your input",
1008
- file: FEEDBACK
1009
- });
1010
- await gate("start-gate", startGateBlockedMessage());
1011
- await planAndBuild(start(), stated);
1012
- };
1013
- const feature = shipped(ordinaryStart);
1014
- //#endregion
1015
- //#region src/workflows/feature/index.ts
1016
- var feature_default = feature;
1017
- //#endregion
1018
- export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, hasImpact, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireCriteria, requireCriteriaFrozen, requireImpact, requireImpactOnly, requireNoNewFlags, requireNoTestFiles, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, statedCriteria, steering, summary, triage, unstableIds, unwind };