@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.
Files changed (42) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +40 -9
  3. package/dist/chunks/{reserved-DJk_6-ju.mjs → reserved-ulcSkND6.mjs} +142 -5
  4. package/dist/chunks/{review-X7Jfz0dY.mjs → review-Da2ndnB1.mjs} +177 -7
  5. package/dist/chunks/{tail-5w5mxGTq.mjs → tail-BW030xz_.mjs} +1 -1
  6. package/dist/gtd.bundle.mjs +7 -5
  7. package/dist/testkit.mjs +7 -5
  8. package/dist/workflow-kit.mjs +22 -10
  9. package/dist/workflows/cleanup.mjs +2 -2
  10. package/dist/workflows/feature.mjs +3 -3
  11. package/dist/workflows/fix.mjs +2 -2
  12. package/dist/workflows/review.mjs +4 -15
  13. package/dist/workflows/rollout.mjs +1 -1
  14. package/dist/workflows/tests.mjs +193 -0
  15. package/dist/workflows.mjs +533 -40
  16. package/package.json +6 -1
  17. package/src/install/ConfigScaffold.ts +2 -1
  18. package/src/install/Install.ts +5 -4
  19. package/src/workflow-kit/review.test.ts +80 -2
  20. package/src/workflow-kit/review.ts +45 -11
  21. package/src/workflows/feature/review-answers.feature +12 -3
  22. package/src/workflows/feature/standards.feature +286 -0
  23. package/src/workflows/index.ts +2 -1
  24. package/src/workflows/review/review-door.feature +11 -2
  25. package/src/workflows/review/review.ts +3 -16
  26. package/src/workflows/shared/learn.test.ts +154 -0
  27. package/src/workflows/shared/learn.ts +206 -0
  28. package/src/workflows/shared/lenses.ts +5 -1
  29. package/src/workflows/shared/options.ts +22 -1
  30. package/src/workflows/shared/review.ts +29 -2
  31. package/src/workflows/shared/skills.test.ts +2 -0
  32. package/src/workflows/shared/skills.ts +3 -0
  33. package/src/workflows/shared/standards.test.ts +90 -0
  34. package/src/workflows/shared/standards.ts +156 -0
  35. package/src/workflows/shared/text.test.ts +58 -1
  36. package/src/workflows/shared/text.ts +33 -3
  37. package/src/workflows/shared/vars.ts +10 -0
  38. package/src/workflows/tests/index.ts +6 -0
  39. package/src/workflows/tests/tests-options.feature +52 -0
  40. package/src/workflows/tests/tests.feature +266 -0
  41. package/src/workflows/tests/tests.test.ts +51 -0
  42. package/src/workflows/tests/tests.ts +270 -0
@@ -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
- record(p, decisions);
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, 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);
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 decisions = decisionsSince(p, since);
335
- if (settled && !reopened && decisions.length === 0 || edited.length === 0 && !noted) return unfolded();
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
- record(p, decisions);
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, since.get(p.file)?.before, decisions, reopened, {
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 { At as registryEntry, C as coder, Ot as flagReferences, Tt as envDefaults, i as skills, jt as requireFlag, n as summary, o as flagShipped, r as access, t as steering, wt as defaults } from "../chunks/reserved-DJk_6-ju.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-5w5mxGTq.mjs";
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 naiveApplyFailure, B as coverageMessage, C as coder, Ct as unwindFailure, E as fixNits, Et as testFileGlobs, G as designSystem, H as coveringPrompt, I as builderSystem, K as designTriagePrompt, L as clearCriteriaMessage, N as agentWithSkills, P as buildReviewCollectingPrompt, Q as implementFixPrompt, R as clearCriteriaQuestion, St as unwindFailedMessage, T as fixCheck, Tt as envDefaults, U as criterionLine, V as coverageQuestion, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REFACTOR_NOTE, _t as seamByHandMessage, a as flagModeOn, at as qualityUnresolvedMessage, b as SLICES, bt as startGateBlockedMessage, ct as refactorFixPrompt, et as naivePassPrompt, f as healthy, g as REFACTOR, gt as reviewerSystem, h as QUALITY, i as skills, k as planner, lt as refactorNotePrompt, m as HANDOFF, n as summary, nt as oversizedMessage, ot as refactorBuildingPrompt, p as FEEDBACK, r as access, s as shipped, st as refactorDroppedRefusal, t as steering, tt as normaliseCriteriaScript, u as gate, ut as refactorRules, v as REQUIREMENTS, vt as seamSteerMessage, w as collecting, wt as defaults, x as answerReviewQuestions, xt as uncoveredMessage, yt as slicingPrompt } from "../chunks/reserved-DJk_6-ju.mjs";
4
- import { a as refusePendingHandOff, o as tail } from "../chunks/tail-5w5mxGTq.mjs";
5
- import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-X7Jfz0dY.mjs";
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) => {
@@ -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 { C as coder, J as fixReviewNotesPrompt, N as agentWithSkills, Tt as envDefaults, c as baseline, i as skills, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, wt as defaults, y as REVIEW } from "../chunks/reserved-DJk_6-ju.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-5w5mxGTq.mjs";
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, removeScript, run, start } from "../flows.mjs";
2
- import { C as coder, N as agentWithSkills, Tt as envDefaults, i as skills, mt as reviewGateBlockedMessage, n as summary, pt as reviewFeedbackFixPrompt, q as finisherSystem, r as access, s as shipped, t as steering, u as gate, v as REQUIREMENTS, wt as defaults } from "../chunks/reserved-DJk_6-ju.mjs";
3
- import { a as refusePendingHandOff } from "../chunks/tail-5w5mxGTq.mjs";
4
- import { i as reUnwind, t as buildTail } from "../chunks/review-X7Jfz0dY.mjs";
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 { C as coder, Dt as flagMentions, Tt as envDefaults, i as skills, jt as requireFlag, kt as flipProblems, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as defaults } from "../chunks/reserved-DJk_6-ju.mjs";
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 };