@pmelab/gtd 45.1.0 → 46.0.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 +18 -29
- 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-ulcSkND6.mjs → reserved-BENPy6IJ.mjs} +4 -29
- package/dist/chunks/{review-Da2ndnB1.mjs → review-DcbgUwr7.mjs} +6 -17
- package/dist/chunks/{steering-D4LUQ-av.mjs → steering-D7hWeMgE.mjs} +1 -1
- package/dist/chunks/{tail-BW030xz_.mjs → tail-CDNMZ-2E.mjs} +1 -1
- package/dist/gtd.bundle.mjs +5 -7
- package/dist/steering.mjs +1 -1
- package/dist/testkit.mjs +6 -8
- 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 +14 -4
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +18 -229
- package/package.json +1 -6
- package/src/install/ConfigScaffold.ts +1 -2
- package/src/install/Install.ts +4 -5
- 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 +1 -2
- package/src/workflows/review/review-door.feature +38 -0
- package/src/workflows/review/review.ts +15 -3
- package/src/workflows/shared/lenses.ts +1 -5
- package/src/workflows/shared/options.ts +1 -21
- package/src/workflows/shared/review.ts +2 -19
- package/src/workflows/shared/skills.test.ts +0 -2
- package/src/workflows/shared/skills.ts +0 -3
- package/src/workflows/shared/text.ts +2 -5
- package/src/workflows/shared/vars.ts +0 -8
- package/dist/workflows/tests.mjs +0 -193
- package/src/workflows/tests/index.ts +0 -6
- package/src/workflows/tests/tests-options.feature +0 -52
- package/src/workflows/tests/tests.feature +0 -266
- package/src/workflows/tests/tests.test.ts +0 -51
- package/src/workflows/tests/tests.ts +0 -270
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,11 +17197,6 @@ 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
|
-
};
|
|
17205
17200
|
/** Process settings. */
|
|
17206
17201
|
const defaults = {
|
|
17207
17202
|
reviewBase: "",
|
|
@@ -17534,7 +17529,7 @@ const composedReview = (base, report) => {
|
|
|
17534
17529
|
return name;
|
|
17535
17530
|
};
|
|
17536
17531
|
const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
|
|
17537
|
-
const answerBoxes =
|
|
17532
|
+
const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
|
|
17538
17533
|
const conflicts = (report.standards?.undecided ?? []).map((c) => chunk(unique(conflictTitle(c)), conflictBody(c)));
|
|
17539
17534
|
const added = report.standards?.added ?? [];
|
|
17540
17535
|
const notes = report.notes.map((n) => ({
|
|
@@ -17546,7 +17541,7 @@ const composedReview = (base, report) => {
|
|
|
17546
17541
|
...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.`)],
|
|
17547
17542
|
...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
|
|
17548
17543
|
...conflicts,
|
|
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
|
|
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}`)),
|
|
17550
17545
|
...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
|
|
17551
17546
|
...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
|
|
17552
17547
|
...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
|
|
@@ -38233,24 +38228,6 @@ const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions,
|
|
|
38233
38228
|
base: void 0
|
|
38234
38229
|
};
|
|
38235
38230
|
});
|
|
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
|
-
}));
|
|
38254
38231
|
//#endregion
|
|
38255
38232
|
//#region src/workflows/shared/lenses.ts
|
|
38256
38233
|
const NO_FINDINGS = {
|
|
@@ -38259,7 +38236,7 @@ const NO_FINDINGS = {
|
|
|
38259
38236
|
noise: []
|
|
38260
38237
|
};
|
|
38261
38238
|
const UNTITLED = "Quality findings";
|
|
38262
|
-
const below
|
|
38239
|
+
const below = (section) => section.split("\n").slice(1).join("\n").trim();
|
|
38263
38240
|
const strayProse = (text, sections) => sections.reduce((rest, body) => rest.replace(body, "\n"), text).split("\n").filter((line) => !/^#\s/.test(line)).join("\n").trim();
|
|
38264
38241
|
/** The findings in `text`, one per `## ` section, in file order, numbered `Q<n>`. */
|
|
38265
38242
|
const findingsOf = (text) => {
|
|
@@ -38270,7 +38247,7 @@ const findingsOf = (text) => {
|
|
|
38270
38247
|
body: stray
|
|
38271
38248
|
}], ...sections.map((s) => ({
|
|
38272
38249
|
title: s.title,
|
|
38273
|
-
body: below
|
|
38250
|
+
body: below(s.body)
|
|
38274
38251
|
}))].map((f, i) => ({
|
|
38275
38252
|
id: `Q${i + 1}`,
|
|
38276
38253
|
...f
|
|
@@ -39048,7 +39025,7 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
|
|
|
39048
39025
|
* praise dropped.
|
|
39049
39026
|
*/
|
|
39050
39027
|
const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
39051
|
-
const { guard, guardRests, fixAccess, rules
|
|
39028
|
+
const { guard, guardRests, fixAccess, rules } = options;
|
|
39052
39029
|
const feedback = options.feedback ?? {
|
|
39053
39030
|
file: ".gtd/REQUIREMENTS.md",
|
|
39054
39031
|
collecting,
|
|
@@ -39073,7 +39050,7 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
39073
39050
|
guardRests,
|
|
39074
39051
|
rules
|
|
39075
39052
|
}),
|
|
39076
|
-
awaitReview
|
|
39053
|
+
awaitReview,
|
|
39077
39054
|
reviewMissing,
|
|
39078
39055
|
collecting: feedback.collecting,
|
|
39079
39056
|
triage: triage$1(severities),
|
|
@@ -39133,17 +39110,6 @@ const callForm = (first, second, [third, fourth, ...extra]) => {
|
|
|
39133
39110
|
if (typeof second === "string") return legacyForm(first, second, third, fourth);
|
|
39134
39111
|
if (typeof first === "string") return currentForm(first, second, third, fourth);
|
|
39135
39112
|
};
|
|
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
|
-
};
|
|
39147
39113
|
/**
|
|
39148
39114
|
* Undo the human's review-round code edits. A path changed since the human
|
|
39149
39115
|
* left it is not overwritten; requireRevert names it instead.
|
|
@@ -40157,8 +40123,6 @@ const skills = (vars) => {
|
|
|
40157
40123
|
design: ["spec-driven-development", "planning-and-task-breakdown"],
|
|
40158
40124
|
notes: ["incremental-implementation", "code-simplification"],
|
|
40159
40125
|
feedback: ["test-driven-development", "incremental-implementation"],
|
|
40160
|
-
tests: ["test-audit", "test-driven-development"],
|
|
40161
|
-
"tests.health": ["debugging-and-error-recovery"],
|
|
40162
40126
|
...build,
|
|
40163
40127
|
...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
|
|
40164
40128
|
refactor: ["ponytail"],
|
|
@@ -40192,6 +40156,16 @@ const steering = {
|
|
|
40192
40156
|
};
|
|
40193
40157
|
//#endregion
|
|
40194
40158
|
//#region src/workflows/review/review.ts
|
|
40159
|
+
const fixFeedback = async () => {
|
|
40160
|
+
await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
|
|
40161
|
+
label: "Fixing your review feedback",
|
|
40162
|
+
file: REQUIREMENTS,
|
|
40163
|
+
model: coder(),
|
|
40164
|
+
system: finisherSystem(),
|
|
40165
|
+
allowEmpty: true
|
|
40166
|
+
});
|
|
40167
|
+
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
|
|
40168
|
+
};
|
|
40195
40169
|
/**
|
|
40196
40170
|
* The feature's review of everything since `reviewBase` — a green gate, the
|
|
40197
40171
|
* triaged quality lap, then the human review — with no design phase: a
|
|
@@ -40325,191 +40299,6 @@ const cleanupFlow = async () => {
|
|
|
40325
40299
|
};
|
|
40326
40300
|
const cleanup = flagShipped(cleanupFlow, "cleanup");
|
|
40327
40301
|
//#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
|
|
40513
40302
|
//#region src/workflows/index.ts
|
|
40514
40303
|
var workflows_default = feature;
|
|
40515
40304
|
/**
|
|
@@ -40519,4 +40308,4 @@ var workflows_default = feature;
|
|
|
40519
40308
|
*/
|
|
40520
40309
|
const doors = {};
|
|
40521
40310
|
//#endregion
|
|
40522
|
-
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions,
|
|
40311
|
+
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "46.0.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,11 +62,6 @@
|
|
|
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
|
-
},
|
|
70
65
|
"./testkit": {
|
|
71
66
|
"types": "./src/testkit/index.ts",
|
|
72
67
|
"source": "./src/testkit/index.ts",
|
|
@@ -6,8 +6,7 @@ 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
|
|
10
|
-
// to add \`gtd door tests\`.
|
|
9
|
+
// \`gtd door cleanup <flag>\`.
|
|
11
10
|
export {
|
|
12
11
|
feature as default,
|
|
13
12
|
feature,
|
package/src/install/Install.ts
CHANGED
|
@@ -516,11 +516,10 @@ 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)
|
|
521
|
-
|
|
522
|
-
\`@pmelab/gtd/workflows/cleanup\`
|
|
523
|
-
\`@pmelab/gtd/workflows/tests\` — plus any it declares). Refuses with
|
|
519
|
+
\`rollout <flag>\` (turn a release flag default-on) and \`cleanup <flag>\`
|
|
520
|
+
(remove it), which a config adds by exporting \`rollout\` from
|
|
521
|
+
\`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
|
|
522
|
+
\`@pmelab/gtd/workflows/cleanup\` — plus any it declares). Refuses with
|
|
524
523
|
\`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
|
|
525
524
|
missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
|
|
526
525
|
|
|
@@ -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
|
|
8
|
+
export { FlagOptions, ShippedOptions } 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,7 +38,6 @@ 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"
|
|
42
41
|
|
|
43
42
|
export default feature
|
|
44
43
|
|
|
@@ -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,11 +1,23 @@
|
|
|
1
|
-
import { read, refuse, start, type WorkflowBase } from "../../flows/index.js"
|
|
2
|
-
import { buildTail,
|
|
1
|
+
import { read, refuse, removeScript, run, start, type WorkflowBase } from "../../flows/index.js"
|
|
2
|
+
import { buildTail, reUnwind } from "../shared/review.js"
|
|
3
3
|
import { gate } from "../shared/health.js"
|
|
4
4
|
import { shipped } from "../shared/options.js"
|
|
5
|
-
import { REQUIREMENTS } from "../shared/steps.js"
|
|
5
|
+
import { coder, 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
|
+
const fixFeedback = async (): Promise<void> => {
|
|
10
|
+
await t.agentWithSkills("feedback.fixing", t.reviewFeedbackFixPrompt(), {
|
|
11
|
+
label: "Fixing your review feedback",
|
|
12
|
+
file: REQUIREMENTS,
|
|
13
|
+
model: coder(),
|
|
14
|
+
system: t.finisherSystem(),
|
|
15
|
+
// An earlier nit fix may already have built the concern.
|
|
16
|
+
allowEmpty: true,
|
|
17
|
+
})
|
|
18
|
+
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" })
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
/**
|
|
10
22
|
* The feature's review of everything since `reviewBase` — a green gate, the
|
|
11
23
|
* triaged quality lap, then the human review — with no design phase: a
|
|
@@ -29,11 +29,7 @@ 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 & {
|
|
33
|
-
readonly unresolved?: true
|
|
34
|
-
readonly unclear?: true
|
|
35
|
-
readonly answers?: readonly [string, string]
|
|
36
|
-
})[]
|
|
32
|
+
readonly notes: readonly (Finding & { readonly unresolved?: true; readonly unclear?: true })[]
|
|
37
33
|
readonly fixed: readonly (Finding & { readonly commit: string })[]
|
|
38
34
|
readonly noise: readonly (Finding & { readonly p: number })[]
|
|
39
35
|
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
scope,
|
|
10
10
|
vars,
|
|
11
11
|
} from "../../flows/index.js"
|
|
12
|
-
import { defaultFlagRegistry, defaults, envDefaults
|
|
12
|
+
import { defaultFlagRegistry, defaults, envDefaults } from "./vars.js"
|
|
13
13
|
|
|
14
14
|
const setting = Schema.optionalWith(Schema.String, { exact: true })
|
|
15
15
|
|
|
@@ -107,23 +107,3 @@ 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,7 +5,6 @@ import {
|
|
|
5
5
|
judge,
|
|
6
6
|
numeric,
|
|
7
7
|
read,
|
|
8
|
-
removeScript,
|
|
9
8
|
requireRevert,
|
|
10
9
|
restoreScript,
|
|
11
10
|
run,
|
|
@@ -23,7 +22,6 @@ import { NO_FINDINGS, type LensReport } from "./lenses.js"
|
|
|
23
22
|
import {
|
|
24
23
|
answerReviewQuestions,
|
|
25
24
|
awaitReview,
|
|
26
|
-
coder,
|
|
27
25
|
collecting,
|
|
28
26
|
fixCheck,
|
|
29
27
|
fixNits,
|
|
@@ -220,7 +218,7 @@ export const humanReview = (
|
|
|
220
218
|
escalations: EscalationCount = { rounds: 0 },
|
|
221
219
|
options: Omit<BuildTailOptions, "fixFirst" | "green"> = {},
|
|
222
220
|
): Promise<kit.ReviewOutcome> => {
|
|
223
|
-
const { guard, guardRests, fixAccess, rules
|
|
221
|
+
const { guard, guardRests, fixAccess, rules } = options
|
|
224
222
|
const feedback: FeedbackTarget = options.feedback ?? {
|
|
225
223
|
file: REQUIREMENTS,
|
|
226
224
|
collecting,
|
|
@@ -243,7 +241,7 @@ export const humanReview = (
|
|
|
243
241
|
},
|
|
244
242
|
fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
|
|
245
243
|
keepGreen: () => healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules }),
|
|
246
|
-
awaitReview
|
|
244
|
+
awaitReview,
|
|
247
245
|
reviewMissing,
|
|
248
246
|
collecting: feedback.collecting,
|
|
249
247
|
triage: triage(severities),
|
|
@@ -305,8 +303,6 @@ export interface BuildTailOptions extends TailOptions {
|
|
|
305
303
|
readonly lap?: t.ReviewRange | undefined
|
|
306
304
|
/** What the triaged quality lap left for the review; absent: nothing. */
|
|
307
305
|
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
|
|
310
306
|
}
|
|
311
307
|
|
|
312
308
|
/** The build tail: fix (when entered red), keep green unless `green`, the triaged quality lap, then human review since `base`. */
|
|
@@ -372,19 +368,6 @@ const callForm = (
|
|
|
372
368
|
return undefined
|
|
373
369
|
}
|
|
374
370
|
|
|
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
|
-
|
|
388
371
|
/**
|
|
389
372
|
* Undo the human's review-round code edits. A path changed since the human
|
|
390
373
|
* left it is not overwritten; requireRevert names it instead.
|
|
@@ -24,9 +24,6 @@ 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"],
|
|
30
27
|
// The refactor's own turns load `ponytail`; `refactor.build.*` carry the `build.*` lists.
|
|
31
28
|
...build,
|
|
32
29
|
...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
|