@pmelab/gtd 46.0.0 → 48.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 +21 -19
- package/dist/chunks/{reserved-BENPy6IJ.mjs → reserved-DF1HD6jL.mjs} +41 -22
- package/dist/chunks/{review-DcbgUwr7.mjs → review-Bv40kZu9.mjs} +5 -8
- package/dist/chunks/{tail-CDNMZ-2E.mjs → tail-D89fU_wf.mjs} +68 -39
- package/dist/gtd.bundle.mjs +203 -867
- package/dist/testkit.mjs +89 -53
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +26 -22
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +257 -205
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +0 -2
- package/src/cli/Cli.ts +0 -15
- package/src/cli/Program.test.ts +0 -1
- package/src/cli/Program.ts +0 -8
- package/src/cli/ToolCommands.ts +1 -2
- package/src/install/Install.ts +89 -35
- package/src/testkit/Layers.ts +0 -1
- package/src/wire/ContextLimit.ts +1 -1
- package/src/workflows/cleanup/cleanup.test.ts +11 -9
- package/src/workflows/feature/feature.ts +38 -28
- package/src/workflows/feature/lens-review.feature +3 -1
- package/src/workflows/feature/slice-findings.feature +198 -0
- package/src/workflows/fix/fix-lens-origin.feature +101 -0
- package/src/workflows/fix/fix-quality.feature +131 -0
- package/src/workflows/index.ts +0 -1
- package/src/workflows/shared/lenses.ts +30 -5
- package/src/workflows/shared/review.ts +3 -8
- package/src/workflows/shared/tail.ts +74 -57
- package/src/workflows/shared/text.ts +85 -29
- package/src/workflows/feature/quality-unresolved.feature +0 -121
package/README.md
CHANGED
|
@@ -302,17 +302,19 @@ this with your environment. See
|
|
|
302
302
|
[Driving the loop](https://github.com/pmelab/gtd/blob/main/docs/driver.md) for
|
|
303
303
|
the full protocol.
|
|
304
304
|
|
|
305
|
-
Or skip writing one:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
to
|
|
309
|
-
|
|
305
|
+
Or skip writing one: the
|
|
306
|
+
[complete minimal driver](https://github.com/pmelab/gtd/blob/main/docs/driver.md#a-complete-minimal-driver)
|
|
307
|
+
is a paste to own outright. It drives `claude` until the process rests on you,
|
|
308
|
+
logs every turn to the file `gtd next --json=log` names, records each turn's
|
|
309
|
+
cost on its landing, answers judge gates through `gtd judge run`, and maps the
|
|
310
|
+
workflow's `smart`/`base` model hints to real model names through
|
|
311
|
+
`GTD_LOOP_MODEL_SMART`/`GTD_LOOP_MODEL_BASE`.
|
|
310
312
|
|
|
311
313
|
Both it and the Claude Code mod stop an agent turn that outgrows its context:
|
|
312
314
|
past 60% of the model's window the agent is asked to wrap up with a handoff
|
|
313
|
-
note, past 70% (or when the context is about to be compacted)
|
|
314
|
-
nothing lands. A step's `context` option, or the bundled
|
|
315
|
-
`contextLimit` option, moves the two limits — see
|
|
315
|
+
note, past 70% after a tool call (or when the context is about to be compacted)
|
|
316
|
+
the turn stops and nothing lands. A step's `context` option, or the bundled
|
|
317
|
+
workflows' `contextLimit` option, moves the two limits — see
|
|
316
318
|
[Context limits](https://github.com/pmelab/gtd/blob/main/docs/driver.md#context-limits).
|
|
317
319
|
In `feature`, a naive pass, refactor or build turn that wrapped up that way is a
|
|
318
320
|
snapshot: a fresh turn picks up from it with the handoff note and the criteria
|
|
@@ -478,10 +480,10 @@ noted in steps 3 and 4 below.
|
|
|
478
480
|
- After you review: is each of your notes an edit, a question, a nit or
|
|
479
481
|
praise?
|
|
480
482
|
|
|
481
|
-
gtd itself never calls a judge:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
483
|
+
gtd itself never calls a judge: the Claude Code mod and the reference driver
|
|
484
|
+
answer these through `gtd judge run` (auto selection: jev when
|
|
485
|
+
`TYPESAFE_API_KEY` is set, else `llm` via `claude`, default model sonnet,
|
|
486
|
+
while the Claude Code mod's own judge subagent stays on `haiku` —
|
|
485
487
|
`--model <name>` overrides); if that fails it shows you the message and
|
|
486
488
|
stops. **The `llm` provider's `p` is self-reported by the model, not a
|
|
487
489
|
measured probability, so a confidently wrong verdict can clear the 0.7 floor
|
|
@@ -504,12 +506,12 @@ noted in steps 3 and 4 below.
|
|
|
504
506
|
unsure verdict (below `qualityTriageMinP`, `0.7`) is `design`; mechanical
|
|
505
507
|
findings the fix turns leave become notes too. This lap is where code quality
|
|
506
508
|
is looked at, and every round pays for it. It never replaces step 4 — your
|
|
507
|
-
review stays the final gate, and nothing here skips it.
|
|
508
|
-
[coarse
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
509
|
+
review stays the final gate, and nothing here skips it. Every
|
|
510
|
+
[coarse slice](docs/configuration.md#coarse-slices) runs the same triaged lap
|
|
511
|
+
and never rests for its findings: its notes are carried to the review after
|
|
512
|
+
the last slice, each naming its slice and lens, beside every slice's fixes.
|
|
513
|
+
The `fix` and `cleanup` doors triage their lap too, and hand its notes to
|
|
514
|
+
their own review.
|
|
513
515
|
|
|
514
516
|
A red suite that keeps failing past a few fix attempts escalates instead of
|
|
515
517
|
retrying forever: an agent turn reads the failing output and writes
|
|
@@ -896,7 +898,7 @@ install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
|
|
|
896
898
|
The `gtd` bin's commands — `next`, `land`, `abandon`, `restore`, `validate`,
|
|
897
899
|
`check`, `uncheck`, `install`, `summary`, `base`, `door`, `doors`, `slices`,
|
|
898
900
|
`version`, `help`, `history list`, `history show`, `history metrics`,
|
|
899
|
-
`history judges`, `history fetch`, `strays`, and the tools `ui`, `lsp`,
|
|
901
|
+
`history judges`, `history fetch`, `strays`, and the tools `ui`, `lsp`,
|
|
900
902
|
`context-hook`, `judge` (`judge answer`, `judge run`) — are listed in the
|
|
901
903
|
[CLI reference](https://github.com/pmelab/gtd/blob/main/docs/cli.md).
|
|
902
904
|
`gtd slices` lists the spans a flow's `slice()` calls mark. It refuses (exit 1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, displayPath, env, head, human, judge, mark, numeric, quote, read, refuse, scope, skillsFor, start, vars } from "../flows.mjs";
|
|
2
2
|
import { d as ANSWER_FIX, f as ANSWER_LEAD, p as ANSWER_WONT_FIX, v as FREE_TEXT_PLACEHOLDER } from "./steering-D7hWeMgE.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
|
|
4
4
|
import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct, n as Boolean$, s as Number$ } from "./Schema-C6fLlQzx.mjs";
|
|
5
5
|
//#region src/workflows/shared/registry.ts
|
|
6
6
|
const FILL = "";
|
|
@@ -544,6 +544,20 @@ const SUMMARY_TITLES = [
|
|
|
544
544
|
];
|
|
545
545
|
const unboxed = (body) => body.replace(/^(\s*(?:>\s*)*(?:[-*+]|\d+[.)])\s+)\[[ xX]\]\s?/gm, "$1");
|
|
546
546
|
const short = (hash) => hash.slice(0, 7);
|
|
547
|
+
/** `slice 2, lens tests`; empty when neither is known. */
|
|
548
|
+
const originOf = (f) => [...f.slice === void 0 ? [] : [`slice ${f.slice}`], ...f.lens === void 0 ? [] : [`lens ${f.lens}`]].join(", ");
|
|
549
|
+
const originLine = (f) => {
|
|
550
|
+
const origin = originOf(f);
|
|
551
|
+
return origin === "" ? "" : `_From ${origin}._\n\n`;
|
|
552
|
+
};
|
|
553
|
+
const withOrigin = (f, rest) => [rest, originOf(f)].filter((s) => s !== "").join(", ");
|
|
554
|
+
const noteBody = (n) => {
|
|
555
|
+
const body = unboxed(n.body) || "_No detail given._";
|
|
556
|
+
if (n.unresolved === true) return `_The fixer left this unresolved._\n\n${body}`;
|
|
557
|
+
if (n.unclear === true) return `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${body}`;
|
|
558
|
+
return body;
|
|
559
|
+
};
|
|
560
|
+
const carriedOnly = (f) => f.slice === void 0 ? {} : f;
|
|
547
561
|
/**
|
|
548
562
|
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
|
|
549
563
|
* answer boxes, then the fixed and noise lists. Chunk titles are made unique —
|
|
@@ -570,25 +584,14 @@ const composedReview = (base, report) => {
|
|
|
570
584
|
...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.`)],
|
|
571
585
|
...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
|
|
572
586
|
...conflicts,
|
|
573
|
-
...notes.map((n) => chunk(n.title, `${
|
|
574
|
-
...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) =>
|
|
587
|
+
...notes.map((n) => chunk(n.title, `${originLine(carriedOnly(n))}${noteBody(n)}\n\n${answerBoxes}`)),
|
|
588
|
+
...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => fixedLine(f, carriedOnly(f))).join("\n"))],
|
|
575
589
|
...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
|
|
576
|
-
...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) =>
|
|
590
|
+
...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => noiseLine(f, carriedOnly(f))).join("\n"))]
|
|
577
591
|
];
|
|
578
592
|
const body = chunks.length > 0 ? chunks : [chunk("Nothing to note", "No quality lens raised anything for you — or none ran. Review the diff yourself.")];
|
|
579
593
|
return `# Review: ${short(base)}\n\n<!-- base: ${base} -->\n\n${body.join("\n\n")}\n`;
|
|
580
594
|
};
|
|
581
|
-
const qualityUnresolvedMessage = () => `The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
|
|
582
|
-
holds findings. Nothing after this fixes them, so the process will not
|
|
583
|
-
go on with them open.
|
|
584
|
-
|
|
585
|
-
Fix what matters by hand, then delete or empty \`.gtd/QUALITY.md\` —
|
|
586
|
-
dropping a finding is your call to make.
|
|
587
|
-
|
|
588
|
-
What each change does next (then run \`gtd land\`):
|
|
589
|
-
- **Delete or empty \`.gtd/QUALITY.md\`** — the suite runs on your edits, then the next slice is built, or after the last one you review the change (**build.review.reviewing**).
|
|
590
|
-
- **Anything else** — rests here again.
|
|
591
|
-
`;
|
|
592
595
|
const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
|
|
593
596
|
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
594
597
|
|
|
@@ -1254,6 +1257,28 @@ const lapRange = {
|
|
|
1254
1257
|
feedback: "it starts at the previous review's boundary, so it covers only what is new since the previous review.",
|
|
1255
1258
|
refactor: "it covers the whole refactor since it started, on every round. A refactor may update an existing test's call sites only: give any test-file hunk its own chunk, and say so if it changes a test, an assertion or an expected value."
|
|
1256
1259
|
};
|
|
1260
|
+
const fixedLine = (f, origin = f) => `- ${f.title} (${withOrigin(origin, short(f.commit))})`;
|
|
1261
|
+
const noiseLine = (f, origin = f) => `- ${f.title} (${withOrigin(origin, `p=${f.p}`)})`;
|
|
1262
|
+
/**
|
|
1263
|
+
* `review` with the lap's notes appended as prose chunks — no pointer, no
|
|
1264
|
+
* answer boxes — then what it fixed and dropped. A title the review already
|
|
1265
|
+
* holds gets a numbered suffix, as notes are told apart by title.
|
|
1266
|
+
*/
|
|
1267
|
+
const withLapFindings = (review, { notes, fixed, noise }) => {
|
|
1268
|
+
const used = new Set(review.split("\n").filter((line) => line.startsWith("## ")).map((line) => line.slice(3).trim()));
|
|
1269
|
+
const unique = (title) => {
|
|
1270
|
+
let name = title;
|
|
1271
|
+
for (let n = 2; used.has(name); n++) name = `${title} (${n})`;
|
|
1272
|
+
used.add(name);
|
|
1273
|
+
return name;
|
|
1274
|
+
};
|
|
1275
|
+
const chunks = [
|
|
1276
|
+
...notes.map((n) => `## ${unique(n.title)}\n\n${originLine(n)}${noteBody(n)}`),
|
|
1277
|
+
...fixed.length === 0 ? [] : [`## ${unique("Fixed automatically")}\n\n${fixed.map((f) => fixedLine(f)).join("\n")}`],
|
|
1278
|
+
...noise.length === 0 ? [] : [`## ${unique("Dropped as noise")}\n\n${noise.map((f) => noiseLine(f)).join("\n")}`]
|
|
1279
|
+
];
|
|
1280
|
+
return chunks.length === 0 ? review : `${review.trimEnd()}\n\n${chunks.join("\n\n")}\n`;
|
|
1281
|
+
};
|
|
1257
1282
|
const buildReviewReviewingPrompt = (base, lap, carry, removed = []) => `${styleBlock}
|
|
1258
1283
|
|
|
1259
1284
|
${styleFormatContract}
|
|
@@ -1798,12 +1823,6 @@ const guardedSteps = (guard, describeGuard, rests = true, rules) => {
|
|
|
1798
1823
|
exhausted: guarded(steps.exhausted, rests ? guard : void 0)
|
|
1799
1824
|
};
|
|
1800
1825
|
};
|
|
1801
|
-
/**
|
|
1802
|
-
* Hand a red suite that repeated attempts could not fix to a person: an
|
|
1803
|
-
* agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
|
|
1804
|
-
* on, the human is told the budget is spent instead.
|
|
1805
|
-
*/
|
|
1806
|
-
const escalation = (count, guard, guardRests = true, rules) => escalation$1(count, guardedSteps(guard, void 0, guardRests, rules));
|
|
1807
1826
|
/** "Is this red round the same failure as the last one?" — asked of two reports, or of nothing when one is empty. */
|
|
1808
1827
|
const retryQuestion = (comparable) => comparable ? {
|
|
1809
1828
|
id: "verdict",
|
|
@@ -1986,4 +2005,4 @@ const steering = {
|
|
|
1986
2005
|
[REVIEW]: "review"
|
|
1987
2006
|
};
|
|
1988
2007
|
//#endregion
|
|
1989
|
-
export {
|
|
2008
|
+
export { naivePassPrompt as $, reviewQuality as A, isConflictTitle as At, coverageQuestion as B, collecting as C, defaults as Ct, fixRisks as D, conflictBody as Dt, fixQuality as E, STANDARDS as Et, builderSystem as F, flagMentions as Ft, designTriagePrompt as G, criterionLine as H, clearCriteriaMessage as I, flagReferences as It, handOffAwaitReviewMessage as J, finisherSystem as K, clearCriteriaQuestion as L, flipProblems as Lt, agentWithSkills as M, parseStandards as Mt, buildReviewCollectingPrompt as N, renderStandards as Nt, planner as O, conflictFor as Ot, buildReviewTriageMessage as P, standardKey as Pt, naiveApplyFailure as Q, composedReview as R, registryEntry as Rt, coder as S, withLapFindings as St, fixNits as T, testFileGlobs as Tt, designGateAnswerMessage as U, coveringPrompt as V, designSystem as W, implementBuildingPrompt as X, idleMessage as Y, implementFixPrompt as Z, REQUIREMENTS as _, slicingPrompt as _t, flagModeOn as a, refactorDroppedRefusal as at, answerReviewQuestions as b, unwindFailedMessage as bt, baseline as c, refactorRules as ct, healthy as d, reviewFeedbackFixPrompt as dt, normaliseCriteriaScript as et, FEEDBACK as f, reviewGateBlockedMessage as ft, REFACTOR_NOTE as g, seamSteerMessage as gt, REFACTOR as h, seamByHandMessage as ht, skills as i, refactorBuildingPrompt as it, reviewing as j, oneLine as jt, reviewMissing as k, conflictTitle as kt, gate as l, reviewEditNotesCapture as lt, QUALITY as m, reviewerSystem as mt, summary as n, qualityFindingsFile as nt, flagShipped as o, refactorFixPrompt as ot, HANDOFF as p, reviewNotesCapture as pt, fixReviewNotesPrompt as q, access as r, qualityTriageMessage as rt, shipped as s, refactorNotePrompt as st, steering as t, oversizedMessage as tt, guarded as u, reviewEditsCapture as ut, REVIEW as v, startGateBlockedMessage as vt, fixCheck as w, envDefaults as wt, awaitReview as x, unwindFailure as xt, SLICES as y, uncoveredMessage as yt, coverageMessage as z, requireFlag as zt };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { answered, changesSince, head, judge, numeric, read, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
|
|
2
2
|
import { removedAssertions, review } from "../workflow-kit.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-
|
|
3
|
+
import { At as isConflictTitle, C as collecting, D as fixRisks, Dt as conflictBody, Et as STANDARDS, Mt as parseStandards, Nt as renderStandards, Ot as conflictFor, P as buildReviewTriageMessage, Pt as standardKey, R as composedReview, T as fixNits, b as answerReviewQuestions, d as healthy, jt as oneLine, k as reviewMissing, kt as conflictTitle, lt as reviewEditNotesCapture, pt as reviewNotesCapture, u as guarded, ut as reviewEditsCapture, v as REVIEW, w as fixCheck, x as awaitReview } from "./reserved-DF1HD6jL.mjs";
|
|
4
|
+
import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-D89fU_wf.mjs";
|
|
5
5
|
//#region src/workflows/shared/learn.ts
|
|
6
6
|
const SUMMARY_MAX = 200;
|
|
7
7
|
const BANNER = /^_(?:The fixer left this unresolved\.|A fix turn removed this while renaming another finding — check whether it was fixed\.|No detail given\.)_\s*/;
|
|
@@ -276,7 +276,7 @@ const triage = (severities) => async (notes) => {
|
|
|
276
276
|
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
277
277
|
/**
|
|
278
278
|
* gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
|
|
279
|
-
* turn
|
|
279
|
+
* turn. A re-review
|
|
280
280
|
* after fixed nits restores the doc as committed at `carry`, answers kept;
|
|
281
281
|
* a fresh one leaves out the notes the human already answered.
|
|
282
282
|
*/
|
|
@@ -289,7 +289,7 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
|
|
|
289
289
|
added: standardsAddedSince(base),
|
|
290
290
|
undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
|
|
291
291
|
}
|
|
292
|
-
})
|
|
292
|
+
})) : showScript(carry, REVIEW, REVIEW), {
|
|
293
293
|
label: "Composing the review",
|
|
294
294
|
file: REVIEW,
|
|
295
295
|
base
|
|
@@ -371,10 +371,7 @@ function buildTail(first, second, ...rest) {
|
|
|
371
371
|
return tail((escalations, report) => humanReview(base, escalations, {
|
|
372
372
|
...form,
|
|
373
373
|
report
|
|
374
|
-
}),
|
|
375
|
-
...form,
|
|
376
|
-
triage: true
|
|
377
|
-
});
|
|
374
|
+
}), form);
|
|
378
375
|
}
|
|
379
376
|
const isOptionalBoolean = (value) => value == null || typeof value === "boolean";
|
|
380
377
|
const legacyForm = (fixFirst, base, green, options) => isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options) ? [base, {
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { answered, head, judge, mark, numeric, read, refuse, removeScript, run, scope, sectionBodies, showScript, vars, writeScript } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { A as
|
|
2
|
+
import { lensEntriesOf, lensOf, qualityLap, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
+
import { A as reviewQuality, D as fixRisks, E as fixQuality, J as handOffAwaitReviewMessage, St as withLapFindings, d as healthy, j as reviewing, k as reviewMissing, m as QUALITY, nt as qualityFindingsFile, rt as qualityTriageMessage, u as guarded, v as REVIEW, w as fixCheck, x as awaitReview } from "./reserved-DF1HD6jL.mjs";
|
|
4
4
|
//#region src/workflows/shared/lenses.ts
|
|
5
5
|
const NO_FINDINGS = {
|
|
6
6
|
notes: [],
|
|
7
7
|
fixed: [],
|
|
8
8
|
noise: []
|
|
9
9
|
};
|
|
10
|
+
/** `reports` as one, each finding labelled with the slice it came from, in slice order. */
|
|
11
|
+
const joinReports = (reports) => {
|
|
12
|
+
const tag = (slice, fs) => fs.map((f) => ({
|
|
13
|
+
...f,
|
|
14
|
+
slice
|
|
15
|
+
}));
|
|
16
|
+
return {
|
|
17
|
+
notes: reports.flatMap((r) => tag(r.slice, r.report.notes)),
|
|
18
|
+
fixed: reports.flatMap((r) => tag(r.slice, r.report.fixed)),
|
|
19
|
+
noise: reports.flatMap((r) => tag(r.slice, r.report.noise))
|
|
20
|
+
};
|
|
21
|
+
};
|
|
10
22
|
const UNTITLED = "Quality findings";
|
|
11
23
|
const below = (section) => section.split("\n").slice(1).join("\n").trim();
|
|
12
24
|
const strayProse = (text, sections) => sections.reduce((rest, body) => rest.replace(body, "\n"), text).split("\n").filter((line) => !/^#\s/.test(line)).join("\n").trim();
|
|
@@ -86,7 +98,14 @@ const classify = async (findings) => {
|
|
|
86
98
|
*/
|
|
87
99
|
const triageFindings = async (options) => {
|
|
88
100
|
const { escalations, guard, guardRests, rules } = options;
|
|
89
|
-
const
|
|
101
|
+
const withLens = (f) => {
|
|
102
|
+
const lens = options.lensOf?.(f);
|
|
103
|
+
return lens === void 0 ? f : {
|
|
104
|
+
...f,
|
|
105
|
+
lens
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
const findings = findingsOf(read(".gtd/QUALITY.md") ?? "").map(withLens);
|
|
90
109
|
if (findings.length === 0) return NO_FINDINGS;
|
|
91
110
|
const { mechanical, noise, design } = await classify(findings);
|
|
92
111
|
const sorted = qualityFindingsFile(mechanical);
|
|
@@ -103,7 +122,10 @@ const triageFindings = async (options) => {
|
|
|
103
122
|
commit
|
|
104
123
|
})));
|
|
105
124
|
unclear.push(...turn.unclear);
|
|
106
|
-
open = turn.open
|
|
125
|
+
open = turn.open.map((f) => ({
|
|
126
|
+
...mechanical.find((m) => m.title === f.title),
|
|
127
|
+
...f
|
|
128
|
+
}));
|
|
107
129
|
}
|
|
108
130
|
if (mechanical.length > 0) await healthy(() => fixCheck(rules), {
|
|
109
131
|
escalations,
|
|
@@ -131,22 +153,15 @@ const triageFindings = async (options) => {
|
|
|
131
153
|
//#region src/workflows/shared/tail.ts
|
|
132
154
|
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
133
155
|
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
134
|
-
|
|
135
|
-
* One review turn per lens over the whole change, each appending every
|
|
136
|
-
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
137
|
-
* has any.
|
|
138
|
-
*/
|
|
139
|
-
const qualityLap = (guard, lenses, base) => qualityLap$1(lenses ?? lensEntriesOf(vars.qualityReviews ?? ""), guarded((entry) => {
|
|
156
|
+
const lensLap = (guard, lenses, base, seen) => qualityLap(lenses ?? lensEntriesOf(vars.qualityReviews ?? ""), guarded(async (entry) => {
|
|
140
157
|
const { lens, model } = lensOf(entry);
|
|
141
|
-
|
|
158
|
+
await reviewQuality(lens, base, model);
|
|
159
|
+
seen(lens, findingsOf(read(".gtd/QUALITY.md") ?? ""));
|
|
142
160
|
}, guard), QUALITY);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
cap: 3,
|
|
148
|
-
escalate: () => escalation(escalations, guard, guardRests, rules)
|
|
149
|
-
});
|
|
161
|
+
const closeQuality = async () => {
|
|
162
|
+
if (read(".gtd/QUALITY.md") === void 0) return;
|
|
163
|
+
await run("quality.closing", removeScript([QUALITY]), { label: "Clearing quality findings" });
|
|
164
|
+
};
|
|
150
165
|
/** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
|
|
151
166
|
const closeReview = (round) => run("review.closing", removeScript([REVIEW, REVIEW_ORIG]), {
|
|
152
167
|
label: "Closing the review",
|
|
@@ -183,7 +198,7 @@ const runTail = (finish, options) => scope("build", async () => {
|
|
|
183
198
|
const guardRests = options.guardRests === void 0 ? true : options.guardRests;
|
|
184
199
|
const escalations = { rounds: 0 };
|
|
185
200
|
const fix = () => fixCheck(rules);
|
|
186
|
-
|
|
201
|
+
if (options.fixFirst) {
|
|
187
202
|
await guarded(fix, guard)();
|
|
188
203
|
await healthy(fix, {
|
|
189
204
|
fixesSoFar: 1,
|
|
@@ -192,38 +207,52 @@ const runTail = (finish, options) => scope("build", async () => {
|
|
|
192
207
|
guardRests,
|
|
193
208
|
rules
|
|
194
209
|
});
|
|
195
|
-
}
|
|
196
|
-
if (options.fixFirst) await repair();
|
|
197
|
-
else if (!options.green) await healthy(fix, {
|
|
210
|
+
} else if (!options.green) await healthy(fix, {
|
|
198
211
|
escalations,
|
|
199
212
|
guard,
|
|
200
213
|
guardRests,
|
|
201
214
|
rules
|
|
202
215
|
});
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
const byTitle = [];
|
|
217
|
+
const lap = await lensLap(guard, lenses, lensBase, (lens, findings) => {
|
|
218
|
+
const before = byTitle.map((f) => f.title);
|
|
219
|
+
for (const { title } of findings) {
|
|
220
|
+
const at = before.indexOf(title);
|
|
221
|
+
if (at >= 0) before.splice(at, 1);
|
|
222
|
+
else byTitle.push({
|
|
223
|
+
title,
|
|
224
|
+
lens
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
const lensOf = (f) => {
|
|
229
|
+
const at = byTitle.findIndex((e) => e.title === f.title);
|
|
230
|
+
return at < 0 ? void 0 : byTitle.splice(at, 1)[0].lens;
|
|
231
|
+
};
|
|
232
|
+
const report = lap === "findings" ? await triageFindings({
|
|
211
233
|
escalations,
|
|
212
234
|
guard,
|
|
213
235
|
guardRests,
|
|
214
|
-
rules
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
236
|
+
rules,
|
|
237
|
+
lensOf
|
|
238
|
+
}) : NO_FINDINGS;
|
|
239
|
+
await closeQuality();
|
|
240
|
+
return finish(escalations, report);
|
|
218
241
|
});
|
|
219
242
|
/**
|
|
220
243
|
* The fix workflow's human review since `base`: a sign-off closes
|
|
221
244
|
* `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
|
|
222
245
|
* `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
|
|
223
246
|
*/
|
|
224
|
-
const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
|
|
247
|
+
const handOffReview = (base, escalations, guard, report = NO_FINDINGS) => reviewToHandOff(base, {
|
|
225
248
|
file: REVIEW,
|
|
226
|
-
reviewing: guarded((b, carry) =>
|
|
249
|
+
reviewing: guarded(async (b, carry) => {
|
|
250
|
+
await reviewing(b, carry);
|
|
251
|
+
const review = read(REVIEW);
|
|
252
|
+
if (review === void 0) return;
|
|
253
|
+
const full = withLapFindings(review, report);
|
|
254
|
+
if (full !== review) await run("review.carrying", writeScript(REVIEW, full), { label: "Adding the quality lap's findings" });
|
|
255
|
+
}, guard),
|
|
227
256
|
fixRisks: guarded(fixRisks, guard),
|
|
228
257
|
keepGreen: () => healthy(fixCheck, {
|
|
229
258
|
escalations,
|
|
@@ -237,10 +266,10 @@ const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
|
|
|
237
266
|
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.",
|
|
238
267
|
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
|
|
239
268
|
});
|
|
240
|
-
/** The tail the fix workflow runs: ends at the hand-off review since `base
|
|
241
|
-
const handOffTail = (fixFirst, base, guard) => tail((escalations) => handOffReview(base, escalations, guard), {
|
|
269
|
+
/** The tail the fix workflow runs: ends at the hand-off review since `base`, which gets what the lap left. */
|
|
270
|
+
const handOffTail = (fixFirst, base, guard) => tail((escalations, report) => handOffReview(base, escalations, guard, report), {
|
|
242
271
|
fixFirst,
|
|
243
272
|
guard
|
|
244
273
|
});
|
|
245
274
|
//#endregion
|
|
246
|
-
export { refusePendingHandOff as a, isOptionalObject as i, closeReview as n, tail as o, handOffTail as r, NO_FINDINGS as s, REVIEW_ORIG as t };
|
|
275
|
+
export { refusePendingHandOff as a, joinReports as c, isOptionalObject as i, closeReview as n, tail as o, handOffTail as r, NO_FINDINGS as s, REVIEW_ORIG as t };
|