@pmelab/gtd 31.0.0 → 32.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 +33 -29
- package/claude/hooks/ship.ts +7 -3
- package/dist/chunks/{reserved-CHTJTzpA.mjs → reserved-2fV0mj14.mjs} +7 -2
- package/dist/chunks/{tail-CCsDBbZe.mjs → tail-BynhCfuv.mjs} +6 -3
- package/dist/workflow-kit.mjs +15 -3
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +4 -4
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +27 -7
- package/package.json +1 -1
- package/src/workflow-kit/quality.test.ts +25 -1
- package/src/workflow-kit/quality.ts +22 -4
- package/src/workflows/feature/lens-models.feature +60 -0
- package/src/workflows/feature/refactor-lens-models.feature +62 -0
- package/src/workflows/feature/refactor.ts +2 -2
- package/src/workflows/feature/seam.test.ts +16 -0
- package/src/workflows/feature/steps.test.ts +13 -0
- package/src/workflows/index.ts +1 -1
- package/src/workflows/shared/skills.test.ts +7 -0
- package/src/workflows/shared/steps.test.ts +45 -0
- package/src/workflows/shared/steps.ts +4 -2
- package/src/workflows/shared/tail.test.ts +31 -1
- package/src/workflows/shared/tail.ts +6 -3
package/README.md
CHANGED
|
@@ -339,28 +339,29 @@ asks, and moves the process on only when you say so; a review you leave
|
|
|
339
339
|
untouched signs off. While a step runs you can keep asking; requests for changes
|
|
340
340
|
wait for the next rest. When the process finishes, Claude offers to squash it
|
|
341
341
|
and open its pull request(s) (or run `/gtd ship`; `GTD_SHIP_MODE` picks one
|
|
342
|
-
|
|
343
|
-
[driver doc](docs/driver.md)); a process
|
|
344
|
-
`Closes #N` on its top pull request; a
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
a slice
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
342
|
+
commit per slice under one pull request (the default) or, set to `stack`, one
|
|
343
|
+
stacked pull request per slice, see the [driver doc](docs/driver.md)); a process
|
|
344
|
+
started for an issue ships with `Closes #N` on its top pull request; a closed
|
|
345
|
+
pull request whose branch was deleted from origin is left behind and a new one
|
|
346
|
+
opens; on a branch an earlier ship in this clone stacked (unless
|
|
347
|
+
`GTD_SHIP_MODE=commits`), a later process, such as review fixes, lands one
|
|
348
|
+
commit per slice on the top pull request and keeps the stack, and re-running
|
|
349
|
+
`/gtd ship` after a lower stacked pull request merged restacks the rest (it
|
|
350
|
+
refuses, with nothing changed, when that pull request merged anywhere but the
|
|
351
|
+
default branch (unless into a lower slice branch before that one merged into
|
|
352
|
+
it), a slice branch is checked out in another worktree, an unrelated local
|
|
353
|
+
branch sits inside the rebased commits, or a slice's change is already upstream;
|
|
354
|
+
a failed rebase is rolled back; to tear a stack down from its worktree, delete
|
|
355
|
+
the top branch with `git push origin --delete`, since
|
|
356
|
+
`gh pr close --delete-branch` cannot). A process can change hands at any gate:
|
|
357
|
+
ask Claude to hand it off (or run `/gtd throw @dev`) and it opens a draft pull
|
|
358
|
+
request assigned to them, and `/gtd catch <pr>` picks it up exactly where it
|
|
359
|
+
waits, so whoever wrote the requirements can hand the build to someone else.
|
|
360
|
+
Ship, hand-off and throw answer at once and run in the background, with progress
|
|
361
|
+
on the status line; they report by waking Claude, a toast, a transcript line and
|
|
362
|
+
a push notification, and a failed push or `gh` call reports as a failure. While
|
|
363
|
+
a ship or hand-off runs or is queued, `/gtd catch`, the doors,
|
|
364
|
+
`/gtd <requirements>` and bare `/gtd` wait for its report. See
|
|
364
365
|
[Inside Claude Code](https://github.com/pmelab/gtd/blob/main/docs/driver.md#inside-claude-code-the-gtd-mod).
|
|
365
366
|
|
|
366
367
|
### Then let an agent build your own
|
|
@@ -470,13 +471,16 @@ noted in steps 3 and 4 below.
|
|
|
470
471
|
Once the change is built, the whole change goes through a qualitative review
|
|
471
472
|
lap before you see anything: six lenses, one turn each, in order —
|
|
472
473
|
`correctness`, `owasp-security`, `ponytail-review`, `test-audit`,
|
|
473
|
-
`conventions`, `spec-challenge`.
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
474
|
+
`conventions`, `spec-challenge`. `correctness`, `owasp-security` and
|
|
475
|
+
`spec-challenge` run on the planner model (`smart`), the other three on the
|
|
476
|
+
coder model (`base`); a `qualityReviews` entry `<lens>=<model>` picks a
|
|
477
|
+
lens's own. Each traces the change from its own angle and records every
|
|
478
|
+
finding, blocking or not; one fix turn then fixes ALL of them once, with no
|
|
479
|
+
re-review after the fix. A clean turn means approval only when that lens
|
|
480
|
+
found nothing at all. This lap is where code quality is looked at, and every
|
|
481
|
+
round pays for it. It never replaces step 4 — your review stays the final
|
|
482
|
+
gate, and nothing here skips it. The `fix` door (below) repairs a red
|
|
483
|
+
baseline through this same lap.
|
|
480
484
|
|
|
481
485
|
A red suite that keeps failing past a few fix attempts escalates instead of
|
|
482
486
|
retrying forever: an agent turn reads the failing output and writes
|
package/claude/hooks/ship.ts
CHANGED
|
@@ -49,7 +49,7 @@ export function costTrailers(log: string) {
|
|
|
49
49
|
export type ShipMode = "stack" | "commits"
|
|
50
50
|
|
|
51
51
|
export function shipMode(raw: string | undefined): ShipMode | { refusal: string } {
|
|
52
|
-
if (!raw) return "
|
|
52
|
+
if (!raw) return "commits"
|
|
53
53
|
if (raw === "stack" || raw === "commits") return raw
|
|
54
54
|
return { refusal: `GTD_SHIP_MODE must be 'stack' or 'commits', not '${raw}'.` }
|
|
55
55
|
}
|
|
@@ -209,8 +209,8 @@ async function preflight(git: Git, ok: Ok) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
export async function ship(io: ShipIo, isDry: boolean, rawMode?: string): Promise<Shipped> {
|
|
212
|
-
const
|
|
213
|
-
if (typeof
|
|
212
|
+
const valid = shipMode(rawMode)
|
|
213
|
+
if (typeof valid === "object") return fail(valid.refusal)
|
|
214
214
|
const git = async (...args: string[]) => (await io.run(["git", ...args])).out.trim()
|
|
215
215
|
const ok = async (...args: string[]) => (await io.run(["git", ...args])).code === 0
|
|
216
216
|
|
|
@@ -223,6 +223,10 @@ export async function ship(io: ShipIo, isDry: boolean, rawMode?: string): Promis
|
|
|
223
223
|
const live = names(await git("config", "--local", "--get", STACK_KEY(branch)))
|
|
224
224
|
const retired = names(await git("config", "--local", "--get", RETIRED_KEY(branch)))
|
|
225
225
|
const recorded = [...live, ...retired]
|
|
226
|
+
// Unset keeps a live stack stacking: commits mode would retarget its top PR
|
|
227
|
+
// and strand the lower ones. A dead record ships as commits.
|
|
228
|
+
const stacked = !rawMode && live.length > 0 && (await chainsToHead(git, ok, branch, live))
|
|
229
|
+
const mode: ShipMode = stacked ? "stack" : valid
|
|
226
230
|
const squashed = await squash(io, git, ok, branch, baseBranch, isDry, mode)
|
|
227
231
|
if (!squashed.ok) return squashed
|
|
228
232
|
// Until the PR step succeeds the record only moves aside: a retry must not
|
|
@@ -1322,11 +1322,16 @@ const builtInLenses = {
|
|
|
1322
1322
|
brief: specChallengeBrief
|
|
1323
1323
|
}
|
|
1324
1324
|
};
|
|
1325
|
+
const coderLenses = /* @__PURE__ */ new Set([
|
|
1326
|
+
"conventions",
|
|
1327
|
+
"ponytail-review",
|
|
1328
|
+
"test-audit"
|
|
1329
|
+
]);
|
|
1325
1330
|
/** One quality review through `lens`; each lens is its own scope, so its own conversation and skills. */
|
|
1326
|
-
const reviewQuality = (lens, base) => agentWithSkills(`quality.${lens}.reviewing`, buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief, base), {
|
|
1331
|
+
const reviewQuality = (lens, base, model) => agentWithSkills(`quality.${lens}.reviewing`, buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief, base), {
|
|
1327
1332
|
label: "Reviewing (one quality lens)",
|
|
1328
1333
|
file: QUALITY,
|
|
1329
|
-
model: planner(),
|
|
1334
|
+
model: model || (coderLenses.has(lens) ? coder() : planner()),
|
|
1330
1335
|
system: reviewerSystem(),
|
|
1331
1336
|
allowEmpty: true
|
|
1332
1337
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
|
|
2
|
-
import { fixFindings,
|
|
3
|
-
import { E as reviewing, S as fixRisks, T as reviewQuality, U as handOffAwaitReviewMessage, _ as REVIEW, b as fixCheck, d as guarded, f as healthy, l as escalation, m as QUALITY, v as awaitReview, w as reviewMissing, x as fixQuality } from "./reserved-
|
|
2
|
+
import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
+
import { E as reviewing, S as fixRisks, T as reviewQuality, U as handOffAwaitReviewMessage, _ as REVIEW, b as fixCheck, d as guarded, f as healthy, l as escalation, m as QUALITY, v as awaitReview, w as reviewMissing, x as fixQuality } from "./reserved-2fV0mj14.mjs";
|
|
4
4
|
//#region src/workflows/shared/tail.ts
|
|
5
5
|
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
6
6
|
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
@@ -9,7 +9,10 @@ const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
|
9
9
|
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
10
10
|
* has any.
|
|
11
11
|
*/
|
|
12
|
-
const qualityLap = (guard, lenses, base) => qualityLap$1(lenses ??
|
|
12
|
+
const qualityLap = (guard, lenses, base) => qualityLap$1(lenses ?? lensEntriesOf(vars.qualityReviews ?? ""), guarded((entry) => {
|
|
13
|
+
const { lens, model } = lensOf(entry);
|
|
14
|
+
return reviewQuality(lens, base, model);
|
|
15
|
+
}, guard), QUALITY);
|
|
13
16
|
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
14
17
|
const fixQualityFindings = (escalations, guard, guardRests = true, rules) => fixFindings({
|
|
15
18
|
fix: guarded(() => fixQuality(rules), guard),
|
package/dist/workflow-kit.mjs
CHANGED
|
@@ -47,8 +47,20 @@ const gate = (name, passes, blocked) => scope(name, async () => {
|
|
|
47
47
|
});
|
|
48
48
|
//#endregion
|
|
49
49
|
//#region src/workflow-kit/quality.ts
|
|
50
|
-
|
|
51
|
-
const
|
|
50
|
+
const lensOf = (entry) => {
|
|
51
|
+
const at = entry.indexOf("=");
|
|
52
|
+
return at < 0 ? {
|
|
53
|
+
lens: entry.trim(),
|
|
54
|
+
model: ""
|
|
55
|
+
} : {
|
|
56
|
+
lens: entry.slice(0, at).trim(),
|
|
57
|
+
model: entry.slice(at + 1).trim()
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** Split a comma-separated lens list into whole entries, `<lens>` or `<lens>=<model>`: trimmed, blanks dropped. */
|
|
61
|
+
const lensEntriesOf = (value) => value.split(",").map((entry) => entry.trim()).filter((entry) => lensOf(entry).lens.length > 0);
|
|
62
|
+
/** The same list's lens names, model hints cut off. */
|
|
63
|
+
const lensesOf = (value) => lensEntriesOf(value).map((entry) => lensOf(entry).lens);
|
|
52
64
|
/** One `<scope>.<lens>` entry per lens — the per-lens part of a skills or access export. */
|
|
53
65
|
const lensScopes = (scope, lenses, entry) => Object.fromEntries(lenses.map((lens) => [`${scope}.${lens}`, entry(lens)]));
|
|
54
66
|
/** One `review` turn per lens, each appending its findings to `findings`; resolves `"findings"` when that file has any. */
|
|
@@ -338,4 +350,4 @@ const reviewToHandOff = async (base, p) => {
|
|
|
338
350
|
}
|
|
339
351
|
};
|
|
340
352
|
//#endregion
|
|
341
|
-
export { assertionLines, escalation, fixFindings, gate, healthy, lensScopes, lensesOf, qualityLap, removedAssertions, review, reviewToHandOff, routeNotes, untilSettled };
|
|
353
|
+
export { assertionLines, escalation, fixFindings, gate, healthy, lensEntriesOf, lensOf, lensScopes, lensesOf, qualityLap, removedAssertions, review, reviewToHandOff, routeNotes, untilSettled };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import { gt as requireFlag, ht as registryEntry, i as skills, lt as defaults, n as summary, o as flagShipped, pt as flagReferences, r as access, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { gt as requireFlag, ht as registryEntry, i as skills, lt as defaults, n as summary, o as flagShipped, pt as flagReferences, r as access, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-2fV0mj14.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-BynhCfuv.mjs";
|
|
4
4
|
//#region src/workflows/cleanup/cleanup.ts
|
|
5
5
|
const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
|
|
6
6
|
const cleanupFlow = async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { answered, changes, changesSince, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { $ as reviewEditNotesCapture, A as buildReviewCollectingPrompt, B as designTriagePrompt, C as planner, D as agentWithSkills, E as reviewing, F as clearCriteriaQuestion, G as implementBuildingPrompt, I as criterionLine, J as normaliseCriteriaScript, K as implementFixPrompt, L as deferNotesScript, M as buildReviewTriageMessage, N as builderSystem, O as buildReviewAnswerQuestionsPrompt, P as clearCriteriaMessage, Q as refactorRules, R as designGateAnswerMessage, S as fixRisks, W as idleMessage, X as refactorDroppedRefusal, Y as refactorBuildingPrompt, Z as refactorFixPrompt, _ as REVIEW, a as flagModeOn, at as seamSteerMessage, b as fixCheck, ct as unwindFailure, d as guarded, dt as testFileGlobs, et as reviewEditsCapture, f as healthy, g as REQUIREMENTS, h as REFACTOR, i as skills, it as seamByHandMessage, j as buildReviewFixNitsPrompt, k as buildReviewAwaitAfterDeferMessage, lt as defaults, n as summary, nt as reviewNotesCapture, ot as startGateBlockedMessage, p as FEEDBACK, q as naivePassPrompt, r as access, rt as reviewerSystem, s as shipped, st as unwindFailedMessage, t as steering, u as gate, ut as envDefaults, v as awaitReview, w as reviewMissing, y as coder, z as designSystem } from "../chunks/reserved-
|
|
4
|
-
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-
|
|
2
|
+
import { lensEntriesOf, review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
+
import { $ as reviewEditNotesCapture, A as buildReviewCollectingPrompt, B as designTriagePrompt, C as planner, D as agentWithSkills, E as reviewing, F as clearCriteriaQuestion, G as implementBuildingPrompt, I as criterionLine, J as normaliseCriteriaScript, K as implementFixPrompt, L as deferNotesScript, M as buildReviewTriageMessage, N as builderSystem, O as buildReviewAnswerQuestionsPrompt, P as clearCriteriaMessage, Q as refactorRules, R as designGateAnswerMessage, S as fixRisks, W as idleMessage, X as refactorDroppedRefusal, Y as refactorBuildingPrompt, Z as refactorFixPrompt, _ as REVIEW, a as flagModeOn, at as seamSteerMessage, b as fixCheck, ct as unwindFailure, d as guarded, dt as testFileGlobs, et as reviewEditsCapture, f as healthy, g as REQUIREMENTS, h as REFACTOR, i as skills, it as seamByHandMessage, j as buildReviewFixNitsPrompt, k as buildReviewAwaitAfterDeferMessage, lt as defaults, n as summary, nt as reviewNotesCapture, ot as startGateBlockedMessage, p as FEEDBACK, q as naivePassPrompt, r as access, rt as reviewerSystem, s as shipped, st as unwindFailedMessage, t as steering, u as gate, ut as envDefaults, v as awaitReview, w as reviewMissing, y as coder, z as designSystem } from "../chunks/reserved-2fV0mj14.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-BynhCfuv.mjs";
|
|
5
5
|
//#region src/workflows/shared/fences.ts
|
|
6
6
|
const markFenced = (text) => {
|
|
7
7
|
let fence;
|
|
@@ -711,7 +711,7 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
711
711
|
green: true,
|
|
712
712
|
guard,
|
|
713
713
|
guardRests: false,
|
|
714
|
-
lenses:
|
|
714
|
+
lenses: lensEntriesOf(vars.refactorQualityReviews ?? ""),
|
|
715
715
|
feedback,
|
|
716
716
|
fixAccess: strayAccess(p.strays),
|
|
717
717
|
lensBase: entry,
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
|
|
3
|
-
import { D as agentWithSkills, H as fixReviewNotesPrompt, V as finisherSystem, _ as REVIEW, c as baseline, i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { D as agentWithSkills, H as fixReviewNotesPrompt, V as finisherSystem, _ as REVIEW, c as baseline, i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-2fV0mj14.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-BynhCfuv.mjs";
|
|
5
5
|
//#region src/workflows/fix/fix.ts
|
|
6
6
|
/**
|
|
7
7
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import { i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, tt as reviewGateBlockedMessage, u as gate, ut as envDefaults } from "../chunks/reserved-
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, tt as reviewGateBlockedMessage, u as gate, ut as envDefaults } from "../chunks/reserved-2fV0mj14.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-BynhCfuv.mjs";
|
|
4
4
|
//#region src/workflows/review/review.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import { ft as flagMentions, gt as requireFlag, i as skills, lt as defaults, mt as flipProblems, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, ut as envDefaults, y as coder } from "../chunks/reserved-
|
|
2
|
+
import { ft as flagMentions, gt as requireFlag, i as skills, lt as defaults, mt as flipProblems, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, ut as envDefaults, y as coder } from "../chunks/reserved-2fV0mj14.mjs";
|
|
3
3
|
//#region src/workflows/rollout/rollout.ts
|
|
4
4
|
const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
|
|
5
5
|
const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
|
package/dist/workflows.mjs
CHANGED
|
@@ -476,8 +476,20 @@ const gate$1 = (name, passes, blocked) => scope(name, async () => {
|
|
|
476
476
|
});
|
|
477
477
|
//#endregion
|
|
478
478
|
//#region src/workflow-kit/quality.ts
|
|
479
|
-
|
|
480
|
-
const
|
|
479
|
+
const lensOf = (entry) => {
|
|
480
|
+
const at = entry.indexOf("=");
|
|
481
|
+
return at < 0 ? {
|
|
482
|
+
lens: entry.trim(),
|
|
483
|
+
model: ""
|
|
484
|
+
} : {
|
|
485
|
+
lens: entry.slice(0, at).trim(),
|
|
486
|
+
model: entry.slice(at + 1).trim()
|
|
487
|
+
};
|
|
488
|
+
};
|
|
489
|
+
/** Split a comma-separated lens list into whole entries, `<lens>` or `<lens>=<model>`: trimmed, blanks dropped. */
|
|
490
|
+
const lensEntriesOf = (value) => value.split(",").map((entry) => entry.trim()).filter((entry) => lensOf(entry).lens.length > 0);
|
|
491
|
+
/** The same list's lens names, model hints cut off. */
|
|
492
|
+
const lensesOf = (value) => lensEntriesOf(value).map((entry) => lensOf(entry).lens);
|
|
481
493
|
/** One `<scope>.<lens>` entry per lens — the per-lens part of a skills or access export. */
|
|
482
494
|
const lensScopes = (scope, lenses, entry) => Object.fromEntries(lenses.map((lens) => [`${scope}.${lens}`, entry(lens)]));
|
|
483
495
|
/** One `review` turn per lens, each appending its findings to `findings`; resolves `"findings"` when that file has any. */
|
|
@@ -18106,11 +18118,16 @@ const builtInLenses = {
|
|
|
18106
18118
|
brief: specChallengeBrief
|
|
18107
18119
|
}
|
|
18108
18120
|
};
|
|
18121
|
+
const coderLenses = /* @__PURE__ */ new Set([
|
|
18122
|
+
"conventions",
|
|
18123
|
+
"ponytail-review",
|
|
18124
|
+
"test-audit"
|
|
18125
|
+
]);
|
|
18109
18126
|
/** One quality review through `lens`; each lens is its own scope, so its own conversation and skills. */
|
|
18110
|
-
const reviewQuality = (lens, base) => agentWithSkills(`quality.${lens}.reviewing`, buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief, base), {
|
|
18127
|
+
const reviewQuality = (lens, base, model) => agentWithSkills(`quality.${lens}.reviewing`, buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief, base), {
|
|
18111
18128
|
label: "Reviewing (one quality lens)",
|
|
18112
18129
|
file: QUALITY,
|
|
18113
|
-
model: planner(),
|
|
18130
|
+
model: model || (coderLenses.has(lens) ? coder() : planner()),
|
|
18114
18131
|
system: reviewerSystem(),
|
|
18115
18132
|
allowEmpty: true
|
|
18116
18133
|
});
|
|
@@ -37631,7 +37648,10 @@ const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
|
37631
37648
|
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
37632
37649
|
* has any.
|
|
37633
37650
|
*/
|
|
37634
|
-
const qualityLap = (guard, lenses, base) => qualityLap$1(lenses ??
|
|
37651
|
+
const qualityLap = (guard, lenses, base) => qualityLap$1(lenses ?? lensEntriesOf(vars.qualityReviews ?? ""), guarded((entry) => {
|
|
37652
|
+
const { lens, model } = lensOf(entry);
|
|
37653
|
+
return reviewQuality(lens, base, model);
|
|
37654
|
+
}, guard), QUALITY);
|
|
37635
37655
|
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
37636
37656
|
const fixQualityFindings = (escalations, guard, guardRests = true, rules) => fixFindings({
|
|
37637
37657
|
fix: guarded(() => fixQuality(rules), guard),
|
|
@@ -38437,7 +38457,7 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
38437
38457
|
green: true,
|
|
38438
38458
|
guard,
|
|
38439
38459
|
guardRests: false,
|
|
38440
|
-
lenses:
|
|
38460
|
+
lenses: lensEntriesOf(vars.refactorQualityReviews ?? ""),
|
|
38441
38461
|
feedback,
|
|
38442
38462
|
fixAccess: strayAccess(p.strays),
|
|
38443
38463
|
lensBase: entry,
|
|
@@ -38803,4 +38823,4 @@ var workflows_default = feature;
|
|
|
38803
38823
|
*/
|
|
38804
38824
|
const doors = {};
|
|
38805
38825
|
//#endregion
|
|
38806
|
-
export { ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, implement, judgeSeam, lensesOf, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unwind };
|
|
38826
|
+
export { ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, implement, judgeSeam, lensEntriesOf, lensOf, lensesOf, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unwind };
|
package/package.json
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { lensesOf, lensScopes } from "./quality.js"
|
|
2
|
+
import { lensEntriesOf, lensesOf, lensOf, lensScopes } from "./quality.js"
|
|
3
3
|
|
|
4
4
|
describe("lensesOf", () => {
|
|
5
5
|
it("splits on commas, trims, drops blanks", () => {
|
|
6
6
|
expect(lensesOf(" a ,, b,")).toEqual(["a", "b"])
|
|
7
7
|
})
|
|
8
|
+
|
|
9
|
+
it("cuts each entry's model hint off at its first =", () => {
|
|
10
|
+
expect(lensesOf("a = smart, plugin:b=vendor=x, c=")).toEqual(["a", "plugin:b", "c"])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("drops an entry with a hint but no lens", () => {
|
|
14
|
+
expect(lensesOf("=smart, a")).toEqual(["a"])
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe("lensEntriesOf", () => {
|
|
19
|
+
it("keeps each entry whole, hint included", () => {
|
|
20
|
+
expect(lensEntriesOf(" a = smart ,, b, =x")).toEqual(["a = smart", "b"])
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe("lensOf", () => {
|
|
25
|
+
it("splits an entry at its first =, both sides trimmed", () => {
|
|
26
|
+
expect(lensOf(" plugin:b = vendor=x ")).toEqual({ lens: "plugin:b", model: "vendor=x" })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("an entry without = has a blank hint", () => {
|
|
30
|
+
expect(lensOf("a")).toEqual({ lens: "a", model: "" })
|
|
31
|
+
})
|
|
8
32
|
})
|
|
9
33
|
|
|
10
34
|
describe("lensScopes", () => {
|
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { read } from "../flows/index.js"
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
export interface LensEntry {
|
|
4
|
+
readonly lens: string
|
|
5
|
+
/** Blank when the entry carries none. */
|
|
6
|
+
readonly model: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Split at the first `=`: skill names may hold `:` (`plugin:skill`), never `=`.
|
|
10
|
+
export const lensOf = (entry: string): LensEntry => {
|
|
11
|
+
const at = entry.indexOf("=")
|
|
12
|
+
return at < 0
|
|
13
|
+
? { lens: entry.trim(), model: "" }
|
|
14
|
+
: { lens: entry.slice(0, at).trim(), model: entry.slice(at + 1).trim() }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Split a comma-separated lens list into whole entries, `<lens>` or `<lens>=<model>`: trimmed, blanks dropped. */
|
|
18
|
+
export const lensEntriesOf = (value: string): readonly string[] =>
|
|
5
19
|
value
|
|
6
20
|
.split(",")
|
|
7
|
-
.map((
|
|
8
|
-
.filter((
|
|
21
|
+
.map((entry) => entry.trim())
|
|
22
|
+
.filter((entry) => lensOf(entry).lens.length > 0)
|
|
23
|
+
|
|
24
|
+
/** The same list's lens names, model hints cut off. */
|
|
25
|
+
export const lensesOf = (value: string): readonly string[] =>
|
|
26
|
+
lensEntriesOf(value).map((entry) => lensOf(entry).lens)
|
|
9
27
|
|
|
10
28
|
/** One `<scope>.<lens>` entry per lens — the per-lens part of a skills or access export. */
|
|
11
29
|
export const lensScopes = <T>(
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: each quality lens runs on its own model hint
|
|
3
|
+
|
|
4
|
+
A `qualityReviews` entry may carry a model hint, `<lens>=<model>`. A lens
|
|
5
|
+
without one runs on the planner model (`smart`), except `conventions`,
|
|
6
|
+
`ponytail-review` and `test-audit`, which run on the coder model (`base`).
|
|
7
|
+
|
|
8
|
+
Background:
|
|
9
|
+
Given a gtd config with:
|
|
10
|
+
"""
|
|
11
|
+
import { feature } from "@pmelab/gtd/workflows/feature"
|
|
12
|
+
export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
|
|
13
|
+
export default feature({
|
|
14
|
+
flagMode: false,
|
|
15
|
+
qualityReviews: "correctness, conventions, test-audit=opus",
|
|
16
|
+
})
|
|
17
|
+
"""
|
|
18
|
+
And a file "src/calc.ts" with:
|
|
19
|
+
"""
|
|
20
|
+
export const add = (a: number, b: number) => a + b
|
|
21
|
+
"""
|
|
22
|
+
And the working tree is committed as "feat: add calculator"
|
|
23
|
+
And a file "TODO.md" with:
|
|
24
|
+
"""
|
|
25
|
+
# Add subtract
|
|
26
|
+
|
|
27
|
+
subtract(a, b) returns the difference.
|
|
28
|
+
"""
|
|
29
|
+
And step "unwind" changes nothing
|
|
30
|
+
And step "start-gate.check" changes nothing
|
|
31
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
32
|
+
"""
|
|
33
|
+
# Requirements
|
|
34
|
+
|
|
35
|
+
## Acceptance Criteria
|
|
36
|
+
|
|
37
|
+
- AC-1: `subtract(a, b)` returns `a - b`.
|
|
38
|
+
"""
|
|
39
|
+
And step "design.gate.answer" changes nothing
|
|
40
|
+
And step "implement.building" writes "src/calc.ts" with:
|
|
41
|
+
"""
|
|
42
|
+
export const add = (a: number, b: number) => a + b
|
|
43
|
+
export const subtract = (a: number, b: number) => a - b
|
|
44
|
+
"""
|
|
45
|
+
And step "implement.health.check" changes nothing
|
|
46
|
+
And step "implement.closing" runs its script
|
|
47
|
+
|
|
48
|
+
Scenario: a hinted lens runs on its hint, every other lens on its default
|
|
49
|
+
When the process is driven
|
|
50
|
+
Then the process rests at "build.quality.correctness.reviewing"
|
|
51
|
+
And the output of gtd "next --json" contains "\"model\":\"smart\""
|
|
52
|
+
Given step "build.quality.correctness.reviewing" changes nothing
|
|
53
|
+
When the process is driven
|
|
54
|
+
Then the process rests at "build.quality.conventions.reviewing"
|
|
55
|
+
And the output of gtd "next --json" contains "\"model\":\"base\""
|
|
56
|
+
Given step "build.quality.conventions.reviewing" changes nothing
|
|
57
|
+
When the process is driven
|
|
58
|
+
Then the process rests at "build.quality.test-audit.reviewing"
|
|
59
|
+
And the output of gtd "next --json" contains "\"model\":\"opus\""
|
|
60
|
+
And the resting step's content contains "test-audit"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: the refactor review's lenses take their model hints from refactorQualityReviews alone
|
|
3
|
+
|
|
4
|
+
Background:
|
|
5
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
6
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness=base"
|
|
7
|
+
And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to "correctness, conventions=opus"
|
|
8
|
+
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
9
|
+
And a file "src/calc.ts" with:
|
|
10
|
+
"""
|
|
11
|
+
export const add = (a: number, b: number) => a + b
|
|
12
|
+
"""
|
|
13
|
+
And the working tree is committed as "feat: add calculator"
|
|
14
|
+
And a file "TODO.md" with:
|
|
15
|
+
"""
|
|
16
|
+
# Add subtract
|
|
17
|
+
|
|
18
|
+
subtract(a, b) returns the difference.
|
|
19
|
+
"""
|
|
20
|
+
And step "unwind" changes nothing
|
|
21
|
+
And step "start-gate.check" changes nothing
|
|
22
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
23
|
+
"""
|
|
24
|
+
# Requirements
|
|
25
|
+
|
|
26
|
+
## Concerns
|
|
27
|
+
|
|
28
|
+
### Subtract
|
|
29
|
+
|
|
30
|
+
- `subtract(a, b)` returns `a - b`.
|
|
31
|
+
|
|
32
|
+
## Acceptance Criteria
|
|
33
|
+
|
|
34
|
+
- AC-1: the change behaves as described.
|
|
35
|
+
"""
|
|
36
|
+
And step "design.gate.answer" changes nothing
|
|
37
|
+
|
|
38
|
+
Scenario: a refactor lens runs on its refactorQualityReviews hint, else its default
|
|
39
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
40
|
+
"""
|
|
41
|
+
export const add = (a: number, b: number) => a + b
|
|
42
|
+
export const subtract = (a: number, b: number) => a - b
|
|
43
|
+
"""
|
|
44
|
+
And judge step "seam.judge" answers:
|
|
45
|
+
"""
|
|
46
|
+
[{"id":"shape","answer":"refactor-first","p":0.9}]
|
|
47
|
+
"""
|
|
48
|
+
And step "refactor.deriving" runs its script
|
|
49
|
+
And step "refactor.building" writes "src/calc.ts" with:
|
|
50
|
+
"""
|
|
51
|
+
export const op = (a: number, b: number, sign: number) => a + sign * b
|
|
52
|
+
export const add = (a: number, b: number) => op(a, b, 1)
|
|
53
|
+
"""
|
|
54
|
+
And step "refactor.health.check" changes nothing
|
|
55
|
+
And step "refactor.closing" runs its script
|
|
56
|
+
When the process is driven
|
|
57
|
+
Then the process rests at "refactor.build.quality.correctness.reviewing"
|
|
58
|
+
And the output of gtd "next --json" contains "\"model\":\"smart\""
|
|
59
|
+
Given step "refactor.build.quality.correctness.reviewing" changes nothing
|
|
60
|
+
When the process is driven
|
|
61
|
+
Then the process rests at "refactor.build.quality.conventions.reviewing"
|
|
62
|
+
And the output of gtd "next --json" contains "\"model\":\"opus\""
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type Change,
|
|
14
14
|
type DiffShapeResult,
|
|
15
15
|
} from "../../flows/index.js"
|
|
16
|
-
import {
|
|
16
|
+
import { lensEntriesOf } from "../../workflow-kit/index.js"
|
|
17
17
|
import { markFenced } from "../shared/fences.js"
|
|
18
18
|
import { healthy, type Guard } from "../shared/health.js"
|
|
19
19
|
import { coder, FEEDBACK, REFACTOR } from "../shared/steps.js"
|
|
@@ -343,7 +343,7 @@ export const reviewedRefactor = async (
|
|
|
343
343
|
green: true,
|
|
344
344
|
guard,
|
|
345
345
|
guardRests: false,
|
|
346
|
-
lenses:
|
|
346
|
+
lenses: lensEntriesOf(vars.refactorQualityReviews ?? ""),
|
|
347
347
|
feedback,
|
|
348
348
|
fixAccess: strayAccess(p.strays),
|
|
349
349
|
lensBase: entry,
|
|
@@ -79,6 +79,17 @@ describe("naivePass", () => {
|
|
|
79
79
|
expect(request.options).toMatchObject({ keep: true })
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
+
it("runs on the coder model, base by default, the coderModel setting otherwise", async () => {
|
|
83
|
+
const plain = await capture()
|
|
84
|
+
const set = await captureStep(() => naivePass(), {
|
|
85
|
+
scope: "seam",
|
|
86
|
+
env: { coderModel: "sonnet" },
|
|
87
|
+
})
|
|
88
|
+
if (plain.kind !== "agent" || set.kind !== "agent") throw new Error("unreachable")
|
|
89
|
+
expect(plain.options.model).toBe("base")
|
|
90
|
+
expect(set.options.model).toBe("sonnet")
|
|
91
|
+
})
|
|
92
|
+
|
|
82
93
|
it("carries the rejection into the retry prompt", async () => {
|
|
83
94
|
const request = await capture({ strays: ["src/core.ts"], p: 0.8 })
|
|
84
95
|
if (request.kind !== "agent") throw new Error("unreachable")
|
|
@@ -251,6 +262,11 @@ describe("implement", () => {
|
|
|
251
262
|
return request.prompt
|
|
252
263
|
}
|
|
253
264
|
|
|
265
|
+
it("builds on the coder model", async () => {
|
|
266
|
+
await building("abc")
|
|
267
|
+
expect(lastOptions?.model).toBe("base")
|
|
268
|
+
})
|
|
269
|
+
|
|
254
270
|
it("after a refactor, lets an empty turn complete the step and says so", async () => {
|
|
255
271
|
const prompt = await building("abc", undefined, undefined, () =>
|
|
256
272
|
implement({ reference: "abc", afterRefactor: true }),
|
|
@@ -124,3 +124,16 @@ describe("the feature workflow's own steps declare skills — a step's rendered
|
|
|
124
124
|
expect(request.options).toMatchObject({ file: ".gtd/TODO.md" })
|
|
125
125
|
})
|
|
126
126
|
})
|
|
127
|
+
|
|
128
|
+
describe("the feature workflow's planning steps run on the planner model", () => {
|
|
129
|
+
it("design triage defaults to smart and follows plannerModel", async () => {
|
|
130
|
+
const plain = await captureStep(() => steps.triage("base"), { scope: "design" })
|
|
131
|
+
const set = await captureStep(() => steps.triage("base"), {
|
|
132
|
+
scope: "design",
|
|
133
|
+
env: { plannerModel: "opus" },
|
|
134
|
+
})
|
|
135
|
+
if (plain.kind !== "agent" || set.kind !== "agent") throw new Error("unreachable")
|
|
136
|
+
expect(plain.options.model).toBe("smart")
|
|
137
|
+
expect(set.options.model).toBe("opus")
|
|
138
|
+
})
|
|
139
|
+
})
|
package/src/workflows/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ export {
|
|
|
19
19
|
tail,
|
|
20
20
|
type TailOptions,
|
|
21
21
|
} from "./shared/tail.js"
|
|
22
|
-
export { lensesOf } from "../workflow-kit/index.js"
|
|
22
|
+
export { lensEntriesOf, lensesOf, lensOf } from "../workflow-kit/index.js"
|
|
23
23
|
export * from "./feature/steps.js"
|
|
24
24
|
export * from "./feature/criteria.js"
|
|
25
25
|
export * from "./feature/planning.js"
|
|
@@ -107,4 +107,11 @@ describe("the bundled workflow's skills export", () => {
|
|
|
107
107
|
.sort(),
|
|
108
108
|
).toEqual(["build.quality.correctness", "build.quality.my-lens"])
|
|
109
109
|
})
|
|
110
|
+
|
|
111
|
+
it("keys a hinted entry by its lens alone, with the lens's own skills", () => {
|
|
112
|
+
const custom = skills({ ...defaults, qualityReviews: "my-lens=smart, correctness = base" })
|
|
113
|
+
expect(custom["build.quality.my-lens"]).toEqual(["my-lens"])
|
|
114
|
+
expect(custom["build.quality.correctness"]).toEqual(["code-review-and-quality"])
|
|
115
|
+
expect(Object.keys(custom).some((k) => k.includes("="))).toBe(false)
|
|
116
|
+
})
|
|
110
117
|
})
|
|
@@ -107,3 +107,48 @@ describe("the bundled workflow's steps declare skills — a bundled step's rende
|
|
|
107
107
|
expect(carried.prompt).toContain("Carry-over: commit `abc1234`")
|
|
108
108
|
})
|
|
109
109
|
})
|
|
110
|
+
|
|
111
|
+
describe("the bundled workflow's model hints", () => {
|
|
112
|
+
const modelOf = async (
|
|
113
|
+
fn: () => Promise<void>,
|
|
114
|
+
context: Parameters<typeof captureStep>[1] = {},
|
|
115
|
+
): Promise<string | undefined> => {
|
|
116
|
+
const request = await captureStep(fn, { scope: "build", ...context })
|
|
117
|
+
if (request.kind !== "agent") throw new Error(`expected an agent step, got ${request.kind}`)
|
|
118
|
+
return request.options.model
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
it.each([
|
|
122
|
+
["correctness", "smart"],
|
|
123
|
+
["owasp-security", "smart"],
|
|
124
|
+
["spec-challenge", "smart"],
|
|
125
|
+
["conventions", "base"],
|
|
126
|
+
["ponytail-review", "base"],
|
|
127
|
+
["test-audit", "base"],
|
|
128
|
+
["my-lens", "smart"],
|
|
129
|
+
])("lens %s defaults to %s", async (lens, model) => {
|
|
130
|
+
expect(await modelOf(() => steps.reviewQuality(lens))).toBe(model)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it("a lens's default follows the planner or coder setting it stands for", async () => {
|
|
134
|
+
const env = { plannerModel: "opus", coderModel: "sonnet" }
|
|
135
|
+
expect(await modelOf(() => steps.reviewQuality("correctness"), { env })).toBe("opus")
|
|
136
|
+
expect(await modelOf(() => steps.reviewQuality("conventions"), { env })).toBe("sonnet")
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it("a model passed in wins over the lens's default; a blank one is none", async () => {
|
|
140
|
+
expect(await modelOf(() => steps.reviewQuality("correctness", undefined, "base"))).toBe("base")
|
|
141
|
+
expect(await modelOf(() => steps.reviewQuality("conventions", undefined, "opus"))).toBe("opus")
|
|
142
|
+
expect(await modelOf(() => steps.reviewQuality("conventions", undefined, ""))).toBe("base")
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("a lens named like an Object member still gets its default", async () => {
|
|
146
|
+
expect(await modelOf(() => steps.reviewQuality("constructor"))).toBe("smart")
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it("the fix loop runs on the coder model, base by default", async () => {
|
|
150
|
+
expect(await modelOf(() => steps.fixCheck())).toBe("base")
|
|
151
|
+
expect(await modelOf(() => steps.fixQuality())).toBe("base")
|
|
152
|
+
expect(await modelOf(() => steps.fixCheck(), { env: { coderModel: "sonnet" } })).toBe("sonnet")
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -66,15 +66,17 @@ export const builtInLenses: Readonly<Record<string, BuiltInLens>> = {
|
|
|
66
66
|
"spec-challenge": { skills: [], brief: t.specChallengeBrief },
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
const coderLenses: ReadonlySet<string> = new Set(["conventions", "ponytail-review", "test-audit"])
|
|
70
|
+
|
|
69
71
|
/** One quality review through `lens`; each lens is its own scope, so its own conversation and skills. */
|
|
70
|
-
export const reviewQuality = (lens: string, base?: string): Promise<void> =>
|
|
72
|
+
export const reviewQuality = (lens: string, base?: string, model?: string): Promise<void> =>
|
|
71
73
|
t.agentWithSkills(
|
|
72
74
|
`quality.${lens}.reviewing`,
|
|
73
75
|
t.buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief, base),
|
|
74
76
|
{
|
|
75
77
|
label: "Reviewing (one quality lens)",
|
|
76
78
|
file: QUALITY,
|
|
77
|
-
model: planner(),
|
|
79
|
+
model: model || (coderLenses.has(lens) ? coder() : planner()),
|
|
78
80
|
system: t.reviewerSystem(),
|
|
79
81
|
allowEmpty: true,
|
|
80
82
|
},
|
|
@@ -208,18 +208,23 @@ describe("the hand-off review", () => {
|
|
|
208
208
|
})
|
|
209
209
|
|
|
210
210
|
const prompts: string[] = []
|
|
211
|
+
const models: (string | undefined)[] = []
|
|
211
212
|
|
|
212
213
|
describe("the quality lap's lenses", () => {
|
|
213
214
|
const lap = async (run: () => Promise<unknown>, vars: Record<string, string> = {}) => {
|
|
214
215
|
const log: string[] = []
|
|
215
216
|
prompts.length = 0
|
|
217
|
+
models.length = 0
|
|
216
218
|
installContext(
|
|
217
219
|
fixtureContext(
|
|
218
220
|
{ vars },
|
|
219
221
|
{
|
|
220
222
|
step: (request: StepRequest) => {
|
|
221
223
|
if (request.kind !== "restart") log.push(request.name)
|
|
222
|
-
if (request.kind === "agent")
|
|
224
|
+
if (request.kind === "agent") {
|
|
225
|
+
prompts.push(request.prompt)
|
|
226
|
+
models.push(request.options.model)
|
|
227
|
+
}
|
|
223
228
|
return Promise.resolve()
|
|
224
229
|
},
|
|
225
230
|
pushScope: () => undefined,
|
|
@@ -279,6 +284,31 @@ describe("the quality lap's lenses", () => {
|
|
|
279
284
|
expect(prompts[0]).not.toContain("from `R`")
|
|
280
285
|
})
|
|
281
286
|
|
|
287
|
+
it("each lens runs on its qualityReviews hint, else its default, under its plain name", async () => {
|
|
288
|
+
const log = await lap(() => qualityLap(), {
|
|
289
|
+
qualityReviews: "correctness=base, conventions, my-lens",
|
|
290
|
+
})
|
|
291
|
+
expect(log).toEqual([
|
|
292
|
+
"quality.correctness.reviewing",
|
|
293
|
+
"quality.conventions.reviewing",
|
|
294
|
+
"quality.my-lens.reviewing",
|
|
295
|
+
])
|
|
296
|
+
expect(models).toEqual(["base", "base", "smart"])
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it("lenses passed in carry their own hints, never qualityReviews'", async () => {
|
|
300
|
+
const log = await lap(
|
|
301
|
+
() =>
|
|
302
|
+
tail(() => Promise.resolve(), {
|
|
303
|
+
green: true,
|
|
304
|
+
lenses: ["correctness", "conventions=opus"],
|
|
305
|
+
}),
|
|
306
|
+
{ qualityReviews: "correctness=base" },
|
|
307
|
+
)
|
|
308
|
+
expect(log).toEqual(["quality.correctness.reviewing", "quality.conventions.reviewing"])
|
|
309
|
+
expect(models).toEqual(["smart", "opus"])
|
|
310
|
+
})
|
|
311
|
+
|
|
282
312
|
it("tail passes lenses to its lap and a guard after each lens turn", async () => {
|
|
283
313
|
let guards = 0
|
|
284
314
|
const log = await lap(() =>
|
|
@@ -35,8 +35,11 @@ export const qualityLap = (
|
|
|
35
35
|
base?: string,
|
|
36
36
|
): Promise<"clean" | "findings"> =>
|
|
37
37
|
kit.qualityLap(
|
|
38
|
-
lenses ?? kit.
|
|
39
|
-
guarded((
|
|
38
|
+
lenses ?? kit.lensEntriesOf(vars.qualityReviews ?? ""),
|
|
39
|
+
guarded((entry: string) => {
|
|
40
|
+
const { lens, model } = kit.lensOf(entry)
|
|
41
|
+
return reviewQuality(lens, base, model)
|
|
42
|
+
}, guard),
|
|
40
43
|
QUALITY,
|
|
41
44
|
)
|
|
42
45
|
|
|
@@ -77,7 +80,7 @@ export interface TailOptions {
|
|
|
77
80
|
readonly green?: boolean | undefined
|
|
78
81
|
/** Runs after every code-changing turn: fix-check, quality fix, risk fix, nit fix, escalation describe. */
|
|
79
82
|
readonly guard?: Guard | undefined
|
|
80
|
-
/** Quality-lap
|
|
83
|
+
/** Quality-lap lens entries, `<lens>` or `<lens>=<model>`; default `lensEntriesOf(vars.qualityReviews)`. */
|
|
81
84
|
readonly lenses?: readonly string[] | undefined
|
|
82
85
|
/** `false` leaves the escalation's human rests unguarded; default `true`. */
|
|
83
86
|
readonly guardRests?: boolean | undefined
|