@pmelab/gtd 44.0.0 → 45.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 +40 -9
- package/dist/chunks/{reserved-DJk_6-ju.mjs → reserved-ulcSkND6.mjs} +142 -5
- package/dist/chunks/{review-X7Jfz0dY.mjs → review-Da2ndnB1.mjs} +177 -7
- package/dist/chunks/{tail-5w5mxGTq.mjs → tail-BW030xz_.mjs} +1 -1
- package/dist/gtd.bundle.mjs +7 -5
- package/dist/testkit.mjs +7 -5
- 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 +4 -15
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows/tests.mjs +193 -0
- package/dist/workflows.mjs +533 -40
- package/package.json +6 -1
- package/src/install/ConfigScaffold.ts +2 -1
- package/src/install/Install.ts +5 -4
- 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/index.ts +2 -1
- package/src/workflows/review/review-door.feature +11 -2
- package/src/workflows/review/review.ts +3 -16
- package/src/workflows/shared/learn.test.ts +154 -0
- package/src/workflows/shared/learn.ts +206 -0
- package/src/workflows/shared/lenses.ts +5 -1
- package/src/workflows/shared/options.ts +22 -1
- package/src/workflows/shared/review.ts +29 -2
- package/src/workflows/shared/skills.test.ts +2 -0
- package/src/workflows/shared/skills.ts +3 -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 +33 -3
- package/src/workflows/shared/vars.ts +10 -0
- package/src/workflows/tests/index.ts +6 -0
- package/src/workflows/tests/tests-options.feature +52 -0
- package/src/workflows/tests/tests.feature +266 -0
- package/src/workflows/tests/tests.test.ts +51 -0
- package/src/workflows/tests/tests.ts +270 -0
package/dist/workflows.mjs
CHANGED
|
@@ -16773,11 +16773,17 @@ const decisionNote = (d, id, text) => ({
|
|
|
16773
16773
|
forced: /^!(\s|$)/.test(text)
|
|
16774
16774
|
});
|
|
16775
16775
|
/** One `Gtd-Mark` value per line: a control character would make mark() throw on every replay. */
|
|
16776
|
-
const oneLine = (text) => text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim() || "untitled";
|
|
16776
|
+
const oneLine$1 = (text) => text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim() || "untitled";
|
|
16777
16777
|
/** Tell the policy, and mark each won't-fix on the gate's commit. */
|
|
16778
16778
|
const record$2 = (p, decisions) => {
|
|
16779
16779
|
p.decided?.(decisions);
|
|
16780
|
-
for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine(d.anchor));
|
|
16780
|
+
for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine$1(d.anchor));
|
|
16781
|
+
};
|
|
16782
|
+
const routed = (p, answers) => answers.filter((d) => p.settles?.(d) !== true);
|
|
16783
|
+
/** Record the routed answers on the gate's commit, then hand every answer to `learn`. */
|
|
16784
|
+
const recordAndLearn = async (p, decisions, answers) => {
|
|
16785
|
+
record$2(p, decisions);
|
|
16786
|
+
if (answers.length > 0 && p.learn !== void 0) await p.learn(answers);
|
|
16781
16787
|
};
|
|
16782
16788
|
/**
|
|
16783
16789
|
* Judge every note, then run each verdict's route: answers, then nit fixes,
|
|
@@ -16785,8 +16791,8 @@ const record$2 = (p, decisions) => {
|
|
|
16785
16791
|
* unjudged, an own answer is judged like a note, and a won't-fix is dropped
|
|
16786
16792
|
* with a `Gtd-Mark` on the gate's commit.
|
|
16787
16793
|
*/
|
|
16788
|
-
const routeNotes = async (p, notes, r, decisions = []) => {
|
|
16789
|
-
|
|
16794
|
+
const routeNotes = async (p, notes, r, decisions = [], answers = decisions) => {
|
|
16795
|
+
await recordAndLearn(p, decisions, answers);
|
|
16790
16796
|
const of = (c) => decisions.filter((d) => d.choice === c);
|
|
16791
16797
|
const fixes = of("fix").map((d, i) => decisionNote(d, `fix-${i + 1}`, d.body));
|
|
16792
16798
|
const judged = [...notes, ...of("answer").map((d, i) => decisionNote(d, `answer-${i + 1}`, d.text))];
|
|
@@ -16840,9 +16846,8 @@ const settledAfter = (p, collectedAt) => ({
|
|
|
16840
16846
|
folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
|
|
16841
16847
|
settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
|
|
16842
16848
|
});
|
|
16843
|
-
const finishNotes = async (p,
|
|
16844
|
-
|
|
16845
|
-
if (notes.length + decisions.length > 0) return routeNotes(p, notes, ctx, decisions);
|
|
16849
|
+
const finishNotes = async (p, notes, answers, reopened, ctx) => {
|
|
16850
|
+
if (notes.length + answers.length > 0) return routeNotes(p, notes, ctx, routed(p, answers), answers);
|
|
16846
16851
|
if (reopened) return ctx.unfolded();
|
|
16847
16852
|
await ctx.close();
|
|
16848
16853
|
return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"));
|
|
@@ -16857,6 +16862,7 @@ const decisionsSince = (p, since) => {
|
|
|
16857
16862
|
const after = read(p.file) ?? "";
|
|
16858
16863
|
return reviewDecisions(since.get(p.file)?.before ?? after, after);
|
|
16859
16864
|
};
|
|
16865
|
+
const notesSince = (p, since) => reviewNotes(since.get(p.file)?.before ?? "", read(p.file) ?? "");
|
|
16860
16866
|
const finish = async (p, reviewed, collectedAt) => {
|
|
16861
16867
|
const round = head$1();
|
|
16862
16868
|
const since = changesSince(reviewed);
|
|
@@ -16877,23 +16883,29 @@ const finish = async (p, reviewed, collectedAt) => {
|
|
|
16877
16883
|
settled,
|
|
16878
16884
|
folded
|
|
16879
16885
|
});
|
|
16880
|
-
const
|
|
16881
|
-
if (settled && !reopened &&
|
|
16886
|
+
const answers = decisionsSince(p, since);
|
|
16887
|
+
if (settled && !reopened && answers.length === 0 || edited.length === 0 && !noted) return unfolded();
|
|
16888
|
+
const notes = notesSince(p, since);
|
|
16882
16889
|
if (edited.length > 0) {
|
|
16883
|
-
|
|
16890
|
+
await recordAndLearn(p, routed(p, answers), answers);
|
|
16884
16891
|
await close();
|
|
16885
16892
|
return outcome(await collect(p, p.captures.edits(round, reopened ? reviewed : void 0), "edits", {
|
|
16886
16893
|
notes: [],
|
|
16887
16894
|
paths: edited.map((c) => c.path)
|
|
16888
16895
|
}));
|
|
16889
16896
|
}
|
|
16890
|
-
return finishNotes(p,
|
|
16897
|
+
return finishNotes(p, notes, answers, reopened, {
|
|
16891
16898
|
round,
|
|
16892
16899
|
close,
|
|
16893
16900
|
outcome,
|
|
16894
16901
|
unfolded
|
|
16895
16902
|
});
|
|
16896
16903
|
};
|
|
16904
|
+
/** A sign-off while something waits on the human writes the review again instead. */
|
|
16905
|
+
const unlessPending = (p, result) => "verdict" in result && result.verdict === "signoff" && p.pending?.() === true ? {
|
|
16906
|
+
next: "rereview",
|
|
16907
|
+
carry: void 0
|
|
16908
|
+
} : result;
|
|
16897
16909
|
/** 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. */
|
|
16898
16910
|
const reviewOnce = async (p, base, carry) => {
|
|
16899
16911
|
await p.reviewing(base, carry);
|
|
@@ -16919,7 +16931,7 @@ const review$1 = async (base, p) => {
|
|
|
16919
16931
|
for (;;) {
|
|
16920
16932
|
collectedAt = await converse(p, base, reviewed, collectedAt);
|
|
16921
16933
|
if (collectedAt === "missing") break;
|
|
16922
|
-
const result = await finish(p, reviewed, collectedAt);
|
|
16934
|
+
const result = unlessPending(p, await finish(p, reviewed, collectedAt));
|
|
16923
16935
|
if (!("next" in result)) return result;
|
|
16924
16936
|
if (result.next === "rereview") {
|
|
16925
16937
|
carry = result.carry;
|
|
@@ -17072,6 +17084,108 @@ const requireFlag = (workflow, registryPath, name, defaultOn) => {
|
|
|
17072
17084
|
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`);
|
|
17073
17085
|
};
|
|
17074
17086
|
//#endregion
|
|
17087
|
+
//#region src/workflows/shared/standards.ts
|
|
17088
|
+
const STANDARDS = ".gtd/STANDARDS.md";
|
|
17089
|
+
const UNDECIDED = "Undecided";
|
|
17090
|
+
const oneLine = (text) => text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim();
|
|
17091
|
+
const ITEM$1 = /^(?:[-*+]|\d+[.)])\s+(.*)$/;
|
|
17092
|
+
const CONTRADICTS = /^\s+[-*+]\s+contradicts:\s*(.*)$/i;
|
|
17093
|
+
const wrapped = (item, line) => {
|
|
17094
|
+
const olds = item.olds;
|
|
17095
|
+
if (olds !== void 0 && olds.length > 0) olds[olds.length - 1] += ` ${line.trim()}`;
|
|
17096
|
+
else item.text += ` ${line.trim()}`;
|
|
17097
|
+
};
|
|
17098
|
+
/** What tells two standards apart: not the emphasis or spacing a formatter may rewrite. */
|
|
17099
|
+
const standardKey = (text) => oneLine(text.replace(/[*_`\\]/g, "")).toLowerCase();
|
|
17100
|
+
const toStandards = (items) => {
|
|
17101
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17102
|
+
const fresh = (text) => {
|
|
17103
|
+
const key = standardKey(text);
|
|
17104
|
+
if (key === "" || seen.has(key)) return false;
|
|
17105
|
+
seen.add(key);
|
|
17106
|
+
return true;
|
|
17107
|
+
};
|
|
17108
|
+
const decided = items.filter((i) => i.olds === void 0).map((i) => oneLine(i.text));
|
|
17109
|
+
const undecided = items.flatMap((i) => i.olds === void 0 ? [] : [{
|
|
17110
|
+
text: oneLine(i.text),
|
|
17111
|
+
olds: i.olds.map(oneLine).filter((o) => o !== "")
|
|
17112
|
+
}]);
|
|
17113
|
+
return {
|
|
17114
|
+
decided: decided.filter(fresh),
|
|
17115
|
+
undecided: undecided.filter((c) => fresh(c.text))
|
|
17116
|
+
};
|
|
17117
|
+
};
|
|
17118
|
+
/**
|
|
17119
|
+
* Every top-level bullet is one standard, those under `## Undecided` waiting
|
|
17120
|
+
* on the human; a hand-edited file may group the rest under any heading.
|
|
17121
|
+
*/
|
|
17122
|
+
const parseStandards = (text) => {
|
|
17123
|
+
const items = [];
|
|
17124
|
+
let pending = false;
|
|
17125
|
+
let last;
|
|
17126
|
+
for (const line of text.split("\n")) {
|
|
17127
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
17128
|
+
const item = ITEM$1.exec(line);
|
|
17129
|
+
const old = CONTRADICTS.exec(line);
|
|
17130
|
+
if (heading !== null) {
|
|
17131
|
+
pending = heading[1].length > 1 && heading[2].trim() === UNDECIDED;
|
|
17132
|
+
last = void 0;
|
|
17133
|
+
} else if (item !== null) {
|
|
17134
|
+
last = pending ? {
|
|
17135
|
+
text: item[1],
|
|
17136
|
+
olds: []
|
|
17137
|
+
} : { text: item[1] };
|
|
17138
|
+
items.push(last);
|
|
17139
|
+
} else if (old !== null && last?.olds !== void 0) last.olds.push(old[1]);
|
|
17140
|
+
else if (/^\s+\S/.test(line) && last !== void 0) wrapped(last, line);
|
|
17141
|
+
else if (line.trim() !== "") last = void 0;
|
|
17142
|
+
}
|
|
17143
|
+
return toStandards(items);
|
|
17144
|
+
};
|
|
17145
|
+
const renderStandards = ({ decided, undecided }) => {
|
|
17146
|
+
return `${[
|
|
17147
|
+
"# Standards",
|
|
17148
|
+
"Kept from your review answers for every agent turn. One standard per bullet.",
|
|
17149
|
+
...decided.length === 0 ? [] : [decided.map((s) => `- ${s}`).join("\n")],
|
|
17150
|
+
...undecided.length === 0 ? [] : [`## ${UNDECIDED}`, undecided.map((c) => [`- ${c.text}`, ...c.olds.map((o) => ` - contradicts: ${o}`)].join("\n")).join("\n")]
|
|
17151
|
+
].join("\n\n")}\n`;
|
|
17152
|
+
};
|
|
17153
|
+
const hash$1 = (raw) => {
|
|
17154
|
+
const text = standardKey(raw);
|
|
17155
|
+
let h = 2166136261;
|
|
17156
|
+
for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);
|
|
17157
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
17158
|
+
};
|
|
17159
|
+
const CONFLICT_TITLE = /^Standard conflict ([0-9a-f]{8})$/;
|
|
17160
|
+
const conflictTitle = (c) => `Standard conflict ${hash$1(c.text)}`;
|
|
17161
|
+
const conflictFor = (standards, title) => {
|
|
17162
|
+
const id = CONFLICT_TITLE.exec(title)?.[1];
|
|
17163
|
+
return id === void 0 ? void 0 : standards.undecided.find((c) => hash$1(c.text) === id);
|
|
17164
|
+
};
|
|
17165
|
+
const isConflictTitle = (title) => CONFLICT_TITLE.test(title);
|
|
17166
|
+
const KEEP_NEW = "keep the new one (drop the old)";
|
|
17167
|
+
const KEEP_OLD = "keep the old one (discard the new)";
|
|
17168
|
+
/** A conflict's chunk body; its three boxes read as fix / won't fix / own answer by position. */
|
|
17169
|
+
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}`] : [
|
|
17170
|
+
`A standard learned from your answers contradicts ${c.olds.length === 1 ? "one" : "some"} already in \`${STANDARDS}\`. Your own answer replaces both with its wording.`,
|
|
17171
|
+
`New: ${c.text}`,
|
|
17172
|
+
`Old:\n\n${c.olds.map((o) => `- ${o}`).join("\n")}`
|
|
17173
|
+
], `${ANSWER_LEAD}\n\n- [ ] ${KEEP_NEW}\n- [ ] ${KEEP_OLD}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`].join("\n\n");
|
|
17174
|
+
/** What every agent turn is told while `.gtd/STANDARDS.md` holds decided standards. */
|
|
17175
|
+
const standardsBlock = (text) => {
|
|
17176
|
+
const { decided } = parseStandards(text ?? "");
|
|
17177
|
+
if (decided.length === 0) return "";
|
|
17178
|
+
return `## Standards
|
|
17179
|
+
|
|
17180
|
+
\`${STANDARDS}\` holds the standards the human set in earlier reviews.
|
|
17181
|
+
Follow every one in what you write. Reviewing, never raise a finding a
|
|
17182
|
+
standard rules out — a \`Won't fix:\` standard is a finding the human
|
|
17183
|
+
rejected, so never raise it or one like it again — and raise breaking a
|
|
17184
|
+
standard as a finding.
|
|
17185
|
+
|
|
17186
|
+
${decided.map((s) => `- ${s}`).join("\n")}`;
|
|
17187
|
+
};
|
|
17188
|
+
//#endregion
|
|
17075
17189
|
//#region src/workflows/shared/vars.ts
|
|
17076
17190
|
/** The refactor guard's fixed list, and `seamAllow`'s default minus `.gtd/**`. */
|
|
17077
17191
|
const testFileGlobs = [
|
|
@@ -17083,6 +17197,11 @@ const testFileGlobs = [
|
|
|
17083
17197
|
"**/__tests__/**"
|
|
17084
17198
|
];
|
|
17085
17199
|
const defaultFlagRegistry = "src/flags/index.ts";
|
|
17200
|
+
/** The `tests` workflow's own process settings: where it looks for skipped tests, and how a skip reads. */
|
|
17201
|
+
const testsDefaults = {
|
|
17202
|
+
testFiles: testFileGlobs.join(", "),
|
|
17203
|
+
skipPattern: "^\\s*(?:(?:describe|context|suite|it|test|specify)\\.(?:skip|todo)\\b|x(?:describe|context|it|test|specify)\\s*\\(|(?:@\\S+\\s+)*@(?:skip|ignore)(?:\\s|$))"
|
|
17204
|
+
};
|
|
17086
17205
|
/** Process settings. */
|
|
17087
17206
|
const defaults = {
|
|
17088
17207
|
reviewBase: "",
|
|
@@ -17091,6 +17210,7 @@ const defaults = {
|
|
|
17091
17210
|
reviewNoteActionable: "0.7",
|
|
17092
17211
|
reviewSeverityMinP: "0.7",
|
|
17093
17212
|
qualityTriageMinP: "0.7",
|
|
17213
|
+
standardMinP: "0.8",
|
|
17094
17214
|
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
17095
17215
|
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
17096
17216
|
flagMode: "true",
|
|
@@ -17350,8 +17470,11 @@ const withSkills = (skills, prompt) => {
|
|
|
17350
17470
|
};
|
|
17351
17471
|
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills, then its restricted file access. */
|
|
17352
17472
|
const agentWithSkills = (name, prompt, options = {}) => {
|
|
17353
|
-
const
|
|
17354
|
-
|
|
17473
|
+
const body = [
|
|
17474
|
+
accessPreamble(accessFor(name, options.file)),
|
|
17475
|
+
prompt,
|
|
17476
|
+
standardsBlock(read(STANDARDS))
|
|
17477
|
+
].filter((part) => part !== "").join("\n\n");
|
|
17355
17478
|
return agent(name, withSkills(skillsFor(name).join(", "), body), options);
|
|
17356
17479
|
};
|
|
17357
17480
|
const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
|
|
@@ -17391,6 +17514,7 @@ const SUMMARY_TITLES = [
|
|
|
17391
17514
|
"Weakened scenarios",
|
|
17392
17515
|
"Fixed from your notes",
|
|
17393
17516
|
"Fixed automatically",
|
|
17517
|
+
"Standards added",
|
|
17394
17518
|
"Dropped as noise",
|
|
17395
17519
|
"Nothing to note"
|
|
17396
17520
|
];
|
|
@@ -17410,7 +17534,9 @@ const composedReview = (base, report) => {
|
|
|
17410
17534
|
return name;
|
|
17411
17535
|
};
|
|
17412
17536
|
const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
|
|
17413
|
-
const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${
|
|
17537
|
+
const answerBoxes = ([fix, wontFix] = [ANSWER_FIX, ANSWER_WONT_FIX]) => `${ANSWER_LEAD}\n\n- [ ] ${fix}\n- [ ] ${wontFix}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
|
|
17538
|
+
const conflicts = (report.standards?.undecided ?? []).map((c) => chunk(unique(conflictTitle(c)), conflictBody(c)));
|
|
17539
|
+
const added = report.standards?.added ?? [];
|
|
17414
17540
|
const notes = report.notes.map((n) => ({
|
|
17415
17541
|
...n,
|
|
17416
17542
|
title: unique(n.title)
|
|
@@ -17419,8 +17545,10 @@ const composedReview = (base, report) => {
|
|
|
17419
17545
|
const chunks = [
|
|
17420
17546
|
...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.`)],
|
|
17421
17547
|
...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
|
|
17422
|
-
...
|
|
17548
|
+
...conflicts,
|
|
17549
|
+
...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(n.answers)}`)),
|
|
17423
17550
|
...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
|
|
17551
|
+
...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
|
|
17424
17552
|
...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
|
|
17425
17553
|
];
|
|
17426
17554
|
const body = chunks.length > 0 ? chunks : [chunk("Nothing to note", "No quality lens raised anything for you — or none ran. Review the diff yourself.")];
|
|
@@ -25619,9 +25747,9 @@ const HashMapProto = {
|
|
|
25619
25747
|
return new HashMapIterator(this, (k, v) => [k, v]);
|
|
25620
25748
|
},
|
|
25621
25749
|
[symbol$1]() {
|
|
25622
|
-
let hash$
|
|
25623
|
-
for (const item of this) hash$
|
|
25624
|
-
return cached(this, hash$
|
|
25750
|
+
let hash$2 = hash(HashMapSymbolKey);
|
|
25751
|
+
for (const item of this) hash$2 ^= pipe(hash(item[0]), combine$5(hash(item[1])));
|
|
25752
|
+
return cached(this, hash$2);
|
|
25625
25753
|
},
|
|
25626
25754
|
[symbol](that) {
|
|
25627
25755
|
if (isHashMap(that)) {
|
|
@@ -31627,7 +31755,7 @@ const gauge$1 = (count) => new GaugeState(count);
|
|
|
31627
31755
|
/** @internal */
|
|
31628
31756
|
const histogram$2 = (options) => new HistogramState(options.buckets, options.count, options.min, options.max, options.sum);
|
|
31629
31757
|
/** @internal */
|
|
31630
|
-
const summary$
|
|
31758
|
+
const summary$3 = (options) => new SummaryState(options.error, options.quantiles, options.count, options.min, options.max, options.sum);
|
|
31631
31759
|
/** @internal */
|
|
31632
31760
|
const isCounterState = (u) => hasProperty(u, CounterStateTypeId);
|
|
31633
31761
|
/**
|
|
@@ -31760,7 +31888,7 @@ const histogram$1 = (key) => {
|
|
|
31760
31888
|
});
|
|
31761
31889
|
};
|
|
31762
31890
|
/** @internal */
|
|
31763
|
-
const summary$
|
|
31891
|
+
const summary$2 = (key) => {
|
|
31764
31892
|
const { error, maxAge, maxSize, quantiles } = key.keyType;
|
|
31765
31893
|
const sortedQuantiles = pipe(quantiles, sort(Order$1));
|
|
31766
31894
|
const values = allocate(maxSize);
|
|
@@ -31795,7 +31923,7 @@ const summary$1 = (key) => {
|
|
|
31795
31923
|
sum = sum + value;
|
|
31796
31924
|
};
|
|
31797
31925
|
return make$5({
|
|
31798
|
-
get: () => summary$
|
|
31926
|
+
get: () => summary$3({
|
|
31799
31927
|
error,
|
|
31800
31928
|
quantiles: snapshot(Date.now()),
|
|
31801
31929
|
count,
|
|
@@ -31995,7 +32123,7 @@ var MetricRegistryImpl = class {
|
|
|
31995
32123
|
getSummary(key) {
|
|
31996
32124
|
let value = pipe(this.map, get$1(key), getOrUndefined);
|
|
31997
32125
|
if (value == null) {
|
|
31998
|
-
const summary = summary$
|
|
32126
|
+
const summary = summary$2(key);
|
|
31999
32127
|
if (!pipe(this.map, has(key))) pipe(this.map, set(key, summary));
|
|
32000
32128
|
value = summary;
|
|
32001
32129
|
}
|
|
@@ -38042,6 +38170,7 @@ const ShippedOptions = Struct({
|
|
|
38042
38170
|
reviewNoteActionable: setting$1,
|
|
38043
38171
|
reviewSeverityMinP: setting$1,
|
|
38044
38172
|
qualityTriageMinP: setting$1,
|
|
38173
|
+
standardMinP: setting$1,
|
|
38045
38174
|
refactorFlagPattern: setting$1,
|
|
38046
38175
|
seamAllow: setting$1,
|
|
38047
38176
|
sliceMaxCriteria: setting$1,
|
|
@@ -38104,6 +38233,24 @@ const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions,
|
|
|
38104
38233
|
base: void 0
|
|
38105
38234
|
};
|
|
38106
38235
|
});
|
|
38236
|
+
const TestsOptions = Struct({
|
|
38237
|
+
...ShippedOptions.fields,
|
|
38238
|
+
testFiles: setting$1,
|
|
38239
|
+
skipPattern: setting$1
|
|
38240
|
+
});
|
|
38241
|
+
const testsShipped = (flow) => defineWorkflow(standardSchemaV1(TestsOptions, {
|
|
38242
|
+
onExcessProperty: "error",
|
|
38243
|
+
errors: "all"
|
|
38244
|
+
}), ({ contextLimit, ...options }) => ({
|
|
38245
|
+
flow: limited(flow, contextLimit),
|
|
38246
|
+
defaults: over({
|
|
38247
|
+
...defaults,
|
|
38248
|
+
...testsDefaults
|
|
38249
|
+
}, options),
|
|
38250
|
+
envDefaults: over(envDefaults, options),
|
|
38251
|
+
doors: { tests: {} },
|
|
38252
|
+
base: void 0
|
|
38253
|
+
}));
|
|
38107
38254
|
//#endregion
|
|
38108
38255
|
//#region src/workflows/shared/lenses.ts
|
|
38109
38256
|
const NO_FINDINGS = {
|
|
@@ -38112,7 +38259,7 @@ const NO_FINDINGS = {
|
|
|
38112
38259
|
noise: []
|
|
38113
38260
|
};
|
|
38114
38261
|
const UNTITLED = "Quality findings";
|
|
38115
|
-
const below = (section) => section.split("\n").slice(1).join("\n").trim();
|
|
38262
|
+
const below$1 = (section) => section.split("\n").slice(1).join("\n").trim();
|
|
38116
38263
|
const strayProse = (text, sections) => sections.reduce((rest, body) => rest.replace(body, "\n"), text).split("\n").filter((line) => !/^#\s/.test(line)).join("\n").trim();
|
|
38117
38264
|
/** The findings in `text`, one per `## ` section, in file order, numbered `Q<n>`. */
|
|
38118
38265
|
const findingsOf = (text) => {
|
|
@@ -38123,7 +38270,7 @@ const findingsOf = (text) => {
|
|
|
38123
38270
|
body: stray
|
|
38124
38271
|
}], ...sections.map((s) => ({
|
|
38125
38272
|
title: s.title,
|
|
38126
|
-
body: below(s.body)
|
|
38273
|
+
body: below$1(s.body)
|
|
38127
38274
|
}))].map((f, i) => ({
|
|
38128
38275
|
id: `Q${i + 1}`,
|
|
38129
38276
|
...f
|
|
@@ -38600,6 +38747,158 @@ const design = (base, guard) => scope("design", async () => {
|
|
|
38600
38747
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
38601
38748
|
});
|
|
38602
38749
|
//#endregion
|
|
38750
|
+
//#region src/workflows/shared/learn.ts
|
|
38751
|
+
const SUMMARY_MAX = 200;
|
|
38752
|
+
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*/;
|
|
38753
|
+
const summary$1 = (body) => {
|
|
38754
|
+
const text = oneLine(body).replace(BANNER, "");
|
|
38755
|
+
return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1)}…` : text;
|
|
38756
|
+
};
|
|
38757
|
+
/** The standard a won't-fix or an own answer would make; a fix states none. */
|
|
38758
|
+
const candidateOf = (d) => {
|
|
38759
|
+
const anchor = oneLine(d.anchor);
|
|
38760
|
+
if (d.choice === "wontfix") {
|
|
38761
|
+
const why = summary$1(d.body);
|
|
38762
|
+
return [{
|
|
38763
|
+
decision: d,
|
|
38764
|
+
text: `Won't fix: ${anchor}${why === "" ? "" : ` — ${why}`}`
|
|
38765
|
+
}];
|
|
38766
|
+
}
|
|
38767
|
+
if (d.choice === "answer") return [{
|
|
38768
|
+
decision: d,
|
|
38769
|
+
text: `${oneLine(d.text)} (re: ${anchor})`
|
|
38770
|
+
}];
|
|
38771
|
+
return [];
|
|
38772
|
+
};
|
|
38773
|
+
const without = (list, drop) => {
|
|
38774
|
+
const keys = new Set(drop.map(standardKey));
|
|
38775
|
+
return list.filter((s) => !keys.has(standardKey(s)));
|
|
38776
|
+
};
|
|
38777
|
+
const adding = (list, text) => list.some((s) => standardKey(s) === standardKey(text)) ? [...list] : [...list, text];
|
|
38778
|
+
/**
|
|
38779
|
+
* Keep the new one, keep the old one, or replace both with the human's own
|
|
38780
|
+
* wording. Another open conflict stops naming a replaced standard: the human
|
|
38781
|
+
* never judged it against the replacement, so keeping it must not drop that.
|
|
38782
|
+
*/
|
|
38783
|
+
const resolve = (standards, d) => {
|
|
38784
|
+
const c = conflictFor(standards, d.anchor);
|
|
38785
|
+
if (c === void 0) return standards;
|
|
38786
|
+
const others = standards.undecided.filter((u) => u !== c);
|
|
38787
|
+
if (d.choice === "wontfix") return {
|
|
38788
|
+
decided: standards.decided,
|
|
38789
|
+
undecided: others
|
|
38790
|
+
};
|
|
38791
|
+
const kept = d.choice === "fix" ? c.text : oneLine(d.text);
|
|
38792
|
+
const undecided = others.map((u) => ({
|
|
38793
|
+
text: u.text,
|
|
38794
|
+
olds: without(u.olds, c.olds)
|
|
38795
|
+
}));
|
|
38796
|
+
return {
|
|
38797
|
+
decided: adding(without(standards.decided, c.olds), kept),
|
|
38798
|
+
undecided
|
|
38799
|
+
};
|
|
38800
|
+
};
|
|
38801
|
+
const contradicts = (i, id, other) => ({
|
|
38802
|
+
id: `standard-${i}.contradicts-${id}`,
|
|
38803
|
+
primitive: "noul",
|
|
38804
|
+
instructions: `Does the "Standard:" line of evidence "standard-${i}" contradict ${other}?`,
|
|
38805
|
+
criteria: "yes: following one breaks the other. no: both can be followed — they are compatible or unrelated."
|
|
38806
|
+
});
|
|
38807
|
+
const questionsFor = (i, anchor, decided) => [
|
|
38808
|
+
{
|
|
38809
|
+
id: `standard-${i}`,
|
|
38810
|
+
primitive: "noul",
|
|
38811
|
+
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.`,
|
|
38812
|
+
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."
|
|
38813
|
+
},
|
|
38814
|
+
...decided.map((_, k) => contradicts(i, `${k + 1}`, `kept standard ${k + 1} (evidence "kept-${k + 1}")`)),
|
|
38815
|
+
...Array.from({ length: i - 1 }, (_, j) => contradicts(i, `new-${j + 1}`, `the "Standard:" line of evidence "standard-${j + 1}"`))
|
|
38816
|
+
];
|
|
38817
|
+
const evidenceOf = (c) => [
|
|
38818
|
+
`Note: ${c.decision.anchor}`,
|
|
38819
|
+
`Reviewer's text: ${c.decision.body}`,
|
|
38820
|
+
`Human's answer: ${c.decision.choice === "wontfix" ? "won't fix" : c.decision.text}`,
|
|
38821
|
+
`Standard: ${c.text}`
|
|
38822
|
+
].join("\n");
|
|
38823
|
+
/**
|
|
38824
|
+
* One judge rest over every candidate. Only a confident `yes` on uncut
|
|
38825
|
+
* evidence makes a standard; a contradiction the judge does not confidently
|
|
38826
|
+
* rule out goes to the human instead of overwriting what they decided before.
|
|
38827
|
+
* A cut kept standard leaves the round unlearned: raise `judgeBudgetBytes`.
|
|
38828
|
+
*/
|
|
38829
|
+
const judged = async (candidates, decided, minP) => {
|
|
38830
|
+
const { answers, truncated } = await judge("review.standards.judge", {
|
|
38831
|
+
questions: candidates.flatMap((c, i) => questionsFor(i + 1, c.decision.anchor, decided)),
|
|
38832
|
+
evidence: Object.fromEntries([...candidates.map((c, i) => [`standard-${i + 1}`, evidenceOf(c)]), ...decided.map((s, k) => [`kept-${k + 1}`, s])]),
|
|
38833
|
+
message: "Judging whether your answers are standards to keep: run `gtd judge run`, or land without a verdict, which keeps none.",
|
|
38834
|
+
label: "Learning your standards"
|
|
38835
|
+
});
|
|
38836
|
+
const ruledOut = (i, id) => answered(answers[`standard-${i}.contradicts-${id}`], "no", minP);
|
|
38837
|
+
const added = [];
|
|
38838
|
+
const raised = [];
|
|
38839
|
+
candidates.forEach((c, n) => {
|
|
38840
|
+
const i = n + 1;
|
|
38841
|
+
if (truncated.includes(`standard-${i}`)) return;
|
|
38842
|
+
if (decided.some((_, k) => truncated.includes(`kept-${k + 1}`))) return;
|
|
38843
|
+
if (!answered(answers[`standard-${i}`], "yes", minP)) return;
|
|
38844
|
+
const olds = [...decided.filter((_, k) => !ruledOut(i, `${k + 1}`)), ...added.filter((a) => !ruledOut(i, `new-${a.i}`)).map((a) => a.text)];
|
|
38845
|
+
if (olds.length === 0) added.push({
|
|
38846
|
+
i,
|
|
38847
|
+
text: c.text
|
|
38848
|
+
});
|
|
38849
|
+
else raised.push({
|
|
38850
|
+
text: c.text,
|
|
38851
|
+
olds
|
|
38852
|
+
});
|
|
38853
|
+
});
|
|
38854
|
+
return {
|
|
38855
|
+
added: added.map((a) => a.text),
|
|
38856
|
+
raised
|
|
38857
|
+
};
|
|
38858
|
+
};
|
|
38859
|
+
/**
|
|
38860
|
+
* Settles every answer to a standard conflict, then learns standards from
|
|
38861
|
+
* the won't-fix ticks and own answers: kept in `.gtd/STANDARDS.md`, a
|
|
38862
|
+
* contradicting one as a new conflict note in `.gtd/REVIEW.md`.
|
|
38863
|
+
*/
|
|
38864
|
+
const learnStandards = async (decisions) => {
|
|
38865
|
+
const before = parseStandards(read(".gtd/STANDARDS.md") ?? "");
|
|
38866
|
+
const rest = decisions.filter((d) => !isConflictTitle(d.anchor));
|
|
38867
|
+
const settled = decisions.filter((d) => isConflictTitle(d.anchor)).reduce(resolve, before);
|
|
38868
|
+
const known = new Set([...settled.decided, ...settled.undecided.map((c) => c.text)].map(standardKey));
|
|
38869
|
+
const candidates = rest.flatMap(candidateOf).filter((c) => {
|
|
38870
|
+
const key = standardKey(c.text);
|
|
38871
|
+
if (known.has(key)) return false;
|
|
38872
|
+
known.add(key);
|
|
38873
|
+
return true;
|
|
38874
|
+
});
|
|
38875
|
+
const minP = numeric(vars.standardMinP, Infinity);
|
|
38876
|
+
const { added, raised } = candidates.length === 0 || !Number.isFinite(minP) ? {
|
|
38877
|
+
added: [],
|
|
38878
|
+
raised: []
|
|
38879
|
+
} : await judged(candidates, settled.decided, minP);
|
|
38880
|
+
const after = {
|
|
38881
|
+
decided: [...settled.decided, ...added],
|
|
38882
|
+
undecided: [...settled.undecided, ...raised]
|
|
38883
|
+
};
|
|
38884
|
+
if (renderStandards(after) !== renderStandards(before)) await run("review.standards.recording", writeScript(STANDARDS, renderStandards(after)), { label: "Recording your standards" });
|
|
38885
|
+
if (raised.length > 0) {
|
|
38886
|
+
const chunks = raised.map((c) => `## ${conflictTitle(c)}\n\n${conflictBody(c)}`);
|
|
38887
|
+
await run("review.standards.raising", writeScript(REVIEW, `${(read(".gtd/REVIEW.md") ?? "").trimEnd()}\n\n${chunks.join("\n\n")}\n`), {
|
|
38888
|
+
label: "Asking about a contradicting standard",
|
|
38889
|
+
file: REVIEW
|
|
38890
|
+
});
|
|
38891
|
+
}
|
|
38892
|
+
};
|
|
38893
|
+
const settlesStandard = (d) => isConflictTitle(d.anchor);
|
|
38894
|
+
const standardsPending = () => parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided.length > 0;
|
|
38895
|
+
const standardsAddedSince = (base) => {
|
|
38896
|
+
const change = changesSince(base).get(STANDARDS);
|
|
38897
|
+
if (change === void 0) return [];
|
|
38898
|
+
const was = new Set(parseStandards(change.before ?? "").decided);
|
|
38899
|
+
return parseStandards(change.after ?? "").decided.filter((s) => !was.has(s));
|
|
38900
|
+
};
|
|
38901
|
+
//#endregion
|
|
38603
38902
|
//#region src/workflows/shared/review.ts
|
|
38604
38903
|
const VERDICTS = [
|
|
38605
38904
|
"edit",
|
|
@@ -38730,7 +39029,11 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
|
|
|
38730
39029
|
...report,
|
|
38731
39030
|
weakened: removedAssertions(changesSince(base)),
|
|
38732
39031
|
nitFix,
|
|
38733
|
-
answered
|
|
39032
|
+
answered,
|
|
39033
|
+
standards: {
|
|
39034
|
+
added: standardsAddedSince(base),
|
|
39035
|
+
undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
|
|
39036
|
+
}
|
|
38734
39037
|
}), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
|
|
38735
39038
|
label: "Composing the review",
|
|
38736
39039
|
file: REVIEW,
|
|
@@ -38745,7 +39048,7 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
|
|
|
38745
39048
|
* praise dropped.
|
|
38746
39049
|
*/
|
|
38747
39050
|
const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
38748
|
-
const { guard, guardRests, fixAccess, rules } = options;
|
|
39051
|
+
const { guard, guardRests, fixAccess, rules, awaitMessage } = options;
|
|
38749
39052
|
const feedback = options.feedback ?? {
|
|
38750
39053
|
file: ".gtd/REQUIREMENTS.md",
|
|
38751
39054
|
collecting,
|
|
@@ -38770,11 +39073,14 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
38770
39073
|
guardRests,
|
|
38771
39074
|
rules
|
|
38772
39075
|
}),
|
|
38773
|
-
awaitReview: (b) => awaitReview(b),
|
|
39076
|
+
awaitReview: (b) => awaitReview(b, awaitMessage?.(b)),
|
|
38774
39077
|
reviewMissing,
|
|
38775
39078
|
collecting: feedback.collecting,
|
|
38776
39079
|
triage: triage$1(severities),
|
|
38777
39080
|
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
39081
|
+
settles: settlesStandard,
|
|
39082
|
+
learn: learnStandards,
|
|
39083
|
+
pending: standardsPending,
|
|
38778
39084
|
answerQuestions: answerReviewQuestions,
|
|
38779
39085
|
fixNits: async (notes) => {
|
|
38780
39086
|
await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
|
|
@@ -38827,6 +39133,17 @@ const callForm = (first, second, [third, fourth, ...extra]) => {
|
|
|
38827
39133
|
if (typeof second === "string") return legacyForm(first, second, third, fourth);
|
|
38828
39134
|
if (typeof first === "string") return currentForm(first, second, third, fourth);
|
|
38829
39135
|
};
|
|
39136
|
+
/** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
|
|
39137
|
+
const fixFeedback = async () => {
|
|
39138
|
+
await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
|
|
39139
|
+
label: "Fixing your review feedback",
|
|
39140
|
+
file: REQUIREMENTS,
|
|
39141
|
+
model: coder(),
|
|
39142
|
+
system: finisherSystem(),
|
|
39143
|
+
allowEmpty: true
|
|
39144
|
+
});
|
|
39145
|
+
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
|
|
39146
|
+
};
|
|
38830
39147
|
/**
|
|
38831
39148
|
* Undo the human's review-round code edits. A path changed since the human
|
|
38832
39149
|
* left it is not overwritten; requireRevert names it instead.
|
|
@@ -39840,6 +40157,8 @@ const skills = (vars) => {
|
|
|
39840
40157
|
design: ["spec-driven-development", "planning-and-task-breakdown"],
|
|
39841
40158
|
notes: ["incremental-implementation", "code-simplification"],
|
|
39842
40159
|
feedback: ["test-driven-development", "incremental-implementation"],
|
|
40160
|
+
tests: ["test-audit", "test-driven-development"],
|
|
40161
|
+
"tests.health": ["debugging-and-error-recovery"],
|
|
39843
40162
|
...build,
|
|
39844
40163
|
...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
|
|
39845
40164
|
refactor: ["ponytail"],
|
|
@@ -39873,17 +40192,6 @@ const steering = {
|
|
|
39873
40192
|
};
|
|
39874
40193
|
//#endregion
|
|
39875
40194
|
//#region src/workflows/review/review.ts
|
|
39876
|
-
/** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
|
|
39877
|
-
const fixFeedback = async () => {
|
|
39878
|
-
await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
|
|
39879
|
-
label: "Fixing your review feedback",
|
|
39880
|
-
file: REQUIREMENTS,
|
|
39881
|
-
model: coder(),
|
|
39882
|
-
system: finisherSystem(),
|
|
39883
|
-
allowEmpty: true
|
|
39884
|
-
});
|
|
39885
|
-
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
|
|
39886
|
-
};
|
|
39887
40195
|
/**
|
|
39888
40196
|
* The feature's review of everything since `reviewBase` — a green gate, the
|
|
39889
40197
|
* triaged quality lap, then the human review — with no design phase: a
|
|
@@ -40017,6 +40325,191 @@ const cleanupFlow = async () => {
|
|
|
40017
40325
|
};
|
|
40018
40326
|
const cleanup = flagShipped(cleanupFlow, "cleanup");
|
|
40019
40327
|
//#endregion
|
|
40328
|
+
//#region src/workflows/tests/tests.ts
|
|
40329
|
+
const TESTS = ".gtd/TESTS.md";
|
|
40330
|
+
const RETIRE = ".gtd/RETIRE.md";
|
|
40331
|
+
/** The first two answer boxes of a deletion proposal; the third stays the human's own answer. */
|
|
40332
|
+
const RETIRE_ANSWERS = ["delete it", "keep it skipped"];
|
|
40333
|
+
testsDefaults.skipPattern;
|
|
40334
|
+
/** `skipPattern` as a per-line expression; refuses one that is blank, invalid, or matches every line. */
|
|
40335
|
+
const skipExpression = (pattern) => {
|
|
40336
|
+
if (pattern.trim() === "") return refuse("gtd: skipPattern must not be blank");
|
|
40337
|
+
let expression;
|
|
40338
|
+
try {
|
|
40339
|
+
expression = new RegExp(pattern);
|
|
40340
|
+
} catch (error) {
|
|
40341
|
+
return refuse(`gtd: skipPattern is not a valid regular expression: ${String(error)}`);
|
|
40342
|
+
}
|
|
40343
|
+
if (expression.test("")) return refuse("gtd: skipPattern must not match the empty string");
|
|
40344
|
+
return expression;
|
|
40345
|
+
};
|
|
40346
|
+
/** Every line of `files` the expression calls skipped, in file then line order. */
|
|
40347
|
+
const skipsIn = (files, expression) => files.flatMap(({ path, text }) => text.split("\n").flatMap((line, i) => expression.test(line) ? [{
|
|
40348
|
+
path,
|
|
40349
|
+
line: i + 1,
|
|
40350
|
+
text: line.trim()
|
|
40351
|
+
}] : []));
|
|
40352
|
+
const testPaths = () => {
|
|
40353
|
+
const globs = (vars.testFiles ?? testsDefaults.testFiles).split(",").map((g) => g.trim()).filter((g) => g !== "");
|
|
40354
|
+
return [...new Set(globs.flatMap((g) => glob(g)))].filter(isCode$1).sort();
|
|
40355
|
+
};
|
|
40356
|
+
const scan = (paths, expression) => skipsIn(paths.map((path) => ({
|
|
40357
|
+
path,
|
|
40358
|
+
text: read(path) ?? ""
|
|
40359
|
+
})), expression);
|
|
40360
|
+
/** A section title as a repository path: backticks and a leading `./` dropped. */
|
|
40361
|
+
const pathOf = (title) => title.replace(/`/g, "").trim().replace(/^\.\//, "");
|
|
40362
|
+
const below = (section) => section.split("\n").slice(1).join("\n").trim();
|
|
40363
|
+
const sweepingPrompt = (skips) => `Sweep this repository's tests for ones to revive.
|
|
40364
|
+
|
|
40365
|
+
Skipped tests gtd found:
|
|
40366
|
+
|
|
40367
|
+
${skips.length === 0 ? "- none" : skips.map((s) => `- ${s.path}:${s.line} — ${s.text}`).join("\n")}
|
|
40368
|
+
|
|
40369
|
+
- Audit every test in the repository with the test-audit skill — the whole
|
|
40370
|
+
suite, not a recent change
|
|
40371
|
+
- Write \`${TESTS}\`: \`# Tests\`, then one \`## <path>\` section per test file
|
|
40372
|
+
that needs work — every file listed above, and each file the audit found
|
|
40373
|
+
worth rewriting — saying which tests are skipped and what the audit found
|
|
40374
|
+
- Change nothing else; write nothing when there is nothing to do
|
|
40375
|
+
- Leave everything uncommitted and finish your turn`;
|
|
40376
|
+
const rewritingPrompt = (path, found) => `Rewrite the tests in \`${path}\` until none is skipped and the suite is green.
|
|
40377
|
+
|
|
40378
|
+
What the sweep found:
|
|
40379
|
+
|
|
40380
|
+
${found || "_Nothing more than the skipped tests._"}
|
|
40381
|
+
|
|
40382
|
+
- Unskip every skipped test in the file and make it pass, rewriting it where
|
|
40383
|
+
it rotted, and fix what the audit found
|
|
40384
|
+
- A test that is obsolete — what it tests is gone, or another test already
|
|
40385
|
+
covers it — stays skipped: never delete it. Propose its deletion in
|
|
40386
|
+
\`${RETIRE}\` instead: a \`## <title>\` section naming \`${path}\`, the test,
|
|
40387
|
+
and why it should go. A person decides. Propose a line gtd misread as
|
|
40388
|
+
skipped (fixture text, a comment) the same way, saying so
|
|
40389
|
+
- Delete no test file
|
|
40390
|
+
- Leave everything uncommitted and finish your turn`;
|
|
40391
|
+
const gateMessage = () => `The test baseline is red — gtd will not revive skipped tests on a broken suite.
|
|
40392
|
+
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
40393
|
+
|
|
40394
|
+
What each change does next (then run \`gtd land\`):
|
|
40395
|
+
- **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**tests-gate.check**).
|
|
40396
|
+
`;
|
|
40397
|
+
const awaitMessage = (base) => `\`.gtd/REVIEW.md\` holds one chunk per test the sweep would delete. Each ends in
|
|
40398
|
+
three boxes — tick exactly one: \`delete it\` (the test is deleted, then the
|
|
40399
|
+
suite runs), \`keep it skipped\` (dropped, recorded in the commit), or write your
|
|
40400
|
+
own answer over \`_your answer_\` and tick it (judged like a note). Every
|
|
40401
|
+
landing here is refused until each proposal holds exactly one tick. An agent
|
|
40402
|
+
turn that deleted a test file was refused; check the diff for tests removed
|
|
40403
|
+
inside a file.
|
|
40404
|
+
|
|
40405
|
+
Review the revived tests yourself:
|
|
40406
|
+
|
|
40407
|
+
git diff ${base}
|
|
40408
|
+
|
|
40409
|
+
Then run \`gtd land\`. A note or code edit of your own is read as feedback and
|
|
40410
|
+
built before you review again.
|
|
40411
|
+
`;
|
|
40412
|
+
/** A proposal's own text, made a request the nit fixer acts on once the human ticks `delete it`. */
|
|
40413
|
+
const proposals = (text) => sectionBodies(text).map((s, i) => ({
|
|
40414
|
+
id: `R${i + 1}`,
|
|
40415
|
+
title: s.title,
|
|
40416
|
+
body: `Delete this skipped test.\n\n${below(s.body)}`,
|
|
40417
|
+
answers: RETIRE_ANSWERS
|
|
40418
|
+
}));
|
|
40419
|
+
/** Sections of `text` that name `path` as a whole token — a trailing full stop still counts. */
|
|
40420
|
+
const naming = (text, path) => {
|
|
40421
|
+
const token = new RegExp(`(?<![\\w./-])${path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w/-])`);
|
|
40422
|
+
return sectionBodies(text).filter((s) => token.test(s.body.replace(/(^|[^\w.])\.\//g, "$1"))).length;
|
|
40423
|
+
};
|
|
40424
|
+
/** Refuses a test file a turn deleted, and a skip left in the file being rewritten that `.gtd/RETIRE.md` does not name. */
|
|
40425
|
+
const guardFor = (known, expression, path) => () => {
|
|
40426
|
+
const deleted = changes().filter((c) => c.status === "deleted" && known.has(c.path)).map((c) => c.path);
|
|
40427
|
+
if (deleted.length > 0) refuse(`gtd land: tests: ${deleted.join(", ")} deleted — a test goes only once a person ticks "delete it" at the review`);
|
|
40428
|
+
if (path === void 0) return;
|
|
40429
|
+
const left = scan([path], expression).slice(naming(read(RETIRE) ?? "", path));
|
|
40430
|
+
if (left.length > 0) refuse(`gtd land: tests: ${left.map((s) => `${s.path}:${s.line}`).join(", ")} still skipped — revive it, or propose its deletion in ${RETIRE}`);
|
|
40431
|
+
};
|
|
40432
|
+
/** The sweep's packages, refused when it changed more than its plan or the plan misses a file. */
|
|
40433
|
+
const planned = (skips, known) => {
|
|
40434
|
+
const strays = changes().filter((c) => c.path !== TESTS);
|
|
40435
|
+
if (strays.length > 0) refuse(`gtd land: tests: the sweep changed ${strays.map((c) => c.path).join(", ")} — it writes only ${TESTS}`);
|
|
40436
|
+
const packages = sectionBodies(read(TESTS) ?? "").map((s) => ({
|
|
40437
|
+
path: pathOf(s.title),
|
|
40438
|
+
found: below(s.body)
|
|
40439
|
+
}));
|
|
40440
|
+
const missing = [...packages.filter((p) => !known.has(p.path)).map((p) => `"${p.path}" is no test file`), ...[...new Set(skips.map((s) => s.path))].filter((path) => !packages.some((p) => p.path === path)).map((path) => `${path} has no section`)];
|
|
40441
|
+
if (missing.length > 0) refuse(`gtd land: tests: in ${TESTS}, ${missing.join("; ")}`);
|
|
40442
|
+
return packages;
|
|
40443
|
+
};
|
|
40444
|
+
/**
|
|
40445
|
+
* Revive or retire skipped tests: a sweep plans one package per test file, each
|
|
40446
|
+
* is rewritten until unskipped and green, and what an agent calls obsolete
|
|
40447
|
+
* reaches the human review as a deletion proposal only a person can tick.
|
|
40448
|
+
*/
|
|
40449
|
+
const testsFlow = async () => {
|
|
40450
|
+
refusePendingHandOff();
|
|
40451
|
+
for (const file of [
|
|
40452
|
+
REQUIREMENTS,
|
|
40453
|
+
TESTS,
|
|
40454
|
+
RETIRE
|
|
40455
|
+
]) if (read(file) !== void 0) refuse(`gtd: ${file} is left over — finish or remove it first.`);
|
|
40456
|
+
const expression = skipExpression(vars.skipPattern ?? testsDefaults.skipPattern);
|
|
40457
|
+
await gate("tests-gate", gateMessage());
|
|
40458
|
+
const base = start();
|
|
40459
|
+
const known = new Set(testPaths());
|
|
40460
|
+
const guard = guardFor(known, expression);
|
|
40461
|
+
const report = await scope("tests", async () => {
|
|
40462
|
+
const skips = scan([...known], expression);
|
|
40463
|
+
await agentWithSkills("sweeping", sweepingPrompt(skips), {
|
|
40464
|
+
label: "Sweeping the tests",
|
|
40465
|
+
file: TESTS,
|
|
40466
|
+
model: coder(),
|
|
40467
|
+
system: finisherSystem(),
|
|
40468
|
+
allowEmpty: true
|
|
40469
|
+
});
|
|
40470
|
+
guard();
|
|
40471
|
+
const packages = planned(skips, known);
|
|
40472
|
+
for (const { path, found } of packages) {
|
|
40473
|
+
const guarded = guardFor(known, expression, path);
|
|
40474
|
+
await agentWithSkills("rewriting", rewritingPrompt(path, found), {
|
|
40475
|
+
label: `Rewriting ${path}`,
|
|
40476
|
+
file: RETIRE,
|
|
40477
|
+
model: coder(),
|
|
40478
|
+
system: finisherSystem(),
|
|
40479
|
+
allowEmpty: true
|
|
40480
|
+
});
|
|
40481
|
+
guarded();
|
|
40482
|
+
if (!changes().some((c) => isCode$1(c.path))) continue;
|
|
40483
|
+
await healthy(() => fixCheck(), {
|
|
40484
|
+
guard: guarded,
|
|
40485
|
+
guardRests: false
|
|
40486
|
+
});
|
|
40487
|
+
}
|
|
40488
|
+
const retired = proposals(read(RETIRE) ?? "");
|
|
40489
|
+
if (read(TESTS) !== void 0 || read(RETIRE) !== void 0) await run("closing", removeScript([TESTS, RETIRE]), { label: "Closing the sweep" });
|
|
40490
|
+
const changed = changesSince(base).paths.some(isCode$1);
|
|
40491
|
+
return retired.length === 0 && !changed ? void 0 : {
|
|
40492
|
+
notes: retired,
|
|
40493
|
+
fixed: [],
|
|
40494
|
+
noise: []
|
|
40495
|
+
};
|
|
40496
|
+
});
|
|
40497
|
+
if (report === void 0) return;
|
|
40498
|
+
let outcome = await tail((escalations) => humanReview(base, escalations, {
|
|
40499
|
+
report,
|
|
40500
|
+
awaitMessage: report.notes.length > 0 ? awaitMessage : void 0
|
|
40501
|
+
}), {
|
|
40502
|
+
lenses: [],
|
|
40503
|
+
green: true
|
|
40504
|
+
});
|
|
40505
|
+
while (outcome.verdict === "feedback") {
|
|
40506
|
+
await reUnwind(outcome);
|
|
40507
|
+
await fixFeedback();
|
|
40508
|
+
outcome = await tail((escalations) => humanReview(base, escalations), { lenses: [] });
|
|
40509
|
+
}
|
|
40510
|
+
};
|
|
40511
|
+
const tests = testsShipped(testsFlow);
|
|
40512
|
+
//#endregion
|
|
40020
40513
|
//#region src/workflows/index.ts
|
|
40021
40514
|
var workflows_default = feature;
|
|
40022
40515
|
/**
|
|
@@ -40026,4 +40519,4 @@ var workflows_default = feature;
|
|
|
40026
40519
|
*/
|
|
40027
40520
|
const doors = {};
|
|
40028
40521
|
//#endregion
|
|
40029
|
-
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unstableIds, unwind };
|
|
40522
|
+
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, TestsOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, tests, testsFlow, threads, triage, unstableIds, unwind };
|