@pmelab/gtd 42.0.0 → 43.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 +79 -35
- package/claude/hooks/sketch.ts +1 -1
- package/dist/chunks/{reserved-D7MNY9QJ.mjs → reserved-COEMFEpy.mjs} +35 -46
- package/dist/chunks/{steering-oCDdQlpP.mjs → steering-D4LUQ-av.mjs} +95 -12
- package/dist/chunks/{tail-D2o2h5cc.mjs → tail-C_xn8U7U.mjs} +1 -1
- package/dist/gtd.bundle.mjs +488 -30
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +447 -27
- package/dist/web/main.js +42 -13
- package/dist/workflow-kit.mjs +61 -25
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +31 -42
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +217 -121
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +33 -1
- package/src/cli/Cli.ts +70 -6
- package/src/cli/History.ts +15 -0
- package/src/cli/Judges.test.ts +373 -0
- package/src/cli/Judges.ts +624 -0
- package/src/cli/Metrics.ts +3 -16
- package/src/cli/Program.test.ts +3 -1
- package/src/cli/Program.ts +9 -0
- package/src/steering/index.ts +12 -1
- package/src/steering/review.test.ts +107 -0
- package/src/steering/review.ts +159 -9
- package/src/testkit/EmittedScriptRecognizer.ts +0 -14
- package/src/testkit/steps.ts +4 -0
- package/src/workflow-kit/review.test.ts +170 -26
- package/src/workflow-kit/review.ts +95 -19
- package/src/workflows/feature/lens-review.feature +16 -4
- package/src/workflows/feature/review-answers.feature +179 -0
- package/src/workflows/feature/review.test.ts +5 -53
- package/src/workflows/feature/review.ts +31 -42
- package/src/workflows/feature/steps.test.ts +0 -12
- package/src/workflows/feature/steps.ts +3 -10
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +6 -5
- package/src/workflows/shared/access.test.ts +2 -11
- package/src/workflows/shared/access.ts +0 -2
- package/src/workflows/shared/text.test.ts +40 -89
- package/src/workflows/shared/text.ts +52 -54
package/dist/workflows.mjs
CHANGED
|
@@ -13931,8 +13931,8 @@ const eolOf = (content) => content.includes("\r\n") ? "\r\n" : "\n";
|
|
|
13931
13931
|
const PREFIX_RE$1 = /^(H|A):\s*/;
|
|
13932
13932
|
const lineOf = (node, which) => toLspPosition(node.position[which]).line;
|
|
13933
13933
|
/** The item's own blocks joined — never the item node's span, which can run past its last line into the next bullet's marker. */
|
|
13934
|
-
const itemText = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
|
|
13935
|
-
const prefixOf = (content, item) => PREFIX_RE$1.exec(itemText(content, item));
|
|
13934
|
+
const itemText$1 = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
|
|
13935
|
+
const prefixOf = (content, item) => PREFIX_RE$1.exec(itemText$1(content, item));
|
|
13936
13936
|
/** A thread is a definition whose body OPENS with a list whose first item carries a prefix — anything else stays a one-shot footnote. */
|
|
13937
13937
|
const isThreadBody = (content, children) => {
|
|
13938
13938
|
const first = children[0];
|
|
@@ -13952,7 +13952,7 @@ const entryOf = (content, item, previous, report) => {
|
|
|
13952
13952
|
return;
|
|
13953
13953
|
}
|
|
13954
13954
|
const author = match[1] === "H" ? "me" : "agent";
|
|
13955
|
-
const text = itemText(content, item).slice(match[0].length).trim();
|
|
13955
|
+
const text = itemText$1(content, item).slice(match[0].length).trim();
|
|
13956
13956
|
if (text === "") report(`empty "${match[1]}:" entry — write your own words there`, start, end);
|
|
13957
13957
|
if (previous === void 0 && author === "agent") report(`the agent never starts a thread — the first entry must be "H:"`, start, end);
|
|
13958
13958
|
else if (previous === author) report(`two consecutive "${match[1]}:" entries — entries must alternate`, start, end);
|
|
@@ -15654,6 +15654,9 @@ const qaDescriptor = {
|
|
|
15654
15654
|
};
|
|
15655
15655
|
//#endregion
|
|
15656
15656
|
//#region src/steering/review.ts
|
|
15657
|
+
const ANSWER_LEAD = "Tick exactly one:";
|
|
15658
|
+
const ANSWER_FIX = "fix as suggested";
|
|
15659
|
+
const ANSWER_WONT_FIX = "won't fix";
|
|
15657
15660
|
/**
|
|
15658
15661
|
* The canonical sample. Every detail here is load-bearing for the formatter
|
|
15659
15662
|
* round-trip: both footnote bodies exceed 80 characters so a reflow actually
|
|
@@ -15920,16 +15923,42 @@ const parseHunk = (content, lines, title, item) => {
|
|
|
15920
15923
|
errors: [hunkSecondPointerFinding(content, lines, item, token, sourceLine, title, file), hunkLineWithoutRangeFinding(content, item, token, sourceLine, title, file)].filter((e) => e !== void 0)
|
|
15921
15924
|
};
|
|
15922
15925
|
};
|
|
15926
|
+
const itemText = (content, item) => item.children.map((c) => sourceText(content, c)).join(" ").replace(/^\[[ xX]\]\s*/, "").trim();
|
|
15927
|
+
/**
|
|
15928
|
+
* The task list right after the `Tick exactly one:` paragraph, with that
|
|
15929
|
+
* paragraph's index. Read by position, never by label, so an edited label or
|
|
15930
|
+
* a free text opening with `./path` is still an answer, not a pointer. Any
|
|
15931
|
+
* count but three boxes is kept, and reads as unanswered.
|
|
15932
|
+
*/
|
|
15933
|
+
const answerListOf = (content, body) => {
|
|
15934
|
+
const lead = body.findIndex((n, i) => n.type === "paragraph" && sourceText(content, n) === "Tick exactly one:" && body[i + 1]?.type === "list" && body[i + 1].children.every((item) => item.checked != null));
|
|
15935
|
+
if (lead === -1) return void 0;
|
|
15936
|
+
const list = body[lead + 1];
|
|
15937
|
+
const last = list.children.length - 1;
|
|
15938
|
+
return {
|
|
15939
|
+
lead,
|
|
15940
|
+
list,
|
|
15941
|
+
options: list.children.map((item, i) => {
|
|
15942
|
+
const text = itemText(content, item);
|
|
15943
|
+
return {
|
|
15944
|
+
checked: item.checked === true,
|
|
15945
|
+
text: i === last && text.toLowerCase() === "_your answer_".toLowerCase() ? "" : text
|
|
15946
|
+
};
|
|
15947
|
+
})
|
|
15948
|
+
};
|
|
15949
|
+
};
|
|
15923
15950
|
/** Splits one chunk's body nodes into its file pointers (hunk pointers are task items, collected recursively at ANY nesting depth via `taskItems` — a nested hunk is the same kind of hunk as a top-level one) and description prose. The description is the chunk's own leading run and never a node that contains a hunk pointer: it stops at the first node whose `taskItems` include a real pointer (at any depth — a blockquote wrapping a pointer list stops it just as a top-level list would). Every node kind in what's left before that point contributes — a `footnoteDefinition` is the one exception, excluded by `blockNodesOfRun` itself (it's a note, never document content). */
|
|
15924
15951
|
const parseChunkBody = (content, lines, title, body) => {
|
|
15925
|
-
const
|
|
15926
|
-
const
|
|
15952
|
+
const answer = answerListOf(content, body);
|
|
15953
|
+
const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
|
|
15954
|
+
const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
|
|
15955
|
+
const leadingRun = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
|
|
15927
15956
|
const descriptionNodes = blockNodesOfRun(content, leadingRun);
|
|
15928
15957
|
const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
|
|
15929
15958
|
const description = leadingRun.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
|
|
15930
15959
|
const files = [];
|
|
15931
15960
|
const errors = [];
|
|
15932
|
-
for (const item of body.flatMap((n) => taskItems(n))) {
|
|
15961
|
+
for (const item of body.filter((n) => !isAnswer(n)).flatMap((n) => taskItems(n))) {
|
|
15933
15962
|
const parsed = parseHunk(content, lines, title, item);
|
|
15934
15963
|
if (!parsed) continue;
|
|
15935
15964
|
files.push(parsed.file);
|
|
@@ -15940,7 +15969,9 @@ const parseChunkBody = (content, lines, title, body) => {
|
|
|
15940
15969
|
descriptionNodes,
|
|
15941
15970
|
descriptionInline,
|
|
15942
15971
|
files,
|
|
15943
|
-
errors
|
|
15972
|
+
errors,
|
|
15973
|
+
...answer === void 0 ? {} : { answers: answer.options },
|
|
15974
|
+
boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
|
|
15944
15975
|
};
|
|
15945
15976
|
};
|
|
15946
15977
|
/** Splits the tree's top-level nodes into consecutive `##` chunk blocks — a `## ` line found only inside a fence never becomes a heading node at all, so it's never mistaken for a chunk. */
|
|
@@ -15976,9 +16007,9 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
15976
16007
|
const errors = [];
|
|
15977
16008
|
for (const { heading, headingLine, body } of splitChunks(tree)) {
|
|
15978
16009
|
const title = headingText$1(content, heading);
|
|
15979
|
-
const { description, descriptionNodes, descriptionInline, files, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
16010
|
+
const { description, descriptionNodes, descriptionInline, files, answers, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
15980
16011
|
errors.push(...bodyErrors);
|
|
15981
|
-
if (files.length === 0 &&
|
|
16012
|
+
if (files.length === 0 && boxed) errors.push({
|
|
15982
16013
|
message: `Chunk "${title}" has no file pointers`,
|
|
15983
16014
|
line: headingLine,
|
|
15984
16015
|
range: nodeRange(heading)
|
|
@@ -15989,7 +16020,8 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
15989
16020
|
descriptionNodes,
|
|
15990
16021
|
descriptionInline,
|
|
15991
16022
|
files,
|
|
15992
|
-
headingLine
|
|
16023
|
+
headingLine,
|
|
16024
|
+
...answers === void 0 ? {} : { answers }
|
|
15993
16025
|
});
|
|
15994
16026
|
}
|
|
15995
16027
|
if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
|
|
@@ -16070,7 +16102,9 @@ const isHunkPointerItem = (content, item) => {
|
|
|
16070
16102
|
* clear, so a caller can skip the write and leave the mtime untouched.
|
|
16071
16103
|
*/
|
|
16072
16104
|
const reviewClearTicks = (content) => {
|
|
16073
|
-
const
|
|
16105
|
+
const tree = parseMarkdown(content);
|
|
16106
|
+
const answers = new Set(splitChunks(tree).flatMap(({ body }) => answerListOf(content, body)?.list.children ?? []));
|
|
16107
|
+
const offsets = taskItems(tree).filter((item) => item.checked === true && !answers.has(item) && isHunkPointerItem(content, item)).map((item) => checkboxOffset(content, item)).filter((o) => o !== void 0).sort((a, b) => a - b);
|
|
16074
16108
|
if (offsets.length === 0) return content;
|
|
16075
16109
|
let result = "";
|
|
16076
16110
|
let cursor = 0;
|
|
@@ -16504,6 +16538,55 @@ const reviewRisks = (content) => parseReviewDoc(content).changesets.flatMap((chu
|
|
|
16504
16538
|
id: `risk-${i + 1}`,
|
|
16505
16539
|
...r
|
|
16506
16540
|
}));
|
|
16541
|
+
const CHOICES = [
|
|
16542
|
+
"fix",
|
|
16543
|
+
"wontfix",
|
|
16544
|
+
"answer"
|
|
16545
|
+
];
|
|
16546
|
+
const decisionOf = (chunk) => {
|
|
16547
|
+
if (chunk.answers?.length !== 3) return void 0;
|
|
16548
|
+
const ticked = chunk.answers.flatMap((o, i) => o.checked ? [i] : []);
|
|
16549
|
+
if (ticked.length !== 1) return void 0;
|
|
16550
|
+
const i = ticked[0];
|
|
16551
|
+
const text = i === 2 ? chunk.answers[2].text : "";
|
|
16552
|
+
if (i === 2 && text === "") return void 0;
|
|
16553
|
+
return {
|
|
16554
|
+
anchor: chunk.title,
|
|
16555
|
+
body: chunk.description,
|
|
16556
|
+
choice: CHOICES[i],
|
|
16557
|
+
text
|
|
16558
|
+
};
|
|
16559
|
+
};
|
|
16560
|
+
/**
|
|
16561
|
+
* Notes whose answer boxes do not hold exactly one tick (a ticked free-text
|
|
16562
|
+
* slot filled in), with their 1-based heading line and why; and, given the
|
|
16563
|
+
* file `before` the edit, every note whose boxes were deleted.
|
|
16564
|
+
*/
|
|
16565
|
+
const unansweredNotes = (content, before) => {
|
|
16566
|
+
const now = parseReviewDoc(content).changesets;
|
|
16567
|
+
const boxed = new Set(now.filter((c) => c.answers !== void 0).map((c) => c.title));
|
|
16568
|
+
const removed = (before === void 0 ? [] : parseReviewDoc(before).changesets).filter((c) => c.answers !== void 0 && !boxed.has(c.title)).map((c) => ({
|
|
16569
|
+
title: c.title,
|
|
16570
|
+
line: 0,
|
|
16571
|
+
reason: "its answer boxes were deleted — restore them"
|
|
16572
|
+
}));
|
|
16573
|
+
return [...now.filter((c) => c.answers !== void 0 && decisionOf(c) === void 0).map((c) => ({
|
|
16574
|
+
title: c.title,
|
|
16575
|
+
line: c.headingLine + 1,
|
|
16576
|
+
reason: c.answers.length === 3 ? "tick exactly one box" : `it has ${c.answers.length} boxes — restore the three gtd wrote`
|
|
16577
|
+
})), ...removed];
|
|
16578
|
+
};
|
|
16579
|
+
/** Every answer in `after` that `before` did not already hold for the same note, in document order. */
|
|
16580
|
+
const reviewDecisions = (before, after) => {
|
|
16581
|
+
const prior = new Map(parseReviewDoc(before).changesets.map((c) => [c.title, c]));
|
|
16582
|
+
return parseReviewDoc(after).changesets.flatMap((chunk) => {
|
|
16583
|
+
const decision = decisionOf(chunk);
|
|
16584
|
+
if (decision === void 0) return [];
|
|
16585
|
+
const was = prior.get(chunk.title);
|
|
16586
|
+
const old = was === void 0 ? void 0 : decisionOf(was);
|
|
16587
|
+
return old?.choice === decision.choice && old.text === decision.text ? [] : [decision];
|
|
16588
|
+
});
|
|
16589
|
+
};
|
|
16507
16590
|
//#endregion
|
|
16508
16591
|
//#region src/steering/CodeThreads.ts
|
|
16509
16592
|
const TOKENS = [
|
|
@@ -16635,6 +16718,13 @@ const collect = async (p, capture, round, requested) => {
|
|
|
16635
16718
|
}
|
|
16636
16719
|
return refuse(p.refusals.unmatched);
|
|
16637
16720
|
};
|
|
16721
|
+
/** Refuse while a note's answer boxes hold anything but exactly one tick, or a note's boxes were deleted. */
|
|
16722
|
+
const requireDecisions = (file, reviewed) => {
|
|
16723
|
+
const open = unansweredNotes(read(file) ?? "", changesSince(reviewed).get(file)?.before);
|
|
16724
|
+
if (open.length === 0) return;
|
|
16725
|
+
const list = open.map((n) => ` - ${file}${n.line > 0 ? `:${n.line}` : ""}: ${n.title} — ${n.reason}`).join("\n");
|
|
16726
|
+
refuse(`gtd land: answer-completeness: ${open.length} note(s) in ${file} not answered — tick exactly one box per note: fix as suggested, won't fix, or your own answer written over _your answer_:\n${list}`);
|
|
16727
|
+
};
|
|
16638
16728
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16639
16729
|
/**
|
|
16640
16730
|
* A review with ticks and THREAD footnotes (definition plus markers) removed:
|
|
@@ -16654,7 +16744,7 @@ const notesOf = (text) => {
|
|
|
16654
16744
|
* commit of the last collecting turn, `"missing"` when the round left no
|
|
16655
16745
|
* review, or `undefined` when no such turn ran.
|
|
16656
16746
|
*/
|
|
16657
|
-
const converse = async (p, base, collectedBefore) => {
|
|
16747
|
+
const converse = async (p, base, reviewed, collectedBefore) => {
|
|
16658
16748
|
let collectedAt = collectedBefore;
|
|
16659
16749
|
for (;;) {
|
|
16660
16750
|
await p.awaitReview(base);
|
|
@@ -16664,24 +16754,47 @@ const converse = async (p, base, collectedBefore) => {
|
|
|
16664
16754
|
return "missing";
|
|
16665
16755
|
}
|
|
16666
16756
|
requireThreadsClosed(p.file);
|
|
16667
|
-
if (!hasThreadFor("agent", p.file))
|
|
16757
|
+
if (!hasThreadFor("agent", p.file)) {
|
|
16758
|
+
requireDecisions(p.file, reviewed);
|
|
16759
|
+
return collectedAt;
|
|
16760
|
+
}
|
|
16668
16761
|
await p.collecting(p.captures.edits(head$1()), "reply");
|
|
16669
16762
|
collectedAt = head$1();
|
|
16670
16763
|
requireReplies(p.file);
|
|
16671
|
-
if (!hasThreadFor("human", p.file)) return collectedAt;
|
|
16672
|
-
}
|
|
16673
|
-
};
|
|
16674
|
-
|
|
16675
|
-
|
|
16676
|
-
|
|
16677
|
-
|
|
16678
|
-
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
16684
|
-
|
|
16764
|
+
if (!hasThreadFor("human", p.file) && unansweredNotes(read(p.file) ?? "", changesSince(reviewed).get(p.file)?.before).length === 0) return collectedAt;
|
|
16765
|
+
}
|
|
16766
|
+
};
|
|
16767
|
+
const decisionNote = (d, id, text) => ({
|
|
16768
|
+
id,
|
|
16769
|
+
kind: "chunk",
|
|
16770
|
+
anchor: d.anchor,
|
|
16771
|
+
before: d.body,
|
|
16772
|
+
text,
|
|
16773
|
+
forced: /^!(\s|$)/.test(text)
|
|
16774
|
+
});
|
|
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";
|
|
16777
|
+
/** Tell the policy, and mark each won't-fix on the gate's commit. */
|
|
16778
|
+
const record$2 = (p, decisions) => {
|
|
16779
|
+
p.decided?.(decisions);
|
|
16780
|
+
for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine(d.anchor));
|
|
16781
|
+
};
|
|
16782
|
+
/**
|
|
16783
|
+
* Judge every note, then run each verdict's route: answers, then nit fixes,
|
|
16784
|
+
* then the edits' lap. Of the human's `decisions`, a fix joins the nit batch
|
|
16785
|
+
* unjudged, an own answer is judged like a note, and a won't-fix is dropped
|
|
16786
|
+
* with a `Gtd-Mark` on the gate's commit.
|
|
16787
|
+
*/
|
|
16788
|
+
const routeNotes = async (p, notes, r, decisions = []) => {
|
|
16789
|
+
record$2(p, decisions);
|
|
16790
|
+
const of = (c) => decisions.filter((d) => d.choice === c);
|
|
16791
|
+
const fixes = of("fix").map((d, i) => decisionNote(d, `fix-${i + 1}`, d.body));
|
|
16792
|
+
const judged = [...notes, ...of("answer").map((d, i) => decisionNote(d, `answer-${i + 1}`, d.text))];
|
|
16793
|
+
const verdicts = judged.length === 0 ? /* @__PURE__ */ new Map() : await p.triage(judged);
|
|
16794
|
+
const ofVerdict = (v) => judged.filter((n) => (verdicts.get(n.id) ?? "edit") === v);
|
|
16795
|
+
const [edits, questions] = [ofVerdict("edit"), ofVerdict("question")];
|
|
16796
|
+
const nits = [...ofVerdict("nit"), ...fixes];
|
|
16797
|
+
if (edits.length + questions.length + nits.length === 0) return r.unfolded();
|
|
16685
16798
|
let answeredAt;
|
|
16686
16799
|
if (questions.length > 0) {
|
|
16687
16800
|
await p.answerQuestions(questions);
|
|
@@ -16691,7 +16804,6 @@ const routeNotes = async (p, notes, r) => {
|
|
|
16691
16804
|
await p.fixNits(nits);
|
|
16692
16805
|
await p.keepGreen();
|
|
16693
16806
|
}
|
|
16694
|
-
if (deferred.length > 0) await p.defer(deferred);
|
|
16695
16807
|
if (edits.length > 0) {
|
|
16696
16808
|
await r.close();
|
|
16697
16809
|
return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt), "triaged", {
|
|
@@ -16713,8 +16825,8 @@ const routeNotes = async (p, notes, r) => {
|
|
|
16713
16825
|
};
|
|
16714
16826
|
/** A code edit, not a thread-only change: lines that only add, answer or remove code threads don't count. */
|
|
16715
16827
|
const isCodeEdit = (p, c) => p.isCode(c.path) && stripCodeThreads(c.path, c.before ?? "") !== stripCodeThreads(c.path, c.after ?? "");
|
|
16716
|
-
/** Whether
|
|
16717
|
-
const notedSince = (p, commit) => changesSince(commit).some((c) => c.path === p.file && notesOf(c.before ?? "") !== notesOf(read(p.file) ?? ""));
|
|
16828
|
+
/** Whether a note or an answer, not just thread edits and pointer ticks, landed in the review file since `commit`. */
|
|
16829
|
+
const notedSince = (p, commit) => changesSince(commit).some((c) => c.path === p.file && (notesOf(c.before ?? "") !== notesOf(read(p.file) ?? "") || reviewDecisions(c.before ?? "", read(p.file) ?? "").length > 0));
|
|
16718
16830
|
/**
|
|
16719
16831
|
* What the in-loop collecting turn left: `folded` when it wrote `feedback`
|
|
16720
16832
|
* (those folds need their lap whatever is judged later), `settled` when only
|
|
@@ -16728,9 +16840,9 @@ const settledAfter = (p, collectedAt) => ({
|
|
|
16728
16840
|
folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
|
|
16729
16841
|
settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
|
|
16730
16842
|
});
|
|
16731
|
-
const finishNotes = async (p, before, reopened, ctx) => {
|
|
16732
|
-
const notes = reviewNotes(before, read(p.file) ?? "");
|
|
16733
|
-
if (notes.length > 0) return routeNotes(p, notes, ctx);
|
|
16843
|
+
const finishNotes = async (p, before, decisions, reopened, ctx) => {
|
|
16844
|
+
const notes = reviewNotes(before ?? "", read(p.file) ?? "");
|
|
16845
|
+
if (notes.length + decisions.length > 0) return routeNotes(p, notes, ctx, decisions);
|
|
16734
16846
|
if (reopened) return ctx.unfolded();
|
|
16735
16847
|
await ctx.close();
|
|
16736
16848
|
return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"));
|
|
@@ -16740,6 +16852,11 @@ const toOutcome = (verdict, rest) => verdict === "signoff" ? { verdict } : {
|
|
|
16740
16852
|
...rest
|
|
16741
16853
|
};
|
|
16742
16854
|
const isReopened = (p, r) => r.settled && !r.folded && p.refusals.dropped !== void 0;
|
|
16855
|
+
/** A carried answer is unchanged, never routed twice; one ticked beside a thread still routes once the thread has settled. */
|
|
16856
|
+
const decisionsSince = (p, since) => {
|
|
16857
|
+
const after = read(p.file) ?? "";
|
|
16858
|
+
return reviewDecisions(since.get(p.file)?.before ?? after, after);
|
|
16859
|
+
};
|
|
16743
16860
|
const finish = async (p, reviewed, collectedAt) => {
|
|
16744
16861
|
const round = head$1();
|
|
16745
16862
|
const since = changesSince(reviewed);
|
|
@@ -16760,15 +16877,17 @@ const finish = async (p, reviewed, collectedAt) => {
|
|
|
16760
16877
|
settled,
|
|
16761
16878
|
folded
|
|
16762
16879
|
});
|
|
16763
|
-
|
|
16880
|
+
const decisions = decisionsSince(p, since);
|
|
16881
|
+
if (settled && !reopened && decisions.length === 0 || edited.length === 0 && !noted) return unfolded();
|
|
16764
16882
|
if (edited.length > 0) {
|
|
16883
|
+
record$2(p, decisions);
|
|
16765
16884
|
await close();
|
|
16766
16885
|
return outcome(await collect(p, p.captures.edits(round, reopened ? reviewed : void 0), "edits", {
|
|
16767
16886
|
notes: [],
|
|
16768
16887
|
paths: edited.map((c) => c.path)
|
|
16769
16888
|
}));
|
|
16770
16889
|
}
|
|
16771
|
-
return finishNotes(p, since.get(p.file)?.before
|
|
16890
|
+
return finishNotes(p, since.get(p.file)?.before, decisions, reopened, {
|
|
16772
16891
|
round,
|
|
16773
16892
|
close,
|
|
16774
16893
|
outcome,
|
|
@@ -16798,7 +16917,7 @@ const review$2 = async (base, p) => {
|
|
|
16798
16917
|
let reviewed = head$1();
|
|
16799
16918
|
let collectedAt;
|
|
16800
16919
|
for (;;) {
|
|
16801
|
-
collectedAt = await converse(p, base, collectedAt);
|
|
16920
|
+
collectedAt = await converse(p, base, reviewed, collectedAt);
|
|
16802
16921
|
if (collectedAt === "missing") break;
|
|
16803
16922
|
const result = await finish(p, reviewed, collectedAt);
|
|
16804
16923
|
if (!("next" in result)) return result;
|
|
@@ -17278,9 +17397,9 @@ const SUMMARY_TITLES = [
|
|
|
17278
17397
|
const unboxed = (body) => body.replace(/^(\s*(?:>\s*)*(?:[-*+]|\d+[.)])\s+)\[[ xX]\]\s?/gm, "$1");
|
|
17279
17398
|
const short = (hash) => hash.slice(0, 7);
|
|
17280
17399
|
/**
|
|
17281
|
-
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note,
|
|
17282
|
-
*
|
|
17283
|
-
* matched to their chunk by title.
|
|
17400
|
+
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
|
|
17401
|
+
* answer boxes, then the fixed and noise lists. Chunk titles are made unique —
|
|
17402
|
+
* notes and answers are matched to their chunk by title.
|
|
17284
17403
|
*/
|
|
17285
17404
|
const composedReview = (base, report) => {
|
|
17286
17405
|
const used = new Set(SUMMARY_TITLES);
|
|
@@ -17291,11 +17410,16 @@ const composedReview = (base, report) => {
|
|
|
17291
17410
|
return name;
|
|
17292
17411
|
};
|
|
17293
17412
|
const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
|
|
17413
|
+
const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
|
|
17414
|
+
const notes = report.notes.map((n) => ({
|
|
17415
|
+
...n,
|
|
17416
|
+
title: unique(n.title)
|
|
17417
|
+
})).filter((n) => report.answered?.has(n.title) !== true);
|
|
17294
17418
|
const weakened = (report.weakened ?? []).flatMap(({ path, lines }) => lines.map((a) => `- ${path} — ${a.scenario} — ${a.line}`));
|
|
17295
17419
|
const chunks = [
|
|
17296
|
-
...report.nitFix === void 0 ? [] : [chunk("Fixed from your notes", `Your nits were
|
|
17420
|
+
...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.`)],
|
|
17297
17421
|
...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
|
|
17298
|
-
...
|
|
17422
|
+
...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}`)),
|
|
17299
17423
|
...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
|
|
17300
17424
|
...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
|
|
17301
17425
|
];
|
|
@@ -17378,10 +17502,6 @@ ${stateFileRules}
|
|
|
17378
17502
|
${footnoteFoldIn}${codeThreadReplies()}
|
|
17379
17503
|
- The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
|
|
17380
17504
|
— no other files for notes or output
|
|
17381
|
-
- Lines of a \`## Deferred review notes\` section of \`.gtd/TODO.md\`
|
|
17382
|
-
that the start diff does not touch are parked by review, never folded;
|
|
17383
|
-
the human's edits to such a line (or its removal) are input like any
|
|
17384
|
-
other part of the start diff
|
|
17385
17505
|
- \`.gtd/TODO.md\` is the likely home of the sketch that started
|
|
17386
17506
|
this process — the human's input, folded into the concerns
|
|
17387
17507
|
below like any other part of the start diff; never a state
|
|
@@ -17674,8 +17794,12 @@ const buildReviewAwaitReviewMessage = (base) => `\`.gtd/REVIEW.md\` holds what t
|
|
|
17674
17794
|
per finding that needs your judgement — it questions a criterion or the
|
|
17675
17795
|
approach, or the fixer could not resolve it — then, for reference only, what
|
|
17676
17796
|
was **fixed automatically** (with the fixing commit) and what was **dropped
|
|
17677
|
-
as noise**.
|
|
17678
|
-
|
|
17797
|
+
as noise**. Each finding chunk ends in three boxes — tick exactly one:
|
|
17798
|
+
\`fix as suggested\` (fixed in the nit batch below), \`won't fix\` (dropped,
|
|
17799
|
+
recorded in the commit), or write your own answer over \`_your answer_\` and
|
|
17800
|
+
tick it (judged like a note). Every landing here is refused until each
|
|
17801
|
+
finding holds exactly one tick. The fixed and noise lists need no answer;
|
|
17802
|
+
disagreeing with a fix or a drop means writing a note of your own.
|
|
17679
17803
|
|
|
17680
17804
|
Review the diff yourself, with whatever tool you like — gtd checks
|
|
17681
17805
|
nothing out and touches no ref; \`gtd base\` prints this same hash any
|
|
@@ -17685,8 +17809,8 @@ working tree:
|
|
|
17685
17809
|
git diff ${base}
|
|
17686
17810
|
When you've been through the whole diff, run \`gtd land\`:
|
|
17687
17811
|
|
|
17688
|
-
- **Sign off** — leave no comment — no note
|
|
17689
|
-
\`.gtd/REVIEW.md\`, no code edit — to close the process.
|
|
17812
|
+
- **Sign off** — answer every finding, leave no other comment — no note
|
|
17813
|
+
in \`.gtd/REVIEW.md\`, no code edit — to close the process.
|
|
17690
17814
|
Every turn commit stays on the
|
|
17691
17815
|
branch; run \`gtd summary\` afterward for a closing-message
|
|
17692
17816
|
prompt.
|
|
@@ -17833,27 +17957,6 @@ const wrapItem = (item) => {
|
|
|
17833
17957
|
} else line = line === "" ? word : `${line} ${word}`;
|
|
17834
17958
|
return [...out, line];
|
|
17835
17959
|
};
|
|
17836
|
-
/** Appends one section of `notes` to `.gtd/TODO.md`, wrapped the way oxfmt writes it; an existing file is only ever appended to. */
|
|
17837
|
-
const deferNotesScript = (notes, round) => {
|
|
17838
|
-
return `mkdir -p .gtd
|
|
17839
|
-
if [ -s .gtd/TODO.md ]; then
|
|
17840
|
-
[ -z "$(tail -c1 .gtd/TODO.md)" ] || printf '\\n' >> .gtd/TODO.md
|
|
17841
|
-
printf '\\n' >> .gtd/TODO.md
|
|
17842
|
-
fi
|
|
17843
|
-
printf '%s\\n' ${[
|
|
17844
|
-
`## Deferred review notes (${round.slice(0, 7)})`,
|
|
17845
|
-
"",
|
|
17846
|
-
...notes.flatMap((n) => wrapItem(`- ${noteHead(n)}: ${n.text.replace(/\s+/g, " ")}`))
|
|
17847
|
-
].map(quote).join(" ")} >> .gtd/TODO.md
|
|
17848
|
-
`;
|
|
17849
|
-
};
|
|
17850
|
-
/** The gate right after a deferral: landing clean signs off without these notes. */
|
|
17851
|
-
const buildReviewAwaitAfterDeferMessage = (base, deferred) => `${buildReviewAwaitReviewMessage(base)}
|
|
17852
|
-
Deferred to \`.gtd/TODO.md\` just now, NOT acted on in this process — landing
|
|
17853
|
-
clean signs off without them:
|
|
17854
|
-
|
|
17855
|
-
${deferred.map((n) => `- ${noteHead(n)}: ${n.text.replace(/\s*\n\s*/g, " ")}`).join("\n")}
|
|
17856
|
-
`;
|
|
17857
17960
|
const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
|
|
17858
17961
|
footnote, or prose under a chunk) with one verdict: \`edit\` (a change
|
|
17859
17962
|
request), \`question\`, \`nit\` (a small fix needing no re-plan) or
|
|
@@ -17863,16 +17966,18 @@ default; a skipped judgment never dismisses a note).
|
|
|
17863
17966
|
|
|
17864
17967
|
Also judge each note's severity, one id per note: \`note-<n>.security\`,
|
|
17865
17968
|
\`note-<n>.reversibility\`, \`note-<n>.blastRadius\` and
|
|
17866
|
-
\`note-<n>.newConcept\`.
|
|
17969
|
+
\`note-<n>.newConcept\`. An own answer under a finding's boxes is judged
|
|
17970
|
+
the same way, keyed \`answer-<n>\`.
|
|
17867
17971
|
`;
|
|
17868
17972
|
const closedFold = (round, target) => round === "edits" ? `- Fold every hand-edit and every note the human left that round into
|
|
17869
|
-
\`${target}\`
|
|
17973
|
+
\`${target}\` — a note ticked \`fix as suggested\` or answered in its
|
|
17974
|
+
own words included; one ticked \`won't fix\` is dropped
|
|
17870
17975
|
` : round === "notes" ? `- Fold each note that asks for a change into \`${target}\`
|
|
17871
17976
|
` : `- Fold each \`edit\` note the capture lists, no other note, and every
|
|
17872
17977
|
hand-edit, into \`${target}\`
|
|
17873
17978
|
`;
|
|
17874
17979
|
/** `refactor` retargets the prompt at `.gtd/REFACTOR.md` and fences it to the strays. A concern outside the fence is not folded; if nothing at all is written the landing is refused, but a partly buildable round drops the rest silently. */
|
|
17875
|
-
const buildReviewCollectingPrompt = (capture, refactor) => {
|
|
17980
|
+
const buildReviewCollectingPrompt = (capture, refactor, round) => {
|
|
17876
17981
|
const target = refactor ? ".gtd/REFACTOR.md" : ".gtd/REQUIREMENTS.md";
|
|
17877
17982
|
const fence = refactor ? `- The next refactor may change only these paths, plus an existing test
|
|
17878
17983
|
file's call sites, and never a flag read:
|
|
@@ -17947,12 +18052,16 @@ ${refactor ? refactor.round === "reply" ? `- This turn replies inside \`.gtd/REV
|
|
|
17947
18052
|
${closedFold(refactor.round, target)}` : `- This turn writes \`${target}\` (the folded concerns) and
|
|
17948
18053
|
replies inside \`.gtd/REVIEW.md\` (thread replies only; the file stays
|
|
17949
18054
|
in the tree) — you classify, you do not build
|
|
17950
|
-
- Fold every concluded thread
|
|
17951
|
-
|
|
17952
|
-
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
18055
|
+
- Fold every concluded thread and hand-edit — and each \`edit\` note
|
|
18056
|
+
the capture lists, no other note — into \`${target}\` and
|
|
18057
|
+
delete the folded threads from \`.gtd/REVIEW.md\`; append one \`- A:\`
|
|
18058
|
+
reply to each thread whose last entry is a \`- H:\` question
|
|
18059
|
+
${round === "edits" ? `- A hand-edit round routes no answer box itself: fold each note
|
|
18060
|
+
ticked \`fix as suggested\` or answered in its own words, too; drop one
|
|
18061
|
+
ticked \`won't fix\`
|
|
18062
|
+
` : `- Never fold a note's ticked answer box the capture does not list: gtd
|
|
18063
|
+
routes those itself
|
|
18064
|
+
`}`}- When the capture names an answering commit, carry each answered
|
|
17956
18065
|
question into \`${target}\`'s \`## Answered Questions\` as
|
|
17957
18066
|
\`### <the note>\` plus its answer — the lap replaces \`.gtd/REVIEW.md\`
|
|
17958
18067
|
- Keep capture order, and carry each note's \`(tier: reason)\`
|
|
@@ -38437,7 +38546,7 @@ const fixNits = (notes, access, rules) => {
|
|
|
38437
38546
|
}, () => fixing("fixing"));
|
|
38438
38547
|
};
|
|
38439
38548
|
/** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
|
|
38440
|
-
const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
|
|
38549
|
+
const collecting = (capture, round) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, void 0, round), {
|
|
38441
38550
|
label: "Collecting your feedback",
|
|
38442
38551
|
file: REQUIREMENTS,
|
|
38443
38552
|
mode: "qa",
|
|
@@ -38456,11 +38565,6 @@ const refactorCollecting = (capture, round, strays) => agentWithSkills("review.c
|
|
|
38456
38565
|
system: reviewerSystem(),
|
|
38457
38566
|
allowEmpty: true
|
|
38458
38567
|
});
|
|
38459
|
-
/** `build.review.defer`: append `notes` to `.gtd/TODO.md` as one section; the step's landing is the commit. */
|
|
38460
|
-
const deferNotes = (notes, round) => run("review.defer", deferNotesScript(notes, round), {
|
|
38461
|
-
label: "Deferring could-notes to TODO.md",
|
|
38462
|
-
file: ".gtd/TODO.md"
|
|
38463
|
-
});
|
|
38464
38568
|
//#endregion
|
|
38465
38569
|
//#region src/workflows/feature/planning.ts
|
|
38466
38570
|
/**
|
|
@@ -38501,6 +38605,13 @@ const verdictQuestion = (note) => ({
|
|
|
38501
38605
|
instructions: `Classify the human's note on review item "${note.anchor}" (in .gtd/REVIEW.md). Its evidence holds the anchor, the reviewer's text and the human's text.`,
|
|
38502
38606
|
criteria: "edit: a request to change behaviour, design or scope — anything needing a plan, or any note you are unsure about. question: asks something and wants an answer, with no change requested. nit: a small, local, unambiguous fix (naming, typo, formatting, comment wording) needing no re-plan. praise: an approving remark with nothing to do."
|
|
38503
38607
|
});
|
|
38608
|
+
const TIER_ORDER = {
|
|
38609
|
+
critical: 0,
|
|
38610
|
+
should: 1,
|
|
38611
|
+
could: 2
|
|
38612
|
+
};
|
|
38613
|
+
/** Capture order: critical, should, could; within a tier one-way doors, then new concepts. */
|
|
38614
|
+
const captureKey = (s) => s === void 0 ? 2 : TIER_ORDER[s.tier] * 3 + s.rank;
|
|
38504
38615
|
const SEVERITY_ANSWERS = {
|
|
38505
38616
|
security: ["yes", "no"],
|
|
38506
38617
|
reversibility: ["one-way", "two-way"],
|
|
@@ -38511,10 +38622,7 @@ const SEVERITY_ANSWERS = {
|
|
|
38511
38622
|
],
|
|
38512
38623
|
newConcept: ["yes", "no"]
|
|
38513
38624
|
};
|
|
38514
|
-
/**
|
|
38515
|
-
* Deferring is the risky direction: anything unanswered, unrecognised, cut or
|
|
38516
|
-
* below `minP` is `critical`, never `could`.
|
|
38517
|
-
*/
|
|
38625
|
+
/** Anything unanswered, unrecognised, cut or below `minP` is `critical`, never `could`. */
|
|
38518
38626
|
const severityOf = (note, judgment, minP) => {
|
|
38519
38627
|
const ans = (q) => judgment.answers[`${note.id}.${q}`];
|
|
38520
38628
|
const a = (q) => ans(q)?.answer;
|
|
@@ -38578,7 +38686,7 @@ const severityQuestions = (note) => [
|
|
|
38578
38686
|
* nothing) plus four severity answers. Dismissing a note is the risky
|
|
38579
38687
|
* direction, so only a confident non-`edit` verdict on uncut evidence counts;
|
|
38580
38688
|
* a cut, unanswered or below-floor note is an `edit`. Severity answers on a
|
|
38581
|
-
* non-`edit` note are ignored
|
|
38689
|
+
* non-`edit` note are ignored; on an `edit` they order the collecting turn.
|
|
38582
38690
|
*/
|
|
38583
38691
|
const triage$1 = (severities) => async (notes) => {
|
|
38584
38692
|
const asked = notes.filter((n) => !n.forced);
|
|
@@ -38598,21 +38706,22 @@ const triage$1 = (severities) => async (notes) => {
|
|
|
38598
38706
|
return new Map(notes.map((n) => {
|
|
38599
38707
|
const verdict = n.forced ? void 0 : VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
|
|
38600
38708
|
if (verdict !== void 0 && !truncated.includes(n.id)) return [n.id, verdict];
|
|
38601
|
-
|
|
38602
|
-
|
|
38603
|
-
return [n.id, severity.tier === "could" ? "defer" : "edit"];
|
|
38709
|
+
severities.set(n.id, severityOf(n, judgment, severityMinP));
|
|
38710
|
+
return [n.id, "edit"];
|
|
38604
38711
|
}));
|
|
38605
38712
|
};
|
|
38606
38713
|
const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
38607
38714
|
/**
|
|
38608
38715
|
* gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
|
|
38609
38716
|
* turn — and removes what the fixer left in `.gtd/QUALITY.md`. A re-review
|
|
38610
|
-
* after fixed nits restores the doc as committed at `carry`, answers kept
|
|
38717
|
+
* after fixed nits restores the doc as committed at `carry`, answers kept;
|
|
38718
|
+
* a fresh one leaves out the notes the human already answered.
|
|
38611
38719
|
*/
|
|
38612
|
-
const composing = (base, report, carry, nitFix) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
|
|
38720
|
+
const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new Set()) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
|
|
38613
38721
|
...report,
|
|
38614
38722
|
weakened: removedAssertions(changesSince(base)),
|
|
38615
|
-
nitFix
|
|
38723
|
+
nitFix,
|
|
38724
|
+
answered
|
|
38616
38725
|
}), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
|
|
38617
38726
|
label: "Composing the review",
|
|
38618
38727
|
file: REVIEW,
|
|
@@ -38620,9 +38729,10 @@ const composing = (base, report, carry, nitFix) => run("review.composing", carry
|
|
|
38620
38729
|
});
|
|
38621
38730
|
/**
|
|
38622
38731
|
* The quality lap's notes reach a human over everything since `base`, who
|
|
38623
|
-
*
|
|
38624
|
-
*
|
|
38625
|
-
*
|
|
38732
|
+
* answers each one — fix as suggested, won't fix, or their own answer — and
|
|
38733
|
+
* may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
|
|
38734
|
+
* answer or comment is judged: edits go to another planning lap, questions
|
|
38735
|
+
* are answered in place, nits fixed in one batch, praise dropped.
|
|
38626
38736
|
*/
|
|
38627
38737
|
const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
38628
38738
|
const { guard, guardRests, fixAccess, rules } = options;
|
|
@@ -38632,17 +38742,16 @@ const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
38632
38742
|
unmatched: "gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing"
|
|
38633
38743
|
};
|
|
38634
38744
|
const severities = /* @__PURE__ */ new Map();
|
|
38635
|
-
let deferred = [];
|
|
38636
38745
|
let nitFix;
|
|
38746
|
+
const answered = /* @__PURE__ */ new Set();
|
|
38637
38747
|
return review$2(base, {
|
|
38638
38748
|
file: REVIEW,
|
|
38639
38749
|
feedback: feedback.file,
|
|
38640
38750
|
isCode: isCode$1,
|
|
38641
38751
|
reviewing: (b, carry) => {
|
|
38642
|
-
deferred = [];
|
|
38643
38752
|
const fixedAt = nitFix;
|
|
38644
38753
|
nitFix = void 0;
|
|
38645
|
-
return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt);
|
|
38754
|
+
return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt, answered);
|
|
38646
38755
|
},
|
|
38647
38756
|
fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
|
|
38648
38757
|
keepGreen: () => healthy(() => fixCheck(rules), {
|
|
@@ -38651,23 +38760,11 @@ const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
38651
38760
|
guardRests,
|
|
38652
38761
|
rules
|
|
38653
38762
|
}),
|
|
38654
|
-
awaitReview: (b) =>
|
|
38655
|
-
const notes = deferred;
|
|
38656
|
-
deferred = [];
|
|
38657
|
-
return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)) : awaitReview(b);
|
|
38658
|
-
},
|
|
38763
|
+
awaitReview: (b) => awaitReview(b),
|
|
38659
38764
|
reviewMissing,
|
|
38660
38765
|
collecting: feedback.collecting,
|
|
38661
38766
|
triage: triage$1(severities),
|
|
38662
|
-
|
|
38663
|
-
deferred = notes.map((n) => ({
|
|
38664
|
-
id: n.id,
|
|
38665
|
-
anchor: n.anchor,
|
|
38666
|
-
text: n.text,
|
|
38667
|
-
label: severities.get(n.id).reason
|
|
38668
|
-
}));
|
|
38669
|
-
return deferNotes(deferred, head$1());
|
|
38670
|
-
},
|
|
38767
|
+
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
38671
38768
|
answerQuestions: answerReviewQuestions,
|
|
38672
38769
|
fixNits: async (notes) => {
|
|
38673
38770
|
await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
|
|
@@ -38680,8 +38777,9 @@ const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
38680
38777
|
editNotes: (round, edits, answeredAt) => reviewEditNotesCapture(round, edits.map((n, i) => ({
|
|
38681
38778
|
n,
|
|
38682
38779
|
i,
|
|
38683
|
-
s: severities.get(n.id)
|
|
38684
|
-
|
|
38780
|
+
s: severities.get(n.id),
|
|
38781
|
+
key: captureKey(severities.get(n.id))
|
|
38782
|
+
})).sort((x, y) => x.key - y.key || x.i - y.i).map(({ n, s }) => ({
|
|
38685
38783
|
id: n.id,
|
|
38686
38784
|
anchor: n.anchor,
|
|
38687
38785
|
text: n.forced ? n.text.replace(/^!\s*/, "") : n.text,
|
|
@@ -39749,11 +39847,9 @@ const skills = (vars) => {
|
|
|
39749
39847
|
const access = (vars) => ({
|
|
39750
39848
|
design: { write: [] },
|
|
39751
39849
|
"build.review": { write: [".gtd/REVIEW.md"] },
|
|
39752
|
-
"build.review.defer": { write: [".gtd/TODO.md"] },
|
|
39753
39850
|
"build.review.fix.nits": {},
|
|
39754
39851
|
"build.review.fix.risks": {},
|
|
39755
39852
|
"refactor.build.review": { write: [".gtd/REVIEW.md"] },
|
|
39756
|
-
"refactor.build.review.defer": { write: [".gtd/TODO.md"] },
|
|
39757
39853
|
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] })),
|
|
39758
39854
|
...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), () => ({ write: [] }))
|
|
39759
39855
|
});
|
|
@@ -39900,4 +39996,4 @@ var workflows_default = feature;
|
|
|
39900
39996
|
*/
|
|
39901
39997
|
const doors = {};
|
|
39902
39998
|
//#endregion
|
|
39903
|
-
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,
|
|
39999
|
+
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 };
|