@pmelab/gtd 43.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 +33 -13
- package/dist/chunks/{reserved-COEMFEpy.mjs → reserved-ty1mtlnS.mjs} +155 -3
- package/dist/chunks/review-BmoH-9im.mjs +408 -0
- package/dist/chunks/{tail-C_xn8U7U.mjs → tail-CM3sVoAb.mjs} +3 -3
- package/dist/workflow-kit.mjs +22 -10
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +6 -278
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +27 -7
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +379 -66
- package/package.json +1 -1
- package/src/steering/FORMATS.md +4 -4
- package/src/workflow-kit/review.test.ts +80 -2
- package/src/workflow-kit/review.ts +45 -11
- package/src/workflows/feature/coverage.ts +1 -1
- package/src/workflows/feature/feature.test.ts +8 -0
- package/src/workflows/feature/feature.ts +2 -2
- package/src/workflows/feature/index.ts +1 -1
- package/src/workflows/feature/refactor.ts +1 -1
- package/src/workflows/feature/review-answers.feature +12 -3
- package/src/workflows/feature/seam.ts +1 -1
- package/src/workflows/feature/standards.feature +286 -0
- package/src/workflows/feature/steps.test.ts +2 -64
- package/src/workflows/feature/steps.ts +5 -41
- package/src/workflows/fix/fix.feature +20 -21
- package/src/workflows/index.ts +1 -1
- package/src/workflows/review/review-door.feature +158 -0
- package/src/workflows/review/review.test.ts +1 -73
- package/src/workflows/review/review.ts +34 -6
- 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/{feature → shared}/review.test.ts +4 -8
- package/src/workflows/{feature → shared}/review.ts +23 -10
- package/src/workflows/shared/skills.test.ts +2 -0
- package/src/workflows/shared/skills.ts +2 -0
- package/src/workflows/shared/standards.test.ts +90 -0
- package/src/workflows/shared/standards.ts +156 -0
- package/src/workflows/shared/steps.test.ts +64 -2
- package/src/workflows/shared/steps.ts +40 -1
- package/src/workflows/shared/tail.ts +2 -2
- package/src/workflows/shared/text.test.ts +58 -1
- package/src/workflows/shared/text.ts +39 -1
- package/src/workflows/shared/vars.ts +2 -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);
|
|
@@ -16909,7 +16921,7 @@ const reviewOnce = async (p, base, carry) => {
|
|
|
16909
16921
|
* another lap, questions are answered in place, nits fixed in one batch,
|
|
16910
16922
|
* praise dropped.
|
|
16911
16923
|
*/
|
|
16912
|
-
const review$
|
|
16924
|
+
const review$1 = async (base, p) => {
|
|
16913
16925
|
let carry;
|
|
16914
16926
|
for (;;) {
|
|
16915
16927
|
await reviewOnce(p, base, carry);
|
|
@@ -16919,7 +16931,7 @@ const review$2 = 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 = [
|
|
@@ -17091,6 +17205,7 @@ const defaults = {
|
|
|
17091
17205
|
reviewNoteActionable: "0.7",
|
|
17092
17206
|
reviewSeverityMinP: "0.7",
|
|
17093
17207
|
qualityTriageMinP: "0.7",
|
|
17208
|
+
standardMinP: "0.8",
|
|
17094
17209
|
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
17095
17210
|
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
17096
17211
|
flagMode: "true",
|
|
@@ -17350,8 +17465,11 @@ const withSkills = (skills, prompt) => {
|
|
|
17350
17465
|
};
|
|
17351
17466
|
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills, then its restricted file access. */
|
|
17352
17467
|
const agentWithSkills = (name, prompt, options = {}) => {
|
|
17353
|
-
const
|
|
17354
|
-
|
|
17468
|
+
const body = [
|
|
17469
|
+
accessPreamble(accessFor(name, options.file)),
|
|
17470
|
+
prompt,
|
|
17471
|
+
standardsBlock(read(STANDARDS))
|
|
17472
|
+
].filter((part) => part !== "").join("\n\n");
|
|
17355
17473
|
return agent(name, withSkills(skillsFor(name).join(", "), body), options);
|
|
17356
17474
|
};
|
|
17357
17475
|
const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
|
|
@@ -17391,6 +17509,7 @@ const SUMMARY_TITLES = [
|
|
|
17391
17509
|
"Weakened scenarios",
|
|
17392
17510
|
"Fixed from your notes",
|
|
17393
17511
|
"Fixed automatically",
|
|
17512
|
+
"Standards added",
|
|
17394
17513
|
"Dropped as noise",
|
|
17395
17514
|
"Nothing to note"
|
|
17396
17515
|
];
|
|
@@ -17411,6 +17530,8 @@ const composedReview = (base, report) => {
|
|
|
17411
17530
|
};
|
|
17412
17531
|
const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
|
|
17413
17532
|
const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
|
|
17533
|
+
const conflicts = (report.standards?.undecided ?? []).map((c) => chunk(unique(conflictTitle(c)), conflictBody(c)));
|
|
17534
|
+
const added = report.standards?.added ?? [];
|
|
17414
17535
|
const notes = report.notes.map((n) => ({
|
|
17415
17536
|
...n,
|
|
17416
17537
|
title: unique(n.title)
|
|
@@ -17419,8 +17540,10 @@ const composedReview = (base, report) => {
|
|
|
17419
17540
|
const chunks = [
|
|
17420
17541
|
...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
17542
|
...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
|
|
17543
|
+
...conflicts,
|
|
17422
17544
|
...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}`)),
|
|
17423
17545
|
...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
|
|
17546
|
+
...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
|
|
17424
17547
|
...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
|
|
17425
17548
|
];
|
|
17426
17549
|
const body = chunks.length > 0 ? chunks : [chunk("Nothing to note", "No quality lens raised anything for you — or none ran. Review the diff yourself.")];
|
|
@@ -17937,6 +18060,15 @@ ${notes.length === 0 ? "(none)\n" : notesCapture(notes)}
|
|
|
17937
18060
|
The reviewer's risks are:
|
|
17938
18061
|
|
|
17939
18062
|
${risks.length === 0 ? "(none)\n" : notesCapture(risks)}`;
|
|
18063
|
+
const reviewFeedbackFixPrompt = () => `${stateFileRules}
|
|
18064
|
+
- Read \`.gtd/REQUIREMENTS.md\` — the concerns the human's review asked for,
|
|
18065
|
+
one \`## \` heading each, in build order
|
|
18066
|
+
- Build every concern in this one turn; an answered question is context,
|
|
18067
|
+
not a concern
|
|
18068
|
+
- Where a change is behavioural, add a test that fails without it
|
|
18069
|
+
- Leave \`.gtd/REQUIREMENTS.md\` untouched — the next step removes it
|
|
18070
|
+
- Leave everything uncommitted and finish your turn
|
|
18071
|
+
`;
|
|
17940
18072
|
const buildReviewReviewMissingMessage = (commit) => `The review round committed no \`.gtd/REVIEW.md\` (at ${commit}), so there is
|
|
17941
18073
|
nothing to sign off on.
|
|
17942
18074
|
|
|
@@ -18565,6 +18697,36 @@ const awaitReview = (base, message = buildReviewAwaitReviewMessage(base)) => hum
|
|
|
18565
18697
|
base,
|
|
18566
18698
|
acceptClean: true
|
|
18567
18699
|
});
|
|
18700
|
+
/** Answer every `question` note inline in `.gtd/REVIEW.md`. */
|
|
18701
|
+
const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", buildReviewAnswerQuestionsPrompt(notes), {
|
|
18702
|
+
label: "Answering your questions",
|
|
18703
|
+
file: REVIEW,
|
|
18704
|
+
mode: "review",
|
|
18705
|
+
model: planner(),
|
|
18706
|
+
system: reviewerSystem()
|
|
18707
|
+
});
|
|
18708
|
+
/** Fix every `nit` note in one turn. */
|
|
18709
|
+
const fixNits = (notes, access, rules) => {
|
|
18710
|
+
const fixing = (name) => agentWithSkills(name, buildReviewFixNitsPrompt(notes, rules), {
|
|
18711
|
+
label: "Fixing your nits",
|
|
18712
|
+
file: REVIEW,
|
|
18713
|
+
model: planner(),
|
|
18714
|
+
system: reviewerSystem()
|
|
18715
|
+
});
|
|
18716
|
+
return access === void 0 ? fixing("review.fix.nits.fixing") : scope({
|
|
18717
|
+
name: "review.fix.nits",
|
|
18718
|
+
access
|
|
18719
|
+
}, () => fixing("fixing"));
|
|
18720
|
+
};
|
|
18721
|
+
/** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
|
|
18722
|
+
const collecting = (capture, round) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, void 0, round), {
|
|
18723
|
+
label: "Collecting your feedback",
|
|
18724
|
+
file: REQUIREMENTS,
|
|
18725
|
+
mode: "qa",
|
|
18726
|
+
model: planner(),
|
|
18727
|
+
system: reviewerSystem(),
|
|
18728
|
+
allowEmpty: true
|
|
18729
|
+
});
|
|
18568
18730
|
/** The round at commit `round` left no `.gtd/REVIEW.md`. */
|
|
18569
18731
|
const reviewMissing = (round) => human("review.review-missing", {
|
|
18570
18732
|
message: buildReviewReviewMissingMessage(round),
|
|
@@ -25580,9 +25742,9 @@ const HashMapProto = {
|
|
|
25580
25742
|
return new HashMapIterator(this, (k, v) => [k, v]);
|
|
25581
25743
|
},
|
|
25582
25744
|
[symbol$1]() {
|
|
25583
|
-
let hash$
|
|
25584
|
-
for (const item of this) hash$
|
|
25585
|
-
return cached(this, hash$
|
|
25745
|
+
let hash$2 = hash(HashMapSymbolKey);
|
|
25746
|
+
for (const item of this) hash$2 ^= pipe(hash(item[0]), combine$5(hash(item[1])));
|
|
25747
|
+
return cached(this, hash$2);
|
|
25586
25748
|
},
|
|
25587
25749
|
[symbol](that) {
|
|
25588
25750
|
if (isHashMap(that)) {
|
|
@@ -31588,7 +31750,7 @@ const gauge$1 = (count) => new GaugeState(count);
|
|
|
31588
31750
|
/** @internal */
|
|
31589
31751
|
const histogram$2 = (options) => new HistogramState(options.buckets, options.count, options.min, options.max, options.sum);
|
|
31590
31752
|
/** @internal */
|
|
31591
|
-
const summary$
|
|
31753
|
+
const summary$3 = (options) => new SummaryState(options.error, options.quantiles, options.count, options.min, options.max, options.sum);
|
|
31592
31754
|
/** @internal */
|
|
31593
31755
|
const isCounterState = (u) => hasProperty(u, CounterStateTypeId);
|
|
31594
31756
|
/**
|
|
@@ -31721,7 +31883,7 @@ const histogram$1 = (key) => {
|
|
|
31721
31883
|
});
|
|
31722
31884
|
};
|
|
31723
31885
|
/** @internal */
|
|
31724
|
-
const summary$
|
|
31886
|
+
const summary$2 = (key) => {
|
|
31725
31887
|
const { error, maxAge, maxSize, quantiles } = key.keyType;
|
|
31726
31888
|
const sortedQuantiles = pipe(quantiles, sort(Order$1));
|
|
31727
31889
|
const values = allocate(maxSize);
|
|
@@ -31756,7 +31918,7 @@ const summary$1 = (key) => {
|
|
|
31756
31918
|
sum = sum + value;
|
|
31757
31919
|
};
|
|
31758
31920
|
return make$5({
|
|
31759
|
-
get: () => summary$
|
|
31921
|
+
get: () => summary$3({
|
|
31760
31922
|
error,
|
|
31761
31923
|
quantiles: snapshot(Date.now()),
|
|
31762
31924
|
count,
|
|
@@ -31956,7 +32118,7 @@ var MetricRegistryImpl = class {
|
|
|
31956
32118
|
getSummary(key) {
|
|
31957
32119
|
let value = pipe(this.map, get$1(key), getOrUndefined);
|
|
31958
32120
|
if (value == null) {
|
|
31959
|
-
const summary = summary$
|
|
32121
|
+
const summary = summary$2(key);
|
|
31960
32122
|
if (!pipe(this.map, has(key))) pipe(this.map, set(key, summary));
|
|
31961
32123
|
value = summary;
|
|
31962
32124
|
}
|
|
@@ -38003,6 +38165,7 @@ const ShippedOptions = Struct({
|
|
|
38003
38165
|
reviewNoteActionable: setting$1,
|
|
38004
38166
|
reviewSeverityMinP: setting$1,
|
|
38005
38167
|
qualityTriageMinP: setting$1,
|
|
38168
|
+
standardMinP: setting$1,
|
|
38006
38169
|
refactorFlagPattern: setting$1,
|
|
38007
38170
|
seamAllow: setting$1,
|
|
38008
38171
|
sliceMaxCriteria: setting$1,
|
|
@@ -38282,7 +38445,7 @@ const runTail = (finish, options) => scope("build", async () => {
|
|
|
38282
38445
|
return finish(escalations, NO_FINDINGS);
|
|
38283
38446
|
});
|
|
38284
38447
|
/**
|
|
38285
|
-
* The
|
|
38448
|
+
* The fix workflow's human review since `base`: a sign-off closes
|
|
38286
38449
|
* `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
|
|
38287
38450
|
* `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
|
|
38288
38451
|
*/
|
|
@@ -38302,7 +38465,7 @@ const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
|
|
|
38302
38465
|
unparsed: "gtd land: review-doc: your edit to .gtd/REVIEW.md contains no note fix can read — restore the deleted chunk, or write the note as a line under its entry.",
|
|
38303
38466
|
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
|
|
38304
38467
|
});
|
|
38305
|
-
/** The tail the
|
|
38468
|
+
/** The tail the fix workflow runs: ends at the hand-off review since `base`. */
|
|
38306
38469
|
const handOffTail = (fixFirst, base, guard) => tail((escalations) => handOffReview(base, escalations, guard), {
|
|
38307
38470
|
fixFirst,
|
|
38308
38471
|
guard
|
|
@@ -38524,36 +38687,6 @@ const answerProductQuestions = () => human("gate.answer", {
|
|
|
38524
38687
|
mode: "qa",
|
|
38525
38688
|
acceptClean: true
|
|
38526
38689
|
});
|
|
38527
|
-
/** Answer every `question` note inline in `.gtd/REVIEW.md`. */
|
|
38528
|
-
const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", buildReviewAnswerQuestionsPrompt(notes), {
|
|
38529
|
-
label: "Answering your questions",
|
|
38530
|
-
file: REVIEW,
|
|
38531
|
-
mode: "review",
|
|
38532
|
-
model: planner(),
|
|
38533
|
-
system: reviewerSystem()
|
|
38534
|
-
});
|
|
38535
|
-
/** Fix every `nit` note in one turn. */
|
|
38536
|
-
const fixNits = (notes, access, rules) => {
|
|
38537
|
-
const fixing = (name) => agentWithSkills(name, buildReviewFixNitsPrompt(notes, rules), {
|
|
38538
|
-
label: "Fixing your nits",
|
|
38539
|
-
file: REVIEW,
|
|
38540
|
-
model: planner(),
|
|
38541
|
-
system: reviewerSystem()
|
|
38542
|
-
});
|
|
38543
|
-
return access === void 0 ? fixing("review.fix.nits.fixing") : scope({
|
|
38544
|
-
name: "review.fix.nits",
|
|
38545
|
-
access
|
|
38546
|
-
}, () => fixing("fixing"));
|
|
38547
|
-
};
|
|
38548
|
-
/** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
|
|
38549
|
-
const collecting = (capture, round) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, void 0, round), {
|
|
38550
|
-
label: "Collecting your feedback",
|
|
38551
|
-
file: REQUIREMENTS,
|
|
38552
|
-
mode: "qa",
|
|
38553
|
-
model: planner(),
|
|
38554
|
-
system: reviewerSystem(),
|
|
38555
|
-
allowEmpty: true
|
|
38556
|
-
});
|
|
38557
38690
|
/** Turn the refactor review round `capture` describes into `.gtd/REFACTOR.md`, or change nothing. */
|
|
38558
38691
|
const refactorCollecting = (capture, round, strays) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, {
|
|
38559
38692
|
strays,
|
|
@@ -38591,7 +38724,159 @@ const design = (base, guard) => scope("design", async () => {
|
|
|
38591
38724
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
38592
38725
|
});
|
|
38593
38726
|
//#endregion
|
|
38594
|
-
//#region src/workflows/
|
|
38727
|
+
//#region src/workflows/shared/learn.ts
|
|
38728
|
+
const SUMMARY_MAX = 200;
|
|
38729
|
+
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*/;
|
|
38730
|
+
const summary$1 = (body) => {
|
|
38731
|
+
const text = oneLine(body).replace(BANNER, "");
|
|
38732
|
+
return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1)}…` : text;
|
|
38733
|
+
};
|
|
38734
|
+
/** The standard a won't-fix or an own answer would make; a fix states none. */
|
|
38735
|
+
const candidateOf = (d) => {
|
|
38736
|
+
const anchor = oneLine(d.anchor);
|
|
38737
|
+
if (d.choice === "wontfix") {
|
|
38738
|
+
const why = summary$1(d.body);
|
|
38739
|
+
return [{
|
|
38740
|
+
decision: d,
|
|
38741
|
+
text: `Won't fix: ${anchor}${why === "" ? "" : ` — ${why}`}`
|
|
38742
|
+
}];
|
|
38743
|
+
}
|
|
38744
|
+
if (d.choice === "answer") return [{
|
|
38745
|
+
decision: d,
|
|
38746
|
+
text: `${oneLine(d.text)} (re: ${anchor})`
|
|
38747
|
+
}];
|
|
38748
|
+
return [];
|
|
38749
|
+
};
|
|
38750
|
+
const without = (list, drop) => {
|
|
38751
|
+
const keys = new Set(drop.map(standardKey));
|
|
38752
|
+
return list.filter((s) => !keys.has(standardKey(s)));
|
|
38753
|
+
};
|
|
38754
|
+
const adding = (list, text) => list.some((s) => standardKey(s) === standardKey(text)) ? [...list] : [...list, text];
|
|
38755
|
+
/**
|
|
38756
|
+
* Keep the new one, keep the old one, or replace both with the human's own
|
|
38757
|
+
* wording. Another open conflict stops naming a replaced standard: the human
|
|
38758
|
+
* never judged it against the replacement, so keeping it must not drop that.
|
|
38759
|
+
*/
|
|
38760
|
+
const resolve = (standards, d) => {
|
|
38761
|
+
const c = conflictFor(standards, d.anchor);
|
|
38762
|
+
if (c === void 0) return standards;
|
|
38763
|
+
const others = standards.undecided.filter((u) => u !== c);
|
|
38764
|
+
if (d.choice === "wontfix") return {
|
|
38765
|
+
decided: standards.decided,
|
|
38766
|
+
undecided: others
|
|
38767
|
+
};
|
|
38768
|
+
const kept = d.choice === "fix" ? c.text : oneLine(d.text);
|
|
38769
|
+
const undecided = others.map((u) => ({
|
|
38770
|
+
text: u.text,
|
|
38771
|
+
olds: without(u.olds, c.olds)
|
|
38772
|
+
}));
|
|
38773
|
+
return {
|
|
38774
|
+
decided: adding(without(standards.decided, c.olds), kept),
|
|
38775
|
+
undecided
|
|
38776
|
+
};
|
|
38777
|
+
};
|
|
38778
|
+
const contradicts = (i, id, other) => ({
|
|
38779
|
+
id: `standard-${i}.contradicts-${id}`,
|
|
38780
|
+
primitive: "noul",
|
|
38781
|
+
instructions: `Does the "Standard:" line of evidence "standard-${i}" contradict ${other}?`,
|
|
38782
|
+
criteria: "yes: following one breaks the other. no: both can be followed — they are compatible or unrelated."
|
|
38783
|
+
});
|
|
38784
|
+
const questionsFor = (i, anchor, decided) => [
|
|
38785
|
+
{
|
|
38786
|
+
id: `standard-${i}`,
|
|
38787
|
+
primitive: "noul",
|
|
38788
|
+
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.`,
|
|
38789
|
+
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."
|
|
38790
|
+
},
|
|
38791
|
+
...decided.map((_, k) => contradicts(i, `${k + 1}`, `kept standard ${k + 1} (evidence "kept-${k + 1}")`)),
|
|
38792
|
+
...Array.from({ length: i - 1 }, (_, j) => contradicts(i, `new-${j + 1}`, `the "Standard:" line of evidence "standard-${j + 1}"`))
|
|
38793
|
+
];
|
|
38794
|
+
const evidenceOf = (c) => [
|
|
38795
|
+
`Note: ${c.decision.anchor}`,
|
|
38796
|
+
`Reviewer's text: ${c.decision.body}`,
|
|
38797
|
+
`Human's answer: ${c.decision.choice === "wontfix" ? "won't fix" : c.decision.text}`,
|
|
38798
|
+
`Standard: ${c.text}`
|
|
38799
|
+
].join("\n");
|
|
38800
|
+
/**
|
|
38801
|
+
* One judge rest over every candidate. Only a confident `yes` on uncut
|
|
38802
|
+
* evidence makes a standard; a contradiction the judge does not confidently
|
|
38803
|
+
* rule out goes to the human instead of overwriting what they decided before.
|
|
38804
|
+
* A cut kept standard leaves the round unlearned: raise `judgeBudgetBytes`.
|
|
38805
|
+
*/
|
|
38806
|
+
const judged = async (candidates, decided, minP) => {
|
|
38807
|
+
const { answers, truncated } = await judge("review.standards.judge", {
|
|
38808
|
+
questions: candidates.flatMap((c, i) => questionsFor(i + 1, c.decision.anchor, decided)),
|
|
38809
|
+
evidence: Object.fromEntries([...candidates.map((c, i) => [`standard-${i + 1}`, evidenceOf(c)]), ...decided.map((s, k) => [`kept-${k + 1}`, s])]),
|
|
38810
|
+
message: "Judging whether your answers are standards to keep: run `gtd judge run`, or land without a verdict, which keeps none.",
|
|
38811
|
+
label: "Learning your standards"
|
|
38812
|
+
});
|
|
38813
|
+
const ruledOut = (i, id) => answered(answers[`standard-${i}.contradicts-${id}`], "no", minP);
|
|
38814
|
+
const added = [];
|
|
38815
|
+
const raised = [];
|
|
38816
|
+
candidates.forEach((c, n) => {
|
|
38817
|
+
const i = n + 1;
|
|
38818
|
+
if (truncated.includes(`standard-${i}`)) return;
|
|
38819
|
+
if (decided.some((_, k) => truncated.includes(`kept-${k + 1}`))) return;
|
|
38820
|
+
if (!answered(answers[`standard-${i}`], "yes", minP)) return;
|
|
38821
|
+
const olds = [...decided.filter((_, k) => !ruledOut(i, `${k + 1}`)), ...added.filter((a) => !ruledOut(i, `new-${a.i}`)).map((a) => a.text)];
|
|
38822
|
+
if (olds.length === 0) added.push({
|
|
38823
|
+
i,
|
|
38824
|
+
text: c.text
|
|
38825
|
+
});
|
|
38826
|
+
else raised.push({
|
|
38827
|
+
text: c.text,
|
|
38828
|
+
olds
|
|
38829
|
+
});
|
|
38830
|
+
});
|
|
38831
|
+
return {
|
|
38832
|
+
added: added.map((a) => a.text),
|
|
38833
|
+
raised
|
|
38834
|
+
};
|
|
38835
|
+
};
|
|
38836
|
+
/**
|
|
38837
|
+
* Settles every answer to a standard conflict, then learns standards from
|
|
38838
|
+
* the won't-fix ticks and own answers: kept in `.gtd/STANDARDS.md`, a
|
|
38839
|
+
* contradicting one as a new conflict note in `.gtd/REVIEW.md`.
|
|
38840
|
+
*/
|
|
38841
|
+
const learnStandards = async (decisions) => {
|
|
38842
|
+
const before = parseStandards(read(".gtd/STANDARDS.md") ?? "");
|
|
38843
|
+
const rest = decisions.filter((d) => !isConflictTitle(d.anchor));
|
|
38844
|
+
const settled = decisions.filter((d) => isConflictTitle(d.anchor)).reduce(resolve, before);
|
|
38845
|
+
const known = new Set([...settled.decided, ...settled.undecided.map((c) => c.text)].map(standardKey));
|
|
38846
|
+
const candidates = rest.flatMap(candidateOf).filter((c) => {
|
|
38847
|
+
const key = standardKey(c.text);
|
|
38848
|
+
if (known.has(key)) return false;
|
|
38849
|
+
known.add(key);
|
|
38850
|
+
return true;
|
|
38851
|
+
});
|
|
38852
|
+
const minP = numeric(vars.standardMinP, Infinity);
|
|
38853
|
+
const { added, raised } = candidates.length === 0 || !Number.isFinite(minP) ? {
|
|
38854
|
+
added: [],
|
|
38855
|
+
raised: []
|
|
38856
|
+
} : await judged(candidates, settled.decided, minP);
|
|
38857
|
+
const after = {
|
|
38858
|
+
decided: [...settled.decided, ...added],
|
|
38859
|
+
undecided: [...settled.undecided, ...raised]
|
|
38860
|
+
};
|
|
38861
|
+
if (renderStandards(after) !== renderStandards(before)) await run("review.standards.recording", writeScript(STANDARDS, renderStandards(after)), { label: "Recording your standards" });
|
|
38862
|
+
if (raised.length > 0) {
|
|
38863
|
+
const chunks = raised.map((c) => `## ${conflictTitle(c)}\n\n${conflictBody(c)}`);
|
|
38864
|
+
await run("review.standards.raising", writeScript(REVIEW, `${(read(".gtd/REVIEW.md") ?? "").trimEnd()}\n\n${chunks.join("\n\n")}\n`), {
|
|
38865
|
+
label: "Asking about a contradicting standard",
|
|
38866
|
+
file: REVIEW
|
|
38867
|
+
});
|
|
38868
|
+
}
|
|
38869
|
+
};
|
|
38870
|
+
const settlesStandard = (d) => isConflictTitle(d.anchor);
|
|
38871
|
+
const standardsPending = () => parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided.length > 0;
|
|
38872
|
+
const standardsAddedSince = (base) => {
|
|
38873
|
+
const change = changesSince(base).get(STANDARDS);
|
|
38874
|
+
if (change === void 0) return [];
|
|
38875
|
+
const was = new Set(parseStandards(change.before ?? "").decided);
|
|
38876
|
+
return parseStandards(change.after ?? "").decided.filter((s) => !was.has(s));
|
|
38877
|
+
};
|
|
38878
|
+
//#endregion
|
|
38879
|
+
//#region src/workflows/shared/review.ts
|
|
38595
38880
|
const VERDICTS = [
|
|
38596
38881
|
"edit",
|
|
38597
38882
|
"question",
|
|
@@ -38721,7 +39006,11 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
|
|
|
38721
39006
|
...report,
|
|
38722
39007
|
weakened: removedAssertions(changesSince(base)),
|
|
38723
39008
|
nitFix,
|
|
38724
|
-
answered
|
|
39009
|
+
answered,
|
|
39010
|
+
standards: {
|
|
39011
|
+
added: standardsAddedSince(base),
|
|
39012
|
+
undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
|
|
39013
|
+
}
|
|
38725
39014
|
}), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
|
|
38726
39015
|
label: "Composing the review",
|
|
38727
39016
|
file: REVIEW,
|
|
@@ -38731,10 +39020,11 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
|
|
|
38731
39020
|
* The quality lap's notes reach a human over everything since `base`, who
|
|
38732
39021
|
* answers each one — fix as suggested, won't fix, or their own answer — and
|
|
38733
39022
|
* may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
|
|
38734
|
-
* answer or comment is judged: edits
|
|
38735
|
-
* are answered in place, nits fixed in one batch,
|
|
39023
|
+
* answer or comment is judged: edits end the review as `feedback` for the
|
|
39024
|
+
* caller to build, questions are answered in place, nits fixed in one batch,
|
|
39025
|
+
* praise dropped.
|
|
38736
39026
|
*/
|
|
38737
|
-
const
|
|
39027
|
+
const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
38738
39028
|
const { guard, guardRests, fixAccess, rules } = options;
|
|
38739
39029
|
const feedback = options.feedback ?? {
|
|
38740
39030
|
file: ".gtd/REQUIREMENTS.md",
|
|
@@ -38744,7 +39034,7 @@ const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
38744
39034
|
const severities = /* @__PURE__ */ new Map();
|
|
38745
39035
|
let nitFix;
|
|
38746
39036
|
const answered = /* @__PURE__ */ new Set();
|
|
38747
|
-
return review$
|
|
39037
|
+
return review$1(base, {
|
|
38748
39038
|
file: REVIEW,
|
|
38749
39039
|
feedback: feedback.file,
|
|
38750
39040
|
isCode: isCode$1,
|
|
@@ -38765,6 +39055,9 @@ const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
38765
39055
|
collecting: feedback.collecting,
|
|
38766
39056
|
triage: triage$1(severities),
|
|
38767
39057
|
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
39058
|
+
settles: settlesStandard,
|
|
39059
|
+
learn: learnStandards,
|
|
39060
|
+
pending: standardsPending,
|
|
38768
39061
|
answerQuestions: answerReviewQuestions,
|
|
38769
39062
|
fixNits: async (notes) => {
|
|
38770
39063
|
await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
|
|
@@ -38797,7 +39090,7 @@ function buildTail(first, second, ...rest) {
|
|
|
38797
39090
|
const options = callForm(first, second, rest);
|
|
38798
39091
|
if (options === void 0) return Promise.reject(/* @__PURE__ */ new Error("buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither"));
|
|
38799
39092
|
const [base, form] = options;
|
|
38800
|
-
return tail((escalations, report) =>
|
|
39093
|
+
return tail((escalations, report) => humanReview(base, escalations, {
|
|
38801
39094
|
...form,
|
|
38802
39095
|
report
|
|
38803
39096
|
}), {
|
|
@@ -39750,7 +40043,7 @@ const settleQuality = async () => {
|
|
|
39750
40043
|
if (rested) await healthy(() => fixCheck());
|
|
39751
40044
|
};
|
|
39752
40045
|
/** The triaged quality lap, then the human review since `base`. */
|
|
39753
|
-
const reviewedTail = (base, lensBase) => tail((escalations, report) =>
|
|
40046
|
+
const reviewedTail = (base, lensBase) => tail((escalations, report) => humanReview(base, escalations, { report }), {
|
|
39754
40047
|
green: true,
|
|
39755
40048
|
lensBase,
|
|
39756
40049
|
triage: true
|
|
@@ -39829,6 +40122,7 @@ const skills = (vars) => {
|
|
|
39829
40122
|
return {
|
|
39830
40123
|
design: ["spec-driven-development", "planning-and-task-breakdown"],
|
|
39831
40124
|
notes: ["incremental-implementation", "code-simplification"],
|
|
40125
|
+
feedback: ["test-driven-development", "incremental-implementation"],
|
|
39832
40126
|
...build,
|
|
39833
40127
|
...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
|
|
39834
40128
|
refactor: ["ponytail"],
|
|
@@ -39862,15 +40156,34 @@ const steering = {
|
|
|
39862
40156
|
};
|
|
39863
40157
|
//#endregion
|
|
39864
40158
|
//#region src/workflows/review/review.ts
|
|
40159
|
+
/** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
|
|
40160
|
+
const fixFeedback = async () => {
|
|
40161
|
+
await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
|
|
40162
|
+
label: "Fixing your review feedback",
|
|
40163
|
+
file: REQUIREMENTS,
|
|
40164
|
+
model: coder(),
|
|
40165
|
+
system: finisherSystem(),
|
|
40166
|
+
allowEmpty: true
|
|
40167
|
+
});
|
|
40168
|
+
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
|
|
40169
|
+
};
|
|
39865
40170
|
/**
|
|
39866
|
-
*
|
|
39867
|
-
* lap, then the human review
|
|
39868
|
-
*
|
|
40171
|
+
* The feature's review of everything since `reviewBase` — a green gate, the
|
|
40172
|
+
* triaged quality lap, then the human review — with no design phase: a
|
|
40173
|
+
* review that asks for changes is fixed in place and reviewed again, until
|
|
40174
|
+
* sign-off.
|
|
39869
40175
|
*/
|
|
39870
40176
|
const reviewFlow = async () => {
|
|
39871
40177
|
refusePendingHandOff();
|
|
40178
|
+
if (read(".gtd/REQUIREMENTS.md") !== void 0) refuse(`gtd: ${REQUIREMENTS} is left over — finish or remove it before a review.`);
|
|
39872
40179
|
await gate("review-gate", reviewGateBlockedMessage());
|
|
39873
|
-
|
|
40180
|
+
const base = start();
|
|
40181
|
+
let outcome = await buildTail(base, { green: true });
|
|
40182
|
+
while (outcome.verdict === "feedback") {
|
|
40183
|
+
await reUnwind(outcome);
|
|
40184
|
+
await fixFeedback();
|
|
40185
|
+
outcome = await buildTail(base, { lensBase: outcome.base });
|
|
40186
|
+
}
|
|
39874
40187
|
};
|
|
39875
40188
|
const review = shipped(reviewFlow, { review: {
|
|
39876
40189
|
args: [{
|