@pmelab/gtd 45.0.0 → 45.2.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 (41) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +33 -14
  3. package/claude/hooks/pr.ts +342 -0
  4. package/claude/hooks/register.tsx +148 -5
  5. package/claude/types/index.d.ts +18 -0
  6. package/dist/chunks/{reserved-ty1mtlnS.mjs → reserved-FWrjy7zo.mjs} +29 -4
  7. package/dist/chunks/{review-BmoH-9im.mjs → review-Dhl4vvDe.mjs} +17 -6
  8. package/dist/chunks/{steering-D4LUQ-av.mjs → steering-D7hWeMgE.mjs} +1 -1
  9. package/dist/chunks/{tail-CM3sVoAb.mjs → tail-B-Es6Hwj.mjs} +1 -1
  10. package/dist/gtd.bundle.mjs +7 -5
  11. package/dist/steering.mjs +1 -1
  12. package/dist/testkit.mjs +8 -6
  13. package/dist/workflow-kit.mjs +1 -1
  14. package/dist/workflows/cleanup.mjs +2 -2
  15. package/dist/workflows/feature.mjs +3 -3
  16. package/dist/workflows/fix.mjs +3 -3
  17. package/dist/workflows/review.mjs +4 -15
  18. package/dist/workflows/rollout.mjs +1 -1
  19. package/dist/workflows/tests.mjs +193 -0
  20. package/dist/workflows.mjs +229 -19
  21. package/package.json +6 -1
  22. package/src/install/ConfigScaffold.ts +2 -1
  23. package/src/install/Install.ts +5 -4
  24. package/src/steering/review.test.ts +6 -0
  25. package/src/steering/review.ts +4 -1
  26. package/src/testkit/steps.ts +5 -0
  27. package/src/workflows/index.ts +2 -1
  28. package/src/workflows/review/review-door.feature +38 -0
  29. package/src/workflows/review/review.ts +3 -16
  30. package/src/workflows/shared/lenses.ts +5 -1
  31. package/src/workflows/shared/options.ts +21 -1
  32. package/src/workflows/shared/review.ts +19 -2
  33. package/src/workflows/shared/skills.test.ts +2 -0
  34. package/src/workflows/shared/skills.ts +3 -0
  35. package/src/workflows/shared/text.ts +5 -2
  36. package/src/workflows/shared/vars.ts +8 -0
  37. package/src/workflows/tests/index.ts +6 -0
  38. package/src/workflows/tests/tests-options.feature +52 -0
  39. package/src/workflows/tests/tests.feature +266 -0
  40. package/src/workflows/tests/tests.test.ts +51 -0
  41. package/src/workflows/tests/tests.ts +270 -0
@@ -1,5 +1,5 @@
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
- import { d as ANSWER_FIX, f as ANSWER_LEAD, p as ANSWER_WONT_FIX, v as FREE_TEXT_PLACEHOLDER } from "./steering-D4LUQ-av.mjs";
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
3
  import { escalation as escalation$1, 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
@@ -226,6 +226,11 @@ const testFileGlobs = [
226
226
  "**/__tests__/**"
227
227
  ];
228
228
  const defaultFlagRegistry = "src/flags/index.ts";
229
+ /** The `tests` workflow's own process settings: where it looks for skipped tests, and how a skip reads. */
230
+ const testsDefaults = {
231
+ testFiles: testFileGlobs.join(", "),
232
+ skipPattern: "^\\s*(?:(?:describe|context|suite|it|test|specify)\\.(?:skip|todo)\\b|x(?:describe|context|it|test|specify)\\s*\\(|(?:@\\S+\\s+)*@(?:skip|ignore)(?:\\s|$))"
233
+ };
229
234
  /** Process settings. */
230
235
  const defaults = {
231
236
  reviewBase: "",
@@ -558,7 +563,7 @@ const composedReview = (base, report) => {
558
563
  return name;
559
564
  };
560
565
  const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
561
- const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
566
+ const answerBoxes = ([fix, wontFix] = [ANSWER_FIX, ANSWER_WONT_FIX]) => `${ANSWER_LEAD}\n\n- [ ] ${fix}\n- [ ] ${wontFix}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
562
567
  const conflicts = (report.standards?.undecided ?? []).map((c) => chunk(unique(conflictTitle(c)), conflictBody(c)));
563
568
  const added = report.standards?.added ?? [];
564
569
  const notes = report.notes.map((n) => ({
@@ -570,7 +575,7 @@ const composedReview = (base, report) => {
570
575
  ...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
576
  ...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
572
577
  ...conflicts,
573
- ...notes.map((n) => chunk(n.title, `${n.unresolved === true ? `_The fixer left this unresolved._\n\n${unboxed(n.body)}` : n.unclear === true ? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}` : unboxed(n.body) || "_No detail given._"}\n\n${answerBoxes}`)),
578
+ ...notes.map((n) => chunk(n.title, `${n.unresolved === true ? `_The fixer left this unresolved._\n\n${unboxed(n.body)}` : n.unclear === true ? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}` : unboxed(n.body) || "_No detail given._"}\n\n${answerBoxes(n.answers)}`)),
574
579
  ...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
575
580
  ...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
576
581
  ...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
@@ -1939,6 +1944,24 @@ const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions,
1939
1944
  base: void 0
1940
1945
  };
1941
1946
  });
1947
+ const TestsOptions = Struct({
1948
+ ...ShippedOptions.fields,
1949
+ testFiles: setting,
1950
+ skipPattern: setting
1951
+ });
1952
+ const testsShipped = (flow) => defineWorkflow(standardSchemaV1(TestsOptions, {
1953
+ onExcessProperty: "error",
1954
+ errors: "all"
1955
+ }), ({ contextLimit, ...options }) => ({
1956
+ flow: limited(flow, contextLimit),
1957
+ defaults: over({
1958
+ ...defaults,
1959
+ ...testsDefaults
1960
+ }, options),
1961
+ envDefaults: over(envDefaults, options),
1962
+ doors: { tests: {} },
1963
+ base: void 0
1964
+ }));
1942
1965
  //#endregion
1943
1966
  //#region src/workflows/shared/skills.ts
1944
1967
  const skills = (vars) => {
@@ -1954,6 +1977,8 @@ const skills = (vars) => {
1954
1977
  design: ["spec-driven-development", "planning-and-task-breakdown"],
1955
1978
  notes: ["incremental-implementation", "code-simplification"],
1956
1979
  feedback: ["test-driven-development", "incremental-implementation"],
1980
+ tests: ["test-audit", "test-driven-development"],
1981
+ "tests.health": ["debugging-and-error-recovery"],
1957
1982
  ...build,
1958
1983
  ...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
1959
1984
  refactor: ["ponytail"],
@@ -1986,4 +2011,4 @@ const steering = {
1986
2011
  [REVIEW]: "review"
1987
2012
  };
1988
2013
  //#endregion
1989
- export { naiveApplyFailure as $, reviewMissing as A, conflictTitle as At, coverageMessage as B, requireFlag as Bt, coder as C, unwindFailure as Ct, fixQuality as D, STANDARDS as Dt, fixNits as E, testFileGlobs as Et, buildReviewTriageMessage as F, standardKey as Ft, designSystem as G, coveringPrompt as H, builderSystem as I, flagMentions as It, fixReviewNotesPrompt as J, designTriagePrompt as K, clearCriteriaMessage as L, flagReferences as Lt, reviewing as M, oneLine as Mt, agentWithSkills as N, parseStandards as Nt, fixRisks as O, conflictBody as Ot, buildReviewCollectingPrompt as P, renderStandards as Pt, implementFixPrompt as Q, clearCriteriaQuestion as R, flipProblems as Rt, awaitReview as S, unwindFailedMessage as St, fixCheck as T, envDefaults as Tt, criterionLine as U, coverageQuestion as V, designGateAnswerMessage as W, idleMessage as X, handOffAwaitReviewMessage as Y, implementBuildingPrompt as Z, REFACTOR_NOTE as _, seamByHandMessage as _t, flagModeOn as a, qualityUnresolvedMessage as at, SLICES as b, startGateBlockedMessage as bt, baseline as c, refactorFixPrompt as ct, guarded as d, reviewEditNotesCapture as dt, naivePassPrompt as et, healthy as f, reviewEditsCapture as ft, REFACTOR as g, reviewerSystem as gt, QUALITY as h, reviewNotesCapture as ht, skills as i, qualityTriageMessage as it, reviewQuality as j, isConflictTitle as jt, planner as k, conflictFor as kt, escalation as l, refactorNotePrompt as lt, HANDOFF as m, reviewGateBlockedMessage as mt, summary as n, oversizedMessage as nt, flagShipped as o, refactorBuildingPrompt as ot, FEEDBACK as p, reviewFeedbackFixPrompt as pt, finisherSystem as q, access as r, qualityFindingsFile as rt, shipped as s, refactorDroppedRefusal as st, steering as t, normaliseCriteriaScript as tt, gate as u, refactorRules as ut, REQUIREMENTS as v, seamSteerMessage as vt, collecting as w, defaults as wt, answerReviewQuestions as x, uncoveredMessage as xt, REVIEW as y, slicingPrompt as yt, composedReview as z, registryEntry as zt };
2014
+ export { implementFixPrompt as $, planner as A, conflictBody as At, composedReview as B, flipProblems as Bt, awaitReview as C, unwindFailedMessage as Ct, fixNits as D, testFileGlobs as Dt, fixCheck as E, envDefaults as Et, buildReviewCollectingPrompt as F, parseStandards as Ft, designGateAnswerMessage as G, coverageQuestion as H, requireFlag as Ht, buildReviewTriageMessage as I, renderStandards as It, finisherSystem as J, designSystem as K, builderSystem as L, standardKey as Lt, reviewQuality as M, conflictTitle as Mt, reviewing as N, isConflictTitle as Nt, fixQuality as O, testsDefaults as Ot, agentWithSkills as P, oneLine as Pt, implementBuildingPrompt as Q, clearCriteriaMessage as R, flagMentions as Rt, answerReviewQuestions as S, uncoveredMessage as St, collecting as T, defaults as Tt, coveringPrompt as U, coverageMessage as V, registryEntry as Vt, criterionLine as W, handOffAwaitReviewMessage as X, fixReviewNotesPrompt as Y, idleMessage as Z, REFACTOR as _, reviewerSystem as _t, flagModeOn as a, qualityTriageMessage as at, REVIEW as b, slicingPrompt as bt, testsShipped as c, refactorDroppedRefusal as ct, gate as d, refactorRules as dt, naiveApplyFailure as et, guarded as f, reviewEditNotesCapture as ft, QUALITY as g, reviewNotesCapture as gt, HANDOFF as h, reviewGateBlockedMessage as ht, skills as i, qualityFindingsFile as it, reviewMissing as j, conflictFor as jt, fixRisks as k, STANDARDS as kt, baseline as l, refactorFixPrompt as lt, FEEDBACK as m, reviewFeedbackFixPrompt as mt, summary as n, normaliseCriteriaScript as nt, flagShipped as o, qualityUnresolvedMessage as ot, healthy as p, reviewEditsCapture as pt, designTriagePrompt as q, access as r, oversizedMessage as rt, shipped as s, refactorBuildingPrompt as st, steering as t, naivePassPrompt as tt, escalation as u, refactorNotePrompt as ut, REFACTOR_NOTE as v, seamByHandMessage as vt, coder as w, unwindFailure as wt, SLICES as x, startGateBlockedMessage as xt, REQUIREMENTS as y, seamSteerMessage as yt, clearCriteriaQuestion as z, flagReferences as zt };
@@ -1,7 +1,7 @@
1
- import { answered, changesSince, head, judge, numeric, read, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
1
+ import { answered, changesSince, head, judge, numeric, read, removeScript, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
2
2
  import { removedAssertions, review } from "../workflow-kit.mjs";
3
- import { A as reviewMissing, At as conflictTitle, Dt as STANDARDS, E as fixNits, F as buildReviewTriageMessage, Ft as standardKey, Mt as oneLine, Nt as parseStandards, O as fixRisks, Ot as conflictBody, Pt as renderStandards, S as awaitReview, T as fixCheck, d as guarded, dt as reviewEditNotesCapture, f as healthy, ft as reviewEditsCapture, h as QUALITY, ht as reviewNotesCapture, jt as isConflictTitle, kt as conflictFor, w as collecting, x as answerReviewQuestions, y as REVIEW, z as composedReview } from "./reserved-ty1mtlnS.mjs";
4
- import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-CM3sVoAb.mjs";
3
+ import { At as conflictBody, B as composedReview, C as awaitReview, D as fixNits, E as fixCheck, Ft as parseStandards, I as buildReviewTriageMessage, It as renderStandards, J as finisherSystem, Lt as standardKey, Mt as conflictTitle, Nt as isConflictTitle, P as agentWithSkills, Pt as oneLine, S as answerReviewQuestions, T as collecting, b as REVIEW, f as guarded, ft as reviewEditNotesCapture, g as QUALITY, gt as reviewNotesCapture, j as reviewMissing, jt as conflictFor, k as fixRisks, kt as STANDARDS, mt as reviewFeedbackFixPrompt, p as healthy, pt as reviewEditsCapture, w as coder, y as REQUIREMENTS } from "./reserved-FWrjy7zo.mjs";
4
+ import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-B-Es6Hwj.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*/;
@@ -303,7 +303,7 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
303
303
  * praise dropped.
304
304
  */
305
305
  const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
306
- const { guard, guardRests, fixAccess, rules } = options;
306
+ const { guard, guardRests, fixAccess, rules, awaitMessage } = options;
307
307
  const feedback = options.feedback ?? {
308
308
  file: ".gtd/REQUIREMENTS.md",
309
309
  collecting,
@@ -328,7 +328,7 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
328
328
  guardRests,
329
329
  rules
330
330
  }),
331
- awaitReview: (b) => awaitReview(b),
331
+ awaitReview: (b) => awaitReview(b, awaitMessage?.(b)),
332
332
  reviewMissing,
333
333
  collecting: feedback.collecting,
334
334
  triage: triage(severities),
@@ -388,6 +388,17 @@ const callForm = (first, second, [third, fourth, ...extra]) => {
388
388
  if (typeof second === "string") return legacyForm(first, second, third, fourth);
389
389
  if (typeof first === "string") return currentForm(first, second, third, fourth);
390
390
  };
391
+ /** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
392
+ const fixFeedback = async () => {
393
+ await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
394
+ label: "Fixing your review feedback",
395
+ file: REQUIREMENTS,
396
+ model: coder(),
397
+ system: finisherSystem(),
398
+ allowEmpty: true
399
+ });
400
+ await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
401
+ };
391
402
  /**
392
403
  * Undo the human's review-round code edits. A path changed since the human
393
404
  * left it is not overwritten; requireRevert names it instead.
@@ -405,4 +416,4 @@ const reUnwind = async (feedback) => {
405
416
  requireRevert(edited, base);
406
417
  };
407
418
  //#endregion
408
- export { reUnwind as i, humanReview as n, isCode as r, buildTail as t };
419
+ export { reUnwind as a, isCode as i, fixFeedback as n, humanReview as r, buildTail as t };
@@ -15901,7 +15901,7 @@ const reviewDescriptor = {
15901
15901
  openThreads: openThreadsOf
15902
15902
  };
15903
15903
  const pointerLabel = (f) => `${f.path}${f.line === void 0 ? "" : `#${f.line}${f.rangeEnd === void 0 ? "" : `-${f.rangeEnd}`}`}`;
15904
- const chunkNote = (chunk, prior) => chunk.description === (prior?.description ?? "") ? [] : [{
15904
+ const chunkNote = (chunk, prior) => chunk.description === (prior?.description ?? "") || prior === void 0 && chunk.answers !== void 0 ? [] : [{
15905
15905
  line: chunk.headingLine,
15906
15906
  note: {
15907
15907
  kind: "chunk",
@@ -1,6 +1,6 @@
1
1
  import { answered, head, judge, mark, numeric, read, refuse, removeScript, run, scope, sectionBodies, showScript, vars, writeScript } from "../flows.mjs";
2
2
  import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
3
- import { A as reviewMissing, D as fixQuality, M as reviewing, O as fixRisks, S as awaitReview, T as fixCheck, Y as handOffAwaitReviewMessage, d as guarded, f as healthy, h as QUALITY, it as qualityTriageMessage, j as reviewQuality, l as escalation, rt as qualityFindingsFile, y as REVIEW } from "./reserved-ty1mtlnS.mjs";
3
+ import { C as awaitReview, E as fixCheck, M as reviewQuality, N as reviewing, O as fixQuality, X as handOffAwaitReviewMessage, at as qualityTriageMessage, b as REVIEW, f as guarded, g as QUALITY, it as qualityFindingsFile, j as reviewMissing, k as fixRisks, p as healthy, u as escalation } from "./reserved-FWrjy7zo.mjs";
4
4
  //#region src/workflows/shared/lenses.ts
5
5
  const NO_FINDINGS = {
6
6
  notes: [],
@@ -21606,10 +21606,11 @@ rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
21606
21606
  straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
21607
21607
  \`gtd doors\` lists the doors a repository offers (those of the workflows its
21608
21608
  config exports — the bundled \`fix\` and \`review [base]\`, plus the opt-in
21609
- \`rollout <flag>\` (turn a release flag default-on) and \`cleanup <flag>\`
21610
- (remove it), which a config adds by exporting \`rollout\` from
21611
- \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
21612
- \`@pmelab/gtd/workflows/cleanup\` — plus any it declares). Refuses with
21609
+ \`rollout <flag>\` (turn a release flag default-on), \`cleanup <flag>\`
21610
+ (remove it) and \`tests\` (revive skipped tests), which a config adds by
21611
+ exporting \`rollout\` from \`@pmelab/gtd/workflows/rollout\`, \`cleanup\` from
21612
+ \`@pmelab/gtd/workflows/cleanup\` and \`tests\` from
21613
+ \`@pmelab/gtd/workflows/tests\` — plus any it declares). Refuses with
21613
21614
  \`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
21614
21615
  missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
21615
21616
 
@@ -21722,7 +21723,8 @@ const CONFIG_SCAFFOLD = `// The workflows gtd runs here: gtd's shipped feature,
21722
21723
  // its own doors: \`gtd door review [base]\` and \`gtd door fix\`. Export \`rollout\`
21723
21724
  // from \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
21724
21725
  // \`@pmelab/gtd/workflows/cleanup\` to add \`gtd door rollout <flag>\` and
21725
- // \`gtd door cleanup <flag>\`.
21726
+ // \`gtd door cleanup <flag>\`, and \`tests\` from \`@pmelab/gtd/workflows/tests\`
21727
+ // to add \`gtd door tests\`.
21726
21728
  export {
21727
21729
  feature as default,
21728
21730
  feature,
package/dist/steering.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { C as parseFootnotes, D as headingSectionBodies, E as getParseCount, O as headingSections, S as openThreadFindings, T as parseThreadsWithFindings, _ as unansweredNotes, a as unansweredQuestions, b as FOOTNOTE_ACTION_TITLE, c as stripCodeThreads, d as ANSWER_FIX, f as ANSWER_LEAD, g as reviewRisks, h as reviewNotes, i as steeringFormatOrFreeForm, k as formatFinding, l as freeFormFormat, m as reviewDecisions, n as openThreads, o as viewOf, p as ANSWER_WONT_FIX, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES, u as applySteeringEdits, v as FREE_TEXT_PLACEHOLDER, w as parseThreads, x as THREAD_REPLY_ACTION_TITLE, y as isAnswered } from "./chunks/steering-D4LUQ-av.mjs";
1
+ import { C as parseFootnotes, D as headingSectionBodies, E as getParseCount, O as headingSections, S as openThreadFindings, T as parseThreadsWithFindings, _ as unansweredNotes, a as unansweredQuestions, b as FOOTNOTE_ACTION_TITLE, c as stripCodeThreads, d as ANSWER_FIX, f as ANSWER_LEAD, g as reviewRisks, h as reviewNotes, i as steeringFormatOrFreeForm, k as formatFinding, l as freeFormFormat, m as reviewDecisions, n as openThreads, o as viewOf, p as ANSWER_WONT_FIX, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES, u as applySteeringEdits, v as FREE_TEXT_PLACEHOLDER, w as parseThreads, x as THREAD_REPLY_ACTION_TITLE, y as isAnswered } from "./chunks/steering-D7hWeMgE.mjs";
2
2
  export { ANSWER_FIX, ANSWER_LEAD, ANSWER_WONT_FIX, BUILT_IN_MODE_NAMES, FOOTNOTE_ACTION_TITLE, FREE_TEXT_PLACEHOLDER, THREAD_REPLY_ACTION_TITLE, applySteeringEdits, formatFinding, freeFormFormat, getParseCount, headingSectionBodies, headingSections, isAnswered, openThreadFindings, openThreads, parseCodeThreads, parseFootnotes, parseThreads, parseThreadsWithFindings, reviewDecisions, reviewNotes, reviewRisks, steeringFormatFor, steeringFormatOrFreeForm, stripCodeThreads, unansweredNotes, unansweredQuestions, viewOf };
package/dist/testkit.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { answered, applyScript, checkScript, configProblems, configure, installContext, isFactoryWorkflow, moveScript, numeric, protocolMismatch, removeScript, restoreScript, revertScript, showScript, t as flows_exports, writeScript } from "./flows.mjs";
2
2
  import { A as beatKindOf, B as ACCESS_REFUSAL, C as renderLandPlain, D as renderBeatJson, E as beatDocument, O as statusOf, S as renderLandJson, T as renderBeatPlain, U as renderFormat, _ as selectPath, a as contextLimitFault, g as resolveSession, h as globMatches, j as demandOf, l as parseContextWindow, m as slicesDocument, o as contextShapeFault, p as renderSlicesJson, v as judgeJson, x as noopText, y as landFields } from "./chunks/wire-BJuZWR3C.mjs";
3
- import { D as headingSectionBodies, O as headingSections, T as parseThreadsWithFindings, a as unansweredQuestions, k as formatFinding, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES } from "./chunks/steering-D4LUQ-av.mjs";
3
+ import { D as headingSectionBodies, O as headingSections, T as parseThreadsWithFindings, a as unansweredQuestions, k as formatFinding, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES } from "./chunks/steering-D7hWeMgE.mjs";
4
4
  import { $ as logWarning, $i as BaseProto, $n as map$5, $r as make$19, $t as provide$1, A as catchIf, Ai as add$1, An as scopeMake, Ar as flatMap$3, At as suspend$5, B as flatMap$4, Bi as getOrElse, Bn as get$3, Br as sync$1, Bt as unsafeMakeLatch, C as all$1, Ci as toArray, Cn as isInterrupted, Cr as exitFail, Ct as sandbox, D as async, Di as GenericTag, Dn as squash, Dr as exitZipRight, Dt as scopedWith$1, E as asVoid$1, Ei as unsafeHead, En as reduceWithContext, Er as exitVoid, Et as scoped$4, F as ensuring$2, Fi as fromIterable, Fn as join$3, Fr as map$3, Ft as tryMap, G as forkIn, Gi as match$4, Gn as TypeId$10, Gr as whenEffect, Gt as zipLeft, H as fnUntraced, Hi as isNone, Hn as modify$1, Hr as uninterruptible, Ht as when, I as exit, Ii as splitAt, In as sequential, Ir as onInterrupt, It as tryPromise, J as ignore, Ji as isLeft, Jn as all, Jr as DeferredTypeId$1, Jt as spaced$1, K as forkScoped, Ki as none, Kn as Class, Kr as withFiberRuntime, Kt as zipRight$3, L as fail$5, Li as filter$1, Ln as bind, Lr as scopeClose, Lt as try_, M as die$2, Mi as getOption, Mn as _await$2, Mr as interruptAsFiber, Mt as tap, N as dieMessage, Ni as omit, Nn as inheritAll$1, Nr as interruptFiber, Nt as tapErrorCause, O as catchAll$1, Oi as Reference, On as ensuring$3, Or as fiberId, Ot as serviceOption, P as either, Pi as empty$5, Pn as interruptAllAs$1, Pr as interruptible$1, Pt as timeout, Q as isEffect, Qi as pipeArguments, Qn as isFailure, Qr as ids, Qt as mergeAll$2, R as failCause$4, Ri as flatMap$5, Rn as bindTo, Rr as succeed$4, Rt as uninterruptible$1, S as addFinalizer$1, Si as takeRight, Sn as isDieType, Sr as exitAs, St as runtime, T as as, Ti as unsafeGet, Tn as map$6, Tr as exitSucceed, Tt as scopeWith, U as forEach$2, Ui as isSome, Un as set$1, Ur as uninterruptibleMask$1, Ut as withFiberRuntime$1, V as flatten$4, Vi as getOrUndefined, Vn as make$16, Vr as tap$1, Vt as unsafeMakeSemaphore$1, W as forkDaemon, Wi as map$8, Wn as update$1, Wr as void_$2, Wt as zip$1, X as interruptible, Xi as match$2, Xn as fail$4, Xr as combine, Xt as tagged, Y as interrupt$4, Yi as left, Yn as die, Yr as empty$6, Yt as TaggedError$1, Z as intoDeferred, Zi as right, Zn as failCause$3, Zr as combineAll, Zt as fromEffect$5, _ as decodeSync, _i as of, _n as fail$3, _r as deferredIsDone, _t as raceWith, a as Literal, aa as constFalse, ai as size$2, an as updateContext$1, ar as zipRight$2, at as matchCause, b as acquireRelease, bi as reverse, bn as interrupt$3, br as deferredUnsafeDone, bt as runPromise, c as String$, ca as constVoid, ci as append$1, cn as Scope, cr as asyncInterrupt, ct as negate, d as Union, da as pipe, di as empty$4, dn as close, dr as deferredAwait, dt as orElse, ea as NodeInspectSymbol, ei as none$1, en as provideMerge$1, er as mapBoth, et as makeSemaphore, f as between, fi as filter, fn as extend, fr as deferredComplete, ft as orElseSucceed, gi as map$7, gn as die$1, gr as deferredInterruptWith, gt as race, hi as isEmpty$2, hn as NoSuchElementException, hr as deferredInterrupt, ht as provideService, i as Defect, ia as hasProperty, ii as has, in as unsafeRunPromiseExit, ir as zip$2, it as match$5, j as catchTag, ji as get$5, jn as scopeTag, jr as interrupt$2, jt as sync$2, k as catchAllCause, ki as Tag, kn as forEachConcurrentDiscard, kr as fiberRefLocally, kt as succeed$6, l as Struct, la as dual, li as appendAll, ln as addFinalizer, lr as catchAll, lt as onError, m as optional$1, mi as isChunk, mn as make$18, mr as deferredFailCause, mt as provide$2, n as Boolean$, na as toJSON, ni as make$17, nn as succeed$3, nr as succeed$5, nt as mapBoth$1, o as Never, oa as constTrue, oi as decode, on as forkIn$1, or as as$1, ot as matchCauseEffect, p as decodeUnknown, pi as fromIterable$1, pn as fork, pr as deferredFail, pt as promise, q as gen, qi as some, qn as spanTag, qr as zipRight$1, qt as zipWith, r as Cause, ra as equals, ri as set$2, rn as unsafeFork, rr as void_$3, rt as mapError, s as Number$, sa as constUndefined, si as isFinite, sn as unsafeMakeSemaphore, sr as asVoid, st as matchEffect, t as Array$, ta as format, ti as get$4, tn as scoped$3, tr as match$3, tt as map$4, u as TaggedError, ua as identity, ui as drop, un as addFinalizerExit, ur as currentContext$1, ut as orDie, v as encodeSync, vi as prepend, vn as failureOrCause, vr as deferredMake, vt as repeat, w as annotateLogs, wi as unsafeFromArray, wn as isInterruptedOnly, wr as exitFailCause, wt as scope, x as acquireUseRelease$2, xi as take$2, xn as interruptors, xr as deferredUnsafeMake, xt as runPromiseExit$1, y as _void, yi as reduce, yn as flipCauseOption, yr as deferredSucceed, yt as retry, z as fiberIdWith, zi as fromNullable, zn as sleep, zr as suspend$4, zt as uninterruptibleMask } from "./chunks/Schema-C6fLlQzx.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import * as Crypto from "node:crypto";
@@ -6643,10 +6643,11 @@ rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
6643
6643
  straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
6644
6644
  \`gtd doors\` lists the doors a repository offers (those of the workflows its
6645
6645
  config exports — the bundled \`fix\` and \`review [base]\`, plus the opt-in
6646
- \`rollout <flag>\` (turn a release flag default-on) and \`cleanup <flag>\`
6647
- (remove it), which a config adds by exporting \`rollout\` from
6648
- \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
6649
- \`@pmelab/gtd/workflows/cleanup\` — plus any it declares). Refuses with
6646
+ \`rollout <flag>\` (turn a release flag default-on), \`cleanup <flag>\`
6647
+ (remove it) and \`tests\` (revive skipped tests), which a config adds by
6648
+ exporting \`rollout\` from \`@pmelab/gtd/workflows/rollout\`, \`cleanup\` from
6649
+ \`@pmelab/gtd/workflows/cleanup\` and \`tests\` from
6650
+ \`@pmelab/gtd/workflows/tests\` — plus any it declares). Refuses with
6650
6651
  \`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
6651
6652
  missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
6652
6653
 
@@ -6759,7 +6760,8 @@ const CONFIG_SCAFFOLD = `// The workflows gtd runs here: gtd's shipped feature,
6759
6760
  // its own doors: \`gtd door review [base]\` and \`gtd door fix\`. Export \`rollout\`
6760
6761
  // from \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
6761
6762
  // \`@pmelab/gtd/workflows/cleanup\` to add \`gtd door rollout <flag>\` and
6762
- // \`gtd door cleanup <flag>\`.
6763
+ // \`gtd door cleanup <flag>\`, and \`tests\` from \`@pmelab/gtd/workflows/tests\`
6764
+ // to add \`gtd door tests\`.
6763
6765
  export {
6764
6766
  feature as default,
6765
6767
  feature,
@@ -1,5 +1,5 @@
1
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";
2
+ import { _ as unansweredNotes, c as stripCodeThreads, g as reviewRisks, h as reviewNotes, m as reviewDecisions, w as parseThreads } from "./chunks/steering-D7hWeMgE.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) => {
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { Bt as requireFlag, C as coder, Lt as flagReferences, Tt as envDefaults, i as skills, n as summary, o as flagShipped, r as access, t as steering, wt as defaults, zt as registryEntry } from "../chunks/reserved-ty1mtlnS.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CM3sVoAb.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-FWrjy7zo.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-B-Es6Hwj.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-ty1mtlnS.mjs";
4
- import { a as refusePendingHandOff, o as tail } from "../chunks/tail-CM3sVoAb.mjs";
5
- import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-BmoH-9im.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-FWrjy7zo.mjs";
4
+ import { a as refusePendingHandOff, o as tail } from "../chunks/tail-B-Es6Hwj.mjs";
5
+ import { a as reUnwind, i as isCode$1, r as humanReview, t as buildTail } from "../chunks/review-Dhl4vvDe.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
- 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-ty1mtlnS.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-CM3sVoAb.mjs";
2
+ import { g as reviewRisks, h as reviewNotes } from "../chunks/steering-D7hWeMgE.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-FWrjy7zo.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-B-Es6Hwj.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-ty1mtlnS.mjs";
3
- import { a as refusePendingHandOff } from "../chunks/tail-CM3sVoAb.mjs";
4
- import { i as reUnwind, t as buildTail } from "../chunks/review-BmoH-9im.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-FWrjy7zo.mjs";
3
+ import { a as refusePendingHandOff } from "../chunks/tail-B-Es6Hwj.mjs";
4
+ import { a as reUnwind, n as fixFeedback, t as buildTail } from "../chunks/review-Dhl4vvDe.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 { Bt as requireFlag, C as coder, It as flagMentions, Rt as flipProblems, Tt as envDefaults, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as defaults } from "../chunks/reserved-ty1mtlnS.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-FWrjy7zo.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-FWrjy7zo.mjs";
3
+ import { a as refusePendingHandOff, o as tail } from "../chunks/tail-B-Es6Hwj.mjs";
4
+ import { a as reUnwind, i as isCode, n as fixFeedback, r as humanReview } from "../chunks/review-Dhl4vvDe.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 };