@pmelab/gtd 45.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 +24 -9
- package/dist/chunks/{reserved-ty1mtlnS.mjs → reserved-ulcSkND6.mjs} +28 -3
- package/dist/chunks/{review-BmoH-9im.mjs → review-Da2ndnB1.mjs} +17 -6
- package/dist/chunks/{tail-CM3sVoAb.mjs → tail-BW030xz_.mjs} +1 -1
- package/dist/gtd.bundle.mjs +7 -5
- package/dist/testkit.mjs +7 -5
- 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 +228 -18
- package/package.json +6 -1
- package/src/install/ConfigScaffold.ts +2 -1
- package/src/install/Install.ts +5 -4
- package/src/workflows/index.ts +2 -1
- package/src/workflows/review/review.ts +3 -16
- package/src/workflows/shared/lenses.ts +5 -1
- package/src/workflows/shared/options.ts +21 -1
- package/src/workflows/shared/review.ts +19 -2
- package/src/workflows/shared/skills.test.ts +2 -0
- package/src/workflows/shared/skills.ts +3 -0
- package/src/workflows/shared/text.ts +5 -2
- package/src/workflows/shared/vars.ts +8 -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/README.md
CHANGED
|
@@ -737,6 +737,21 @@ hand or leave it for cleanup.
|
|
|
737
737
|
|
|
738
738
|
The delivery model these workflows implement: [Delivery model](SDLC.md).
|
|
739
739
|
|
|
740
|
+
### Reviving skipped tests
|
|
741
|
+
|
|
742
|
+
The opt-in `tests` workflow revisits tests nobody runs. Export `tests` from
|
|
743
|
+
`@pmelab/gtd/workflows/tests` and run `gtd door tests` (or
|
|
744
|
+
`gtd --workflow tests`). On a green baseline an agent sweeps the whole
|
|
745
|
+
repository — every skipped test plus the `test-audit` skill's findings — into
|
|
746
|
+
`.gtd/TESTS.md`, one section per test file. Each file is then rewritten in its
|
|
747
|
+
own turn until nothing in it is skipped and the suite is green. A test the agent
|
|
748
|
+
calls obsolete stays skipped and is proposed for deletion: the review in
|
|
749
|
+
`.gtd/REVIEW.md` gives each proposal `delete it`, `keep it skipped` or
|
|
750
|
+
`_your answer_`, and refuses sign-off until each holds one tick. An agent turn
|
|
751
|
+
that deletes a test file is refused. Skip detection is a factory option with
|
|
752
|
+
JavaScript/TypeScript and cucumber defaults; see
|
|
753
|
+
[Reviving skipped tests](docs/configuration.md#reviving-skipped-tests).
|
|
754
|
+
|
|
740
755
|
## Checking a range for stray files
|
|
741
756
|
|
|
742
757
|
```sh
|
|
@@ -875,15 +890,15 @@ approvals as `approvals` (`[]` when none), and `gtd history show` prints each as
|
|
|
875
890
|
gtd is one package, `@pmelab/gtd`, and every entrypoint shares its version and
|
|
876
891
|
[SemVer promise](https://github.com/pmelab/gtd/blob/main/COMPATIBILITY.md).
|
|
877
892
|
|
|
878
|
-
| Import
|
|
879
|
-
|
|
|
880
|
-
| `@pmelab/gtd/flows`
|
|
881
|
-
| `@pmelab/gtd/wire`
|
|
882
|
-
| `@pmelab/gtd/steering`
|
|
883
|
-
| `@pmelab/gtd/workflow-kit`
|
|
884
|
-
| `@pmelab/gtd/workflows`
|
|
885
|
-
| `@pmelab/gtd/workflows/feature`, `@pmelab/gtd/workflows/review`, `@pmelab/gtd/workflows/fix`, `@pmelab/gtd/workflows/rollout`, `@pmelab/gtd/workflows/cleanup` | One shipped workflow each: its factory as the default export, its parts, and the reserved exports (`defaults`, `envDefaults`, `summary`, `steering`, `skills`, `access`). `gtd install` imports these. |
|
|
886
|
-
| `@pmelab/gtd/testkit`
|
|
893
|
+
| Import | What it is |
|
|
894
|
+
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
895
|
+
| `@pmelab/gtd/flows` | The flow API a workflow is written in: the steps (`agent`, `human`, `run`, …), `scope` and `slice` (named spans of a process), workflow factories, `defineConfig` and `FLOWS_PROTOCOL`. |
|
|
896
|
+
| `@pmelab/gtd/wire` | The driver protocol: `WIRE_SCHEMA`, the exit codes and the types of every `--json` document. |
|
|
897
|
+
| `@pmelab/gtd/steering` | The steering-file formats (`qa`, `review`) and the review-note readers. |
|
|
898
|
+
| `@pmelab/gtd/workflow-kit` | Policy-free building blocks to compose a workflow from: health checks, quality laps, scenario freezing, review tails. |
|
|
899
|
+
| `@pmelab/gtd/workflows` | Every shipped workflow and all its parts in one module; also resolvable as `@pmelab/gtd/workflow`. |
|
|
900
|
+
| `@pmelab/gtd/workflows/feature`, `@pmelab/gtd/workflows/review`, `@pmelab/gtd/workflows/fix`, `@pmelab/gtd/workflows/rollout`, `@pmelab/gtd/workflows/cleanup`, `@pmelab/gtd/workflows/tests` | One shipped workflow each: its factory as the default export, its parts, and the reserved exports (`defaults`, `envDefaults`, `summary`, `steering`, `skills`, `access`). `gtd install` imports these. |
|
|
901
|
+
| `@pmelab/gtd/testkit` | Runs any workflow against the real engine, in an in-memory or a real repository. Needs `vitest` and `quickpickle`, optional peer dependencies. |
|
|
887
902
|
|
|
888
903
|
Types come from the TypeScript sources the package ships. Configs never need an
|
|
889
904
|
install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
|
|
@@ -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- [ ] ${
|
|
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 {
|
|
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 {
|
|
4
|
-
import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-
|
|
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-ulcSkND6.mjs";
|
|
4
|
+
import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-BW030xz_.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,
|
|
419
|
+
export { reUnwind as a, isCode as i, fixFeedback as n, humanReview as r, buildTail as t };
|
|
@@ -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 {
|
|
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-ulcSkND6.mjs";
|
|
4
4
|
//#region src/workflows/shared/lenses.ts
|
|
5
5
|
const NO_FINDINGS = {
|
|
6
6
|
notes: [],
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -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)
|
|
21610
|
-
(remove it), which a config adds by
|
|
21611
|
-
\`@pmelab/gtd/workflows/rollout
|
|
21612
|
-
\`@pmelab/gtd/workflows/cleanup\`
|
|
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/testkit.mjs
CHANGED
|
@@ -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)
|
|
6647
|
-
(remove it), which a config adds by
|
|
6648
|
-
\`@pmelab/gtd/workflows/rollout
|
|
6649
|
-
\`@pmelab/gtd/workflows/cleanup\`
|
|
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,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 { Bt as
|
|
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 };
|