@pmelab/gtd 32.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 +10 -7
- 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
|
@@ -471,13 +471,16 @@ noted in steps 3 and 4 below.
|
|
|
471
471
|
Once the change is built, the whole change goes through a qualitative review
|
|
472
472
|
lap before you see anything: six lenses, one turn each, in order —
|
|
473
473
|
`correctness`, `owasp-security`, `ponytail-review`, `test-audit`,
|
|
474
|
-
`conventions`, `spec-challenge`.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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.
|
|
481
484
|
|
|
482
485
|
A red suite that keeps failing past a few fix attempts escalates instead of
|
|
483
486
|
retrying forever: an agent turn reads the failing output and writes
|
|
@@ -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
|