@pmelab/gtd 44.0.0 → 45.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +40 -9
- package/dist/chunks/{reserved-DJk_6-ju.mjs → reserved-ulcSkND6.mjs} +142 -5
- package/dist/chunks/{review-X7Jfz0dY.mjs → review-Da2ndnB1.mjs} +177 -7
- package/dist/chunks/{tail-5w5mxGTq.mjs → tail-BW030xz_.mjs} +1 -1
- package/dist/gtd.bundle.mjs +7 -5
- package/dist/testkit.mjs +7 -5
- package/dist/workflow-kit.mjs +22 -10
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +3 -3
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +4 -15
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows/tests.mjs +193 -0
- package/dist/workflows.mjs +533 -40
- package/package.json +6 -1
- package/src/install/ConfigScaffold.ts +2 -1
- package/src/install/Install.ts +5 -4
- package/src/workflow-kit/review.test.ts +80 -2
- package/src/workflow-kit/review.ts +45 -11
- package/src/workflows/feature/review-answers.feature +12 -3
- package/src/workflows/feature/standards.feature +286 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/review/review-door.feature +11 -2
- package/src/workflows/review/review.ts +3 -16
- package/src/workflows/shared/learn.test.ts +154 -0
- package/src/workflows/shared/learn.ts +206 -0
- package/src/workflows/shared/lenses.ts +5 -1
- package/src/workflows/shared/options.ts +22 -1
- package/src/workflows/shared/review.ts +29 -2
- package/src/workflows/shared/skills.test.ts +2 -0
- package/src/workflows/shared/skills.ts +3 -0
- package/src/workflows/shared/standards.test.ts +90 -0
- package/src/workflows/shared/standards.ts +156 -0
- package/src/workflows/shared/text.test.ts +58 -1
- package/src/workflows/shared/text.ts +33 -3
- package/src/workflows/shared/vars.ts +10 -0
- package/src/workflows/tests/index.ts +6 -0
- package/src/workflows/tests/tests-options.feature +52 -0
- package/src/workflows/tests/tests.feature +266 -0
- package/src/workflows/tests/tests.test.ts +51 -0
- package/src/workflows/tests/tests.ts +270 -0
package/dist/workflow-kit.mjs
CHANGED
|
@@ -233,14 +233,20 @@ const record = (p, decisions) => {
|
|
|
233
233
|
p.decided?.(decisions);
|
|
234
234
|
for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine(d.anchor));
|
|
235
235
|
};
|
|
236
|
+
const routed = (p, answers) => answers.filter((d) => p.settles?.(d) !== true);
|
|
237
|
+
/** Record the routed answers on the gate's commit, then hand every answer to `learn`. */
|
|
238
|
+
const recordAndLearn = async (p, decisions, answers) => {
|
|
239
|
+
record(p, decisions);
|
|
240
|
+
if (answers.length > 0 && p.learn !== void 0) await p.learn(answers);
|
|
241
|
+
};
|
|
236
242
|
/**
|
|
237
243
|
* Judge every note, then run each verdict's route: answers, then nit fixes,
|
|
238
244
|
* then the edits' lap. Of the human's `decisions`, a fix joins the nit batch
|
|
239
245
|
* unjudged, an own answer is judged like a note, and a won't-fix is dropped
|
|
240
246
|
* with a `Gtd-Mark` on the gate's commit.
|
|
241
247
|
*/
|
|
242
|
-
const routeNotes = async (p, notes, r, decisions = []) => {
|
|
243
|
-
|
|
248
|
+
const routeNotes = async (p, notes, r, decisions = [], answers = decisions) => {
|
|
249
|
+
await recordAndLearn(p, decisions, answers);
|
|
244
250
|
const of = (c) => decisions.filter((d) => d.choice === c);
|
|
245
251
|
const fixes = of("fix").map((d, i) => decisionNote(d, `fix-${i + 1}`, d.body));
|
|
246
252
|
const judged = [...notes, ...of("answer").map((d, i) => decisionNote(d, `answer-${i + 1}`, d.text))];
|
|
@@ -294,9 +300,8 @@ const settledAfter = (p, collectedAt) => ({
|
|
|
294
300
|
folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
|
|
295
301
|
settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
|
|
296
302
|
});
|
|
297
|
-
const finishNotes = async (p,
|
|
298
|
-
|
|
299
|
-
if (notes.length + decisions.length > 0) return routeNotes(p, notes, ctx, decisions);
|
|
303
|
+
const finishNotes = async (p, notes, answers, reopened, ctx) => {
|
|
304
|
+
if (notes.length + answers.length > 0) return routeNotes(p, notes, ctx, routed(p, answers), answers);
|
|
300
305
|
if (reopened) return ctx.unfolded();
|
|
301
306
|
await ctx.close();
|
|
302
307
|
return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"));
|
|
@@ -311,6 +316,7 @@ const decisionsSince = (p, since) => {
|
|
|
311
316
|
const after = read(p.file) ?? "";
|
|
312
317
|
return reviewDecisions(since.get(p.file)?.before ?? after, after);
|
|
313
318
|
};
|
|
319
|
+
const notesSince = (p, since) => reviewNotes(since.get(p.file)?.before ?? "", read(p.file) ?? "");
|
|
314
320
|
const finish = async (p, reviewed, collectedAt) => {
|
|
315
321
|
const round = head();
|
|
316
322
|
const since = changesSince(reviewed);
|
|
@@ -331,23 +337,29 @@ const finish = async (p, reviewed, collectedAt) => {
|
|
|
331
337
|
settled,
|
|
332
338
|
folded
|
|
333
339
|
});
|
|
334
|
-
const
|
|
335
|
-
if (settled && !reopened &&
|
|
340
|
+
const answers = decisionsSince(p, since);
|
|
341
|
+
if (settled && !reopened && answers.length === 0 || edited.length === 0 && !noted) return unfolded();
|
|
342
|
+
const notes = notesSince(p, since);
|
|
336
343
|
if (edited.length > 0) {
|
|
337
|
-
|
|
344
|
+
await recordAndLearn(p, routed(p, answers), answers);
|
|
338
345
|
await close();
|
|
339
346
|
return outcome(await collect(p, p.captures.edits(round, reopened ? reviewed : void 0), "edits", {
|
|
340
347
|
notes: [],
|
|
341
348
|
paths: edited.map((c) => c.path)
|
|
342
349
|
}));
|
|
343
350
|
}
|
|
344
|
-
return finishNotes(p,
|
|
351
|
+
return finishNotes(p, notes, answers, reopened, {
|
|
345
352
|
round,
|
|
346
353
|
close,
|
|
347
354
|
outcome,
|
|
348
355
|
unfolded
|
|
349
356
|
});
|
|
350
357
|
};
|
|
358
|
+
/** A sign-off while something waits on the human writes the review again instead. */
|
|
359
|
+
const unlessPending = (p, result) => "verdict" in result && result.verdict === "signoff" && p.pending?.() === true ? {
|
|
360
|
+
next: "rereview",
|
|
361
|
+
carry: void 0
|
|
362
|
+
} : result;
|
|
351
363
|
/** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
|
|
352
364
|
const reviewOnce = async (p, base, carry) => {
|
|
353
365
|
await p.reviewing(base, carry);
|
|
@@ -373,7 +385,7 @@ const review = async (base, p) => {
|
|
|
373
385
|
for (;;) {
|
|
374
386
|
collectedAt = await converse(p, base, reviewed, collectedAt);
|
|
375
387
|
if (collectedAt === "missing") break;
|
|
376
|
-
const result = await finish(p, reviewed, collectedAt);
|
|
388
|
+
const result = unlessPending(p, await finish(p, reviewed, collectedAt));
|
|
377
389
|
if (!("next" in result)) return result;
|
|
378
390
|
if (result.next === "rereview") {
|
|
379
391
|
carry = result.carry;
|
|
@@ -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 { Et as envDefaults, Ht as requireFlag, Tt as defaults, Vt as registryEntry, i as skills, n as summary, o as flagShipped, r as access, t as steering, w as coder, zt as flagReferences } from "../chunks/reserved-ulcSkND6.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-BW030xz_.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,8 +1,8 @@
|
|
|
1
1
|
import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
|
|
2
2
|
import { lensEntriesOf } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as
|
|
4
|
-
import { a as refusePendingHandOff, o as tail } from "../chunks/tail-
|
|
5
|
-
import {
|
|
3
|
+
import { $ as implementFixPrompt, A as planner, Ct as unwindFailedMessage, D as fixNits, Dt as testFileGlobs, E as fixCheck, Et as envDefaults, F as buildReviewCollectingPrompt, G as designGateAnswerMessage, H as coverageQuestion, K as designSystem, L as builderSystem, P as agentWithSkills, Q as implementBuildingPrompt, R as clearCriteriaMessage, S as answerReviewQuestions, St as uncoveredMessage, T as collecting, Tt as defaults, U as coveringPrompt, V as coverageMessage, W as criterionLine, Z as idleMessage, _ as REFACTOR, _t as reviewerSystem, a as flagModeOn, bt as slicingPrompt, ct as refactorDroppedRefusal, d as gate, dt as refactorRules, et as naiveApplyFailure, g as QUALITY, h as HANDOFF, i as skills, lt as refactorFixPrompt, m as FEEDBACK, n as summary, nt as normaliseCriteriaScript, ot as qualityUnresolvedMessage, p as healthy, q as designTriagePrompt, r as access, rt as oversizedMessage, s as shipped, st as refactorBuildingPrompt, t as steering, tt as naivePassPrompt, ut as refactorNotePrompt, v as REFACTOR_NOTE, vt as seamByHandMessage, w as coder, wt as unwindFailure, x as SLICES, xt as startGateBlockedMessage, y as REQUIREMENTS, yt as seamSteerMessage, z as clearCriteriaQuestion } from "../chunks/reserved-ulcSkND6.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, o as tail } from "../chunks/tail-BW030xz_.mjs";
|
|
5
|
+
import { a as reUnwind, i as isCode$1, r as humanReview, t as buildTail } from "../chunks/review-Da2ndnB1.mjs";
|
|
6
6
|
import { posix } from "node:path";
|
|
7
7
|
//#region src/workflows/shared/fences.ts
|
|
8
8
|
const markFenced = (text) => {
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { g as reviewRisks, h as reviewNotes } from "../chunks/steering-D4LUQ-av.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { Et as envDefaults, J as finisherSystem, P as agentWithSkills, Tt as defaults, Y as fixReviewNotesPrompt, b as REVIEW, i as skills, l as baseline, n as summary, r as access, s as shipped, t as steering, w as coder } from "../chunks/reserved-ulcSkND6.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-BW030xz_.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,19 +1,8 @@
|
|
|
1
|
-
import { read, refuse,
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff } from "../chunks/tail-
|
|
4
|
-
import {
|
|
1
|
+
import { read, refuse, start } from "../flows.mjs";
|
|
2
|
+
import { Et as envDefaults, Tt as defaults, d as gate, ht as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, y as REQUIREMENTS } from "../chunks/reserved-ulcSkND6.mjs";
|
|
3
|
+
import { a as refusePendingHandOff } from "../chunks/tail-BW030xz_.mjs";
|
|
4
|
+
import { a as reUnwind, n as fixFeedback, t as buildTail } from "../chunks/review-Da2ndnB1.mjs";
|
|
5
5
|
//#region src/workflows/review/review.ts
|
|
6
|
-
/** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
|
|
7
|
-
const fixFeedback = async () => {
|
|
8
|
-
await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
|
|
9
|
-
label: "Fixing your review feedback",
|
|
10
|
-
file: REQUIREMENTS,
|
|
11
|
-
model: coder(),
|
|
12
|
-
system: finisherSystem(),
|
|
13
|
-
allowEmpty: true
|
|
14
|
-
});
|
|
15
|
-
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
|
|
16
|
-
};
|
|
17
6
|
/**
|
|
18
7
|
* The feature's review of everything since `reviewBase` — a green gate, the
|
|
19
8
|
* triaged quality lap, then the human review — with no design phase: a
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { Bt as flipProblems, Et as envDefaults, Ht as requireFlag, Rt as flagMentions, Tt as defaults, d as gate, i as skills, m as FEEDBACK, n as summary, o as flagShipped, r as access, t as steering, w as coder } from "../chunks/reserved-ulcSkND6.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.";
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { changes, changesSince, glob, read, refuse, removeScript, run, scope, sectionBodies, start, vars } from "../flows.mjs";
|
|
2
|
+
import { E as fixCheck, Et as envDefaults, J as finisherSystem, Ot as testsDefaults, P as agentWithSkills, Tt as defaults, c as testsShipped, d as gate, i as skills, n as summary, p as healthy, r as access, t as steering, w as coder, y as REQUIREMENTS } from "../chunks/reserved-ulcSkND6.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, o as tail } from "../chunks/tail-BW030xz_.mjs";
|
|
4
|
+
import { a as reUnwind, i as isCode, n as fixFeedback, r as humanReview } from "../chunks/review-Da2ndnB1.mjs";
|
|
5
|
+
//#region src/workflows/tests/tests.ts
|
|
6
|
+
const TESTS = ".gtd/TESTS.md";
|
|
7
|
+
const RETIRE = ".gtd/RETIRE.md";
|
|
8
|
+
/** The first two answer boxes of a deletion proposal; the third stays the human's own answer. */
|
|
9
|
+
const RETIRE_ANSWERS = ["delete it", "keep it skipped"];
|
|
10
|
+
testsDefaults.skipPattern;
|
|
11
|
+
/** `skipPattern` as a per-line expression; refuses one that is blank, invalid, or matches every line. */
|
|
12
|
+
const skipExpression = (pattern) => {
|
|
13
|
+
if (pattern.trim() === "") return refuse("gtd: skipPattern must not be blank");
|
|
14
|
+
let expression;
|
|
15
|
+
try {
|
|
16
|
+
expression = new RegExp(pattern);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return refuse(`gtd: skipPattern is not a valid regular expression: ${String(error)}`);
|
|
19
|
+
}
|
|
20
|
+
if (expression.test("")) return refuse("gtd: skipPattern must not match the empty string");
|
|
21
|
+
return expression;
|
|
22
|
+
};
|
|
23
|
+
/** Every line of `files` the expression calls skipped, in file then line order. */
|
|
24
|
+
const skipsIn = (files, expression) => files.flatMap(({ path, text }) => text.split("\n").flatMap((line, i) => expression.test(line) ? [{
|
|
25
|
+
path,
|
|
26
|
+
line: i + 1,
|
|
27
|
+
text: line.trim()
|
|
28
|
+
}] : []));
|
|
29
|
+
const testPaths = () => {
|
|
30
|
+
const globs = (vars.testFiles ?? testsDefaults.testFiles).split(",").map((g) => g.trim()).filter((g) => g !== "");
|
|
31
|
+
return [...new Set(globs.flatMap((g) => glob(g)))].filter(isCode).sort();
|
|
32
|
+
};
|
|
33
|
+
const scan = (paths, expression) => skipsIn(paths.map((path) => ({
|
|
34
|
+
path,
|
|
35
|
+
text: read(path) ?? ""
|
|
36
|
+
})), expression);
|
|
37
|
+
/** A section title as a repository path: backticks and a leading `./` dropped. */
|
|
38
|
+
const pathOf = (title) => title.replace(/`/g, "").trim().replace(/^\.\//, "");
|
|
39
|
+
const below = (section) => section.split("\n").slice(1).join("\n").trim();
|
|
40
|
+
const sweepingPrompt = (skips) => `Sweep this repository's tests for ones to revive.
|
|
41
|
+
|
|
42
|
+
Skipped tests gtd found:
|
|
43
|
+
|
|
44
|
+
${skips.length === 0 ? "- none" : skips.map((s) => `- ${s.path}:${s.line} — ${s.text}`).join("\n")}
|
|
45
|
+
|
|
46
|
+
- Audit every test in the repository with the test-audit skill — the whole
|
|
47
|
+
suite, not a recent change
|
|
48
|
+
- Write \`${TESTS}\`: \`# Tests\`, then one \`## <path>\` section per test file
|
|
49
|
+
that needs work — every file listed above, and each file the audit found
|
|
50
|
+
worth rewriting — saying which tests are skipped and what the audit found
|
|
51
|
+
- Change nothing else; write nothing when there is nothing to do
|
|
52
|
+
- Leave everything uncommitted and finish your turn`;
|
|
53
|
+
const rewritingPrompt = (path, found) => `Rewrite the tests in \`${path}\` until none is skipped and the suite is green.
|
|
54
|
+
|
|
55
|
+
What the sweep found:
|
|
56
|
+
|
|
57
|
+
${found || "_Nothing more than the skipped tests._"}
|
|
58
|
+
|
|
59
|
+
- Unskip every skipped test in the file and make it pass, rewriting it where
|
|
60
|
+
it rotted, and fix what the audit found
|
|
61
|
+
- A test that is obsolete — what it tests is gone, or another test already
|
|
62
|
+
covers it — stays skipped: never delete it. Propose its deletion in
|
|
63
|
+
\`${RETIRE}\` instead: a \`## <title>\` section naming \`${path}\`, the test,
|
|
64
|
+
and why it should go. A person decides. Propose a line gtd misread as
|
|
65
|
+
skipped (fixture text, a comment) the same way, saying so
|
|
66
|
+
- Delete no test file
|
|
67
|
+
- Leave everything uncommitted and finish your turn`;
|
|
68
|
+
const gateMessage = () => `The test baseline is red — gtd will not revive skipped tests on a broken suite.
|
|
69
|
+
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
70
|
+
|
|
71
|
+
What each change does next (then run \`gtd land\`):
|
|
72
|
+
- **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**tests-gate.check**).
|
|
73
|
+
`;
|
|
74
|
+
const awaitMessage = (base) => `\`.gtd/REVIEW.md\` holds one chunk per test the sweep would delete. Each ends in
|
|
75
|
+
three boxes — tick exactly one: \`delete it\` (the test is deleted, then the
|
|
76
|
+
suite runs), \`keep it skipped\` (dropped, recorded in the commit), or write your
|
|
77
|
+
own answer over \`_your answer_\` and tick it (judged like a note). Every
|
|
78
|
+
landing here is refused until each proposal holds exactly one tick. An agent
|
|
79
|
+
turn that deleted a test file was refused; check the diff for tests removed
|
|
80
|
+
inside a file.
|
|
81
|
+
|
|
82
|
+
Review the revived tests yourself:
|
|
83
|
+
|
|
84
|
+
git diff ${base}
|
|
85
|
+
|
|
86
|
+
Then run \`gtd land\`. A note or code edit of your own is read as feedback and
|
|
87
|
+
built before you review again.
|
|
88
|
+
`;
|
|
89
|
+
/** A proposal's own text, made a request the nit fixer acts on once the human ticks `delete it`. */
|
|
90
|
+
const proposals = (text) => sectionBodies(text).map((s, i) => ({
|
|
91
|
+
id: `R${i + 1}`,
|
|
92
|
+
title: s.title,
|
|
93
|
+
body: `Delete this skipped test.\n\n${below(s.body)}`,
|
|
94
|
+
answers: RETIRE_ANSWERS
|
|
95
|
+
}));
|
|
96
|
+
/** Sections of `text` that name `path` as a whole token — a trailing full stop still counts. */
|
|
97
|
+
const naming = (text, path) => {
|
|
98
|
+
const token = new RegExp(`(?<![\\w./-])${path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w/-])`);
|
|
99
|
+
return sectionBodies(text).filter((s) => token.test(s.body.replace(/(^|[^\w.])\.\//g, "$1"))).length;
|
|
100
|
+
};
|
|
101
|
+
/** Refuses a test file a turn deleted, and a skip left in the file being rewritten that `.gtd/RETIRE.md` does not name. */
|
|
102
|
+
const guardFor = (known, expression, path) => () => {
|
|
103
|
+
const deleted = changes().filter((c) => c.status === "deleted" && known.has(c.path)).map((c) => c.path);
|
|
104
|
+
if (deleted.length > 0) refuse(`gtd land: tests: ${deleted.join(", ")} deleted — a test goes only once a person ticks "delete it" at the review`);
|
|
105
|
+
if (path === void 0) return;
|
|
106
|
+
const left = scan([path], expression).slice(naming(read(RETIRE) ?? "", path));
|
|
107
|
+
if (left.length > 0) refuse(`gtd land: tests: ${left.map((s) => `${s.path}:${s.line}`).join(", ")} still skipped — revive it, or propose its deletion in ${RETIRE}`);
|
|
108
|
+
};
|
|
109
|
+
/** The sweep's packages, refused when it changed more than its plan or the plan misses a file. */
|
|
110
|
+
const planned = (skips, known) => {
|
|
111
|
+
const strays = changes().filter((c) => c.path !== TESTS);
|
|
112
|
+
if (strays.length > 0) refuse(`gtd land: tests: the sweep changed ${strays.map((c) => c.path).join(", ")} — it writes only ${TESTS}`);
|
|
113
|
+
const packages = sectionBodies(read(TESTS) ?? "").map((s) => ({
|
|
114
|
+
path: pathOf(s.title),
|
|
115
|
+
found: below(s.body)
|
|
116
|
+
}));
|
|
117
|
+
const missing = [...packages.filter((p) => !known.has(p.path)).map((p) => `"${p.path}" is no test file`), ...[...new Set(skips.map((s) => s.path))].filter((path) => !packages.some((p) => p.path === path)).map((path) => `${path} has no section`)];
|
|
118
|
+
if (missing.length > 0) refuse(`gtd land: tests: in ${TESTS}, ${missing.join("; ")}`);
|
|
119
|
+
return packages;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Revive or retire skipped tests: a sweep plans one package per test file, each
|
|
123
|
+
* is rewritten until unskipped and green, and what an agent calls obsolete
|
|
124
|
+
* reaches the human review as a deletion proposal only a person can tick.
|
|
125
|
+
*/
|
|
126
|
+
const testsFlow = async () => {
|
|
127
|
+
refusePendingHandOff();
|
|
128
|
+
for (const file of [
|
|
129
|
+
REQUIREMENTS,
|
|
130
|
+
TESTS,
|
|
131
|
+
RETIRE
|
|
132
|
+
]) if (read(file) !== void 0) refuse(`gtd: ${file} is left over — finish or remove it first.`);
|
|
133
|
+
const expression = skipExpression(vars.skipPattern ?? testsDefaults.skipPattern);
|
|
134
|
+
await gate("tests-gate", gateMessage());
|
|
135
|
+
const base = start();
|
|
136
|
+
const known = new Set(testPaths());
|
|
137
|
+
const guard = guardFor(known, expression);
|
|
138
|
+
const report = await scope("tests", async () => {
|
|
139
|
+
const skips = scan([...known], expression);
|
|
140
|
+
await agentWithSkills("sweeping", sweepingPrompt(skips), {
|
|
141
|
+
label: "Sweeping the tests",
|
|
142
|
+
file: TESTS,
|
|
143
|
+
model: coder(),
|
|
144
|
+
system: finisherSystem(),
|
|
145
|
+
allowEmpty: true
|
|
146
|
+
});
|
|
147
|
+
guard();
|
|
148
|
+
const packages = planned(skips, known);
|
|
149
|
+
for (const { path, found } of packages) {
|
|
150
|
+
const guarded = guardFor(known, expression, path);
|
|
151
|
+
await agentWithSkills("rewriting", rewritingPrompt(path, found), {
|
|
152
|
+
label: `Rewriting ${path}`,
|
|
153
|
+
file: RETIRE,
|
|
154
|
+
model: coder(),
|
|
155
|
+
system: finisherSystem(),
|
|
156
|
+
allowEmpty: true
|
|
157
|
+
});
|
|
158
|
+
guarded();
|
|
159
|
+
if (!changes().some((c) => isCode(c.path))) continue;
|
|
160
|
+
await healthy(() => fixCheck(), {
|
|
161
|
+
guard: guarded,
|
|
162
|
+
guardRests: false
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const retired = proposals(read(RETIRE) ?? "");
|
|
166
|
+
if (read(TESTS) !== void 0 || read(RETIRE) !== void 0) await run("closing", removeScript([TESTS, RETIRE]), { label: "Closing the sweep" });
|
|
167
|
+
const changed = changesSince(base).paths.some(isCode);
|
|
168
|
+
return retired.length === 0 && !changed ? void 0 : {
|
|
169
|
+
notes: retired,
|
|
170
|
+
fixed: [],
|
|
171
|
+
noise: []
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
if (report === void 0) return;
|
|
175
|
+
let outcome = await tail((escalations) => humanReview(base, escalations, {
|
|
176
|
+
report,
|
|
177
|
+
awaitMessage: report.notes.length > 0 ? awaitMessage : void 0
|
|
178
|
+
}), {
|
|
179
|
+
lenses: [],
|
|
180
|
+
green: true
|
|
181
|
+
});
|
|
182
|
+
while (outcome.verdict === "feedback") {
|
|
183
|
+
await reUnwind(outcome);
|
|
184
|
+
await fixFeedback();
|
|
185
|
+
outcome = await tail((escalations) => humanReview(base, escalations), { lenses: [] });
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const tests = testsShipped(testsFlow);
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/workflows/tests/index.ts
|
|
191
|
+
var tests_default = tests;
|
|
192
|
+
//#endregion
|
|
193
|
+
export { access, tests_default as default, defaults, envDefaults, skills, steering, summary, tests, testsFlow };
|