@pmelab/gtd 44.0.0 → 45.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +40 -9
- package/dist/chunks/{reserved-DJk_6-ju.mjs → reserved-ulcSkND6.mjs} +142 -5
- package/dist/chunks/{review-X7Jfz0dY.mjs → review-Da2ndnB1.mjs} +177 -7
- package/dist/chunks/{tail-5w5mxGTq.mjs → tail-BW030xz_.mjs} +1 -1
- package/dist/gtd.bundle.mjs +7 -5
- package/dist/testkit.mjs +7 -5
- package/dist/workflow-kit.mjs +22 -10
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +3 -3
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +4 -15
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows/tests.mjs +193 -0
- package/dist/workflows.mjs +533 -40
- package/package.json +6 -1
- package/src/install/ConfigScaffold.ts +2 -1
- package/src/install/Install.ts +5 -4
- package/src/workflow-kit/review.test.ts +80 -2
- package/src/workflow-kit/review.ts +45 -11
- package/src/workflows/feature/review-answers.feature +12 -3
- package/src/workflows/feature/standards.feature +286 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/review/review-door.feature +11 -2
- package/src/workflows/review/review.ts +3 -16
- package/src/workflows/shared/learn.test.ts +154 -0
- package/src/workflows/shared/learn.ts +206 -0
- package/src/workflows/shared/lenses.ts +5 -1
- package/src/workflows/shared/options.ts +22 -1
- package/src/workflows/shared/review.ts +29 -2
- package/src/workflows/shared/skills.test.ts +2 -0
- package/src/workflows/shared/skills.ts +3 -0
- package/src/workflows/shared/standards.test.ts +90 -0
- package/src/workflows/shared/standards.ts +156 -0
- package/src/workflows/shared/text.test.ts +58 -1
- package/src/workflows/shared/text.ts +33 -3
- package/src/workflows/shared/vars.ts +10 -0
- package/src/workflows/tests/index.ts +6 -0
- package/src/workflows/tests/tests-options.feature +52 -0
- package/src/workflows/tests/tests.feature +266 -0
- package/src/workflows/tests/tests.test.ts +51 -0
- package/src/workflows/tests/tests.ts +270 -0
package/README.md
CHANGED
|
@@ -564,6 +564,22 @@ noted in steps 3 and 4 below.
|
|
|
564
564
|
`gtd judge answer` / conservative-default shape as step 3's own judged
|
|
565
565
|
points.
|
|
566
566
|
|
|
567
|
+
**The review learns your taste.** Each won't-fix and each own answer is a
|
|
568
|
+
candidate standard. A judge (`build.review.standards.judge`) asks whether it
|
|
569
|
+
is reusable beyond this note; at `standardMinP` (`0.8`) or above it is kept
|
|
570
|
+
in `.gtd/STANDARDS.md` with no question asked, and the next review lists it
|
|
571
|
+
under `## Standards added`. Every later agent turn — the lenses, the naive
|
|
572
|
+
pass, the build, every fix — is handed the kept standards, so a finding you
|
|
573
|
+
rejected once is not raised again and a rule you wrote is followed, in this
|
|
574
|
+
process and every later one. One the judge does not confidently rule out
|
|
575
|
+
contradicting a standard already kept is not applied: it waits under
|
|
576
|
+
`## Undecided` and becomes a `## Standard conflict <id>` note in the review,
|
|
577
|
+
with its own three boxes — **keep the new one (drop the old)**, **keep the
|
|
578
|
+
old one (discard the new)**, or **your own wording**, which replaces both —
|
|
579
|
+
and the review does not sign off until it is answered. `.gtd/STANDARDS.md` is
|
|
580
|
+
committed and yours to edit; gtd rewrites it as one bullet per standard when
|
|
581
|
+
it learns.
|
|
582
|
+
|
|
567
583
|
You never talk to it. Every exchange is a file in `.gtd/` that you edit in your
|
|
568
584
|
own editor, and every answer you give is a commit. Your test suite is the gate
|
|
569
585
|
throughout: a red baseline stops the process before it starts, and a red build
|
|
@@ -721,6 +737,21 @@ hand or leave it for cleanup.
|
|
|
721
737
|
|
|
722
738
|
The delivery model these workflows implement: [Delivery model](SDLC.md).
|
|
723
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
|
+
|
|
724
755
|
## Checking a range for stray files
|
|
725
756
|
|
|
726
757
|
```sh
|
|
@@ -859,15 +890,15 @@ approvals as `approvals` (`[]` when none), and `gtd history show` prints each as
|
|
|
859
890
|
gtd is one package, `@pmelab/gtd`, and every entrypoint shares its version and
|
|
860
891
|
[SemVer promise](https://github.com/pmelab/gtd/blob/main/COMPATIBILITY.md).
|
|
861
892
|
|
|
862
|
-
| Import
|
|
863
|
-
|
|
|
864
|
-
| `@pmelab/gtd/flows`
|
|
865
|
-
| `@pmelab/gtd/wire`
|
|
866
|
-
| `@pmelab/gtd/steering`
|
|
867
|
-
| `@pmelab/gtd/workflow-kit`
|
|
868
|
-
| `@pmelab/gtd/workflows`
|
|
869
|
-
| `@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. |
|
|
870
|
-
| `@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. |
|
|
871
902
|
|
|
872
903
|
Types come from the TypeScript sources the package ships. Configs never need an
|
|
873
904
|
install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
|
|
@@ -113,6 +113,108 @@ const requireFlag = (workflow, registryPath, name, defaultOn) => {
|
|
|
113
113
|
if (entry.defaultOn !== defaultOn) return refuse(defaultOn ? `gtd: ${workflow}: flag "${name}" is not default-on — roll it out first` : `gtd: ${workflow}: flag "${name}" is already default-on`);
|
|
114
114
|
};
|
|
115
115
|
//#endregion
|
|
116
|
+
//#region src/workflows/shared/standards.ts
|
|
117
|
+
const STANDARDS = ".gtd/STANDARDS.md";
|
|
118
|
+
const UNDECIDED = "Undecided";
|
|
119
|
+
const oneLine = (text) => text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim();
|
|
120
|
+
const ITEM = /^(?:[-*+]|\d+[.)])\s+(.*)$/;
|
|
121
|
+
const CONTRADICTS = /^\s+[-*+]\s+contradicts:\s*(.*)$/i;
|
|
122
|
+
const wrapped = (item, line) => {
|
|
123
|
+
const olds = item.olds;
|
|
124
|
+
if (olds !== void 0 && olds.length > 0) olds[olds.length - 1] += ` ${line.trim()}`;
|
|
125
|
+
else item.text += ` ${line.trim()}`;
|
|
126
|
+
};
|
|
127
|
+
/** What tells two standards apart: not the emphasis or spacing a formatter may rewrite. */
|
|
128
|
+
const standardKey = (text) => oneLine(text.replace(/[*_`\\]/g, "")).toLowerCase();
|
|
129
|
+
const toStandards = (items) => {
|
|
130
|
+
const seen = /* @__PURE__ */ new Set();
|
|
131
|
+
const fresh = (text) => {
|
|
132
|
+
const key = standardKey(text);
|
|
133
|
+
if (key === "" || seen.has(key)) return false;
|
|
134
|
+
seen.add(key);
|
|
135
|
+
return true;
|
|
136
|
+
};
|
|
137
|
+
const decided = items.filter((i) => i.olds === void 0).map((i) => oneLine(i.text));
|
|
138
|
+
const undecided = items.flatMap((i) => i.olds === void 0 ? [] : [{
|
|
139
|
+
text: oneLine(i.text),
|
|
140
|
+
olds: i.olds.map(oneLine).filter((o) => o !== "")
|
|
141
|
+
}]);
|
|
142
|
+
return {
|
|
143
|
+
decided: decided.filter(fresh),
|
|
144
|
+
undecided: undecided.filter((c) => fresh(c.text))
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Every top-level bullet is one standard, those under `## Undecided` waiting
|
|
149
|
+
* on the human; a hand-edited file may group the rest under any heading.
|
|
150
|
+
*/
|
|
151
|
+
const parseStandards = (text) => {
|
|
152
|
+
const items = [];
|
|
153
|
+
let pending = false;
|
|
154
|
+
let last;
|
|
155
|
+
for (const line of text.split("\n")) {
|
|
156
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
157
|
+
const item = ITEM.exec(line);
|
|
158
|
+
const old = CONTRADICTS.exec(line);
|
|
159
|
+
if (heading !== null) {
|
|
160
|
+
pending = heading[1].length > 1 && heading[2].trim() === UNDECIDED;
|
|
161
|
+
last = void 0;
|
|
162
|
+
} else if (item !== null) {
|
|
163
|
+
last = pending ? {
|
|
164
|
+
text: item[1],
|
|
165
|
+
olds: []
|
|
166
|
+
} : { text: item[1] };
|
|
167
|
+
items.push(last);
|
|
168
|
+
} else if (old !== null && last?.olds !== void 0) last.olds.push(old[1]);
|
|
169
|
+
else if (/^\s+\S/.test(line) && last !== void 0) wrapped(last, line);
|
|
170
|
+
else if (line.trim() !== "") last = void 0;
|
|
171
|
+
}
|
|
172
|
+
return toStandards(items);
|
|
173
|
+
};
|
|
174
|
+
const renderStandards = ({ decided, undecided }) => {
|
|
175
|
+
return `${[
|
|
176
|
+
"# Standards",
|
|
177
|
+
"Kept from your review answers for every agent turn. One standard per bullet.",
|
|
178
|
+
...decided.length === 0 ? [] : [decided.map((s) => `- ${s}`).join("\n")],
|
|
179
|
+
...undecided.length === 0 ? [] : [`## ${UNDECIDED}`, undecided.map((c) => [`- ${c.text}`, ...c.olds.map((o) => ` - contradicts: ${o}`)].join("\n")).join("\n")]
|
|
180
|
+
].join("\n\n")}\n`;
|
|
181
|
+
};
|
|
182
|
+
const hash = (raw) => {
|
|
183
|
+
const text = standardKey(raw);
|
|
184
|
+
let h = 2166136261;
|
|
185
|
+
for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);
|
|
186
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
187
|
+
};
|
|
188
|
+
const CONFLICT_TITLE = /^Standard conflict ([0-9a-f]{8})$/;
|
|
189
|
+
const conflictTitle = (c) => `Standard conflict ${hash(c.text)}`;
|
|
190
|
+
const conflictFor = (standards, title) => {
|
|
191
|
+
const id = CONFLICT_TITLE.exec(title)?.[1];
|
|
192
|
+
return id === void 0 ? void 0 : standards.undecided.find((c) => hash(c.text) === id);
|
|
193
|
+
};
|
|
194
|
+
const isConflictTitle = (title) => CONFLICT_TITLE.test(title);
|
|
195
|
+
const KEEP_NEW = "keep the new one (drop the old)";
|
|
196
|
+
const KEEP_OLD = "keep the old one (discard the new)";
|
|
197
|
+
/** A conflict's chunk body; its three boxes read as fix / won't fix / own answer by position. */
|
|
198
|
+
const conflictBody = (c) => [...c.olds.length === 0 ? [`A standard learned from your answers contradicted ones another answer since replaced in \`${STANDARDS}\`. Keep it, discard it, or reword it.`, `New: ${c.text}`] : [
|
|
199
|
+
`A standard learned from your answers contradicts ${c.olds.length === 1 ? "one" : "some"} already in \`${STANDARDS}\`. Your own answer replaces both with its wording.`,
|
|
200
|
+
`New: ${c.text}`,
|
|
201
|
+
`Old:\n\n${c.olds.map((o) => `- ${o}`).join("\n")}`
|
|
202
|
+
], `${ANSWER_LEAD}\n\n- [ ] ${KEEP_NEW}\n- [ ] ${KEEP_OLD}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`].join("\n\n");
|
|
203
|
+
/** What every agent turn is told while `.gtd/STANDARDS.md` holds decided standards. */
|
|
204
|
+
const standardsBlock = (text) => {
|
|
205
|
+
const { decided } = parseStandards(text ?? "");
|
|
206
|
+
if (decided.length === 0) return "";
|
|
207
|
+
return `## Standards
|
|
208
|
+
|
|
209
|
+
\`${STANDARDS}\` holds the standards the human set in earlier reviews.
|
|
210
|
+
Follow every one in what you write. Reviewing, never raise a finding a
|
|
211
|
+
standard rules out — a \`Won't fix:\` standard is a finding the human
|
|
212
|
+
rejected, so never raise it or one like it again — and raise breaking a
|
|
213
|
+
standard as a finding.
|
|
214
|
+
|
|
215
|
+
${decided.map((s) => `- ${s}`).join("\n")}`;
|
|
216
|
+
};
|
|
217
|
+
//#endregion
|
|
116
218
|
//#region src/workflows/shared/vars.ts
|
|
117
219
|
/** The refactor guard's fixed list, and `seamAllow`'s default minus `.gtd/**`. */
|
|
118
220
|
const testFileGlobs = [
|
|
@@ -124,6 +226,11 @@ const testFileGlobs = [
|
|
|
124
226
|
"**/__tests__/**"
|
|
125
227
|
];
|
|
126
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
|
+
};
|
|
127
234
|
/** Process settings. */
|
|
128
235
|
const defaults = {
|
|
129
236
|
reviewBase: "",
|
|
@@ -132,6 +239,7 @@ const defaults = {
|
|
|
132
239
|
reviewNoteActionable: "0.7",
|
|
133
240
|
reviewSeverityMinP: "0.7",
|
|
134
241
|
qualityTriageMinP: "0.7",
|
|
242
|
+
standardMinP: "0.8",
|
|
135
243
|
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
136
244
|
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
137
245
|
flagMode: "true",
|
|
@@ -391,8 +499,11 @@ const withSkills = (skills, prompt) => {
|
|
|
391
499
|
};
|
|
392
500
|
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills, then its restricted file access. */
|
|
393
501
|
const agentWithSkills = (name, prompt, options = {}) => {
|
|
394
|
-
const
|
|
395
|
-
|
|
502
|
+
const body = [
|
|
503
|
+
accessPreamble(accessFor(name, options.file)),
|
|
504
|
+
prompt,
|
|
505
|
+
standardsBlock(read(STANDARDS))
|
|
506
|
+
].filter((part) => part !== "").join("\n\n");
|
|
396
507
|
return agent(name, withSkills(skillsFor(name).join(", "), body), options);
|
|
397
508
|
};
|
|
398
509
|
const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
|
|
@@ -432,6 +543,7 @@ const SUMMARY_TITLES = [
|
|
|
432
543
|
"Weakened scenarios",
|
|
433
544
|
"Fixed from your notes",
|
|
434
545
|
"Fixed automatically",
|
|
546
|
+
"Standards added",
|
|
435
547
|
"Dropped as noise",
|
|
436
548
|
"Nothing to note"
|
|
437
549
|
];
|
|
@@ -451,7 +563,9 @@ const composedReview = (base, report) => {
|
|
|
451
563
|
return name;
|
|
452
564
|
};
|
|
453
565
|
const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
|
|
454
|
-
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}`;
|
|
567
|
+
const conflicts = (report.standards?.undecided ?? []).map((c) => chunk(unique(conflictTitle(c)), conflictBody(c)));
|
|
568
|
+
const added = report.standards?.added ?? [];
|
|
455
569
|
const notes = report.notes.map((n) => ({
|
|
456
570
|
...n,
|
|
457
571
|
title: unique(n.title)
|
|
@@ -460,8 +574,10 @@ const composedReview = (base, report) => {
|
|
|
460
574
|
const chunks = [
|
|
461
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.`)],
|
|
462
576
|
...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
|
|
463
|
-
...
|
|
577
|
+
...conflicts,
|
|
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)}`)),
|
|
464
579
|
...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
|
|
580
|
+
...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
|
|
465
581
|
...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
|
|
466
582
|
];
|
|
467
583
|
const body = chunks.length > 0 ? chunks : [chunk("Nothing to note", "No quality lens raised anything for you — or none ran. Review the diff yourself.")];
|
|
@@ -1765,6 +1881,7 @@ const ShippedOptions = Struct({
|
|
|
1765
1881
|
reviewNoteActionable: setting,
|
|
1766
1882
|
reviewSeverityMinP: setting,
|
|
1767
1883
|
qualityTriageMinP: setting,
|
|
1884
|
+
standardMinP: setting,
|
|
1768
1885
|
refactorFlagPattern: setting,
|
|
1769
1886
|
seamAllow: setting,
|
|
1770
1887
|
sliceMaxCriteria: setting,
|
|
@@ -1827,6 +1944,24 @@ const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions,
|
|
|
1827
1944
|
base: void 0
|
|
1828
1945
|
};
|
|
1829
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
|
+
}));
|
|
1830
1965
|
//#endregion
|
|
1831
1966
|
//#region src/workflows/shared/skills.ts
|
|
1832
1967
|
const skills = (vars) => {
|
|
@@ -1842,6 +1977,8 @@ const skills = (vars) => {
|
|
|
1842
1977
|
design: ["spec-driven-development", "planning-and-task-breakdown"],
|
|
1843
1978
|
notes: ["incremental-implementation", "code-simplification"],
|
|
1844
1979
|
feedback: ["test-driven-development", "incremental-implementation"],
|
|
1980
|
+
tests: ["test-audit", "test-driven-development"],
|
|
1981
|
+
"tests.health": ["debugging-and-error-recovery"],
|
|
1845
1982
|
...build,
|
|
1846
1983
|
...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
|
|
1847
1984
|
refactor: ["ponytail"],
|
|
@@ -1874,4 +2011,4 @@ const steering = {
|
|
|
1874
2011
|
[REVIEW]: "review"
|
|
1875
2012
|
};
|
|
1876
2013
|
//#endregion
|
|
1877
|
-
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,159 @@
|
|
|
1
|
-
import { answered, changesSince, head, judge, numeric, 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
|
+
//#region src/workflows/shared/learn.ts
|
|
6
|
+
const SUMMARY_MAX = 200;
|
|
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*/;
|
|
8
|
+
const summary = (body) => {
|
|
9
|
+
const text = oneLine(body).replace(BANNER, "");
|
|
10
|
+
return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1)}…` : text;
|
|
11
|
+
};
|
|
12
|
+
/** The standard a won't-fix or an own answer would make; a fix states none. */
|
|
13
|
+
const candidateOf = (d) => {
|
|
14
|
+
const anchor = oneLine(d.anchor);
|
|
15
|
+
if (d.choice === "wontfix") {
|
|
16
|
+
const why = summary(d.body);
|
|
17
|
+
return [{
|
|
18
|
+
decision: d,
|
|
19
|
+
text: `Won't fix: ${anchor}${why === "" ? "" : ` — ${why}`}`
|
|
20
|
+
}];
|
|
21
|
+
}
|
|
22
|
+
if (d.choice === "answer") return [{
|
|
23
|
+
decision: d,
|
|
24
|
+
text: `${oneLine(d.text)} (re: ${anchor})`
|
|
25
|
+
}];
|
|
26
|
+
return [];
|
|
27
|
+
};
|
|
28
|
+
const without = (list, drop) => {
|
|
29
|
+
const keys = new Set(drop.map(standardKey));
|
|
30
|
+
return list.filter((s) => !keys.has(standardKey(s)));
|
|
31
|
+
};
|
|
32
|
+
const adding = (list, text) => list.some((s) => standardKey(s) === standardKey(text)) ? [...list] : [...list, text];
|
|
33
|
+
/**
|
|
34
|
+
* Keep the new one, keep the old one, or replace both with the human's own
|
|
35
|
+
* wording. Another open conflict stops naming a replaced standard: the human
|
|
36
|
+
* never judged it against the replacement, so keeping it must not drop that.
|
|
37
|
+
*/
|
|
38
|
+
const resolve = (standards, d) => {
|
|
39
|
+
const c = conflictFor(standards, d.anchor);
|
|
40
|
+
if (c === void 0) return standards;
|
|
41
|
+
const others = standards.undecided.filter((u) => u !== c);
|
|
42
|
+
if (d.choice === "wontfix") return {
|
|
43
|
+
decided: standards.decided,
|
|
44
|
+
undecided: others
|
|
45
|
+
};
|
|
46
|
+
const kept = d.choice === "fix" ? c.text : oneLine(d.text);
|
|
47
|
+
const undecided = others.map((u) => ({
|
|
48
|
+
text: u.text,
|
|
49
|
+
olds: without(u.olds, c.olds)
|
|
50
|
+
}));
|
|
51
|
+
return {
|
|
52
|
+
decided: adding(without(standards.decided, c.olds), kept),
|
|
53
|
+
undecided
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const contradicts = (i, id, other) => ({
|
|
57
|
+
id: `standard-${i}.contradicts-${id}`,
|
|
58
|
+
primitive: "noul",
|
|
59
|
+
instructions: `Does the "Standard:" line of evidence "standard-${i}" contradict ${other}?`,
|
|
60
|
+
criteria: "yes: following one breaks the other. no: both can be followed — they are compatible or unrelated."
|
|
61
|
+
});
|
|
62
|
+
const questionsFor = (i, anchor, decided) => [
|
|
63
|
+
{
|
|
64
|
+
id: `standard-${i}`,
|
|
65
|
+
primitive: "noul",
|
|
66
|
+
instructions: `Is the human's answer to review note "${anchor}" (evidence "standard-${i}") a reusable standard for this codebase — a rule later changes and reviews should follow — rather than a call about this one note? Its "Standard:" line is the wording gtd would keep.`,
|
|
67
|
+
criteria: "yes: it states a rule or preference that applies beyond this note. no: it is specific to this note, a one-off call, or unclear."
|
|
68
|
+
},
|
|
69
|
+
...decided.map((_, k) => contradicts(i, `${k + 1}`, `kept standard ${k + 1} (evidence "kept-${k + 1}")`)),
|
|
70
|
+
...Array.from({ length: i - 1 }, (_, j) => contradicts(i, `new-${j + 1}`, `the "Standard:" line of evidence "standard-${j + 1}"`))
|
|
71
|
+
];
|
|
72
|
+
const evidenceOf = (c) => [
|
|
73
|
+
`Note: ${c.decision.anchor}`,
|
|
74
|
+
`Reviewer's text: ${c.decision.body}`,
|
|
75
|
+
`Human's answer: ${c.decision.choice === "wontfix" ? "won't fix" : c.decision.text}`,
|
|
76
|
+
`Standard: ${c.text}`
|
|
77
|
+
].join("\n");
|
|
78
|
+
/**
|
|
79
|
+
* One judge rest over every candidate. Only a confident `yes` on uncut
|
|
80
|
+
* evidence makes a standard; a contradiction the judge does not confidently
|
|
81
|
+
* rule out goes to the human instead of overwriting what they decided before.
|
|
82
|
+
* A cut kept standard leaves the round unlearned: raise `judgeBudgetBytes`.
|
|
83
|
+
*/
|
|
84
|
+
const judged = async (candidates, decided, minP) => {
|
|
85
|
+
const { answers, truncated } = await judge("review.standards.judge", {
|
|
86
|
+
questions: candidates.flatMap((c, i) => questionsFor(i + 1, c.decision.anchor, decided)),
|
|
87
|
+
evidence: Object.fromEntries([...candidates.map((c, i) => [`standard-${i + 1}`, evidenceOf(c)]), ...decided.map((s, k) => [`kept-${k + 1}`, s])]),
|
|
88
|
+
message: "Judging whether your answers are standards to keep: run `gtd judge run`, or land without a verdict, which keeps none.",
|
|
89
|
+
label: "Learning your standards"
|
|
90
|
+
});
|
|
91
|
+
const ruledOut = (i, id) => answered(answers[`standard-${i}.contradicts-${id}`], "no", minP);
|
|
92
|
+
const added = [];
|
|
93
|
+
const raised = [];
|
|
94
|
+
candidates.forEach((c, n) => {
|
|
95
|
+
const i = n + 1;
|
|
96
|
+
if (truncated.includes(`standard-${i}`)) return;
|
|
97
|
+
if (decided.some((_, k) => truncated.includes(`kept-${k + 1}`))) return;
|
|
98
|
+
if (!answered(answers[`standard-${i}`], "yes", minP)) return;
|
|
99
|
+
const olds = [...decided.filter((_, k) => !ruledOut(i, `${k + 1}`)), ...added.filter((a) => !ruledOut(i, `new-${a.i}`)).map((a) => a.text)];
|
|
100
|
+
if (olds.length === 0) added.push({
|
|
101
|
+
i,
|
|
102
|
+
text: c.text
|
|
103
|
+
});
|
|
104
|
+
else raised.push({
|
|
105
|
+
text: c.text,
|
|
106
|
+
olds
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
added: added.map((a) => a.text),
|
|
111
|
+
raised
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Settles every answer to a standard conflict, then learns standards from
|
|
116
|
+
* the won't-fix ticks and own answers: kept in `.gtd/STANDARDS.md`, a
|
|
117
|
+
* contradicting one as a new conflict note in `.gtd/REVIEW.md`.
|
|
118
|
+
*/
|
|
119
|
+
const learnStandards = async (decisions) => {
|
|
120
|
+
const before = parseStandards(read(".gtd/STANDARDS.md") ?? "");
|
|
121
|
+
const rest = decisions.filter((d) => !isConflictTitle(d.anchor));
|
|
122
|
+
const settled = decisions.filter((d) => isConflictTitle(d.anchor)).reduce(resolve, before);
|
|
123
|
+
const known = new Set([...settled.decided, ...settled.undecided.map((c) => c.text)].map(standardKey));
|
|
124
|
+
const candidates = rest.flatMap(candidateOf).filter((c) => {
|
|
125
|
+
const key = standardKey(c.text);
|
|
126
|
+
if (known.has(key)) return false;
|
|
127
|
+
known.add(key);
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
const minP = numeric(vars.standardMinP, Infinity);
|
|
131
|
+
const { added, raised } = candidates.length === 0 || !Number.isFinite(minP) ? {
|
|
132
|
+
added: [],
|
|
133
|
+
raised: []
|
|
134
|
+
} : await judged(candidates, settled.decided, minP);
|
|
135
|
+
const after = {
|
|
136
|
+
decided: [...settled.decided, ...added],
|
|
137
|
+
undecided: [...settled.undecided, ...raised]
|
|
138
|
+
};
|
|
139
|
+
if (renderStandards(after) !== renderStandards(before)) await run("review.standards.recording", writeScript(STANDARDS, renderStandards(after)), { label: "Recording your standards" });
|
|
140
|
+
if (raised.length > 0) {
|
|
141
|
+
const chunks = raised.map((c) => `## ${conflictTitle(c)}\n\n${conflictBody(c)}`);
|
|
142
|
+
await run("review.standards.raising", writeScript(REVIEW, `${(read(".gtd/REVIEW.md") ?? "").trimEnd()}\n\n${chunks.join("\n\n")}\n`), {
|
|
143
|
+
label: "Asking about a contradicting standard",
|
|
144
|
+
file: REVIEW
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const settlesStandard = (d) => isConflictTitle(d.anchor);
|
|
149
|
+
const standardsPending = () => parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided.length > 0;
|
|
150
|
+
const standardsAddedSince = (base) => {
|
|
151
|
+
const change = changesSince(base).get(STANDARDS);
|
|
152
|
+
if (change === void 0) return [];
|
|
153
|
+
const was = new Set(parseStandards(change.before ?? "").decided);
|
|
154
|
+
return parseStandards(change.after ?? "").decided.filter((s) => !was.has(s));
|
|
155
|
+
};
|
|
156
|
+
//#endregion
|
|
5
157
|
//#region src/workflows/shared/review.ts
|
|
6
158
|
const VERDICTS = [
|
|
7
159
|
"edit",
|
|
@@ -132,7 +284,11 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
|
|
|
132
284
|
...report,
|
|
133
285
|
weakened: removedAssertions(changesSince(base)),
|
|
134
286
|
nitFix,
|
|
135
|
-
answered
|
|
287
|
+
answered,
|
|
288
|
+
standards: {
|
|
289
|
+
added: standardsAddedSince(base),
|
|
290
|
+
undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
|
|
291
|
+
}
|
|
136
292
|
}), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
|
|
137
293
|
label: "Composing the review",
|
|
138
294
|
file: REVIEW,
|
|
@@ -147,7 +303,7 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
|
|
|
147
303
|
* praise dropped.
|
|
148
304
|
*/
|
|
149
305
|
const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
150
|
-
const { guard, guardRests, fixAccess, rules } = options;
|
|
306
|
+
const { guard, guardRests, fixAccess, rules, awaitMessage } = options;
|
|
151
307
|
const feedback = options.feedback ?? {
|
|
152
308
|
file: ".gtd/REQUIREMENTS.md",
|
|
153
309
|
collecting,
|
|
@@ -172,11 +328,14 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
172
328
|
guardRests,
|
|
173
329
|
rules
|
|
174
330
|
}),
|
|
175
|
-
awaitReview: (b) => awaitReview(b),
|
|
331
|
+
awaitReview: (b) => awaitReview(b, awaitMessage?.(b)),
|
|
176
332
|
reviewMissing,
|
|
177
333
|
collecting: feedback.collecting,
|
|
178
334
|
triage: triage(severities),
|
|
179
335
|
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
336
|
+
settles: settlesStandard,
|
|
337
|
+
learn: learnStandards,
|
|
338
|
+
pending: standardsPending,
|
|
180
339
|
answerQuestions: answerReviewQuestions,
|
|
181
340
|
fixNits: async (notes) => {
|
|
182
341
|
await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
|
|
@@ -229,6 +388,17 @@ const callForm = (first, second, [third, fourth, ...extra]) => {
|
|
|
229
388
|
if (typeof second === "string") return legacyForm(first, second, third, fourth);
|
|
230
389
|
if (typeof first === "string") return currentForm(first, second, third, fourth);
|
|
231
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
|
+
};
|
|
232
402
|
/**
|
|
233
403
|
* Undo the human's review-round code edits. A path changed since the human
|
|
234
404
|
* left it is not overwritten; requireRevert names it instead.
|
|
@@ -246,4 +416,4 @@ const reUnwind = async (feedback) => {
|
|
|
246
416
|
requireRevert(edited, base);
|
|
247
417
|
};
|
|
248
418
|
//#endregion
|
|
249
|
-
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,
|