@pmelab/gtd 51.0.1 → 52.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +46 -40
- package/dist/chunks/{reserved-CUkk9XgL.mjs → reserved-DxB5CROj.mjs} +73 -38
- package/dist/chunks/{review-BaCXlIT2.mjs → review-CKMSdvgW.mjs} +11 -9
- package/dist/chunks/{steering-DtUVUz9l.mjs → steering-CLhu8C7q.mjs} +262 -52
- package/dist/chunks/{tail-CX6Pdsql.mjs → tail-QZc7Uz2U.mjs} +1 -1
- package/dist/gtd.bundle.mjs +198 -43
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +1 -1
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +510 -147
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +228 -222
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +570 -317
- package/package.json +5 -4
- package/src/steering/FORMATS.md +4 -1
- package/src/steering/SteeringFormat.ts +7 -1
- package/src/steering/contract.test.ts +2 -1
- package/src/steering/fixtures/review/sectioned.md +31 -0
- package/src/steering/fixtures/review/sectioned.read.json +119 -0
- package/src/steering/index.ts +4 -0
- package/src/steering/qa.ts +1 -1
- package/src/steering/review.test.ts +270 -4
- package/src/steering/review.ts +328 -69
- package/src/testkit/steps.ts +13 -0
- package/src/workflows/feature/acceptance-criteria.feature +159 -3
- package/src/workflows/feature/continuation-cap.feature +4 -0
- package/src/workflows/feature/continuation-rebuild.feature +4 -0
- package/src/workflows/feature/continuation-slices.feature +4 -0
- package/src/workflows/feature/continuation.feature +4 -0
- package/src/workflows/feature/criteria.test.ts +60 -1
- package/src/workflows/feature/criteria.ts +87 -8
- package/src/workflows/feature/fast-path.feature +5 -6
- package/src/workflows/feature/feature.test.ts +6 -5
- package/src/workflows/feature/feature.ts +1 -2
- package/src/workflows/feature/flag-mode.feature +5 -6
- package/src/workflows/feature/green.ts +22 -0
- package/src/workflows/feature/hands-on-skip.feature +384 -0
- package/src/workflows/feature/lens-models.feature +4 -4
- package/src/workflows/feature/lens-review.feature +10 -8
- package/src/workflows/feature/naive-acceptance.feature +4 -0
- package/src/workflows/feature/one-step-land.feature +5 -6
- package/src/workflows/feature/planning.ts +2 -1
- package/src/workflows/feature/prototype-rounds.feature +5 -1
- package/src/workflows/feature/prototype.feature +18 -9
- package/src/workflows/feature/prototype.ts +48 -17
- package/src/workflows/feature/refactor-absorbs.feature +7 -8
- package/src/workflows/feature/refactor-first.feature +7 -8
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +11 -12
- package/src/workflows/feature/refactor-review.feature +72 -72
- package/src/workflows/feature/review-answers.feature +94 -5
- package/src/workflows/feature/review-gate.feature +23 -53
- package/src/workflows/feature/reviewer-walkthrough.feature +33 -12
- package/src/workflows/feature/risk-tier.feature +32 -0
- package/src/workflows/feature/seam-rejection.feature +9 -6
- package/src/workflows/feature/seam.feature +9 -6
- package/src/workflows/feature/seam.test.ts +5 -3
- package/src/workflows/feature/seam.ts +2 -3
- package/src/workflows/feature/severity-budget.feature +11 -7
- package/src/workflows/feature/severity-sorted-review.feature +54 -54
- package/src/workflows/feature/slice-findings.feature +9 -15
- package/src/workflows/feature/slicing.feature +36 -24
- package/src/workflows/feature/standards.feature +4 -4
- package/src/workflows/feature/tier-pin.feature +4 -0
- package/src/workflows/shared/review.ts +26 -6
- package/src/workflows/shared/text.test.ts +85 -11
- package/src/workflows/shared/text.ts +95 -42
- package/src/workflows/feature/coverage.feature +0 -165
- package/src/workflows/feature/coverage.ts +0 -120
package/dist/workflow-kit.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { changes, changesSince, hasThreadFor, head, mark, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { S as unansweredNotes, _ as reviewNotes, c as stripCodeThreads, g as reviewDecisions, k as parseThreads, v as reviewRisks } from "./chunks/steering-CLhu8C7q.mjs";
|
|
3
3
|
//#region src/workflow-kit/health.ts
|
|
4
4
|
/** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
|
|
5
5
|
const escalation = async (count, steps) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import { C as coder,
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { C as coder, Ft as defaults, It as envDefaults, Jt as flagReferences, Xt as registryEntry, Zt as requireFlag, i as skills, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-DxB5CROj.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-QZc7Uz2U.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,8 +1,8 @@
|
|
|
1
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, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
|
|
2
2
|
import { lensEntriesOf } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as
|
|
4
|
-
import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-
|
|
5
|
-
import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-
|
|
3
|
+
import { $ as naivePassPrompt, At as unwindFailedMessage, C as coder, Ct as seamByHandMessage, Dt as testerSystem, E as fixNits, Et as startGateBlockedMessage, Ft as defaults, G as designTriagePrompt, H as designGateAnswerMessage, I as builderSystem, It as envDefaults, Lt as testFileGlobs, N as agentWithSkills, P as buildReviewCollectingPrompt, Q as naiveApplyFailure, R as clearCriteriaMessage, St as reviewerSystem, Tt as slicingPrompt, U as designImpactPrompt, V as criterionLine, W as designSystem, X as implementBuildingPrompt, Y as idleMessage, Z as implementFixPrompt, _ as REFACTOR_NOTE, a as flagModeOn, at as prototypeOpenMessage, b as SLICES, d as healthy, dt as refactorBuildingPrompt, et as normaliseCriteriaScript, f as FEEDBACK, ft as refactorDroppedRefusal, g as REFACTOR, ht as refactorRules, i as skills, it as prototypeFixingPrompt, jt as unwindFailure, k as planner, l as gate, m as PROTOTYPE, mt as refactorNotePrompt, n as summary, nt as observableQuestion, ot as prototypeTestingPrompt, p as HANDOFF, pt as refactorFixPrompt, r as access, rt as oversizedMessage, s as shipped, st as prototypeUnguidedMessage, t as steering, tt as observableMessage, v as REQUIREMENTS, w as collecting, wt as seamSteerMessage, x as answerReviewQuestions, z as clearCriteriaQuestion } from "../chunks/reserved-DxB5CROj.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-QZc7Uz2U.mjs";
|
|
5
|
+
import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-CKMSdvgW.mjs";
|
|
6
6
|
import { posix } from "node:path";
|
|
7
7
|
//#region src/workflows/shared/fences.ts
|
|
8
8
|
const markFenced = (text) => {
|
|
@@ -26,7 +26,12 @@ const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
|
26
26
|
//#region src/workflows/feature/criteria.ts
|
|
27
27
|
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
28
28
|
const CLEAR_MIN_P = .8;
|
|
29
|
-
const ID$1 = /^(AC-\d+)
|
|
29
|
+
const ID$1 = /^(AC-\d+)(\s*\(no hands-on\))?:\s*(.*)$/;
|
|
30
|
+
const criterion = (id, as = id[1]) => ({
|
|
31
|
+
id: as,
|
|
32
|
+
text: id[3],
|
|
33
|
+
...id[2] === void 0 ? {} : { noHandsOn: true }
|
|
34
|
+
});
|
|
30
35
|
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
31
36
|
const listUnder = (text, title, level) => {
|
|
32
37
|
const lines = markFenced(text);
|
|
@@ -58,25 +63,28 @@ const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((
|
|
|
58
63
|
return id === null ? {
|
|
59
64
|
id: "",
|
|
60
65
|
text: item
|
|
61
|
-
} :
|
|
62
|
-
id: id[1],
|
|
63
|
-
text: id[2]
|
|
64
|
-
};
|
|
66
|
+
} : criterion(id);
|
|
65
67
|
});
|
|
66
68
|
/** The ID'd criteria of `text`, only `slice`'s when given. */
|
|
67
69
|
const criterionIds = (text, slice) => (criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "" && (slice?.only === void 0 || slice.only.includes(id)) && !(slice?.except ?? []).includes(id));
|
|
70
|
+
/** The ID'd criteria of `text` (`slice`'s when given) a person can try by hand: none tagged `(no hands-on)`. */
|
|
71
|
+
const handsOnIds = (text, slice) => {
|
|
72
|
+
const tagged = (criteriaOf(text) ?? []).filter((c) => c.noHandsOn).map((c) => c.id);
|
|
73
|
+
return criterionIds(text, slice).filter((id) => !tagged.includes(id));
|
|
74
|
+
};
|
|
68
75
|
/** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
|
|
69
76
|
const sliceIds = (slice) => slice === void 0 ? void 0 : criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice);
|
|
70
77
|
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
71
78
|
const numbered = (items) => {
|
|
72
79
|
const ids = items.map((item) => ID$1.exec(item));
|
|
73
80
|
const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
|
|
74
|
-
return items.map((item, i) =>
|
|
75
|
-
id
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
return items.map((item, i) => {
|
|
82
|
+
const id = ids[i];
|
|
83
|
+
if (own) return criterion(id);
|
|
84
|
+
return id === null ? {
|
|
85
|
+
id: `AC-${i + 1}`,
|
|
86
|
+
text: item
|
|
87
|
+
} : criterion(id, `AC-${i + 1}`);
|
|
80
88
|
});
|
|
81
89
|
};
|
|
82
90
|
const STATED = /^acceptance criteria:?$/i;
|
|
@@ -113,6 +121,38 @@ const requireCriteria = (file) => {
|
|
|
113
121
|
if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return;
|
|
114
122
|
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`);
|
|
115
123
|
};
|
|
124
|
+
const isImpactHeading = (h) => h !== null && h[1].length === 2 && /^impact$/i.test(h[2].trim());
|
|
125
|
+
/** Whether `text` has an unfenced, level-2 `## Impact` heading. */
|
|
126
|
+
const hasImpact = (text) => markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)));
|
|
127
|
+
/** `text` without its `## Impact` section, whitespace collapsed. */
|
|
128
|
+
const withoutImpact = (text) => {
|
|
129
|
+
let inImpact = false;
|
|
130
|
+
return markFenced(text).filter((l) => {
|
|
131
|
+
const h = l.fenced ? null : HEADING.exec(l.line);
|
|
132
|
+
if (h !== null && h[1].length <= 2) inImpact = isImpactHeading(h);
|
|
133
|
+
return !inImpact;
|
|
134
|
+
}).map((l) => l.line).join(" ").replace(/\s+/g, " ").trim();
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Refuse a turn that leaves `file` without a `## Impact` section. Only the
|
|
138
|
+
* pending turn (`head()` is `""`): a triage lap recorded before Impact existed
|
|
139
|
+
* replays without one, and refusing there would fail every in-flight process.
|
|
140
|
+
* A human's edit at a later gate is not checked — the human owns that file then.
|
|
141
|
+
*/
|
|
142
|
+
const requireImpact = (file) => {
|
|
143
|
+
if (head() !== "") return;
|
|
144
|
+
const text = read(file);
|
|
145
|
+
if (text === void 0) return;
|
|
146
|
+
if (hasImpact(text)) return;
|
|
147
|
+
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`);
|
|
148
|
+
};
|
|
149
|
+
/** Refuse a turn that changed `file` beyond its `## Impact` section. */
|
|
150
|
+
const requireImpactOnly = (file) => {
|
|
151
|
+
const change = changes().get(file);
|
|
152
|
+
if (change === void 0) return;
|
|
153
|
+
if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return;
|
|
154
|
+
refuse(`gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`);
|
|
155
|
+
};
|
|
116
156
|
const idNumber = (id) => Number(id.slice(3));
|
|
117
157
|
/** Why `listed` renumbers or reuses an earlier ID; `undefined` when it does neither. */
|
|
118
158
|
const unstableIds = (listed, ids) => {
|
|
@@ -146,6 +186,19 @@ const clear = async (stated) => {
|
|
|
146
186
|
});
|
|
147
187
|
return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P);
|
|
148
188
|
};
|
|
189
|
+
/** The fast path's impact-only turn: add `## Impact`, leave the criteria alone. */
|
|
190
|
+
const impact = async () => {
|
|
191
|
+
await agentWithSkills("impact", designImpactPrompt(), {
|
|
192
|
+
label: "Mapping the change's impact",
|
|
193
|
+
file: REQUIREMENTS,
|
|
194
|
+
model: planner(),
|
|
195
|
+
system: designSystem()
|
|
196
|
+
});
|
|
197
|
+
requireProgress(REQUIREMENTS);
|
|
198
|
+
requireCriteriaFrozen(REQUIREMENTS);
|
|
199
|
+
requireImpact(REQUIREMENTS);
|
|
200
|
+
requireImpactOnly(REQUIREMENTS);
|
|
201
|
+
};
|
|
149
202
|
/**
|
|
150
203
|
* The skip path: when the sketch stated clear, checkable criteria, write them
|
|
151
204
|
* to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
|
|
@@ -157,6 +210,7 @@ const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scop
|
|
|
157
210
|
label: "Recording the stated acceptance criteria",
|
|
158
211
|
file: REQUIREMENTS
|
|
159
212
|
});
|
|
213
|
+
await impact();
|
|
160
214
|
return true;
|
|
161
215
|
});
|
|
162
216
|
/** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
|
|
@@ -253,10 +307,169 @@ const design = (base, guard) => scope("design", async () => {
|
|
|
253
307
|
requireProgress(REQUIREMENTS);
|
|
254
308
|
requireReplies(REQUIREMENTS);
|
|
255
309
|
requireCriteria(REQUIREMENTS);
|
|
310
|
+
requireImpact(REQUIREMENTS);
|
|
256
311
|
guard?.();
|
|
257
312
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
258
313
|
});
|
|
259
314
|
//#endregion
|
|
315
|
+
//#region src/workflows/feature/green.ts
|
|
316
|
+
const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
317
|
+
label: "Fixing the check",
|
|
318
|
+
file: FEEDBACK,
|
|
319
|
+
model: coder(),
|
|
320
|
+
system: builderSystem()
|
|
321
|
+
}), {
|
|
322
|
+
guard: () => {
|
|
323
|
+
requireCriteriaFrozen(REQUIREMENTS);
|
|
324
|
+
guard?.();
|
|
325
|
+
},
|
|
326
|
+
guardRests: false
|
|
327
|
+
});
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/workflows/feature/prototype.ts
|
|
330
|
+
/** Only a `no` at this or above skips a criterion: no verdict, or an unsure one, sends it to the hands-on test. */
|
|
331
|
+
const UNOBSERVABLE_MIN_P = .5;
|
|
332
|
+
/** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
|
|
333
|
+
const prototypeRounds = () => {
|
|
334
|
+
const value = vars.prototypeRounds ?? defaults.prototypeRounds;
|
|
335
|
+
if (!/^(0|[1-9]\d*)$/.test(value)) refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`);
|
|
336
|
+
return Number(value);
|
|
337
|
+
};
|
|
338
|
+
const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i;
|
|
339
|
+
const UNGUIDED = /^no testing instructions$/i;
|
|
340
|
+
/** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
|
|
341
|
+
const reportOf = (text) => {
|
|
342
|
+
let unguided = false;
|
|
343
|
+
const items = [];
|
|
344
|
+
const malformed = [];
|
|
345
|
+
for (const { line, fenced } of markFenced(text)) {
|
|
346
|
+
const heading = fenced ? null : HEADING.exec(line);
|
|
347
|
+
if (heading?.[1] !== "##") continue;
|
|
348
|
+
if (UNGUIDED.test(heading[2])) unguided = true;
|
|
349
|
+
const chunk = CHUNK.exec(heading[2]);
|
|
350
|
+
if (chunk === null && /^(ac-\d|problem)/i.test(heading[2])) malformed.push(heading[2]);
|
|
351
|
+
if (chunk !== null) items.push({
|
|
352
|
+
subject: /^ac-/i.test(chunk[1]) ? chunk[1].toUpperCase() : chunk[1],
|
|
353
|
+
verdict: chunk[2].toLowerCase()
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
unguided,
|
|
358
|
+
items,
|
|
359
|
+
malformed
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
/**
|
|
363
|
+
* The report the testing turn left, refused unless it is the only change and
|
|
364
|
+
* names each of `ids` once, with its open items: any other criterion is not
|
|
365
|
+
* this test's to fix.
|
|
366
|
+
*/
|
|
367
|
+
const requireReport = (ids, retest) => {
|
|
368
|
+
const others = changes().map((c) => c.path).filter((path) => path !== PROTOTYPE);
|
|
369
|
+
if (others.length > 0) refuse(`gtd land: prototype-report: test in a disposable place and change nothing but ${PROTOTYPE} — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}`);
|
|
370
|
+
const text = read(PROTOTYPE);
|
|
371
|
+
if (text === void 0) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`);
|
|
372
|
+
const report = reportOf(text);
|
|
373
|
+
if (report.malformed.length > 0) refuse(`gtd land: prototype-report: head each chunk \`## <ID>: pass|fail|blocked\` or \`## Problem <title>: pass|fail|blocked\` — these miss it:\n${report.malformed.map((h) => ` - ${h}`).join("\n")}`);
|
|
374
|
+
if (report.unguided && report.items.length === 0 && !retest) return {
|
|
375
|
+
unguided: true,
|
|
376
|
+
open: []
|
|
377
|
+
};
|
|
378
|
+
const elsewhere = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "").filter((id) => !ids.includes(id));
|
|
379
|
+
const count = (id) => report.items.filter((i) => i.subject === id).length;
|
|
380
|
+
const wrong = ids.filter((id) => count(id) !== 1);
|
|
381
|
+
if (wrong.length > 0) refuse(`gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`);
|
|
382
|
+
return {
|
|
383
|
+
unguided: false,
|
|
384
|
+
open: report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
/**
|
|
388
|
+
* The slice's criteria a person can observe by using the change, by the
|
|
389
|
+
* judge: those tagged `(no hands-on)` are never asked about.
|
|
390
|
+
*/
|
|
391
|
+
const observable = async (slice) => {
|
|
392
|
+
const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
|
|
393
|
+
const own = handsOnIds(requirements, slice);
|
|
394
|
+
const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
|
|
395
|
+
if (criteria.length === 0) return [];
|
|
396
|
+
const { answers } = await judge("observable", {
|
|
397
|
+
questions: criteria.map(observableQuestion),
|
|
398
|
+
evidence: { requirements },
|
|
399
|
+
message: observableMessage(),
|
|
400
|
+
label: "Judging which criteria to test by hand"
|
|
401
|
+
});
|
|
402
|
+
return own.filter((id) => !answered(answers[id], "no", UNOBSERVABLE_MIN_P));
|
|
403
|
+
};
|
|
404
|
+
const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
405
|
+
const testing = async (slice, ids, retest) => {
|
|
406
|
+
const skipped = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice).filter((id) => !ids.includes(id));
|
|
407
|
+
await agentWithSkills("testing", prototypeTestingPrompt(sliceIds(slice), skipped, retest), {
|
|
408
|
+
label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
|
|
409
|
+
file: PROTOTYPE,
|
|
410
|
+
model: coder(),
|
|
411
|
+
system: testerSystem(),
|
|
412
|
+
allowEmpty: true
|
|
413
|
+
});
|
|
414
|
+
return requireReport(ids, retest);
|
|
415
|
+
};
|
|
416
|
+
const fixing = async (slice, guard) => {
|
|
417
|
+
await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(sliceIds(slice)), {
|
|
418
|
+
label: "Fixing what the hands-on test found",
|
|
419
|
+
file: PROTOTYPE,
|
|
420
|
+
model: coder(),
|
|
421
|
+
system: builderSystem(),
|
|
422
|
+
allowEmpty: true
|
|
423
|
+
}));
|
|
424
|
+
if (changes().some((c) => c.path === ".gtd/PROTOTYPE.md")) refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`);
|
|
425
|
+
frozen();
|
|
426
|
+
guard?.();
|
|
427
|
+
};
|
|
428
|
+
/** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
|
|
429
|
+
const askPerson = async (name, message, label) => {
|
|
430
|
+
await human(name, {
|
|
431
|
+
message,
|
|
432
|
+
label,
|
|
433
|
+
file: PROTOTYPE,
|
|
434
|
+
acceptClean: true
|
|
435
|
+
});
|
|
436
|
+
if (!changes().some((c) => c.path !== ".gtd/PROTOTYPE.md")) return false;
|
|
437
|
+
frozen();
|
|
438
|
+
return true;
|
|
439
|
+
};
|
|
440
|
+
/**
|
|
441
|
+
* Use the slice by hand: a testing turn reports on every criterion a person
|
|
442
|
+
* can observe, a fix turn takes each failure, and the slice is tested again —
|
|
443
|
+
* until the report is clean, or `prototypeRounds` fixes are spent and a person
|
|
444
|
+
* decides. A slice with no such criterion is not tested by hand. `guard` holds
|
|
445
|
+
* every fix turn, as it holds the implementation's.
|
|
446
|
+
*/
|
|
447
|
+
const prototypeTest = async (slice, guard) => {
|
|
448
|
+
const cap = prototypeRounds();
|
|
449
|
+
if (cap === 0) return;
|
|
450
|
+
await scope("prototype", async () => {
|
|
451
|
+
const ids = await observable(slice);
|
|
452
|
+
if (ids.length === 0) return;
|
|
453
|
+
let fixes = 0;
|
|
454
|
+
let retest = false;
|
|
455
|
+
for (;;) {
|
|
456
|
+
const { unguided, open } = await testing(slice, ids, retest);
|
|
457
|
+
retest = !unguided;
|
|
458
|
+
if (unguided) {
|
|
459
|
+
if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
|
|
460
|
+
} else if (open.length === 0) return;
|
|
461
|
+
else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
|
|
462
|
+
fixes++;
|
|
463
|
+
await fixing(slice, guard);
|
|
464
|
+
} else {
|
|
465
|
+
if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
|
|
466
|
+
fixes = 0;
|
|
467
|
+
}
|
|
468
|
+
await green(guard);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
//#endregion
|
|
260
473
|
//#region src/workflows/shared/continuation.ts
|
|
261
474
|
const phase = (name, steered) => ({
|
|
262
475
|
name,
|
|
@@ -573,211 +786,6 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
573
786
|
});
|
|
574
787
|
};
|
|
575
788
|
//#endregion
|
|
576
|
-
//#region src/workflows/feature/coverage.ts
|
|
577
|
-
/** Below this the judge's `yes` is not trusted: a wrongly covered ID ends the feature without its test. */
|
|
578
|
-
const COVERED_MIN_P = .7;
|
|
579
|
-
/** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
|
|
580
|
-
const COVERING_RETRIES = 2;
|
|
581
|
-
const frozen$1 = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
582
|
-
const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
583
|
-
label: "Fixing the check",
|
|
584
|
-
file: FEEDBACK,
|
|
585
|
-
model: coder(),
|
|
586
|
-
system: builderSystem()
|
|
587
|
-
}), {
|
|
588
|
-
guard: () => {
|
|
589
|
-
frozen$1();
|
|
590
|
-
guard?.();
|
|
591
|
-
},
|
|
592
|
-
guardRests: false
|
|
593
|
-
});
|
|
594
|
-
const testsSince = (base) => changesSince(base).filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path)).sort((a, b) => Number(/\bAC-\d/.test(a.after)) - Number(/\bAC-\d/.test(b.after))).map((c) => `=== ${c.path} (${c.status})\n${c.after}`).join("\n");
|
|
595
|
-
/**
|
|
596
|
-
* The criterion IDs of `.gtd/REQUIREMENTS.md` — read afresh, never carried
|
|
597
|
-
* over, only `slice`'s when given — no passing test since `base` covers, by the judge.
|
|
598
|
-
*/
|
|
599
|
-
const uncoveredSince = async (base, slice) => {
|
|
600
|
-
const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
|
|
601
|
-
const own = criterionIds(requirements, slice);
|
|
602
|
-
const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
|
|
603
|
-
if (criteria.length === 0) return {
|
|
604
|
-
covered: [],
|
|
605
|
-
uncovered: []
|
|
606
|
-
};
|
|
607
|
-
const { answers } = await judge("coverage", {
|
|
608
|
-
questions: criteria.map(coverageQuestion),
|
|
609
|
-
evidence: { tests: testsSince(base) },
|
|
610
|
-
message: coverageMessage(),
|
|
611
|
-
label: "Judging acceptance-criteria coverage"
|
|
612
|
-
});
|
|
613
|
-
const covers = (id) => answered(answers[id], "yes", COVERED_MIN_P);
|
|
614
|
-
const ids = criteria.map((c) => c.id);
|
|
615
|
-
return {
|
|
616
|
-
covered: ids.filter(covers),
|
|
617
|
-
uncovered: ids.filter((id) => !covers(id))
|
|
618
|
-
};
|
|
619
|
-
};
|
|
620
|
-
const record = (covered, uncovered = []) => {
|
|
621
|
-
for (const id of covered) mark("covers", id);
|
|
622
|
-
for (const id of uncovered) mark("uncovered", id);
|
|
623
|
-
};
|
|
624
|
-
/**
|
|
625
|
-
* Hold a green change until every acceptance criterion has a passing test,
|
|
626
|
-
* sending uncovered IDs back to a covering turn; marks the IDs covered on the
|
|
627
|
-
* landing that ends it. A human may overrule once the retries are spent.
|
|
628
|
-
*/
|
|
629
|
-
const requireCoverage = async (base = start(), slice) => {
|
|
630
|
-
let retries = 0;
|
|
631
|
-
for (;;) {
|
|
632
|
-
const { covered, uncovered } = await uncoveredSince(base, slice);
|
|
633
|
-
if (uncovered.length === 0) return record(covered);
|
|
634
|
-
if (retries < COVERING_RETRIES) {
|
|
635
|
-
retries++;
|
|
636
|
-
await agentWithSkills("covering", coveringPrompt(uncovered, sliceIds(slice)), {
|
|
637
|
-
label: "Covering the acceptance criteria",
|
|
638
|
-
model: coder(),
|
|
639
|
-
system: builderSystem(),
|
|
640
|
-
allowEmpty: true
|
|
641
|
-
});
|
|
642
|
-
frozen$1();
|
|
643
|
-
} else {
|
|
644
|
-
await human("uncovered", {
|
|
645
|
-
message: uncoveredMessage(uncovered),
|
|
646
|
-
label: "Acceptance criteria without a test",
|
|
647
|
-
file: REQUIREMENTS,
|
|
648
|
-
acceptClean: true
|
|
649
|
-
});
|
|
650
|
-
if (!changes().some((c) => isCode$1(c.path))) return record(covered, uncovered);
|
|
651
|
-
frozen$1();
|
|
652
|
-
retries = 0;
|
|
653
|
-
}
|
|
654
|
-
await green();
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
//#endregion
|
|
658
|
-
//#region src/workflows/feature/prototype.ts
|
|
659
|
-
/** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
|
|
660
|
-
const prototypeRounds = () => {
|
|
661
|
-
const value = vars.prototypeRounds ?? defaults.prototypeRounds;
|
|
662
|
-
if (!/^(0|[1-9]\d*)$/.test(value)) refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`);
|
|
663
|
-
return Number(value);
|
|
664
|
-
};
|
|
665
|
-
const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i;
|
|
666
|
-
const UNGUIDED = /^no testing instructions$/i;
|
|
667
|
-
/** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
|
|
668
|
-
const reportOf = (text) => {
|
|
669
|
-
let unguided = false;
|
|
670
|
-
const items = [];
|
|
671
|
-
const malformed = [];
|
|
672
|
-
for (const { line, fenced } of markFenced(text)) {
|
|
673
|
-
const heading = fenced ? null : HEADING.exec(line);
|
|
674
|
-
if (heading?.[1] !== "##") continue;
|
|
675
|
-
if (UNGUIDED.test(heading[2])) unguided = true;
|
|
676
|
-
const chunk = CHUNK.exec(heading[2]);
|
|
677
|
-
if (chunk === null && /^(ac-\d|problem)/i.test(heading[2])) malformed.push(heading[2]);
|
|
678
|
-
if (chunk !== null) items.push({
|
|
679
|
-
subject: /^ac-/i.test(chunk[1]) ? chunk[1].toUpperCase() : chunk[1],
|
|
680
|
-
verdict: chunk[2].toLowerCase()
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
return {
|
|
684
|
-
unguided,
|
|
685
|
-
items,
|
|
686
|
-
malformed
|
|
687
|
-
};
|
|
688
|
-
};
|
|
689
|
-
/**
|
|
690
|
-
* The report the testing turn left, refused unless it is the only change and
|
|
691
|
-
* names every criterion once, with its open items: another slice's criteria
|
|
692
|
-
* are not this slice's to fix.
|
|
693
|
-
*/
|
|
694
|
-
const requireReport = (slice, retest) => {
|
|
695
|
-
const others = changes().map((c) => c.path).filter((path) => path !== PROTOTYPE);
|
|
696
|
-
if (others.length > 0) refuse(`gtd land: prototype-report: test in a disposable place and change nothing but ${PROTOTYPE} — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}`);
|
|
697
|
-
const text = read(PROTOTYPE);
|
|
698
|
-
if (text === void 0) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`);
|
|
699
|
-
const report = reportOf(text);
|
|
700
|
-
if (report.malformed.length > 0) refuse(`gtd land: prototype-report: head each chunk \`## <ID>: pass|fail|blocked\` or \`## Problem <title>: pass|fail|blocked\` — these miss it:\n${report.malformed.map((h) => ` - ${h}`).join("\n")}`);
|
|
701
|
-
if (report.unguided && report.items.length === 0 && !retest) return {
|
|
702
|
-
unguided: true,
|
|
703
|
-
open: []
|
|
704
|
-
};
|
|
705
|
-
const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
|
|
706
|
-
const ids = criterionIds(requirements, slice);
|
|
707
|
-
const elsewhere = criterionIds(requirements).filter((id) => !ids.includes(id));
|
|
708
|
-
const count = (id) => report.items.filter((i) => i.subject === id).length;
|
|
709
|
-
const wrong = ids.filter((id) => count(id) !== 1);
|
|
710
|
-
if (wrong.length > 0) refuse(`gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`);
|
|
711
|
-
return {
|
|
712
|
-
unguided: false,
|
|
713
|
-
open: report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
|
|
714
|
-
};
|
|
715
|
-
};
|
|
716
|
-
const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
717
|
-
const testing = async (slice, retest) => {
|
|
718
|
-
await agentWithSkills("testing", prototypeTestingPrompt(sliceIds(slice), retest), {
|
|
719
|
-
label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
|
|
720
|
-
file: PROTOTYPE,
|
|
721
|
-
model: coder(),
|
|
722
|
-
system: testerSystem(),
|
|
723
|
-
allowEmpty: true
|
|
724
|
-
});
|
|
725
|
-
return requireReport(slice, retest);
|
|
726
|
-
};
|
|
727
|
-
const fixing = async (slice, guard) => {
|
|
728
|
-
await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(sliceIds(slice)), {
|
|
729
|
-
label: "Fixing what the hands-on test found",
|
|
730
|
-
file: PROTOTYPE,
|
|
731
|
-
model: coder(),
|
|
732
|
-
system: builderSystem(),
|
|
733
|
-
allowEmpty: true
|
|
734
|
-
}));
|
|
735
|
-
if (changes().some((c) => c.path === ".gtd/PROTOTYPE.md")) refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`);
|
|
736
|
-
frozen();
|
|
737
|
-
guard?.();
|
|
738
|
-
};
|
|
739
|
-
/** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
|
|
740
|
-
const askPerson = async (name, message, label) => {
|
|
741
|
-
await human(name, {
|
|
742
|
-
message,
|
|
743
|
-
label,
|
|
744
|
-
file: PROTOTYPE,
|
|
745
|
-
acceptClean: true
|
|
746
|
-
});
|
|
747
|
-
if (!changes().some((c) => c.path !== ".gtd/PROTOTYPE.md")) return false;
|
|
748
|
-
frozen();
|
|
749
|
-
return true;
|
|
750
|
-
};
|
|
751
|
-
/**
|
|
752
|
-
* Use the slice by hand: a testing turn reports on every criterion, a fix turn
|
|
753
|
-
* takes each failure, and the slice is tested again — until the report is
|
|
754
|
-
* clean, or `prototypeRounds` fixes are spent and a person decides. `guard`
|
|
755
|
-
* holds every fix turn, as it holds the implementation's.
|
|
756
|
-
*/
|
|
757
|
-
const prototypeTest = async (slice, guard) => {
|
|
758
|
-
const cap = prototypeRounds();
|
|
759
|
-
if (cap === 0) return;
|
|
760
|
-
await scope("prototype", async () => {
|
|
761
|
-
let fixes = 0;
|
|
762
|
-
let retest = false;
|
|
763
|
-
for (;;) {
|
|
764
|
-
const { unguided, open } = await testing(slice, retest);
|
|
765
|
-
retest = !unguided;
|
|
766
|
-
if (unguided) {
|
|
767
|
-
if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
|
|
768
|
-
} else if (open.length === 0) return;
|
|
769
|
-
else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
|
|
770
|
-
fixes++;
|
|
771
|
-
await fixing(slice, guard);
|
|
772
|
-
} else {
|
|
773
|
-
if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
|
|
774
|
-
fixes = 0;
|
|
775
|
-
}
|
|
776
|
-
await green(guard);
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
};
|
|
780
|
-
//#endregion
|
|
781
789
|
//#region src/workflows/feature/seam.ts
|
|
782
790
|
/** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
|
|
783
791
|
const NAIVE_RETRIES = 2;
|
|
@@ -986,7 +994,6 @@ const implementing = (options) => scope("implement", async () => {
|
|
|
986
994
|
}, since);
|
|
987
995
|
const keepsTests = built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0;
|
|
988
996
|
await green(keepsTests);
|
|
989
|
-
await requireCoverage(options.base, options.slice);
|
|
990
997
|
await prototypeTest(options.slice, keepsTests);
|
|
991
998
|
const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
992
999
|
await closing(moreToBuild(options.more));
|
|
@@ -1272,7 +1279,6 @@ const buildSlice = async (sliceBase, build) => {
|
|
|
1272
1279
|
naive: seam.land === true ? seam.reference : void 0,
|
|
1273
1280
|
rebuild: seam.rebuild,
|
|
1274
1281
|
slice: build.criteria,
|
|
1275
|
-
base: build.criteria === void 0 ? void 0 : sliceBase,
|
|
1276
1282
|
more: build.more
|
|
1277
1283
|
});
|
|
1278
1284
|
const more = build.more();
|
|
@@ -1386,4 +1392,4 @@ const feature = shipped(ordinaryStart);
|
|
|
1386
1392
|
//#region src/workflows/feature/index.ts
|
|
1387
1393
|
var feature_default = feature;
|
|
1388
1394
|
//#endregion
|
|
1389
|
-
export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, triage, unstableIds, unwind };
|
|
1395
|
+
export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, handsOnIds, hasImpact, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireImpact, requireImpactOnly, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, triage, unstableIds, unwind };
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
|
-
import { _ as
|
|
3
|
-
import { C as coder,
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
2
|
+
import { _ as reviewNotes, v as reviewRisks } from "../chunks/steering-CLhu8C7q.mjs";
|
|
3
|
+
import { C as coder, Ft as defaults, It as envDefaults, K as finisherSystem, N as agentWithSkills, c as baseline, i as skills, n as summary, q as fixReviewNotesPrompt, r as access, s as shipped, t as steering, y as REVIEW } from "../chunks/reserved-DxB5CROj.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-QZc7Uz2U.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,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, start } from "../flows.mjs";
|
|
2
|
-
import { C as coder,
|
|
3
|
-
import { a as refusePendingHandOff } from "../chunks/tail-
|
|
4
|
-
import { i as reUnwind, t as buildTail } from "../chunks/review-
|
|
2
|
+
import { C as coder, Ft as defaults, It as envDefaults, K as finisherSystem, N as agentWithSkills, i as skills, l as gate, n as summary, r as access, s as shipped, t as steering, v as REQUIREMENTS, vt as reviewFeedbackFixPrompt, yt as reviewGateBlockedMessage } from "../chunks/reserved-DxB5CROj.mjs";
|
|
3
|
+
import { a as refusePendingHandOff } from "../chunks/tail-QZc7Uz2U.mjs";
|
|
4
|
+
import { i as reUnwind, t as buildTail } from "../chunks/review-CKMSdvgW.mjs";
|
|
5
5
|
//#region src/workflows/review/review.ts
|
|
6
6
|
const fixFeedback = async () => {
|
|
7
7
|
await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import { C as coder,
|
|
2
|
+
import { C as coder, Ft as defaults, It as envDefaults, Yt as flipProblems, Zt as requireFlag, f as FEEDBACK, i as skills, l as gate, n as summary, o as flagShipped, qt as flagMentions, r as access, t as steering } from "../chunks/reserved-DxB5CROj.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.";
|