@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/web/main.js
CHANGED
|
@@ -29844,8 +29844,8 @@ const eolOf = (content) => content.includes("\r\n") ? "\r\n" : "\n";
|
|
|
29844
29844
|
const PREFIX_RE = /^(H|A):\s*/;
|
|
29845
29845
|
const lineOf = (node, which) => toLspPosition(node.position[which]).line;
|
|
29846
29846
|
/** 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. */
|
|
29847
|
-
const itemText = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
|
|
29848
|
-
const prefixOf = (content, item) => PREFIX_RE.exec(itemText(content, item));
|
|
29847
|
+
const itemText$1 = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
|
|
29848
|
+
const prefixOf = (content, item) => PREFIX_RE.exec(itemText$1(content, item));
|
|
29849
29849
|
/** A thread is a definition whose body OPENS with a list whose first item carries a prefix — anything else stays a one-shot footnote. */
|
|
29850
29850
|
const isThreadBody = (content, children) => {
|
|
29851
29851
|
const first = children[0];
|
|
@@ -29865,7 +29865,7 @@ const entryOf = (content, item, previous, report) => {
|
|
|
29865
29865
|
return;
|
|
29866
29866
|
}
|
|
29867
29867
|
const author = match[1] === "H" ? "me" : "agent";
|
|
29868
|
-
const text = itemText(content, item).slice(match[0].length).trim();
|
|
29868
|
+
const text = itemText$1(content, item).slice(match[0].length).trim();
|
|
29869
29869
|
if (text === "") report(`empty "${match[1]}:" entry — write your own words there`, start, end);
|
|
29870
29870
|
if (previous === void 0 && author === "agent") report(`the agent never starts a thread — the first entry must be "H:"`, start, end);
|
|
29871
29871
|
else if (previous === author) report(`two consecutive "${match[1]}:" entries — entries must alternate`, start, end);
|
|
@@ -31565,8 +31565,6 @@ const qaDescriptor = {
|
|
|
31565
31565
|
unansweredQuestions,
|
|
31566
31566
|
openThreads: openThreadsOf
|
|
31567
31567
|
};
|
|
31568
|
-
//#endregion
|
|
31569
|
-
//#region src/steering/review.ts
|
|
31570
31568
|
/**
|
|
31571
31569
|
* The canonical sample. Every detail here is load-bearing for the formatter
|
|
31572
31570
|
* round-trip: both footnote bodies exceed 80 characters so a reflow actually
|
|
@@ -31833,16 +31831,42 @@ const parseHunk = (content, lines, title, item) => {
|
|
|
31833
31831
|
errors: [hunkSecondPointerFinding(content, lines, item, token, sourceLine, title, file), hunkLineWithoutRangeFinding(content, item, token, sourceLine, title, file)].filter((e) => e !== void 0)
|
|
31834
31832
|
};
|
|
31835
31833
|
};
|
|
31834
|
+
const itemText = (content, item) => item.children.map((c) => sourceText(content, c)).join(" ").replace(/^\[[ xX]\]\s*/, "").trim();
|
|
31835
|
+
/**
|
|
31836
|
+
* The task list right after the `Tick exactly one:` paragraph, with that
|
|
31837
|
+
* paragraph's index. Read by position, never by label, so an edited label or
|
|
31838
|
+
* a free text opening with `./path` is still an answer, not a pointer. Any
|
|
31839
|
+
* count but three boxes is kept, and reads as unanswered.
|
|
31840
|
+
*/
|
|
31841
|
+
const answerListOf = (content, body) => {
|
|
31842
|
+
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));
|
|
31843
|
+
if (lead === -1) return void 0;
|
|
31844
|
+
const list = body[lead + 1];
|
|
31845
|
+
const last = list.children.length - 1;
|
|
31846
|
+
return {
|
|
31847
|
+
lead,
|
|
31848
|
+
list,
|
|
31849
|
+
options: list.children.map((item, i) => {
|
|
31850
|
+
const text = itemText(content, item);
|
|
31851
|
+
return {
|
|
31852
|
+
checked: item.checked === true,
|
|
31853
|
+
text: i === last && text.toLowerCase() === "_your answer_".toLowerCase() ? "" : text
|
|
31854
|
+
};
|
|
31855
|
+
})
|
|
31856
|
+
};
|
|
31857
|
+
};
|
|
31836
31858
|
/** 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). */
|
|
31837
31859
|
const parseChunkBody = (content, lines, title, body) => {
|
|
31838
|
-
const
|
|
31839
|
-
const
|
|
31860
|
+
const answer = answerListOf(content, body);
|
|
31861
|
+
const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
|
|
31862
|
+
const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
|
|
31863
|
+
const leadingRun = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
|
|
31840
31864
|
const descriptionNodes = blockNodesOfRun(content, leadingRun);
|
|
31841
31865
|
const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
|
|
31842
31866
|
const description = leadingRun.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
|
|
31843
31867
|
const files = [];
|
|
31844
31868
|
const errors = [];
|
|
31845
|
-
for (const item of body.flatMap((n) => taskItems(n))) {
|
|
31869
|
+
for (const item of body.filter((n) => !isAnswer(n)).flatMap((n) => taskItems(n))) {
|
|
31846
31870
|
const parsed = parseHunk(content, lines, title, item);
|
|
31847
31871
|
if (!parsed) continue;
|
|
31848
31872
|
files.push(parsed.file);
|
|
@@ -31853,7 +31877,9 @@ const parseChunkBody = (content, lines, title, body) => {
|
|
|
31853
31877
|
descriptionNodes,
|
|
31854
31878
|
descriptionInline,
|
|
31855
31879
|
files,
|
|
31856
|
-
errors
|
|
31880
|
+
errors,
|
|
31881
|
+
...answer === void 0 ? {} : { answers: answer.options },
|
|
31882
|
+
boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
|
|
31857
31883
|
};
|
|
31858
31884
|
};
|
|
31859
31885
|
/** 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. */
|
|
@@ -31889,9 +31915,9 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
31889
31915
|
const errors = [];
|
|
31890
31916
|
for (const { heading, headingLine, body } of splitChunks(tree)) {
|
|
31891
31917
|
const title = headingText$1(content, heading);
|
|
31892
|
-
const { description, descriptionNodes, descriptionInline, files, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
31918
|
+
const { description, descriptionNodes, descriptionInline, files, answers, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
31893
31919
|
errors.push(...bodyErrors);
|
|
31894
|
-
if (files.length === 0 &&
|
|
31920
|
+
if (files.length === 0 && boxed) errors.push({
|
|
31895
31921
|
message: `Chunk "${title}" has no file pointers`,
|
|
31896
31922
|
line: headingLine,
|
|
31897
31923
|
range: nodeRange(heading)
|
|
@@ -31902,7 +31928,8 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
31902
31928
|
descriptionNodes,
|
|
31903
31929
|
descriptionInline,
|
|
31904
31930
|
files,
|
|
31905
|
-
headingLine
|
|
31931
|
+
headingLine,
|
|
31932
|
+
...answers === void 0 ? {} : { answers }
|
|
31906
31933
|
});
|
|
31907
31934
|
}
|
|
31908
31935
|
if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
|
|
@@ -31983,7 +32010,9 @@ const isHunkPointerItem = (content, item) => {
|
|
|
31983
32010
|
* clear, so a caller can skip the write and leave the mtime untouched.
|
|
31984
32011
|
*/
|
|
31985
32012
|
const reviewClearTicks = (content) => {
|
|
31986
|
-
const
|
|
32013
|
+
const tree = parseMarkdown(content);
|
|
32014
|
+
const answers = new Set(splitChunks(tree).flatMap(({ body }) => answerListOf(content, body)?.list.children ?? []));
|
|
32015
|
+
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);
|
|
31987
32016
|
if (offsets.length === 0) return content;
|
|
31988
32017
|
let result = "";
|
|
31989
32018
|
let cursor = 0;
|
package/dist/workflow-kit.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { changes, changesSince, hasThreadFor, head, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
|
|
2
|
-
import { c as stripCodeThreads,
|
|
1
|
+
import { changes, changesSince, hasThreadFor, head, mark, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
|
|
2
|
+
import { _ as unansweredNotes, c as stripCodeThreads, g as reviewRisks, h as reviewNotes, m as reviewDecisions, w as parseThreads } from "./chunks/steering-D4LUQ-av.mjs";
|
|
3
3
|
//#region src/workflow-kit/health.ts
|
|
4
4
|
/** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
|
|
5
5
|
const escalation = async (count, steps) => {
|
|
@@ -172,6 +172,13 @@ const collect = async (p, capture, round, requested) => {
|
|
|
172
172
|
}
|
|
173
173
|
return refuse(p.refusals.unmatched);
|
|
174
174
|
};
|
|
175
|
+
/** Refuse while a note's answer boxes hold anything but exactly one tick, or a note's boxes were deleted. */
|
|
176
|
+
const requireDecisions = (file, reviewed) => {
|
|
177
|
+
const open = unansweredNotes(read(file) ?? "", changesSince(reviewed).get(file)?.before);
|
|
178
|
+
if (open.length === 0) return;
|
|
179
|
+
const list = open.map((n) => ` - ${file}${n.line > 0 ? `:${n.line}` : ""}: ${n.title} — ${n.reason}`).join("\n");
|
|
180
|
+
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}`);
|
|
181
|
+
};
|
|
175
182
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
176
183
|
/**
|
|
177
184
|
* A review with ticks and THREAD footnotes (definition plus markers) removed:
|
|
@@ -191,7 +198,7 @@ const notesOf = (text) => {
|
|
|
191
198
|
* commit of the last collecting turn, `"missing"` when the round left no
|
|
192
199
|
* review, or `undefined` when no such turn ran.
|
|
193
200
|
*/
|
|
194
|
-
const converse = async (p, base, collectedBefore) => {
|
|
201
|
+
const converse = async (p, base, reviewed, collectedBefore) => {
|
|
195
202
|
let collectedAt = collectedBefore;
|
|
196
203
|
for (;;) {
|
|
197
204
|
await p.awaitReview(base);
|
|
@@ -201,24 +208,47 @@ const converse = async (p, base, collectedBefore) => {
|
|
|
201
208
|
return "missing";
|
|
202
209
|
}
|
|
203
210
|
requireThreadsClosed(p.file);
|
|
204
|
-
if (!hasThreadFor("agent", p.file))
|
|
211
|
+
if (!hasThreadFor("agent", p.file)) {
|
|
212
|
+
requireDecisions(p.file, reviewed);
|
|
213
|
+
return collectedAt;
|
|
214
|
+
}
|
|
205
215
|
await p.collecting(p.captures.edits(head()), "reply");
|
|
206
216
|
collectedAt = head();
|
|
207
217
|
requireReplies(p.file);
|
|
208
|
-
if (!hasThreadFor("human", p.file)) return collectedAt;
|
|
218
|
+
if (!hasThreadFor("human", p.file) && unansweredNotes(read(p.file) ?? "", changesSince(reviewed).get(p.file)?.before).length === 0) return collectedAt;
|
|
209
219
|
}
|
|
210
220
|
};
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
const decisionNote = (d, id, text) => ({
|
|
222
|
+
id,
|
|
223
|
+
kind: "chunk",
|
|
224
|
+
anchor: d.anchor,
|
|
225
|
+
before: d.body,
|
|
226
|
+
text,
|
|
227
|
+
forced: /^!(\s|$)/.test(text)
|
|
228
|
+
});
|
|
229
|
+
/** One `Gtd-Mark` value per line: a control character would make mark() throw on every replay. */
|
|
230
|
+
const oneLine = (text) => text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim() || "untitled";
|
|
231
|
+
/** Tell the policy, and mark each won't-fix on the gate's commit. */
|
|
232
|
+
const record = (p, decisions) => {
|
|
233
|
+
p.decided?.(decisions);
|
|
234
|
+
for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine(d.anchor));
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Judge every note, then run each verdict's route: answers, then nit fixes,
|
|
238
|
+
* then the edits' lap. Of the human's `decisions`, a fix joins the nit batch
|
|
239
|
+
* unjudged, an own answer is judged like a note, and a won't-fix is dropped
|
|
240
|
+
* with a `Gtd-Mark` on the gate's commit.
|
|
241
|
+
*/
|
|
242
|
+
const routeNotes = async (p, notes, r, decisions = []) => {
|
|
243
|
+
record(p, decisions);
|
|
244
|
+
const of = (c) => decisions.filter((d) => d.choice === c);
|
|
245
|
+
const fixes = of("fix").map((d, i) => decisionNote(d, `fix-${i + 1}`, d.body));
|
|
246
|
+
const judged = [...notes, ...of("answer").map((d, i) => decisionNote(d, `answer-${i + 1}`, d.text))];
|
|
247
|
+
const verdicts = judged.length === 0 ? /* @__PURE__ */ new Map() : await p.triage(judged);
|
|
248
|
+
const ofVerdict = (v) => judged.filter((n) => (verdicts.get(n.id) ?? "edit") === v);
|
|
249
|
+
const [edits, questions] = [ofVerdict("edit"), ofVerdict("question")];
|
|
250
|
+
const nits = [...ofVerdict("nit"), ...fixes];
|
|
251
|
+
if (edits.length + questions.length + nits.length === 0) return r.unfolded();
|
|
222
252
|
let answeredAt;
|
|
223
253
|
if (questions.length > 0) {
|
|
224
254
|
await p.answerQuestions(questions);
|
|
@@ -228,7 +258,6 @@ const routeNotes = async (p, notes, r) => {
|
|
|
228
258
|
await p.fixNits(nits);
|
|
229
259
|
await p.keepGreen();
|
|
230
260
|
}
|
|
231
|
-
if (deferred.length > 0) await p.defer(deferred);
|
|
232
261
|
if (edits.length > 0) {
|
|
233
262
|
await r.close();
|
|
234
263
|
return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt), "triaged", {
|
|
@@ -250,8 +279,8 @@ const routeNotes = async (p, notes, r) => {
|
|
|
250
279
|
};
|
|
251
280
|
/** A code edit, not a thread-only change: lines that only add, answer or remove code threads don't count. */
|
|
252
281
|
const isCodeEdit = (p, c) => p.isCode(c.path) && stripCodeThreads(c.path, c.before ?? "") !== stripCodeThreads(c.path, c.after ?? "");
|
|
253
|
-
/** Whether
|
|
254
|
-
const notedSince = (p, commit) => changesSince(commit).some((c) => c.path === p.file && notesOf(c.before ?? "") !== notesOf(read(p.file) ?? ""));
|
|
282
|
+
/** Whether a note or an answer, not just thread edits and pointer ticks, landed in the review file since `commit`. */
|
|
283
|
+
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));
|
|
255
284
|
/**
|
|
256
285
|
* What the in-loop collecting turn left: `folded` when it wrote `feedback`
|
|
257
286
|
* (those folds need their lap whatever is judged later), `settled` when only
|
|
@@ -265,9 +294,9 @@ const settledAfter = (p, collectedAt) => ({
|
|
|
265
294
|
folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
|
|
266
295
|
settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
|
|
267
296
|
});
|
|
268
|
-
const finishNotes = async (p, before, reopened, ctx) => {
|
|
269
|
-
const notes = reviewNotes(before, read(p.file) ?? "");
|
|
270
|
-
if (notes.length > 0) return routeNotes(p, notes, ctx);
|
|
297
|
+
const finishNotes = async (p, before, decisions, reopened, ctx) => {
|
|
298
|
+
const notes = reviewNotes(before ?? "", read(p.file) ?? "");
|
|
299
|
+
if (notes.length + decisions.length > 0) return routeNotes(p, notes, ctx, decisions);
|
|
271
300
|
if (reopened) return ctx.unfolded();
|
|
272
301
|
await ctx.close();
|
|
273
302
|
return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"));
|
|
@@ -277,6 +306,11 @@ const toOutcome = (verdict, rest) => verdict === "signoff" ? { verdict } : {
|
|
|
277
306
|
...rest
|
|
278
307
|
};
|
|
279
308
|
const isReopened = (p, r) => r.settled && !r.folded && p.refusals.dropped !== void 0;
|
|
309
|
+
/** A carried answer is unchanged, never routed twice; one ticked beside a thread still routes once the thread has settled. */
|
|
310
|
+
const decisionsSince = (p, since) => {
|
|
311
|
+
const after = read(p.file) ?? "";
|
|
312
|
+
return reviewDecisions(since.get(p.file)?.before ?? after, after);
|
|
313
|
+
};
|
|
280
314
|
const finish = async (p, reviewed, collectedAt) => {
|
|
281
315
|
const round = head();
|
|
282
316
|
const since = changesSince(reviewed);
|
|
@@ -297,15 +331,17 @@ const finish = async (p, reviewed, collectedAt) => {
|
|
|
297
331
|
settled,
|
|
298
332
|
folded
|
|
299
333
|
});
|
|
300
|
-
|
|
334
|
+
const decisions = decisionsSince(p, since);
|
|
335
|
+
if (settled && !reopened && decisions.length === 0 || edited.length === 0 && !noted) return unfolded();
|
|
301
336
|
if (edited.length > 0) {
|
|
337
|
+
record(p, decisions);
|
|
302
338
|
await close();
|
|
303
339
|
return outcome(await collect(p, p.captures.edits(round, reopened ? reviewed : void 0), "edits", {
|
|
304
340
|
notes: [],
|
|
305
341
|
paths: edited.map((c) => c.path)
|
|
306
342
|
}));
|
|
307
343
|
}
|
|
308
|
-
return finishNotes(p, since.get(p.file)?.before
|
|
344
|
+
return finishNotes(p, since.get(p.file)?.before, decisions, reopened, {
|
|
309
345
|
round,
|
|
310
346
|
close,
|
|
311
347
|
outcome,
|
|
@@ -335,7 +371,7 @@ const review = async (base, p) => {
|
|
|
335
371
|
let reviewed = head();
|
|
336
372
|
let collectedAt;
|
|
337
373
|
for (;;) {
|
|
338
|
-
collectedAt = await converse(p, base, collectedAt);
|
|
374
|
+
collectedAt = await converse(p, base, reviewed, collectedAt);
|
|
339
375
|
if (collectedAt === "missing") break;
|
|
340
376
|
const result = await finish(p, reviewed, collectedAt);
|
|
341
377
|
if (!("next" in result)) return result;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { Ct as envDefaults, Et as flagReferences, Ot as registryEntry, S as coder, St as defaults, i as skills, kt as requireFlag, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-COEMFEpy.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C_xn8U7U.mjs";
|
|
4
4
|
//#region src/workflows/cleanup/cleanup.ts
|
|
5
5
|
const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
|
|
6
6
|
const cleanupFlow = async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, showScript, slice, start, vars, writeScript, wrote } from "../flows.mjs";
|
|
2
2
|
import { lensEntriesOf, removedAssertions, review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as
|
|
4
|
-
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "../chunks/tail-
|
|
3
|
+
import { $ as naivePassPrompt, A as agentWithSkills, B as coverageQuestion, C as fixCheck, Ct as envDefaults, D as reviewMissing, E as planner, F as builderSystem, G as designTriagePrompt, H as criterionLine, I as clearCriteriaMessage, L as clearCriteriaQuestion, M as buildReviewCollectingPrompt, N as buildReviewFixNitsPrompt, P as buildReviewTriageMessage, Q as naiveApplyFailure, R as composedReview, S as coder, St as defaults, T as fixRisks, U as designGateAnswerMessage, V as coveringPrompt, W as designSystem, X as implementBuildingPrompt, Y as idleMessage, Z as implementFixPrompt, _ as REFACTOR_NOTE, _t as slicingPrompt, a as flagModeOn, at as refactorBuildingPrompt, b as SLICES, bt as unwindFailedMessage, ct as refactorNotePrompt, d as guarded, dt as reviewEditsCapture, et as normaliseCriteriaScript, f as healthy, g as REFACTOR, gt as seamSteerMessage, h as QUALITY, ht as seamByHandMessage, i as skills, it as qualityUnresolvedMessage, j as buildReviewAnswerQuestionsPrompt, lt as refactorRules, m as HANDOFF, mt as reviewerSystem, n as summary, ot as refactorDroppedRefusal, p as FEEDBACK, pt as reviewNotesCapture, r as access, s as shipped, st as refactorFixPrompt, t as steering, tt as oversizedMessage, u as gate, ut as reviewEditNotesCapture, v as REQUIREMENTS, vt as startGateBlockedMessage, wt as testFileGlobs, x as awaitReview, xt as unwindFailure, y as REVIEW, yt as uncoveredMessage, z as coverageMessage } from "../chunks/reserved-COEMFEpy.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "../chunks/tail-C_xn8U7U.mjs";
|
|
5
5
|
import { posix } from "node:path";
|
|
6
6
|
//#region src/workflows/shared/fences.ts
|
|
7
7
|
const markFenced = (text) => {
|
|
@@ -241,7 +241,7 @@ const fixNits = (notes, access, rules) => {
|
|
|
241
241
|
}, () => fixing("fixing"));
|
|
242
242
|
};
|
|
243
243
|
/** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
|
|
244
|
-
const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
|
|
244
|
+
const collecting = (capture, round) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, void 0, round), {
|
|
245
245
|
label: "Collecting your feedback",
|
|
246
246
|
file: REQUIREMENTS,
|
|
247
247
|
mode: "qa",
|
|
@@ -260,11 +260,6 @@ const refactorCollecting = (capture, round, strays) => agentWithSkills("review.c
|
|
|
260
260
|
system: reviewerSystem(),
|
|
261
261
|
allowEmpty: true
|
|
262
262
|
});
|
|
263
|
-
/** `build.review.defer`: append `notes` to `.gtd/TODO.md` as one section; the step's landing is the commit. */
|
|
264
|
-
const deferNotes = (notes, round) => run("review.defer", deferNotesScript(notes, round), {
|
|
265
|
-
label: "Deferring could-notes to TODO.md",
|
|
266
|
-
file: ".gtd/TODO.md"
|
|
267
|
-
});
|
|
268
263
|
//#endregion
|
|
269
264
|
//#region src/workflows/feature/planning.ts
|
|
270
265
|
/**
|
|
@@ -305,6 +300,13 @@ const verdictQuestion = (note) => ({
|
|
|
305
300
|
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.`,
|
|
306
301
|
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."
|
|
307
302
|
});
|
|
303
|
+
const TIER_ORDER = {
|
|
304
|
+
critical: 0,
|
|
305
|
+
should: 1,
|
|
306
|
+
could: 2
|
|
307
|
+
};
|
|
308
|
+
/** Capture order: critical, should, could; within a tier one-way doors, then new concepts. */
|
|
309
|
+
const captureKey = (s) => s === void 0 ? 2 : TIER_ORDER[s.tier] * 3 + s.rank;
|
|
308
310
|
const SEVERITY_ANSWERS = {
|
|
309
311
|
security: ["yes", "no"],
|
|
310
312
|
reversibility: ["one-way", "two-way"],
|
|
@@ -315,10 +317,7 @@ const SEVERITY_ANSWERS = {
|
|
|
315
317
|
],
|
|
316
318
|
newConcept: ["yes", "no"]
|
|
317
319
|
};
|
|
318
|
-
/**
|
|
319
|
-
* Deferring is the risky direction: anything unanswered, unrecognised, cut or
|
|
320
|
-
* below `minP` is `critical`, never `could`.
|
|
321
|
-
*/
|
|
320
|
+
/** Anything unanswered, unrecognised, cut or below `minP` is `critical`, never `could`. */
|
|
322
321
|
const severityOf = (note, judgment, minP) => {
|
|
323
322
|
const ans = (q) => judgment.answers[`${note.id}.${q}`];
|
|
324
323
|
const a = (q) => ans(q)?.answer;
|
|
@@ -382,7 +381,7 @@ const severityQuestions = (note) => [
|
|
|
382
381
|
* nothing) plus four severity answers. Dismissing a note is the risky
|
|
383
382
|
* direction, so only a confident non-`edit` verdict on uncut evidence counts;
|
|
384
383
|
* a cut, unanswered or below-floor note is an `edit`. Severity answers on a
|
|
385
|
-
* non-`edit` note are ignored
|
|
384
|
+
* non-`edit` note are ignored; on an `edit` they order the collecting turn.
|
|
386
385
|
*/
|
|
387
386
|
const triage$1 = (severities) => async (notes) => {
|
|
388
387
|
const asked = notes.filter((n) => !n.forced);
|
|
@@ -402,21 +401,22 @@ const triage$1 = (severities) => async (notes) => {
|
|
|
402
401
|
return new Map(notes.map((n) => {
|
|
403
402
|
const verdict = n.forced ? void 0 : VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
|
|
404
403
|
if (verdict !== void 0 && !truncated.includes(n.id)) return [n.id, verdict];
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return [n.id, severity.tier === "could" ? "defer" : "edit"];
|
|
404
|
+
severities.set(n.id, severityOf(n, judgment, severityMinP));
|
|
405
|
+
return [n.id, "edit"];
|
|
408
406
|
}));
|
|
409
407
|
};
|
|
410
408
|
const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
411
409
|
/**
|
|
412
410
|
* gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
|
|
413
411
|
* turn — and removes what the fixer left in `.gtd/QUALITY.md`. A re-review
|
|
414
|
-
* after fixed nits restores the doc as committed at `carry`, answers kept
|
|
412
|
+
* after fixed nits restores the doc as committed at `carry`, answers kept;
|
|
413
|
+
* a fresh one leaves out the notes the human already answered.
|
|
415
414
|
*/
|
|
416
|
-
const composing = (base, report, carry, nitFix) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
|
|
415
|
+
const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new Set()) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
|
|
417
416
|
...report,
|
|
418
417
|
weakened: removedAssertions(changesSince(base)),
|
|
419
|
-
nitFix
|
|
418
|
+
nitFix,
|
|
419
|
+
answered
|
|
420
420
|
}), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
|
|
421
421
|
label: "Composing the review",
|
|
422
422
|
file: REVIEW,
|
|
@@ -424,9 +424,10 @@ const composing = (base, report, carry, nitFix) => run("review.composing", carry
|
|
|
424
424
|
});
|
|
425
425
|
/**
|
|
426
426
|
* The quality lap's notes reach a human over everything since `base`, who
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
427
|
+
* answers each one — fix as suggested, won't fix, or their own answer — and
|
|
428
|
+
* may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
|
|
429
|
+
* answer or comment is judged: edits go to another planning lap, questions
|
|
430
|
+
* are answered in place, nits fixed in one batch, praise dropped.
|
|
430
431
|
*/
|
|
431
432
|
const review = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
432
433
|
const { guard, guardRests, fixAccess, rules } = options;
|
|
@@ -436,17 +437,16 @@ const review = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
436
437
|
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"
|
|
437
438
|
};
|
|
438
439
|
const severities = /* @__PURE__ */ new Map();
|
|
439
|
-
let deferred = [];
|
|
440
440
|
let nitFix;
|
|
441
|
+
const answered = /* @__PURE__ */ new Set();
|
|
441
442
|
return review$1(base, {
|
|
442
443
|
file: REVIEW,
|
|
443
444
|
feedback: feedback.file,
|
|
444
445
|
isCode: isCode$1,
|
|
445
446
|
reviewing: (b, carry) => {
|
|
446
|
-
deferred = [];
|
|
447
447
|
const fixedAt = nitFix;
|
|
448
448
|
nitFix = void 0;
|
|
449
|
-
return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt);
|
|
449
|
+
return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt, answered);
|
|
450
450
|
},
|
|
451
451
|
fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
|
|
452
452
|
keepGreen: () => healthy(() => fixCheck(rules), {
|
|
@@ -455,23 +455,11 @@ const review = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
455
455
|
guardRests,
|
|
456
456
|
rules
|
|
457
457
|
}),
|
|
458
|
-
awaitReview: (b) =>
|
|
459
|
-
const notes = deferred;
|
|
460
|
-
deferred = [];
|
|
461
|
-
return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)) : awaitReview(b);
|
|
462
|
-
},
|
|
458
|
+
awaitReview: (b) => awaitReview(b),
|
|
463
459
|
reviewMissing,
|
|
464
460
|
collecting: feedback.collecting,
|
|
465
461
|
triage: triage$1(severities),
|
|
466
|
-
|
|
467
|
-
deferred = notes.map((n) => ({
|
|
468
|
-
id: n.id,
|
|
469
|
-
anchor: n.anchor,
|
|
470
|
-
text: n.text,
|
|
471
|
-
label: severities.get(n.id).reason
|
|
472
|
-
}));
|
|
473
|
-
return deferNotes(deferred, head());
|
|
474
|
-
},
|
|
462
|
+
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
475
463
|
answerQuestions: answerReviewQuestions,
|
|
476
464
|
fixNits: async (notes) => {
|
|
477
465
|
await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
|
|
@@ -484,8 +472,9 @@ const review = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
484
472
|
editNotes: (round, edits, answeredAt) => reviewEditNotesCapture(round, edits.map((n, i) => ({
|
|
485
473
|
n,
|
|
486
474
|
i,
|
|
487
|
-
s: severities.get(n.id)
|
|
488
|
-
|
|
475
|
+
s: severities.get(n.id),
|
|
476
|
+
key: captureKey(severities.get(n.id))
|
|
477
|
+
})).sort((x, y) => x.key - y.key || x.i - y.i).map(({ n, s }) => ({
|
|
489
478
|
id: n.id,
|
|
490
479
|
anchor: n.anchor,
|
|
491
480
|
text: n.forced ? n.text.replace(/^!\s*/, "") : n.text,
|
|
@@ -1525,4 +1514,4 @@ const feature = shipped(ordinaryStart);
|
|
|
1525
1514
|
//#region src/workflows/feature/index.ts
|
|
1526
1515
|
var feature_default = feature;
|
|
1527
1516
|
//#endregion
|
|
1528
|
-
export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults,
|
|
1517
|
+
export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, triage, unstableIds, unwind };
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { A as agentWithSkills,
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
2
|
+
import { g as reviewRisks, h as reviewNotes } from "../chunks/steering-D4LUQ-av.mjs";
|
|
3
|
+
import { A as agentWithSkills, Ct as envDefaults, K as finisherSystem, S as coder, St as defaults, c as baseline, i as skills, n as summary, q as fixReviewNotesPrompt, r as access, s as shipped, t as steering, y as REVIEW } from "../chunks/reserved-COEMFEpy.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-C_xn8U7U.mjs";
|
|
5
5
|
//#region src/workflows/fix/fix.ts
|
|
6
6
|
/**
|
|
7
7
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { Ct as envDefaults, St as defaults, ft as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, u as gate } from "../chunks/reserved-COEMFEpy.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C_xn8U7U.mjs";
|
|
4
4
|
//#region src/workflows/review/review.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import { Dt as
|
|
2
|
+
import { Ct as envDefaults, Dt as flipProblems, S as coder, St as defaults, Tt as flagMentions, i as skills, kt as requireFlag, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate } from "../chunks/reserved-COEMFEpy.mjs";
|
|
3
3
|
//#region src/workflows/rollout/rollout.ts
|
|
4
4
|
const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
|
|
5
5
|
const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
|