@pmelab/gtd 44.0.0 → 45.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -0
- package/dist/chunks/{reserved-DJk_6-ju.mjs → reserved-ty1mtlnS.mjs} +115 -3
- package/dist/chunks/{review-X7Jfz0dY.mjs → review-BmoH-9im.mjs} +163 -4
- package/dist/chunks/{tail-5w5mxGTq.mjs → tail-CM3sVoAb.mjs} +1 -1
- package/dist/workflow-kit.mjs +22 -10
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +3 -3
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +305 -22
- package/package.json +1 -1
- package/src/workflow-kit/review.test.ts +80 -2
- package/src/workflow-kit/review.ts +45 -11
- package/src/workflows/feature/review-answers.feature +12 -3
- package/src/workflows/feature/standards.feature +286 -0
- package/src/workflows/review/review-door.feature +11 -2
- package/src/workflows/shared/learn.test.ts +154 -0
- package/src/workflows/shared/learn.ts +206 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/review.ts +10 -0
- package/src/workflows/shared/standards.test.ts +90 -0
- package/src/workflows/shared/standards.ts +156 -0
- package/src/workflows/shared/text.test.ts +58 -1
- package/src/workflows/shared/text.ts +28 -1
- package/src/workflows/shared/vars.ts +2 -0
package/README.md
CHANGED
|
@@ -564,6 +564,22 @@ noted in steps 3 and 4 below.
|
|
|
564
564
|
`gtd judge answer` / conservative-default shape as step 3's own judged
|
|
565
565
|
points.
|
|
566
566
|
|
|
567
|
+
**The review learns your taste.** Each won't-fix and each own answer is a
|
|
568
|
+
candidate standard. A judge (`build.review.standards.judge`) asks whether it
|
|
569
|
+
is reusable beyond this note; at `standardMinP` (`0.8`) or above it is kept
|
|
570
|
+
in `.gtd/STANDARDS.md` with no question asked, and the next review lists it
|
|
571
|
+
under `## Standards added`. Every later agent turn — the lenses, the naive
|
|
572
|
+
pass, the build, every fix — is handed the kept standards, so a finding you
|
|
573
|
+
rejected once is not raised again and a rule you wrote is followed, in this
|
|
574
|
+
process and every later one. One the judge does not confidently rule out
|
|
575
|
+
contradicting a standard already kept is not applied: it waits under
|
|
576
|
+
`## Undecided` and becomes a `## Standard conflict <id>` note in the review,
|
|
577
|
+
with its own three boxes — **keep the new one (drop the old)**, **keep the
|
|
578
|
+
old one (discard the new)**, or **your own wording**, which replaces both —
|
|
579
|
+
and the review does not sign off until it is answered. `.gtd/STANDARDS.md` is
|
|
580
|
+
committed and yours to edit; gtd rewrites it as one bullet per standard when
|
|
581
|
+
it learns.
|
|
582
|
+
|
|
567
583
|
You never talk to it. Every exchange is a file in `.gtd/` that you edit in your
|
|
568
584
|
own editor, and every answer you give is a commit. Your test suite is the gate
|
|
569
585
|
throughout: a red baseline stops the process before it starts, and a red build
|
|
@@ -113,6 +113,108 @@ const requireFlag = (workflow, registryPath, name, defaultOn) => {
|
|
|
113
113
|
if (entry.defaultOn !== defaultOn) return refuse(defaultOn ? `gtd: ${workflow}: flag "${name}" is not default-on — roll it out first` : `gtd: ${workflow}: flag "${name}" is already default-on`);
|
|
114
114
|
};
|
|
115
115
|
//#endregion
|
|
116
|
+
//#region src/workflows/shared/standards.ts
|
|
117
|
+
const STANDARDS = ".gtd/STANDARDS.md";
|
|
118
|
+
const UNDECIDED = "Undecided";
|
|
119
|
+
const oneLine = (text) => text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim();
|
|
120
|
+
const ITEM = /^(?:[-*+]|\d+[.)])\s+(.*)$/;
|
|
121
|
+
const CONTRADICTS = /^\s+[-*+]\s+contradicts:\s*(.*)$/i;
|
|
122
|
+
const wrapped = (item, line) => {
|
|
123
|
+
const olds = item.olds;
|
|
124
|
+
if (olds !== void 0 && olds.length > 0) olds[olds.length - 1] += ` ${line.trim()}`;
|
|
125
|
+
else item.text += ` ${line.trim()}`;
|
|
126
|
+
};
|
|
127
|
+
/** What tells two standards apart: not the emphasis or spacing a formatter may rewrite. */
|
|
128
|
+
const standardKey = (text) => oneLine(text.replace(/[*_`\\]/g, "")).toLowerCase();
|
|
129
|
+
const toStandards = (items) => {
|
|
130
|
+
const seen = /* @__PURE__ */ new Set();
|
|
131
|
+
const fresh = (text) => {
|
|
132
|
+
const key = standardKey(text);
|
|
133
|
+
if (key === "" || seen.has(key)) return false;
|
|
134
|
+
seen.add(key);
|
|
135
|
+
return true;
|
|
136
|
+
};
|
|
137
|
+
const decided = items.filter((i) => i.olds === void 0).map((i) => oneLine(i.text));
|
|
138
|
+
const undecided = items.flatMap((i) => i.olds === void 0 ? [] : [{
|
|
139
|
+
text: oneLine(i.text),
|
|
140
|
+
olds: i.olds.map(oneLine).filter((o) => o !== "")
|
|
141
|
+
}]);
|
|
142
|
+
return {
|
|
143
|
+
decided: decided.filter(fresh),
|
|
144
|
+
undecided: undecided.filter((c) => fresh(c.text))
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Every top-level bullet is one standard, those under `## Undecided` waiting
|
|
149
|
+
* on the human; a hand-edited file may group the rest under any heading.
|
|
150
|
+
*/
|
|
151
|
+
const parseStandards = (text) => {
|
|
152
|
+
const items = [];
|
|
153
|
+
let pending = false;
|
|
154
|
+
let last;
|
|
155
|
+
for (const line of text.split("\n")) {
|
|
156
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
157
|
+
const item = ITEM.exec(line);
|
|
158
|
+
const old = CONTRADICTS.exec(line);
|
|
159
|
+
if (heading !== null) {
|
|
160
|
+
pending = heading[1].length > 1 && heading[2].trim() === UNDECIDED;
|
|
161
|
+
last = void 0;
|
|
162
|
+
} else if (item !== null) {
|
|
163
|
+
last = pending ? {
|
|
164
|
+
text: item[1],
|
|
165
|
+
olds: []
|
|
166
|
+
} : { text: item[1] };
|
|
167
|
+
items.push(last);
|
|
168
|
+
} else if (old !== null && last?.olds !== void 0) last.olds.push(old[1]);
|
|
169
|
+
else if (/^\s+\S/.test(line) && last !== void 0) wrapped(last, line);
|
|
170
|
+
else if (line.trim() !== "") last = void 0;
|
|
171
|
+
}
|
|
172
|
+
return toStandards(items);
|
|
173
|
+
};
|
|
174
|
+
const renderStandards = ({ decided, undecided }) => {
|
|
175
|
+
return `${[
|
|
176
|
+
"# Standards",
|
|
177
|
+
"Kept from your review answers for every agent turn. One standard per bullet.",
|
|
178
|
+
...decided.length === 0 ? [] : [decided.map((s) => `- ${s}`).join("\n")],
|
|
179
|
+
...undecided.length === 0 ? [] : [`## ${UNDECIDED}`, undecided.map((c) => [`- ${c.text}`, ...c.olds.map((o) => ` - contradicts: ${o}`)].join("\n")).join("\n")]
|
|
180
|
+
].join("\n\n")}\n`;
|
|
181
|
+
};
|
|
182
|
+
const hash = (raw) => {
|
|
183
|
+
const text = standardKey(raw);
|
|
184
|
+
let h = 2166136261;
|
|
185
|
+
for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);
|
|
186
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
187
|
+
};
|
|
188
|
+
const CONFLICT_TITLE = /^Standard conflict ([0-9a-f]{8})$/;
|
|
189
|
+
const conflictTitle = (c) => `Standard conflict ${hash(c.text)}`;
|
|
190
|
+
const conflictFor = (standards, title) => {
|
|
191
|
+
const id = CONFLICT_TITLE.exec(title)?.[1];
|
|
192
|
+
return id === void 0 ? void 0 : standards.undecided.find((c) => hash(c.text) === id);
|
|
193
|
+
};
|
|
194
|
+
const isConflictTitle = (title) => CONFLICT_TITLE.test(title);
|
|
195
|
+
const KEEP_NEW = "keep the new one (drop the old)";
|
|
196
|
+
const KEEP_OLD = "keep the old one (discard the new)";
|
|
197
|
+
/** A conflict's chunk body; its three boxes read as fix / won't fix / own answer by position. */
|
|
198
|
+
const conflictBody = (c) => [...c.olds.length === 0 ? [`A standard learned from your answers contradicted ones another answer since replaced in \`${STANDARDS}\`. Keep it, discard it, or reword it.`, `New: ${c.text}`] : [
|
|
199
|
+
`A standard learned from your answers contradicts ${c.olds.length === 1 ? "one" : "some"} already in \`${STANDARDS}\`. Your own answer replaces both with its wording.`,
|
|
200
|
+
`New: ${c.text}`,
|
|
201
|
+
`Old:\n\n${c.olds.map((o) => `- ${o}`).join("\n")}`
|
|
202
|
+
], `${ANSWER_LEAD}\n\n- [ ] ${KEEP_NEW}\n- [ ] ${KEEP_OLD}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`].join("\n\n");
|
|
203
|
+
/** What every agent turn is told while `.gtd/STANDARDS.md` holds decided standards. */
|
|
204
|
+
const standardsBlock = (text) => {
|
|
205
|
+
const { decided } = parseStandards(text ?? "");
|
|
206
|
+
if (decided.length === 0) return "";
|
|
207
|
+
return `## Standards
|
|
208
|
+
|
|
209
|
+
\`${STANDARDS}\` holds the standards the human set in earlier reviews.
|
|
210
|
+
Follow every one in what you write. Reviewing, never raise a finding a
|
|
211
|
+
standard rules out — a \`Won't fix:\` standard is a finding the human
|
|
212
|
+
rejected, so never raise it or one like it again — and raise breaking a
|
|
213
|
+
standard as a finding.
|
|
214
|
+
|
|
215
|
+
${decided.map((s) => `- ${s}`).join("\n")}`;
|
|
216
|
+
};
|
|
217
|
+
//#endregion
|
|
116
218
|
//#region src/workflows/shared/vars.ts
|
|
117
219
|
/** The refactor guard's fixed list, and `seamAllow`'s default minus `.gtd/**`. */
|
|
118
220
|
const testFileGlobs = [
|
|
@@ -132,6 +234,7 @@ const defaults = {
|
|
|
132
234
|
reviewNoteActionable: "0.7",
|
|
133
235
|
reviewSeverityMinP: "0.7",
|
|
134
236
|
qualityTriageMinP: "0.7",
|
|
237
|
+
standardMinP: "0.8",
|
|
135
238
|
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
136
239
|
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
137
240
|
flagMode: "true",
|
|
@@ -391,8 +494,11 @@ const withSkills = (skills, prompt) => {
|
|
|
391
494
|
};
|
|
392
495
|
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills, then its restricted file access. */
|
|
393
496
|
const agentWithSkills = (name, prompt, options = {}) => {
|
|
394
|
-
const
|
|
395
|
-
|
|
497
|
+
const body = [
|
|
498
|
+
accessPreamble(accessFor(name, options.file)),
|
|
499
|
+
prompt,
|
|
500
|
+
standardsBlock(read(STANDARDS))
|
|
501
|
+
].filter((part) => part !== "").join("\n\n");
|
|
396
502
|
return agent(name, withSkills(skillsFor(name).join(", "), body), options);
|
|
397
503
|
};
|
|
398
504
|
const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
|
|
@@ -432,6 +538,7 @@ const SUMMARY_TITLES = [
|
|
|
432
538
|
"Weakened scenarios",
|
|
433
539
|
"Fixed from your notes",
|
|
434
540
|
"Fixed automatically",
|
|
541
|
+
"Standards added",
|
|
435
542
|
"Dropped as noise",
|
|
436
543
|
"Nothing to note"
|
|
437
544
|
];
|
|
@@ -452,6 +559,8 @@ const composedReview = (base, report) => {
|
|
|
452
559
|
};
|
|
453
560
|
const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
|
|
454
561
|
const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
|
|
562
|
+
const conflicts = (report.standards?.undecided ?? []).map((c) => chunk(unique(conflictTitle(c)), conflictBody(c)));
|
|
563
|
+
const added = report.standards?.added ?? [];
|
|
455
564
|
const notes = report.notes.map((n) => ({
|
|
456
565
|
...n,
|
|
457
566
|
title: unique(n.title)
|
|
@@ -460,8 +569,10 @@ const composedReview = (base, report) => {
|
|
|
460
569
|
const chunks = [
|
|
461
570
|
...report.nitFix === void 0 ? [] : [chunk("Fixed from your notes", `Your nits and fix-as-suggested notes were worked on in ${short(report.nitFix)}; the notes you answered are left out — check the diff.`)],
|
|
462
571
|
...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
|
|
572
|
+
...conflicts,
|
|
463
573
|
...notes.map((n) => chunk(n.title, `${n.unresolved === true ? `_The fixer left this unresolved._\n\n${unboxed(n.body)}` : n.unclear === true ? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}` : unboxed(n.body) || "_No detail given._"}\n\n${answerBoxes}`)),
|
|
464
574
|
...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
|
|
575
|
+
...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
|
|
465
576
|
...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
|
|
466
577
|
];
|
|
467
578
|
const body = chunks.length > 0 ? chunks : [chunk("Nothing to note", "No quality lens raised anything for you — or none ran. Review the diff yourself.")];
|
|
@@ -1765,6 +1876,7 @@ const ShippedOptions = Struct({
|
|
|
1765
1876
|
reviewNoteActionable: setting,
|
|
1766
1877
|
reviewSeverityMinP: setting,
|
|
1767
1878
|
qualityTriageMinP: setting,
|
|
1879
|
+
standardMinP: setting,
|
|
1768
1880
|
refactorFlagPattern: setting,
|
|
1769
1881
|
seamAllow: setting,
|
|
1770
1882
|
sliceMaxCriteria: setting,
|
|
@@ -1874,4 +1986,4 @@ const steering = {
|
|
|
1874
1986
|
[REVIEW]: "review"
|
|
1875
1987
|
};
|
|
1876
1988
|
//#endregion
|
|
1877
|
-
export { naiveApplyFailure as $, reviewMissing as A,
|
|
1989
|
+
export { naiveApplyFailure as $, reviewMissing as A, conflictTitle as At, coverageMessage as B, requireFlag as Bt, coder as C, unwindFailure as Ct, fixQuality as D, STANDARDS as Dt, fixNits as E, testFileGlobs as Et, buildReviewTriageMessage as F, standardKey as Ft, designSystem as G, coveringPrompt as H, builderSystem as I, flagMentions as It, fixReviewNotesPrompt as J, designTriagePrompt as K, clearCriteriaMessage as L, flagReferences as Lt, reviewing as M, oneLine as Mt, agentWithSkills as N, parseStandards as Nt, fixRisks as O, conflictBody as Ot, buildReviewCollectingPrompt as P, renderStandards as Pt, implementFixPrompt as Q, clearCriteriaQuestion as R, flipProblems as Rt, awaitReview as S, unwindFailedMessage as St, fixCheck as T, envDefaults as Tt, criterionLine as U, coverageQuestion as V, designGateAnswerMessage as W, idleMessage as X, handOffAwaitReviewMessage as Y, implementBuildingPrompt as Z, REFACTOR_NOTE as _, seamByHandMessage as _t, flagModeOn as a, qualityUnresolvedMessage as at, SLICES as b, startGateBlockedMessage as bt, baseline as c, refactorFixPrompt as ct, guarded as d, reviewEditNotesCapture as dt, naivePassPrompt as et, healthy as f, reviewEditsCapture as ft, REFACTOR as g, reviewerSystem as gt, QUALITY as h, reviewNotesCapture as ht, skills as i, qualityTriageMessage as it, reviewQuality as j, isConflictTitle as jt, planner as k, conflictFor as kt, escalation as l, refactorNotePrompt as lt, HANDOFF as m, reviewGateBlockedMessage as mt, summary as n, oversizedMessage as nt, flagShipped as o, refactorBuildingPrompt as ot, FEEDBACK as p, reviewFeedbackFixPrompt as pt, finisherSystem as q, access as r, qualityFindingsFile as rt, shipped as s, refactorDroppedRefusal as st, steering as t, normaliseCriteriaScript as tt, gate as u, refactorRules as ut, REQUIREMENTS as v, seamSteerMessage as vt, collecting as w, defaults as wt, answerReviewQuestions as x, uncoveredMessage as xt, REVIEW as y, slicingPrompt as yt, composedReview as z, registryEntry as zt };
|
|
@@ -1,7 +1,159 @@
|
|
|
1
|
-
import { answered, changesSince, head, judge, numeric, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
|
|
1
|
+
import { answered, changesSince, head, judge, numeric, read, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
|
|
2
2
|
import { removedAssertions, review } from "../workflow-kit.mjs";
|
|
3
|
-
import { A as reviewMissing, E as fixNits, F as buildReviewTriageMessage, O as fixRisks, S as awaitReview, T as fixCheck, d as guarded, dt as reviewEditNotesCapture, f as healthy, ft as reviewEditsCapture, h as QUALITY, ht as reviewNotesCapture, w as collecting, x as answerReviewQuestions, y as REVIEW, z as composedReview } from "./reserved-
|
|
4
|
-
import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-
|
|
3
|
+
import { A as reviewMissing, At as conflictTitle, Dt as STANDARDS, E as fixNits, F as buildReviewTriageMessage, Ft as standardKey, Mt as oneLine, Nt as parseStandards, O as fixRisks, Ot as conflictBody, Pt as renderStandards, S as awaitReview, T as fixCheck, d as guarded, dt as reviewEditNotesCapture, f as healthy, ft as reviewEditsCapture, h as QUALITY, ht as reviewNotesCapture, jt as isConflictTitle, kt as conflictFor, w as collecting, x as answerReviewQuestions, y as REVIEW, z as composedReview } from "./reserved-ty1mtlnS.mjs";
|
|
4
|
+
import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-CM3sVoAb.mjs";
|
|
5
|
+
//#region src/workflows/shared/learn.ts
|
|
6
|
+
const SUMMARY_MAX = 200;
|
|
7
|
+
const BANNER = /^_(?:The fixer left this unresolved\.|A fix turn removed this while renaming another finding — check whether it was fixed\.|No detail given\.)_\s*/;
|
|
8
|
+
const summary = (body) => {
|
|
9
|
+
const text = oneLine(body).replace(BANNER, "");
|
|
10
|
+
return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1)}…` : text;
|
|
11
|
+
};
|
|
12
|
+
/** The standard a won't-fix or an own answer would make; a fix states none. */
|
|
13
|
+
const candidateOf = (d) => {
|
|
14
|
+
const anchor = oneLine(d.anchor);
|
|
15
|
+
if (d.choice === "wontfix") {
|
|
16
|
+
const why = summary(d.body);
|
|
17
|
+
return [{
|
|
18
|
+
decision: d,
|
|
19
|
+
text: `Won't fix: ${anchor}${why === "" ? "" : ` — ${why}`}`
|
|
20
|
+
}];
|
|
21
|
+
}
|
|
22
|
+
if (d.choice === "answer") return [{
|
|
23
|
+
decision: d,
|
|
24
|
+
text: `${oneLine(d.text)} (re: ${anchor})`
|
|
25
|
+
}];
|
|
26
|
+
return [];
|
|
27
|
+
};
|
|
28
|
+
const without = (list, drop) => {
|
|
29
|
+
const keys = new Set(drop.map(standardKey));
|
|
30
|
+
return list.filter((s) => !keys.has(standardKey(s)));
|
|
31
|
+
};
|
|
32
|
+
const adding = (list, text) => list.some((s) => standardKey(s) === standardKey(text)) ? [...list] : [...list, text];
|
|
33
|
+
/**
|
|
34
|
+
* Keep the new one, keep the old one, or replace both with the human's own
|
|
35
|
+
* wording. Another open conflict stops naming a replaced standard: the human
|
|
36
|
+
* never judged it against the replacement, so keeping it must not drop that.
|
|
37
|
+
*/
|
|
38
|
+
const resolve = (standards, d) => {
|
|
39
|
+
const c = conflictFor(standards, d.anchor);
|
|
40
|
+
if (c === void 0) return standards;
|
|
41
|
+
const others = standards.undecided.filter((u) => u !== c);
|
|
42
|
+
if (d.choice === "wontfix") return {
|
|
43
|
+
decided: standards.decided,
|
|
44
|
+
undecided: others
|
|
45
|
+
};
|
|
46
|
+
const kept = d.choice === "fix" ? c.text : oneLine(d.text);
|
|
47
|
+
const undecided = others.map((u) => ({
|
|
48
|
+
text: u.text,
|
|
49
|
+
olds: without(u.olds, c.olds)
|
|
50
|
+
}));
|
|
51
|
+
return {
|
|
52
|
+
decided: adding(without(standards.decided, c.olds), kept),
|
|
53
|
+
undecided
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const contradicts = (i, id, other) => ({
|
|
57
|
+
id: `standard-${i}.contradicts-${id}`,
|
|
58
|
+
primitive: "noul",
|
|
59
|
+
instructions: `Does the "Standard:" line of evidence "standard-${i}" contradict ${other}?`,
|
|
60
|
+
criteria: "yes: following one breaks the other. no: both can be followed — they are compatible or unrelated."
|
|
61
|
+
});
|
|
62
|
+
const questionsFor = (i, anchor, decided) => [
|
|
63
|
+
{
|
|
64
|
+
id: `standard-${i}`,
|
|
65
|
+
primitive: "noul",
|
|
66
|
+
instructions: `Is the human's answer to review note "${anchor}" (evidence "standard-${i}") a reusable standard for this codebase — a rule later changes and reviews should follow — rather than a call about this one note? Its "Standard:" line is the wording gtd would keep.`,
|
|
67
|
+
criteria: "yes: it states a rule or preference that applies beyond this note. no: it is specific to this note, a one-off call, or unclear."
|
|
68
|
+
},
|
|
69
|
+
...decided.map((_, k) => contradicts(i, `${k + 1}`, `kept standard ${k + 1} (evidence "kept-${k + 1}")`)),
|
|
70
|
+
...Array.from({ length: i - 1 }, (_, j) => contradicts(i, `new-${j + 1}`, `the "Standard:" line of evidence "standard-${j + 1}"`))
|
|
71
|
+
];
|
|
72
|
+
const evidenceOf = (c) => [
|
|
73
|
+
`Note: ${c.decision.anchor}`,
|
|
74
|
+
`Reviewer's text: ${c.decision.body}`,
|
|
75
|
+
`Human's answer: ${c.decision.choice === "wontfix" ? "won't fix" : c.decision.text}`,
|
|
76
|
+
`Standard: ${c.text}`
|
|
77
|
+
].join("\n");
|
|
78
|
+
/**
|
|
79
|
+
* One judge rest over every candidate. Only a confident `yes` on uncut
|
|
80
|
+
* evidence makes a standard; a contradiction the judge does not confidently
|
|
81
|
+
* rule out goes to the human instead of overwriting what they decided before.
|
|
82
|
+
* A cut kept standard leaves the round unlearned: raise `judgeBudgetBytes`.
|
|
83
|
+
*/
|
|
84
|
+
const judged = async (candidates, decided, minP) => {
|
|
85
|
+
const { answers, truncated } = await judge("review.standards.judge", {
|
|
86
|
+
questions: candidates.flatMap((c, i) => questionsFor(i + 1, c.decision.anchor, decided)),
|
|
87
|
+
evidence: Object.fromEntries([...candidates.map((c, i) => [`standard-${i + 1}`, evidenceOf(c)]), ...decided.map((s, k) => [`kept-${k + 1}`, s])]),
|
|
88
|
+
message: "Judging whether your answers are standards to keep: run `gtd judge run`, or land without a verdict, which keeps none.",
|
|
89
|
+
label: "Learning your standards"
|
|
90
|
+
});
|
|
91
|
+
const ruledOut = (i, id) => answered(answers[`standard-${i}.contradicts-${id}`], "no", minP);
|
|
92
|
+
const added = [];
|
|
93
|
+
const raised = [];
|
|
94
|
+
candidates.forEach((c, n) => {
|
|
95
|
+
const i = n + 1;
|
|
96
|
+
if (truncated.includes(`standard-${i}`)) return;
|
|
97
|
+
if (decided.some((_, k) => truncated.includes(`kept-${k + 1}`))) return;
|
|
98
|
+
if (!answered(answers[`standard-${i}`], "yes", minP)) return;
|
|
99
|
+
const olds = [...decided.filter((_, k) => !ruledOut(i, `${k + 1}`)), ...added.filter((a) => !ruledOut(i, `new-${a.i}`)).map((a) => a.text)];
|
|
100
|
+
if (olds.length === 0) added.push({
|
|
101
|
+
i,
|
|
102
|
+
text: c.text
|
|
103
|
+
});
|
|
104
|
+
else raised.push({
|
|
105
|
+
text: c.text,
|
|
106
|
+
olds
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
added: added.map((a) => a.text),
|
|
111
|
+
raised
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Settles every answer to a standard conflict, then learns standards from
|
|
116
|
+
* the won't-fix ticks and own answers: kept in `.gtd/STANDARDS.md`, a
|
|
117
|
+
* contradicting one as a new conflict note in `.gtd/REVIEW.md`.
|
|
118
|
+
*/
|
|
119
|
+
const learnStandards = async (decisions) => {
|
|
120
|
+
const before = parseStandards(read(".gtd/STANDARDS.md") ?? "");
|
|
121
|
+
const rest = decisions.filter((d) => !isConflictTitle(d.anchor));
|
|
122
|
+
const settled = decisions.filter((d) => isConflictTitle(d.anchor)).reduce(resolve, before);
|
|
123
|
+
const known = new Set([...settled.decided, ...settled.undecided.map((c) => c.text)].map(standardKey));
|
|
124
|
+
const candidates = rest.flatMap(candidateOf).filter((c) => {
|
|
125
|
+
const key = standardKey(c.text);
|
|
126
|
+
if (known.has(key)) return false;
|
|
127
|
+
known.add(key);
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
const minP = numeric(vars.standardMinP, Infinity);
|
|
131
|
+
const { added, raised } = candidates.length === 0 || !Number.isFinite(minP) ? {
|
|
132
|
+
added: [],
|
|
133
|
+
raised: []
|
|
134
|
+
} : await judged(candidates, settled.decided, minP);
|
|
135
|
+
const after = {
|
|
136
|
+
decided: [...settled.decided, ...added],
|
|
137
|
+
undecided: [...settled.undecided, ...raised]
|
|
138
|
+
};
|
|
139
|
+
if (renderStandards(after) !== renderStandards(before)) await run("review.standards.recording", writeScript(STANDARDS, renderStandards(after)), { label: "Recording your standards" });
|
|
140
|
+
if (raised.length > 0) {
|
|
141
|
+
const chunks = raised.map((c) => `## ${conflictTitle(c)}\n\n${conflictBody(c)}`);
|
|
142
|
+
await run("review.standards.raising", writeScript(REVIEW, `${(read(".gtd/REVIEW.md") ?? "").trimEnd()}\n\n${chunks.join("\n\n")}\n`), {
|
|
143
|
+
label: "Asking about a contradicting standard",
|
|
144
|
+
file: REVIEW
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const settlesStandard = (d) => isConflictTitle(d.anchor);
|
|
149
|
+
const standardsPending = () => parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided.length > 0;
|
|
150
|
+
const standardsAddedSince = (base) => {
|
|
151
|
+
const change = changesSince(base).get(STANDARDS);
|
|
152
|
+
if (change === void 0) return [];
|
|
153
|
+
const was = new Set(parseStandards(change.before ?? "").decided);
|
|
154
|
+
return parseStandards(change.after ?? "").decided.filter((s) => !was.has(s));
|
|
155
|
+
};
|
|
156
|
+
//#endregion
|
|
5
157
|
//#region src/workflows/shared/review.ts
|
|
6
158
|
const VERDICTS = [
|
|
7
159
|
"edit",
|
|
@@ -132,7 +284,11 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
|
|
|
132
284
|
...report,
|
|
133
285
|
weakened: removedAssertions(changesSince(base)),
|
|
134
286
|
nitFix,
|
|
135
|
-
answered
|
|
287
|
+
answered,
|
|
288
|
+
standards: {
|
|
289
|
+
added: standardsAddedSince(base),
|
|
290
|
+
undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
|
|
291
|
+
}
|
|
136
292
|
}), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
|
|
137
293
|
label: "Composing the review",
|
|
138
294
|
file: REVIEW,
|
|
@@ -177,6 +333,9 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
177
333
|
collecting: feedback.collecting,
|
|
178
334
|
triage: triage(severities),
|
|
179
335
|
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
336
|
+
settles: settlesStandard,
|
|
337
|
+
learn: learnStandards,
|
|
338
|
+
pending: standardsPending,
|
|
180
339
|
answerQuestions: answerReviewQuestions,
|
|
181
340
|
fixNits: async (notes) => {
|
|
182
341
|
await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { answered, head, judge, mark, numeric, read, refuse, removeScript, run, scope, sectionBodies, showScript, vars, writeScript } from "../flows.mjs";
|
|
2
2
|
import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import { A as reviewMissing, D as fixQuality, M as reviewing, O as fixRisks, S as awaitReview, T as fixCheck, Y as handOffAwaitReviewMessage, d as guarded, f as healthy, h as QUALITY, it as qualityTriageMessage, j as reviewQuality, l as escalation, rt as qualityFindingsFile, y as REVIEW } from "./reserved-
|
|
3
|
+
import { A as reviewMissing, D as fixQuality, M as reviewing, O as fixRisks, S as awaitReview, T as fixCheck, Y as handOffAwaitReviewMessage, d as guarded, f as healthy, h as QUALITY, it as qualityTriageMessage, j as reviewQuality, l as escalation, rt as qualityFindingsFile, y as REVIEW } from "./reserved-ty1mtlnS.mjs";
|
|
4
4
|
//#region src/workflows/shared/lenses.ts
|
|
5
5
|
const NO_FINDINGS = {
|
|
6
6
|
notes: [],
|
package/dist/workflow-kit.mjs
CHANGED
|
@@ -233,14 +233,20 @@ const record = (p, decisions) => {
|
|
|
233
233
|
p.decided?.(decisions);
|
|
234
234
|
for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine(d.anchor));
|
|
235
235
|
};
|
|
236
|
+
const routed = (p, answers) => answers.filter((d) => p.settles?.(d) !== true);
|
|
237
|
+
/** Record the routed answers on the gate's commit, then hand every answer to `learn`. */
|
|
238
|
+
const recordAndLearn = async (p, decisions, answers) => {
|
|
239
|
+
record(p, decisions);
|
|
240
|
+
if (answers.length > 0 && p.learn !== void 0) await p.learn(answers);
|
|
241
|
+
};
|
|
236
242
|
/**
|
|
237
243
|
* Judge every note, then run each verdict's route: answers, then nit fixes,
|
|
238
244
|
* then the edits' lap. Of the human's `decisions`, a fix joins the nit batch
|
|
239
245
|
* unjudged, an own answer is judged like a note, and a won't-fix is dropped
|
|
240
246
|
* with a `Gtd-Mark` on the gate's commit.
|
|
241
247
|
*/
|
|
242
|
-
const routeNotes = async (p, notes, r, decisions = []) => {
|
|
243
|
-
|
|
248
|
+
const routeNotes = async (p, notes, r, decisions = [], answers = decisions) => {
|
|
249
|
+
await recordAndLearn(p, decisions, answers);
|
|
244
250
|
const of = (c) => decisions.filter((d) => d.choice === c);
|
|
245
251
|
const fixes = of("fix").map((d, i) => decisionNote(d, `fix-${i + 1}`, d.body));
|
|
246
252
|
const judged = [...notes, ...of("answer").map((d, i) => decisionNote(d, `answer-${i + 1}`, d.text))];
|
|
@@ -294,9 +300,8 @@ const settledAfter = (p, collectedAt) => ({
|
|
|
294
300
|
folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
|
|
295
301
|
settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
|
|
296
302
|
});
|
|
297
|
-
const finishNotes = async (p,
|
|
298
|
-
|
|
299
|
-
if (notes.length + decisions.length > 0) return routeNotes(p, notes, ctx, decisions);
|
|
303
|
+
const finishNotes = async (p, notes, answers, reopened, ctx) => {
|
|
304
|
+
if (notes.length + answers.length > 0) return routeNotes(p, notes, ctx, routed(p, answers), answers);
|
|
300
305
|
if (reopened) return ctx.unfolded();
|
|
301
306
|
await ctx.close();
|
|
302
307
|
return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"));
|
|
@@ -311,6 +316,7 @@ const decisionsSince = (p, since) => {
|
|
|
311
316
|
const after = read(p.file) ?? "";
|
|
312
317
|
return reviewDecisions(since.get(p.file)?.before ?? after, after);
|
|
313
318
|
};
|
|
319
|
+
const notesSince = (p, since) => reviewNotes(since.get(p.file)?.before ?? "", read(p.file) ?? "");
|
|
314
320
|
const finish = async (p, reviewed, collectedAt) => {
|
|
315
321
|
const round = head();
|
|
316
322
|
const since = changesSince(reviewed);
|
|
@@ -331,23 +337,29 @@ const finish = async (p, reviewed, collectedAt) => {
|
|
|
331
337
|
settled,
|
|
332
338
|
folded
|
|
333
339
|
});
|
|
334
|
-
const
|
|
335
|
-
if (settled && !reopened &&
|
|
340
|
+
const answers = decisionsSince(p, since);
|
|
341
|
+
if (settled && !reopened && answers.length === 0 || edited.length === 0 && !noted) return unfolded();
|
|
342
|
+
const notes = notesSince(p, since);
|
|
336
343
|
if (edited.length > 0) {
|
|
337
|
-
|
|
344
|
+
await recordAndLearn(p, routed(p, answers), answers);
|
|
338
345
|
await close();
|
|
339
346
|
return outcome(await collect(p, p.captures.edits(round, reopened ? reviewed : void 0), "edits", {
|
|
340
347
|
notes: [],
|
|
341
348
|
paths: edited.map((c) => c.path)
|
|
342
349
|
}));
|
|
343
350
|
}
|
|
344
|
-
return finishNotes(p,
|
|
351
|
+
return finishNotes(p, notes, answers, reopened, {
|
|
345
352
|
round,
|
|
346
353
|
close,
|
|
347
354
|
outcome,
|
|
348
355
|
unfolded
|
|
349
356
|
});
|
|
350
357
|
};
|
|
358
|
+
/** A sign-off while something waits on the human writes the review again instead. */
|
|
359
|
+
const unlessPending = (p, result) => "verdict" in result && result.verdict === "signoff" && p.pending?.() === true ? {
|
|
360
|
+
next: "rereview",
|
|
361
|
+
carry: void 0
|
|
362
|
+
} : result;
|
|
351
363
|
/** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
|
|
352
364
|
const reviewOnce = async (p, base, carry) => {
|
|
353
365
|
await p.reviewing(base, carry);
|
|
@@ -373,7 +385,7 @@ const review = async (base, p) => {
|
|
|
373
385
|
for (;;) {
|
|
374
386
|
collectedAt = await converse(p, base, reviewed, collectedAt);
|
|
375
387
|
if (collectedAt === "missing") break;
|
|
376
|
-
const result = await finish(p, reviewed, collectedAt);
|
|
388
|
+
const result = unlessPending(p, await finish(p, reviewed, collectedAt));
|
|
377
389
|
if (!("next" in result)) return result;
|
|
378
390
|
if (result.next === "rereview") {
|
|
379
391
|
carry = result.carry;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { Bt as requireFlag, C as coder, Lt as flagReferences, Tt as envDefaults, i as skills, n as summary, o as flagShipped, r as access, t as steering, wt as defaults, zt as registryEntry } from "../chunks/reserved-ty1mtlnS.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CM3sVoAb.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 naiveApplyFailure, B as coverageMessage, C as coder, Ct as unwindFailure, E as fixNits, Et as testFileGlobs, G as designSystem, H as coveringPrompt, I as builderSystem, K as designTriagePrompt, L as clearCriteriaMessage, N as agentWithSkills, P as buildReviewCollectingPrompt, Q as implementFixPrompt, R as clearCriteriaQuestion, St as unwindFailedMessage, T as fixCheck, Tt as envDefaults, U as criterionLine, V as coverageQuestion, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REFACTOR_NOTE, _t as seamByHandMessage, a as flagModeOn, at as qualityUnresolvedMessage, b as SLICES, bt as startGateBlockedMessage, ct as refactorFixPrompt, et as naivePassPrompt, f as healthy, g as REFACTOR, gt as reviewerSystem, h as QUALITY, i as skills, k as planner, lt as refactorNotePrompt, m as HANDOFF, n as summary, nt as oversizedMessage, ot as refactorBuildingPrompt, p as FEEDBACK, r as access, s as shipped, st as refactorDroppedRefusal, t as steering, tt as normaliseCriteriaScript, u as gate, ut as refactorRules, v as REQUIREMENTS, vt as seamSteerMessage, w as collecting, wt as defaults, x as answerReviewQuestions, xt as uncoveredMessage, yt as slicingPrompt } from "../chunks/reserved-
|
|
4
|
-
import { a as refusePendingHandOff, 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 naiveApplyFailure, B as coverageMessage, C as coder, Ct as unwindFailure, E as fixNits, Et as testFileGlobs, G as designSystem, H as coveringPrompt, I as builderSystem, K as designTriagePrompt, L as clearCriteriaMessage, N as agentWithSkills, P as buildReviewCollectingPrompt, Q as implementFixPrompt, R as clearCriteriaQuestion, St as unwindFailedMessage, T as fixCheck, Tt as envDefaults, U as criterionLine, V as coverageQuestion, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REFACTOR_NOTE, _t as seamByHandMessage, a as flagModeOn, at as qualityUnresolvedMessage, b as SLICES, bt as startGateBlockedMessage, ct as refactorFixPrompt, et as naivePassPrompt, f as healthy, g as REFACTOR, gt as reviewerSystem, h as QUALITY, i as skills, k as planner, lt as refactorNotePrompt, m as HANDOFF, n as summary, nt as oversizedMessage, ot as refactorBuildingPrompt, p as FEEDBACK, r as access, s as shipped, st as refactorDroppedRefusal, t as steering, tt as normaliseCriteriaScript, u as gate, ut as refactorRules, v as REQUIREMENTS, vt as seamSteerMessage, w as collecting, wt as defaults, x as answerReviewQuestions, xt as uncoveredMessage, yt as slicingPrompt } from "../chunks/reserved-ty1mtlnS.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, o as tail } from "../chunks/tail-CM3sVoAb.mjs";
|
|
5
|
+
import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-BmoH-9im.mjs";
|
|
6
6
|
import { posix } from "node:path";
|
|
7
7
|
//#region src/workflows/shared/fences.ts
|
|
8
8
|
const markFenced = (text) => {
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { g as reviewRisks, h as reviewNotes } from "../chunks/steering-D4LUQ-av.mjs";
|
|
3
|
-
import { C as coder, J as fixReviewNotesPrompt, N as agentWithSkills, Tt as envDefaults, c as baseline, i as skills, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, wt as defaults, y as REVIEW } from "../chunks/reserved-
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { C as coder, J as fixReviewNotesPrompt, N as agentWithSkills, Tt as envDefaults, c as baseline, i as skills, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, wt as defaults, y as REVIEW } from "../chunks/reserved-ty1mtlnS.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-CM3sVoAb.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, N as agentWithSkills, Tt as envDefaults, i as skills, mt as reviewGateBlockedMessage, n as summary, pt as reviewFeedbackFixPrompt, q as finisherSystem, r as access, s as shipped, t as steering, u as gate, v as REQUIREMENTS, wt as defaults } from "../chunks/reserved-
|
|
3
|
-
import { a as refusePendingHandOff } from "../chunks/tail-
|
|
4
|
-
import { i as reUnwind, t as buildTail } from "../chunks/review-
|
|
2
|
+
import { C as coder, N as agentWithSkills, Tt as envDefaults, i as skills, mt as reviewGateBlockedMessage, n as summary, pt as reviewFeedbackFixPrompt, q as finisherSystem, r as access, s as shipped, t as steering, u as gate, v as REQUIREMENTS, wt as defaults } from "../chunks/reserved-ty1mtlnS.mjs";
|
|
3
|
+
import { a as refusePendingHandOff } from "../chunks/tail-CM3sVoAb.mjs";
|
|
4
|
+
import { i as reUnwind, t as buildTail } from "../chunks/review-BmoH-9im.mjs";
|
|
5
5
|
//#region src/workflows/review/review.ts
|
|
6
6
|
/** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
|
|
7
7
|
const fixFeedback = async () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { Bt as requireFlag, C as coder, It as flagMentions, Rt as flipProblems, Tt as envDefaults, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as defaults } from "../chunks/reserved-ty1mtlnS.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.";
|