@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +33 -14
- package/claude/hooks/pr.ts +342 -0
- package/claude/hooks/register.tsx +148 -5
- package/claude/types/index.d.ts +18 -0
- package/dist/chunks/{reserved-ty1mtlnS.mjs → reserved-FWrjy7zo.mjs} +29 -4
- package/dist/chunks/{review-BmoH-9im.mjs → review-Dhl4vvDe.mjs} +17 -6
- package/dist/chunks/{steering-D4LUQ-av.mjs → steering-D7hWeMgE.mjs} +1 -1
- package/dist/chunks/{tail-CM3sVoAb.mjs → tail-B-Es6Hwj.mjs} +1 -1
- package/dist/gtd.bundle.mjs +7 -5
- package/dist/steering.mjs +1 -1
- package/dist/testkit.mjs +8 -6
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +3 -3
- package/dist/workflows/fix.mjs +3 -3
- 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 +229 -19
- package/package.json +6 -1
- package/src/install/ConfigScaffold.ts +2 -1
- package/src/install/Install.ts +5 -4
- package/src/steering/review.test.ts +6 -0
- package/src/steering/review.ts +4 -1
- package/src/testkit/steps.ts +5 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/review/review-door.feature +38 -0
- 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/dist/workflows.mjs
CHANGED
|
@@ -16469,7 +16469,7 @@ const reviewDescriptor = {
|
|
|
16469
16469
|
openThreads: openThreadsOf
|
|
16470
16470
|
};
|
|
16471
16471
|
const pointerLabel = (f) => `${f.path}${f.line === void 0 ? "" : `#${f.line}${f.rangeEnd === void 0 ? "" : `-${f.rangeEnd}`}`}`;
|
|
16472
|
-
const chunkNote = (chunk, prior) => chunk.description === (prior?.description ?? "") ? [] : [{
|
|
16472
|
+
const chunkNote = (chunk, prior) => chunk.description === (prior?.description ?? "") || prior === void 0 && chunk.answers !== void 0 ? [] : [{
|
|
16473
16473
|
line: chunk.headingLine,
|
|
16474
16474
|
note: {
|
|
16475
16475
|
kind: "chunk",
|
|
@@ -17197,6 +17197,11 @@ const testFileGlobs = [
|
|
|
17197
17197
|
"**/__tests__/**"
|
|
17198
17198
|
];
|
|
17199
17199
|
const defaultFlagRegistry = "src/flags/index.ts";
|
|
17200
|
+
/** The `tests` workflow's own process settings: where it looks for skipped tests, and how a skip reads. */
|
|
17201
|
+
const testsDefaults = {
|
|
17202
|
+
testFiles: testFileGlobs.join(", "),
|
|
17203
|
+
skipPattern: "^\\s*(?:(?:describe|context|suite|it|test|specify)\\.(?:skip|todo)\\b|x(?:describe|context|it|test|specify)\\s*\\(|(?:@\\S+\\s+)*@(?:skip|ignore)(?:\\s|$))"
|
|
17204
|
+
};
|
|
17200
17205
|
/** Process settings. */
|
|
17201
17206
|
const defaults = {
|
|
17202
17207
|
reviewBase: "",
|
|
@@ -17529,7 +17534,7 @@ const composedReview = (base, report) => {
|
|
|
17529
17534
|
return name;
|
|
17530
17535
|
};
|
|
17531
17536
|
const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
|
|
17532
|
-
const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${
|
|
17537
|
+
const answerBoxes = ([fix, wontFix] = [ANSWER_FIX, ANSWER_WONT_FIX]) => `${ANSWER_LEAD}\n\n- [ ] ${fix}\n- [ ] ${wontFix}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
|
|
17533
17538
|
const conflicts = (report.standards?.undecided ?? []).map((c) => chunk(unique(conflictTitle(c)), conflictBody(c)));
|
|
17534
17539
|
const added = report.standards?.added ?? [];
|
|
17535
17540
|
const notes = report.notes.map((n) => ({
|
|
@@ -17541,7 +17546,7 @@ const composedReview = (base, report) => {
|
|
|
17541
17546
|
...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.`)],
|
|
17542
17547
|
...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
|
|
17543
17548
|
...conflicts,
|
|
17544
|
-
...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}`)),
|
|
17549
|
+
...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)}`)),
|
|
17545
17550
|
...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
|
|
17546
17551
|
...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
|
|
17547
17552
|
...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
|
|
@@ -38228,6 +38233,24 @@ const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions,
|
|
|
38228
38233
|
base: void 0
|
|
38229
38234
|
};
|
|
38230
38235
|
});
|
|
38236
|
+
const TestsOptions = Struct({
|
|
38237
|
+
...ShippedOptions.fields,
|
|
38238
|
+
testFiles: setting$1,
|
|
38239
|
+
skipPattern: setting$1
|
|
38240
|
+
});
|
|
38241
|
+
const testsShipped = (flow) => defineWorkflow(standardSchemaV1(TestsOptions, {
|
|
38242
|
+
onExcessProperty: "error",
|
|
38243
|
+
errors: "all"
|
|
38244
|
+
}), ({ contextLimit, ...options }) => ({
|
|
38245
|
+
flow: limited(flow, contextLimit),
|
|
38246
|
+
defaults: over({
|
|
38247
|
+
...defaults,
|
|
38248
|
+
...testsDefaults
|
|
38249
|
+
}, options),
|
|
38250
|
+
envDefaults: over(envDefaults, options),
|
|
38251
|
+
doors: { tests: {} },
|
|
38252
|
+
base: void 0
|
|
38253
|
+
}));
|
|
38231
38254
|
//#endregion
|
|
38232
38255
|
//#region src/workflows/shared/lenses.ts
|
|
38233
38256
|
const NO_FINDINGS = {
|
|
@@ -38236,7 +38259,7 @@ const NO_FINDINGS = {
|
|
|
38236
38259
|
noise: []
|
|
38237
38260
|
};
|
|
38238
38261
|
const UNTITLED = "Quality findings";
|
|
38239
|
-
const below = (section) => section.split("\n").slice(1).join("\n").trim();
|
|
38262
|
+
const below$1 = (section) => section.split("\n").slice(1).join("\n").trim();
|
|
38240
38263
|
const strayProse = (text, sections) => sections.reduce((rest, body) => rest.replace(body, "\n"), text).split("\n").filter((line) => !/^#\s/.test(line)).join("\n").trim();
|
|
38241
38264
|
/** The findings in `text`, one per `## ` section, in file order, numbered `Q<n>`. */
|
|
38242
38265
|
const findingsOf = (text) => {
|
|
@@ -38247,7 +38270,7 @@ const findingsOf = (text) => {
|
|
|
38247
38270
|
body: stray
|
|
38248
38271
|
}], ...sections.map((s) => ({
|
|
38249
38272
|
title: s.title,
|
|
38250
|
-
body: below(s.body)
|
|
38273
|
+
body: below$1(s.body)
|
|
38251
38274
|
}))].map((f, i) => ({
|
|
38252
38275
|
id: `Q${i + 1}`,
|
|
38253
38276
|
...f
|
|
@@ -39025,7 +39048,7 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
|
|
|
39025
39048
|
* praise dropped.
|
|
39026
39049
|
*/
|
|
39027
39050
|
const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
39028
|
-
const { guard, guardRests, fixAccess, rules } = options;
|
|
39051
|
+
const { guard, guardRests, fixAccess, rules, awaitMessage } = options;
|
|
39029
39052
|
const feedback = options.feedback ?? {
|
|
39030
39053
|
file: ".gtd/REQUIREMENTS.md",
|
|
39031
39054
|
collecting,
|
|
@@ -39050,7 +39073,7 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
39050
39073
|
guardRests,
|
|
39051
39074
|
rules
|
|
39052
39075
|
}),
|
|
39053
|
-
awaitReview: (b) => awaitReview(b),
|
|
39076
|
+
awaitReview: (b) => awaitReview(b, awaitMessage?.(b)),
|
|
39054
39077
|
reviewMissing,
|
|
39055
39078
|
collecting: feedback.collecting,
|
|
39056
39079
|
triage: triage$1(severities),
|
|
@@ -39110,6 +39133,17 @@ const callForm = (first, second, [third, fourth, ...extra]) => {
|
|
|
39110
39133
|
if (typeof second === "string") return legacyForm(first, second, third, fourth);
|
|
39111
39134
|
if (typeof first === "string") return currentForm(first, second, third, fourth);
|
|
39112
39135
|
};
|
|
39136
|
+
/** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
|
|
39137
|
+
const fixFeedback = async () => {
|
|
39138
|
+
await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
|
|
39139
|
+
label: "Fixing your review feedback",
|
|
39140
|
+
file: REQUIREMENTS,
|
|
39141
|
+
model: coder(),
|
|
39142
|
+
system: finisherSystem(),
|
|
39143
|
+
allowEmpty: true
|
|
39144
|
+
});
|
|
39145
|
+
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
|
|
39146
|
+
};
|
|
39113
39147
|
/**
|
|
39114
39148
|
* Undo the human's review-round code edits. A path changed since the human
|
|
39115
39149
|
* left it is not overwritten; requireRevert names it instead.
|
|
@@ -40123,6 +40157,8 @@ const skills = (vars) => {
|
|
|
40123
40157
|
design: ["spec-driven-development", "planning-and-task-breakdown"],
|
|
40124
40158
|
notes: ["incremental-implementation", "code-simplification"],
|
|
40125
40159
|
feedback: ["test-driven-development", "incremental-implementation"],
|
|
40160
|
+
tests: ["test-audit", "test-driven-development"],
|
|
40161
|
+
"tests.health": ["debugging-and-error-recovery"],
|
|
40126
40162
|
...build,
|
|
40127
40163
|
...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
|
|
40128
40164
|
refactor: ["ponytail"],
|
|
@@ -40156,17 +40192,6 @@ const steering = {
|
|
|
40156
40192
|
};
|
|
40157
40193
|
//#endregion
|
|
40158
40194
|
//#region src/workflows/review/review.ts
|
|
40159
|
-
/** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
|
|
40160
|
-
const fixFeedback = async () => {
|
|
40161
|
-
await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
|
|
40162
|
-
label: "Fixing your review feedback",
|
|
40163
|
-
file: REQUIREMENTS,
|
|
40164
|
-
model: coder(),
|
|
40165
|
-
system: finisherSystem(),
|
|
40166
|
-
allowEmpty: true
|
|
40167
|
-
});
|
|
40168
|
-
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
|
|
40169
|
-
};
|
|
40170
40195
|
/**
|
|
40171
40196
|
* The feature's review of everything since `reviewBase` — a green gate, the
|
|
40172
40197
|
* triaged quality lap, then the human review — with no design phase: a
|
|
@@ -40300,6 +40325,191 @@ const cleanupFlow = async () => {
|
|
|
40300
40325
|
};
|
|
40301
40326
|
const cleanup = flagShipped(cleanupFlow, "cleanup");
|
|
40302
40327
|
//#endregion
|
|
40328
|
+
//#region src/workflows/tests/tests.ts
|
|
40329
|
+
const TESTS = ".gtd/TESTS.md";
|
|
40330
|
+
const RETIRE = ".gtd/RETIRE.md";
|
|
40331
|
+
/** The first two answer boxes of a deletion proposal; the third stays the human's own answer. */
|
|
40332
|
+
const RETIRE_ANSWERS = ["delete it", "keep it skipped"];
|
|
40333
|
+
testsDefaults.skipPattern;
|
|
40334
|
+
/** `skipPattern` as a per-line expression; refuses one that is blank, invalid, or matches every line. */
|
|
40335
|
+
const skipExpression = (pattern) => {
|
|
40336
|
+
if (pattern.trim() === "") return refuse("gtd: skipPattern must not be blank");
|
|
40337
|
+
let expression;
|
|
40338
|
+
try {
|
|
40339
|
+
expression = new RegExp(pattern);
|
|
40340
|
+
} catch (error) {
|
|
40341
|
+
return refuse(`gtd: skipPattern is not a valid regular expression: ${String(error)}`);
|
|
40342
|
+
}
|
|
40343
|
+
if (expression.test("")) return refuse("gtd: skipPattern must not match the empty string");
|
|
40344
|
+
return expression;
|
|
40345
|
+
};
|
|
40346
|
+
/** Every line of `files` the expression calls skipped, in file then line order. */
|
|
40347
|
+
const skipsIn = (files, expression) => files.flatMap(({ path, text }) => text.split("\n").flatMap((line, i) => expression.test(line) ? [{
|
|
40348
|
+
path,
|
|
40349
|
+
line: i + 1,
|
|
40350
|
+
text: line.trim()
|
|
40351
|
+
}] : []));
|
|
40352
|
+
const testPaths = () => {
|
|
40353
|
+
const globs = (vars.testFiles ?? testsDefaults.testFiles).split(",").map((g) => g.trim()).filter((g) => g !== "");
|
|
40354
|
+
return [...new Set(globs.flatMap((g) => glob(g)))].filter(isCode$1).sort();
|
|
40355
|
+
};
|
|
40356
|
+
const scan = (paths, expression) => skipsIn(paths.map((path) => ({
|
|
40357
|
+
path,
|
|
40358
|
+
text: read(path) ?? ""
|
|
40359
|
+
})), expression);
|
|
40360
|
+
/** A section title as a repository path: backticks and a leading `./` dropped. */
|
|
40361
|
+
const pathOf = (title) => title.replace(/`/g, "").trim().replace(/^\.\//, "");
|
|
40362
|
+
const below = (section) => section.split("\n").slice(1).join("\n").trim();
|
|
40363
|
+
const sweepingPrompt = (skips) => `Sweep this repository's tests for ones to revive.
|
|
40364
|
+
|
|
40365
|
+
Skipped tests gtd found:
|
|
40366
|
+
|
|
40367
|
+
${skips.length === 0 ? "- none" : skips.map((s) => `- ${s.path}:${s.line} — ${s.text}`).join("\n")}
|
|
40368
|
+
|
|
40369
|
+
- Audit every test in the repository with the test-audit skill — the whole
|
|
40370
|
+
suite, not a recent change
|
|
40371
|
+
- Write \`${TESTS}\`: \`# Tests\`, then one \`## <path>\` section per test file
|
|
40372
|
+
that needs work — every file listed above, and each file the audit found
|
|
40373
|
+
worth rewriting — saying which tests are skipped and what the audit found
|
|
40374
|
+
- Change nothing else; write nothing when there is nothing to do
|
|
40375
|
+
- Leave everything uncommitted and finish your turn`;
|
|
40376
|
+
const rewritingPrompt = (path, found) => `Rewrite the tests in \`${path}\` until none is skipped and the suite is green.
|
|
40377
|
+
|
|
40378
|
+
What the sweep found:
|
|
40379
|
+
|
|
40380
|
+
${found || "_Nothing more than the skipped tests._"}
|
|
40381
|
+
|
|
40382
|
+
- Unskip every skipped test in the file and make it pass, rewriting it where
|
|
40383
|
+
it rotted, and fix what the audit found
|
|
40384
|
+
- A test that is obsolete — what it tests is gone, or another test already
|
|
40385
|
+
covers it — stays skipped: never delete it. Propose its deletion in
|
|
40386
|
+
\`${RETIRE}\` instead: a \`## <title>\` section naming \`${path}\`, the test,
|
|
40387
|
+
and why it should go. A person decides. Propose a line gtd misread as
|
|
40388
|
+
skipped (fixture text, a comment) the same way, saying so
|
|
40389
|
+
- Delete no test file
|
|
40390
|
+
- Leave everything uncommitted and finish your turn`;
|
|
40391
|
+
const gateMessage = () => `The test baseline is red — gtd will not revive skipped tests on a broken suite.
|
|
40392
|
+
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
40393
|
+
|
|
40394
|
+
What each change does next (then run \`gtd land\`):
|
|
40395
|
+
- **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**tests-gate.check**).
|
|
40396
|
+
`;
|
|
40397
|
+
const awaitMessage = (base) => `\`.gtd/REVIEW.md\` holds one chunk per test the sweep would delete. Each ends in
|
|
40398
|
+
three boxes — tick exactly one: \`delete it\` (the test is deleted, then the
|
|
40399
|
+
suite runs), \`keep it skipped\` (dropped, recorded in the commit), or write your
|
|
40400
|
+
own answer over \`_your answer_\` and tick it (judged like a note). Every
|
|
40401
|
+
landing here is refused until each proposal holds exactly one tick. An agent
|
|
40402
|
+
turn that deleted a test file was refused; check the diff for tests removed
|
|
40403
|
+
inside a file.
|
|
40404
|
+
|
|
40405
|
+
Review the revived tests yourself:
|
|
40406
|
+
|
|
40407
|
+
git diff ${base}
|
|
40408
|
+
|
|
40409
|
+
Then run \`gtd land\`. A note or code edit of your own is read as feedback and
|
|
40410
|
+
built before you review again.
|
|
40411
|
+
`;
|
|
40412
|
+
/** A proposal's own text, made a request the nit fixer acts on once the human ticks `delete it`. */
|
|
40413
|
+
const proposals = (text) => sectionBodies(text).map((s, i) => ({
|
|
40414
|
+
id: `R${i + 1}`,
|
|
40415
|
+
title: s.title,
|
|
40416
|
+
body: `Delete this skipped test.\n\n${below(s.body)}`,
|
|
40417
|
+
answers: RETIRE_ANSWERS
|
|
40418
|
+
}));
|
|
40419
|
+
/** Sections of `text` that name `path` as a whole token — a trailing full stop still counts. */
|
|
40420
|
+
const naming = (text, path) => {
|
|
40421
|
+
const token = new RegExp(`(?<![\\w./-])${path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w/-])`);
|
|
40422
|
+
return sectionBodies(text).filter((s) => token.test(s.body.replace(/(^|[^\w.])\.\//g, "$1"))).length;
|
|
40423
|
+
};
|
|
40424
|
+
/** Refuses a test file a turn deleted, and a skip left in the file being rewritten that `.gtd/RETIRE.md` does not name. */
|
|
40425
|
+
const guardFor = (known, expression, path) => () => {
|
|
40426
|
+
const deleted = changes().filter((c) => c.status === "deleted" && known.has(c.path)).map((c) => c.path);
|
|
40427
|
+
if (deleted.length > 0) refuse(`gtd land: tests: ${deleted.join(", ")} deleted — a test goes only once a person ticks "delete it" at the review`);
|
|
40428
|
+
if (path === void 0) return;
|
|
40429
|
+
const left = scan([path], expression).slice(naming(read(RETIRE) ?? "", path));
|
|
40430
|
+
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}`);
|
|
40431
|
+
};
|
|
40432
|
+
/** The sweep's packages, refused when it changed more than its plan or the plan misses a file. */
|
|
40433
|
+
const planned = (skips, known) => {
|
|
40434
|
+
const strays = changes().filter((c) => c.path !== TESTS);
|
|
40435
|
+
if (strays.length > 0) refuse(`gtd land: tests: the sweep changed ${strays.map((c) => c.path).join(", ")} — it writes only ${TESTS}`);
|
|
40436
|
+
const packages = sectionBodies(read(TESTS) ?? "").map((s) => ({
|
|
40437
|
+
path: pathOf(s.title),
|
|
40438
|
+
found: below(s.body)
|
|
40439
|
+
}));
|
|
40440
|
+
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`)];
|
|
40441
|
+
if (missing.length > 0) refuse(`gtd land: tests: in ${TESTS}, ${missing.join("; ")}`);
|
|
40442
|
+
return packages;
|
|
40443
|
+
};
|
|
40444
|
+
/**
|
|
40445
|
+
* Revive or retire skipped tests: a sweep plans one package per test file, each
|
|
40446
|
+
* is rewritten until unskipped and green, and what an agent calls obsolete
|
|
40447
|
+
* reaches the human review as a deletion proposal only a person can tick.
|
|
40448
|
+
*/
|
|
40449
|
+
const testsFlow = async () => {
|
|
40450
|
+
refusePendingHandOff();
|
|
40451
|
+
for (const file of [
|
|
40452
|
+
REQUIREMENTS,
|
|
40453
|
+
TESTS,
|
|
40454
|
+
RETIRE
|
|
40455
|
+
]) if (read(file) !== void 0) refuse(`gtd: ${file} is left over — finish or remove it first.`);
|
|
40456
|
+
const expression = skipExpression(vars.skipPattern ?? testsDefaults.skipPattern);
|
|
40457
|
+
await gate("tests-gate", gateMessage());
|
|
40458
|
+
const base = start();
|
|
40459
|
+
const known = new Set(testPaths());
|
|
40460
|
+
const guard = guardFor(known, expression);
|
|
40461
|
+
const report = await scope("tests", async () => {
|
|
40462
|
+
const skips = scan([...known], expression);
|
|
40463
|
+
await agentWithSkills("sweeping", sweepingPrompt(skips), {
|
|
40464
|
+
label: "Sweeping the tests",
|
|
40465
|
+
file: TESTS,
|
|
40466
|
+
model: coder(),
|
|
40467
|
+
system: finisherSystem(),
|
|
40468
|
+
allowEmpty: true
|
|
40469
|
+
});
|
|
40470
|
+
guard();
|
|
40471
|
+
const packages = planned(skips, known);
|
|
40472
|
+
for (const { path, found } of packages) {
|
|
40473
|
+
const guarded = guardFor(known, expression, path);
|
|
40474
|
+
await agentWithSkills("rewriting", rewritingPrompt(path, found), {
|
|
40475
|
+
label: `Rewriting ${path}`,
|
|
40476
|
+
file: RETIRE,
|
|
40477
|
+
model: coder(),
|
|
40478
|
+
system: finisherSystem(),
|
|
40479
|
+
allowEmpty: true
|
|
40480
|
+
});
|
|
40481
|
+
guarded();
|
|
40482
|
+
if (!changes().some((c) => isCode$1(c.path))) continue;
|
|
40483
|
+
await healthy(() => fixCheck(), {
|
|
40484
|
+
guard: guarded,
|
|
40485
|
+
guardRests: false
|
|
40486
|
+
});
|
|
40487
|
+
}
|
|
40488
|
+
const retired = proposals(read(RETIRE) ?? "");
|
|
40489
|
+
if (read(TESTS) !== void 0 || read(RETIRE) !== void 0) await run("closing", removeScript([TESTS, RETIRE]), { label: "Closing the sweep" });
|
|
40490
|
+
const changed = changesSince(base).paths.some(isCode$1);
|
|
40491
|
+
return retired.length === 0 && !changed ? void 0 : {
|
|
40492
|
+
notes: retired,
|
|
40493
|
+
fixed: [],
|
|
40494
|
+
noise: []
|
|
40495
|
+
};
|
|
40496
|
+
});
|
|
40497
|
+
if (report === void 0) return;
|
|
40498
|
+
let outcome = await tail((escalations) => humanReview(base, escalations, {
|
|
40499
|
+
report,
|
|
40500
|
+
awaitMessage: report.notes.length > 0 ? awaitMessage : void 0
|
|
40501
|
+
}), {
|
|
40502
|
+
lenses: [],
|
|
40503
|
+
green: true
|
|
40504
|
+
});
|
|
40505
|
+
while (outcome.verdict === "feedback") {
|
|
40506
|
+
await reUnwind(outcome);
|
|
40507
|
+
await fixFeedback();
|
|
40508
|
+
outcome = await tail((escalations) => humanReview(base, escalations), { lenses: [] });
|
|
40509
|
+
}
|
|
40510
|
+
};
|
|
40511
|
+
const tests = testsShipped(testsFlow);
|
|
40512
|
+
//#endregion
|
|
40303
40513
|
//#region src/workflows/index.ts
|
|
40304
40514
|
var workflows_default = feature;
|
|
40305
40515
|
/**
|
|
@@ -40309,4 +40519,4 @@ var workflows_default = feature;
|
|
|
40309
40519
|
*/
|
|
40310
40520
|
const doors = {};
|
|
40311
40521
|
//#endregion
|
|
40312
|
-
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unstableIds, unwind };
|
|
40522
|
+
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, TestsOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, tests, testsFlow, threads, triage, unstableIds, unwind };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "45.
|
|
3
|
+
"version": "45.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
@@ -62,6 +62,11 @@
|
|
|
62
62
|
"source": "./src/workflows/cleanup/index.ts",
|
|
63
63
|
"default": "./dist/workflows/cleanup.mjs"
|
|
64
64
|
},
|
|
65
|
+
"./workflows/tests": {
|
|
66
|
+
"types": "./src/workflows/tests/index.ts",
|
|
67
|
+
"source": "./src/workflows/tests/index.ts",
|
|
68
|
+
"default": "./dist/workflows/tests.mjs"
|
|
69
|
+
},
|
|
65
70
|
"./testkit": {
|
|
66
71
|
"types": "./src/testkit/index.ts",
|
|
67
72
|
"source": "./src/testkit/index.ts",
|
|
@@ -6,7 +6,8 @@ export const CONFIG_SCAFFOLD = `// The workflows gtd runs here: gtd's shipped fe
|
|
|
6
6
|
// its own doors: \`gtd door review [base]\` and \`gtd door fix\`. Export \`rollout\`
|
|
7
7
|
// from \`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
|
|
8
8
|
// \`@pmelab/gtd/workflows/cleanup\` to add \`gtd door rollout <flag>\` and
|
|
9
|
-
// \`gtd door cleanup <flag
|
|
9
|
+
// \`gtd door cleanup <flag>\`, and \`tests\` from \`@pmelab/gtd/workflows/tests\`
|
|
10
|
+
// to add \`gtd door tests\`.
|
|
10
11
|
export {
|
|
11
12
|
feature as default,
|
|
12
13
|
feature,
|
package/src/install/Install.ts
CHANGED
|
@@ -516,10 +516,11 @@ rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
|
|
|
516
516
|
straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
|
|
517
517
|
\`gtd doors\` lists the doors a repository offers (those of the workflows its
|
|
518
518
|
config exports — the bundled \`fix\` and \`review [base]\`, plus the opt-in
|
|
519
|
-
\`rollout <flag>\` (turn a release flag default-on)
|
|
520
|
-
(remove it), which a config adds by
|
|
521
|
-
\`@pmelab/gtd/workflows/rollout
|
|
522
|
-
\`@pmelab/gtd/workflows/cleanup\`
|
|
519
|
+
\`rollout <flag>\` (turn a release flag default-on), \`cleanup <flag>\`
|
|
520
|
+
(remove it) and \`tests\` (revive skipped tests), which a config adds by
|
|
521
|
+
exporting \`rollout\` from \`@pmelab/gtd/workflows/rollout\`, \`cleanup\` from
|
|
522
|
+
\`@pmelab/gtd/workflows/cleanup\` and \`tests\` from
|
|
523
|
+
\`@pmelab/gtd/workflows/tests\` — plus any it declares). Refuses with
|
|
523
524
|
\`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
|
|
524
525
|
missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
|
|
525
526
|
|
|
@@ -1875,6 +1875,12 @@ describe("answer boxes", async () => {
|
|
|
1875
1875
|
expect(reviewNotes(open, doc(note("Rounding", [" ", "x", ""])))).toEqual([])
|
|
1876
1876
|
})
|
|
1877
1877
|
|
|
1878
|
+
it("a note added with its own boxes is a decision, never also a note", () => {
|
|
1879
|
+
const added = doc(note("Rounding", [" ", " ", ""]), note("PR thread", ["x", " ", ""]))
|
|
1880
|
+
expect(reviewNotes(open, added)).toEqual([])
|
|
1881
|
+
expect(reviewDecisions(open, added)).toMatchObject([{ anchor: "PR thread", choice: "fix" }])
|
|
1882
|
+
})
|
|
1883
|
+
|
|
1878
1884
|
it("an unticked note, or one with two ticks, is unanswered", () => {
|
|
1879
1885
|
expect(unansweredNotes(open)).toEqual([
|
|
1880
1886
|
{ title: "Rounding", line: 5, reason: "tick exactly one box" },
|
package/src/steering/review.ts
CHANGED
|
@@ -1177,8 +1177,11 @@ const pointerLabel = (f: ReviewFile): string =>
|
|
|
1177
1177
|
|
|
1178
1178
|
type Found = { line: number; note: Omit<ReviewNote, "id" | "forced"> }
|
|
1179
1179
|
|
|
1180
|
+
// A chunk added with its own answer boxes (a driver's imported note) is
|
|
1181
|
+
// answered by its tick, never judged as prose as well.
|
|
1180
1182
|
const chunkNote = (chunk: Changeset, prior: Changeset | undefined): Found[] =>
|
|
1181
|
-
chunk.description === (prior?.description ?? "")
|
|
1183
|
+
chunk.description === (prior?.description ?? "") ||
|
|
1184
|
+
(prior === undefined && chunk.answers !== undefined)
|
|
1182
1185
|
? []
|
|
1183
1186
|
: [
|
|
1184
1187
|
{
|
package/src/testkit/steps.ts
CHANGED
|
@@ -140,6 +140,11 @@ Given(
|
|
|
140
140
|
},
|
|
141
141
|
)
|
|
142
142
|
|
|
143
|
+
Given("{string} gets appended:", (world: TestkitWorld, path: string, text: string) => {
|
|
144
|
+
const content = world.repo().read(path) ?? ""
|
|
145
|
+
world.repo().write(path, `${content.trimEnd()}\n\n${text}\n`)
|
|
146
|
+
})
|
|
147
|
+
|
|
143
148
|
Given("the working tree is committed as {string}", (world: TestkitWorld, message: string) => {
|
|
144
149
|
world.repo().commit(message)
|
|
145
150
|
})
|
package/src/workflows/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { feature } from "./feature/feature.js"
|
|
|
5
5
|
|
|
6
6
|
export { threads, type ThreadInfo } from "../flows/index.js"
|
|
7
7
|
export * from "./shared/reserved.js"
|
|
8
|
-
export { FlagOptions, ShippedOptions } from "./shared/options.js"
|
|
8
|
+
export { FlagOptions, ShippedOptions, TestsOptions } from "./shared/options.js"
|
|
9
9
|
export { agentWithSkills, type ReviewRange } from "./shared/text.js"
|
|
10
10
|
export * from "./shared/steps.js"
|
|
11
11
|
export * from "./shared/health.js"
|
|
@@ -38,6 +38,7 @@ export { base, review, reviewFlow } from "./review/review.js"
|
|
|
38
38
|
export { fix, fixFlow, fixReviewNotes } from "./fix/fix.js"
|
|
39
39
|
export { rollout, rolloutFlow } from "./rollout/rollout.js"
|
|
40
40
|
export { cleanup, cleanupFlow } from "./cleanup/cleanup.js"
|
|
41
|
+
export { tests, testsFlow } from "./tests/tests.js"
|
|
41
42
|
|
|
42
43
|
export default feature
|
|
43
44
|
|
|
@@ -156,3 +156,41 @@ Feature: the review door fixes its notes in the same process
|
|
|
156
156
|
"""
|
|
157
157
|
And the working tree is committed as "chore: leftover requirements"
|
|
158
158
|
Then opening door "review" is refused with ".gtd/REQUIREMENTS.md is left over"
|
|
159
|
+
|
|
160
|
+
Scenario: a note added with its own boxes is fixed as ticked, never judged
|
|
161
|
+
Given judge step "build.review.standards.judge" answers:
|
|
162
|
+
"""
|
|
163
|
+
[{"id":"standard-1","answer":"no","p":0.9}]
|
|
164
|
+
"""
|
|
165
|
+
And step "build.review.fix.nits.fixing" writes "src/calc.ts" with:
|
|
166
|
+
"""
|
|
167
|
+
export const add = (a: number, b: number): number => a + b
|
|
168
|
+
"""
|
|
169
|
+
When gtd opens door "review" with "HEAD~1"
|
|
170
|
+
And the process is driven
|
|
171
|
+
And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
|
|
172
|
+
And ".gtd/REVIEW.md" gets appended:
|
|
173
|
+
"""
|
|
174
|
+
## src/calc.ts:1 (thread PRRT_1)
|
|
175
|
+
|
|
176
|
+
> Annotate the return type.
|
|
177
|
+
|
|
178
|
+
Tick exactly one:
|
|
179
|
+
|
|
180
|
+
- [x] fix as suggested
|
|
181
|
+
- [ ] won't fix
|
|
182
|
+
- [ ] _your answer_
|
|
183
|
+
"""
|
|
184
|
+
And gtd lands
|
|
185
|
+
And the process is driven
|
|
186
|
+
Then the process rests at "build.review.await-review"
|
|
187
|
+
And the commit subjects end with:
|
|
188
|
+
"""
|
|
189
|
+
gtd(human): build.review.await-review → build.review.standards.judge
|
|
190
|
+
gtd(judge): build.review.standards.judge → build.review.fix.nits.fixing
|
|
191
|
+
gtd(agent): build.review.fix.nits.fixing → build.health.check
|
|
192
|
+
gtd(check): build.health.check → build.review.closing
|
|
193
|
+
gtd(check): build.review.closing → build.review.composing
|
|
194
|
+
gtd(check): build.review.composing → build.review.await-review
|
|
195
|
+
"""
|
|
196
|
+
And "src/calc.ts" contains "): number =>"
|
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
import { read, refuse,
|
|
2
|
-
import { buildTail, reUnwind } from "../shared/review.js"
|
|
1
|
+
import { read, refuse, start, type WorkflowBase } from "../../flows/index.js"
|
|
2
|
+
import { buildTail, fixFeedback, reUnwind } from "../shared/review.js"
|
|
3
3
|
import { gate } from "../shared/health.js"
|
|
4
4
|
import { shipped } from "../shared/options.js"
|
|
5
|
-
import {
|
|
5
|
+
import { REQUIREMENTS } from "../shared/steps.js"
|
|
6
6
|
import { refusePendingHandOff } from "../shared/tail.js"
|
|
7
7
|
import * as t from "../shared/text.js"
|
|
8
8
|
|
|
9
|
-
/** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
|
|
10
|
-
const fixFeedback = async (): Promise<void> => {
|
|
11
|
-
await t.agentWithSkills("feedback.fixing", t.reviewFeedbackFixPrompt(), {
|
|
12
|
-
label: "Fixing your review feedback",
|
|
13
|
-
file: REQUIREMENTS,
|
|
14
|
-
model: coder(),
|
|
15
|
-
system: t.finisherSystem(),
|
|
16
|
-
// An earlier nit fix may already have built the concern.
|
|
17
|
-
allowEmpty: true,
|
|
18
|
-
})
|
|
19
|
-
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" })
|
|
20
|
-
}
|
|
21
|
-
|
|
22
9
|
/**
|
|
23
10
|
* The feature's review of everything since `reviewBase` — a green gate, the
|
|
24
11
|
* triaged quality lap, then the human review — with no design phase: a
|
|
@@ -29,7 +29,11 @@ type FindingKind = "mechanical" | "noise" | "design"
|
|
|
29
29
|
/** What the triaged quality lap did with its findings, for the review to list. */
|
|
30
30
|
export interface LensReport {
|
|
31
31
|
/** Design findings, then the mechanical ones the fix cap left. */
|
|
32
|
-
readonly notes: readonly (Finding & {
|
|
32
|
+
readonly notes: readonly (Finding & {
|
|
33
|
+
readonly unresolved?: true
|
|
34
|
+
readonly unclear?: true
|
|
35
|
+
readonly answers?: readonly [string, string]
|
|
36
|
+
})[]
|
|
33
37
|
readonly fixed: readonly (Finding & { readonly commit: string })[]
|
|
34
38
|
readonly noise: readonly (Finding & { readonly p: number })[]
|
|
35
39
|
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
scope,
|
|
10
10
|
vars,
|
|
11
11
|
} from "../../flows/index.js"
|
|
12
|
-
import { defaultFlagRegistry, defaults, envDefaults } from "./vars.js"
|
|
12
|
+
import { defaultFlagRegistry, defaults, envDefaults, testsDefaults } from "./vars.js"
|
|
13
13
|
|
|
14
14
|
const setting = Schema.optionalWith(Schema.String, { exact: true })
|
|
15
15
|
|
|
@@ -107,3 +107,23 @@ export const flagShipped = (
|
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
109
|
)
|
|
110
|
+
|
|
111
|
+
export const TestsOptions = Schema.Struct({
|
|
112
|
+
...ShippedOptions.fields,
|
|
113
|
+
testFiles: setting,
|
|
114
|
+
skipPattern: setting,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
export type TestsOptions = Schema.Schema.Encoded<typeof TestsOptions>
|
|
118
|
+
|
|
119
|
+
export const testsShipped = (flow: Flow): WorkflowFactory<TestsOptions> =>
|
|
120
|
+
defineWorkflow(
|
|
121
|
+
Schema.standardSchemaV1(TestsOptions, { onExcessProperty: "error", errors: "all" }),
|
|
122
|
+
({ contextLimit, ...options }) => ({
|
|
123
|
+
flow: limited(flow, contextLimit),
|
|
124
|
+
defaults: over({ ...defaults, ...testsDefaults }, options),
|
|
125
|
+
envDefaults: over(envDefaults, options),
|
|
126
|
+
doors: { tests: {} },
|
|
127
|
+
base: undefined,
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
judge,
|
|
6
6
|
numeric,
|
|
7
7
|
read,
|
|
8
|
+
removeScript,
|
|
8
9
|
requireRevert,
|
|
9
10
|
restoreScript,
|
|
10
11
|
run,
|
|
@@ -22,6 +23,7 @@ import { NO_FINDINGS, type LensReport } from "./lenses.js"
|
|
|
22
23
|
import {
|
|
23
24
|
answerReviewQuestions,
|
|
24
25
|
awaitReview,
|
|
26
|
+
coder,
|
|
25
27
|
collecting,
|
|
26
28
|
fixCheck,
|
|
27
29
|
fixNits,
|
|
@@ -218,7 +220,7 @@ export const humanReview = (
|
|
|
218
220
|
escalations: EscalationCount = { rounds: 0 },
|
|
219
221
|
options: Omit<BuildTailOptions, "fixFirst" | "green"> = {},
|
|
220
222
|
): Promise<kit.ReviewOutcome> => {
|
|
221
|
-
const { guard, guardRests, fixAccess, rules } = options
|
|
223
|
+
const { guard, guardRests, fixAccess, rules, awaitMessage } = options
|
|
222
224
|
const feedback: FeedbackTarget = options.feedback ?? {
|
|
223
225
|
file: REQUIREMENTS,
|
|
224
226
|
collecting,
|
|
@@ -241,7 +243,7 @@ export const humanReview = (
|
|
|
241
243
|
},
|
|
242
244
|
fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
|
|
243
245
|
keepGreen: () => healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules }),
|
|
244
|
-
awaitReview: (b) => awaitReview(b),
|
|
246
|
+
awaitReview: (b) => awaitReview(b, awaitMessage?.(b)),
|
|
245
247
|
reviewMissing,
|
|
246
248
|
collecting: feedback.collecting,
|
|
247
249
|
triage: triage(severities),
|
|
@@ -303,6 +305,8 @@ export interface BuildTailOptions extends TailOptions {
|
|
|
303
305
|
readonly lap?: t.ReviewRange | undefined
|
|
304
306
|
/** What the triaged quality lap left for the review; absent: nothing. */
|
|
305
307
|
readonly report?: LensReport | undefined
|
|
308
|
+
/** The review gate's message for the review base; absent: the quality lap's. */
|
|
309
|
+
readonly awaitMessage?: ((base: string) => string) | undefined
|
|
306
310
|
}
|
|
307
311
|
|
|
308
312
|
/** The build tail: fix (when entered red), keep green unless `green`, the triaged quality lap, then human review since `base`. */
|
|
@@ -368,6 +372,19 @@ const callForm = (
|
|
|
368
372
|
return undefined
|
|
369
373
|
}
|
|
370
374
|
|
|
375
|
+
/** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
|
|
376
|
+
export const fixFeedback = async (): Promise<void> => {
|
|
377
|
+
await t.agentWithSkills("feedback.fixing", t.reviewFeedbackFixPrompt(), {
|
|
378
|
+
label: "Fixing your review feedback",
|
|
379
|
+
file: REQUIREMENTS,
|
|
380
|
+
model: coder(),
|
|
381
|
+
system: t.finisherSystem(),
|
|
382
|
+
// An earlier nit fix may already have built the concern.
|
|
383
|
+
allowEmpty: true,
|
|
384
|
+
})
|
|
385
|
+
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" })
|
|
386
|
+
}
|
|
387
|
+
|
|
371
388
|
/**
|
|
372
389
|
* Undo the human's review-round code edits. A path changed since the human
|
|
373
390
|
* left it is not overwritten; requireRevert names it instead.
|
|
@@ -24,6 +24,9 @@ export const skills = (
|
|
|
24
24
|
notes: ["incremental-implementation", "code-simplification"],
|
|
25
25
|
// `review`'s turn building what a review round collected.
|
|
26
26
|
feedback: ["test-driven-development", "incremental-implementation"],
|
|
27
|
+
// The `tests` workflow's sweep and rewrites.
|
|
28
|
+
tests: ["test-audit", "test-driven-development"],
|
|
29
|
+
"tests.health": ["debugging-and-error-recovery"],
|
|
27
30
|
// The refactor's own turns load `ponytail`; `refactor.build.*` carry the `build.*` lists.
|
|
28
31
|
...build,
|
|
29
32
|
...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
|