@pmelab/gtd 29.2.6 → 29.3.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 +8 -5
- package/claude/hooks/assist.ts +3 -2
- package/dist/chunks/{tail-BOJYJctg.mjs → tail-CLyE5VFR.mjs} +32 -11
- package/dist/workflows/cleanup.mjs +1 -1
- package/dist/workflows/feature.mjs +4 -6
- package/dist/workflows/fix.mjs +1 -1
- package/dist/workflows/review.mjs +1 -1
- package/dist/workflows.mjs +34 -15
- package/package.json +1 -1
- package/src/workflows/feature/feature.ts +2 -6
- package/src/workflows/feature/index.ts +7 -1
- package/src/workflows/feature/review.test.ts +26 -5
- package/src/workflows/feature/review.ts +8 -39
- package/src/workflows/index.ts +8 -2
- package/src/workflows/shared/tail.test.ts +131 -16
- package/src/workflows/shared/tail.ts +76 -10
package/README.md
CHANGED
|
@@ -348,9 +348,11 @@ nothing changed, when that pull request merged anywhere but the default branch
|
|
|
348
348
|
(unless into a lower slice branch before that one merged into it), a slice
|
|
349
349
|
branch is checked out in another worktree, an unrelated local branch sits inside
|
|
350
350
|
the rebased commits, or a slice's change is already upstream; a failed rebase is
|
|
351
|
-
rolled back
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
rolled back; to tear a stack down from its worktree, delete the top branch with
|
|
352
|
+
`git push origin --delete`, since `gh pr close --delete-branch` cannot). A
|
|
353
|
+
process can change hands at any gate: ask Claude to hand it off (or run
|
|
354
|
+
`/gtd throw @dev`) and it opens a draft pull request assigned to them, and
|
|
355
|
+
`/gtd catch <pr>` picks it up exactly where it waits, so whoever wrote the
|
|
354
356
|
requirements can hand the architecture to someone else. Ship, hand-off and throw
|
|
355
357
|
answer at once and run in the background, with progress on the status line; they
|
|
356
358
|
report by waking Claude, a toast, a transcript line and a push notification, and
|
|
@@ -607,8 +609,9 @@ means `high`. Each lap lands a `Gtd-Mark: risk-tier=<low|high>` trailer and a
|
|
|
607
609
|
lap's only. Its "needs a human reviewer" line for `high` is advisory, not a
|
|
608
610
|
gate.
|
|
609
611
|
|
|
610
|
-
A `gtd.config.ts` call to `buildTail` that fits neither call form is
|
|
611
|
-
|
|
612
|
+
A `gtd.config.ts` call to `buildTail` or `tail` that fits neither call form is
|
|
613
|
+
rejected; `tail`'s positional form is deprecated for `tail(finish, options?)`.
|
|
614
|
+
See [docs/configuration.md](docs/configuration.md).
|
|
612
615
|
|
|
613
616
|
### Rolling out and cleaning up release flags
|
|
614
617
|
|
package/claude/hooks/assist.ts
CHANGED
|
@@ -142,14 +142,15 @@ export function wakePrompt(w: Wake): string {
|
|
|
142
142
|
return [...body, ...notes, RULES, extras].join("\n\n")
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// The wake after a background ship or hand-off ended
|
|
145
|
+
// The wake after a background ship or hand-off ended. It must not forbid further work:
|
|
146
|
+
// an agent with standing instructions (a review loop, a CI watch) would go idle.
|
|
146
147
|
export function jobPrompt(job: JobName, outcome: Shipped): string {
|
|
147
148
|
const name = JOB_NAME[job]
|
|
148
149
|
if (outcome.ok) {
|
|
149
150
|
return [
|
|
150
151
|
`The ${name} the person asked for finished. gtd's report:`,
|
|
151
152
|
fence(outcome.text),
|
|
152
|
-
"Relay the result to the person in a line or two, with any link in it
|
|
153
|
+
"Relay the result to the person in a line or two, with any link in it; then continue with any work the person already asked for.",
|
|
153
154
|
].join("\n\n")
|
|
154
155
|
}
|
|
155
156
|
const lines = [
|
|
@@ -26,13 +26,31 @@ const closeReview = (round) => run("review.closing", removeScript([REVIEW, REVIE
|
|
|
26
26
|
const refusePendingHandOff = () => {
|
|
27
27
|
if (read(".gtd/REVIEW.orig.md") !== void 0) refuse(`gtd: ${REVIEW_ORIG} is a pending hand-off — run \`gtd door fix\`, or remove ${REVIEW} and ${REVIEW_ORIG}.`);
|
|
28
28
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
29
|
+
function tail(first, second, ...rest) {
|
|
30
|
+
const form = tailForm(first, second, rest);
|
|
31
|
+
if (form === void 0) return Promise.reject(/* @__PURE__ */ new Error("tail takes tail(finish, options?) or the deprecated tail(fixFirst, finish, green?, guard?, lenses?, guardRests?, lensBase?, rules?); this call fits neither"));
|
|
32
|
+
const [finish, options] = form;
|
|
33
|
+
return runTail(finish, options);
|
|
34
|
+
}
|
|
35
|
+
const isOptionalObject = (value) => value == null || typeof value === "object" && !Array.isArray(value) && typeof value.then !== "function";
|
|
36
|
+
const tailForm = (first, second, rest) => {
|
|
37
|
+
if (typeof second === "function" && typeof first !== "function") {
|
|
38
|
+
const [green, guard, lenses, guardRests, lensBase, rules] = rest;
|
|
39
|
+
return [second, {
|
|
40
|
+
fixFirst: first,
|
|
41
|
+
green,
|
|
42
|
+
guard,
|
|
43
|
+
lenses,
|
|
44
|
+
guardRests,
|
|
45
|
+
lensBase,
|
|
46
|
+
rules
|
|
47
|
+
}];
|
|
48
|
+
}
|
|
49
|
+
if (typeof first === "function" && isOptionalObject(second) && rest.every((v) => v == null)) return [first, second ?? {}];
|
|
50
|
+
};
|
|
51
|
+
const runTail = (finish, options) => scope("build", async () => {
|
|
52
|
+
const { guard, lenses, lensBase, rules } = options;
|
|
53
|
+
const guardRests = options.guardRests === void 0 ? true : options.guardRests;
|
|
36
54
|
const escalations = { rounds: 0 };
|
|
37
55
|
const fix = () => fixCheck(rules);
|
|
38
56
|
const repair = async () => {
|
|
@@ -45,8 +63,8 @@ const tail = (fixFirst, finish, green = false, guard, lenses, guardRests = true,
|
|
|
45
63
|
rules
|
|
46
64
|
});
|
|
47
65
|
};
|
|
48
|
-
if (fixFirst) await repair();
|
|
49
|
-
else if (!green) await healthy(fix, {
|
|
66
|
+
if (options.fixFirst) await repair();
|
|
67
|
+
else if (!options.green) await healthy(fix, {
|
|
50
68
|
escalations,
|
|
51
69
|
guard,
|
|
52
70
|
guardRests,
|
|
@@ -83,6 +101,9 @@ const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
|
|
|
83
101
|
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
|
|
84
102
|
});
|
|
85
103
|
/** The tail the review and fix workflows run: ends at the hand-off review since `base`. */
|
|
86
|
-
const handOffTail = (fixFirst, base, guard) => tail(
|
|
104
|
+
const handOffTail = (fixFirst, base, guard) => tail((escalations) => handOffReview(base, escalations, guard), {
|
|
105
|
+
fixFirst,
|
|
106
|
+
guard
|
|
107
|
+
});
|
|
87
108
|
//#endregion
|
|
88
|
-
export {
|
|
109
|
+
export { refusePendingHandOff as a, isOptionalObject as i, closeReview as n, tail as o, handOffTail as r, REVIEW_ORIG as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
2
|
import { _t as registryEntry, a as flagShipped, dt as defaults, ft as envDefaults, ht as flagReferences, i as skills, n as summary, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-Dmpg6dAp.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CLyE5VFR.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, glob, 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
2
|
import { lensesOf, review as review$1 } from "../workflow-kit.mjs";
|
|
3
3
|
import { $ as refactorFixPrompt, A as architectureDecomposePrompt, B as designSystem, C as planner, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as idleMessage, I as buildReviewTriageMessage, J as naivePassPrompt, K as implementBuildingPrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as refactorDroppedRefusal, R as deferNotesScript, S as fixRisks, V as designTriagePrompt, X as packagesItemFixSuitePrompt, Y as packagesItemBuildingPrompt, Z as refactorBuildingPrompt, _ as REVIEW, at as reviewerSystem, b as fixCheck, ct as startGateBlockedMessage, d as healthy, dt as defaults, et as refactorRules, f as ARCHITECTURE, ft as envDefaults, g as REQUIREMENTS, h as REFACTOR, i as skills, it as reviewNotesCapture, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as gate, lt as unwindFailedMessage, n as summary, nt as reviewEditsCapture, o as shipped, ot as seamByHandMessage, p as FEEDBACK, pt as testFileGlobs, q as implementFixPrompt, r as access, st as seamSteerMessage, t as steering, tt as reviewEditNotesCapture, u as guarded, ut as unwindFailure, v as awaitReview, w as reviewMissing, y as coder, z as designGateAnswerMessage } from "../chunks/reserved-Dmpg6dAp.mjs";
|
|
4
|
-
import { a as
|
|
4
|
+
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-CLyE5VFR.mjs";
|
|
5
5
|
//#region src/workflows/shared/fences.ts
|
|
6
6
|
const markFenced = (text) => {
|
|
7
7
|
let fence;
|
|
@@ -479,10 +479,8 @@ function buildTail(first, second, ...rest) {
|
|
|
479
479
|
const options = callForm(first, second, rest);
|
|
480
480
|
if (options === void 0) return Promise.reject(/* @__PURE__ */ new Error("buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither"));
|
|
481
481
|
const [base, form] = options;
|
|
482
|
-
|
|
483
|
-
return tail(fixFirst ?? false, (escalations) => review(base, escalations, reviewOptions), green ?? false, form.guard, form.lenses, form.guardRests, form.lensBase, form.rules);
|
|
482
|
+
return tail((escalations) => review(base, escalations, form), form);
|
|
484
483
|
}
|
|
485
|
-
const isOptionalObject = (value) => value == null || typeof value === "object" && !Array.isArray(value) && typeof value.then !== "function";
|
|
486
484
|
const isOptionalBoolean = (value) => value == null || typeof value === "boolean";
|
|
487
485
|
const legacyForm = (fixFirst, base, green, options) => isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options) ? [base, {
|
|
488
486
|
...options,
|
|
@@ -866,7 +864,7 @@ const planAndBuild = async (firstBase, firstLap = "first") => {
|
|
|
866
864
|
await packages();
|
|
867
865
|
const outcome = await buildTail(base, {
|
|
868
866
|
green: true,
|
|
869
|
-
|
|
867
|
+
lap
|
|
870
868
|
});
|
|
871
869
|
if (outcome.verdict === "signoff") return;
|
|
872
870
|
await reUnwind(outcome);
|
|
@@ -903,7 +901,7 @@ const featureFeedback = async (first, lensBase) => {
|
|
|
903
901
|
outcome = await buildTail(outcome.base, {
|
|
904
902
|
green: true,
|
|
905
903
|
lap: "feedback",
|
|
906
|
-
|
|
904
|
+
lensBase
|
|
907
905
|
});
|
|
908
906
|
}
|
|
909
907
|
};
|
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
3
|
import { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, _ as REVIEW, dt as defaults, ft as envDefaults, i as skills, n as summary, o as shipped, r as access, s as baseline, t as steering, y as coder } from "../chunks/reserved-Dmpg6dAp.mjs";
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-CLyE5VFR.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
2
|
import { dt as defaults, ft as envDefaults, i as skills, l as gate, n as summary, o as shipped, r as access, rt as reviewGateBlockedMessage, t as steering } from "../chunks/reserved-Dmpg6dAp.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CLyE5VFR.mjs";
|
|
4
4
|
//#region src/workflows/review/review.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
package/dist/workflows.mjs
CHANGED
|
@@ -37791,13 +37791,31 @@ const closeReview = (round) => run("review.closing", removeScript([REVIEW, REVIE
|
|
|
37791
37791
|
const refusePendingHandOff = () => {
|
|
37792
37792
|
if (read(".gtd/REVIEW.orig.md") !== void 0) refuse(`gtd: ${REVIEW_ORIG} is a pending hand-off — run \`gtd door fix\`, or remove ${REVIEW} and ${REVIEW_ORIG}.`);
|
|
37793
37793
|
};
|
|
37794
|
-
|
|
37795
|
-
|
|
37796
|
-
|
|
37797
|
-
|
|
37798
|
-
|
|
37799
|
-
|
|
37800
|
-
const
|
|
37794
|
+
function tail(first, second, ...rest) {
|
|
37795
|
+
const form = tailForm(first, second, rest);
|
|
37796
|
+
if (form === void 0) return Promise.reject(/* @__PURE__ */ new Error("tail takes tail(finish, options?) or the deprecated tail(fixFirst, finish, green?, guard?, lenses?, guardRests?, lensBase?, rules?); this call fits neither"));
|
|
37797
|
+
const [finish, options] = form;
|
|
37798
|
+
return runTail(finish, options);
|
|
37799
|
+
}
|
|
37800
|
+
const isOptionalObject = (value) => value == null || typeof value === "object" && !Array.isArray(value) && typeof value.then !== "function";
|
|
37801
|
+
const tailForm = (first, second, rest) => {
|
|
37802
|
+
if (typeof second === "function" && typeof first !== "function") {
|
|
37803
|
+
const [green, guard, lenses, guardRests, lensBase, rules] = rest;
|
|
37804
|
+
return [second, {
|
|
37805
|
+
fixFirst: first,
|
|
37806
|
+
green,
|
|
37807
|
+
guard,
|
|
37808
|
+
lenses,
|
|
37809
|
+
guardRests,
|
|
37810
|
+
lensBase,
|
|
37811
|
+
rules
|
|
37812
|
+
}];
|
|
37813
|
+
}
|
|
37814
|
+
if (typeof first === "function" && isOptionalObject(second) && rest.every((v) => v == null)) return [first, second ?? {}];
|
|
37815
|
+
};
|
|
37816
|
+
const runTail = (finish, options) => scope("build", async () => {
|
|
37817
|
+
const { guard, lenses, lensBase, rules } = options;
|
|
37818
|
+
const guardRests = options.guardRests === void 0 ? true : options.guardRests;
|
|
37801
37819
|
const escalations = { rounds: 0 };
|
|
37802
37820
|
const fix = () => fixCheck(rules);
|
|
37803
37821
|
const repair = async () => {
|
|
@@ -37810,8 +37828,8 @@ const tail = (fixFirst, finish, green = false, guard, lenses, guardRests = true,
|
|
|
37810
37828
|
rules
|
|
37811
37829
|
});
|
|
37812
37830
|
};
|
|
37813
|
-
if (fixFirst) await repair();
|
|
37814
|
-
else if (!green) await healthy(fix, {
|
|
37831
|
+
if (options.fixFirst) await repair();
|
|
37832
|
+
else if (!options.green) await healthy(fix, {
|
|
37815
37833
|
escalations,
|
|
37816
37834
|
guard,
|
|
37817
37835
|
guardRests,
|
|
@@ -37848,7 +37866,10 @@ const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
|
|
|
37848
37866
|
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
|
|
37849
37867
|
});
|
|
37850
37868
|
/** The tail the review and fix workflows run: ends at the hand-off review since `base`. */
|
|
37851
|
-
const handOffTail = (fixFirst, base, guard) => tail(
|
|
37869
|
+
const handOffTail = (fixFirst, base, guard) => tail((escalations) => handOffReview(base, escalations, guard), {
|
|
37870
|
+
fixFirst,
|
|
37871
|
+
guard
|
|
37872
|
+
});
|
|
37852
37873
|
//#endregion
|
|
37853
37874
|
//#region src/workflows/shared/fences.ts
|
|
37854
37875
|
const markFenced = (text) => {
|
|
@@ -38327,10 +38348,8 @@ function buildTail(first, second, ...rest) {
|
|
|
38327
38348
|
const options = callForm(first, second, rest);
|
|
38328
38349
|
if (options === void 0) return Promise.reject(/* @__PURE__ */ new Error("buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither"));
|
|
38329
38350
|
const [base, form] = options;
|
|
38330
|
-
|
|
38331
|
-
return tail(fixFirst ?? false, (escalations) => review$1(base, escalations, reviewOptions), green ?? false, form.guard, form.lenses, form.guardRests, form.lensBase, form.rules);
|
|
38351
|
+
return tail((escalations) => review$1(base, escalations, form), form);
|
|
38332
38352
|
}
|
|
38333
|
-
const isOptionalObject = (value) => value == null || typeof value === "object" && !Array.isArray(value) && typeof value.then !== "function";
|
|
38334
38353
|
const isOptionalBoolean = (value) => value == null || typeof value === "boolean";
|
|
38335
38354
|
const legacyForm = (fixFirst, base, green, options) => isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options) ? [base, {
|
|
38336
38355
|
...options,
|
|
@@ -38714,7 +38733,7 @@ const planAndBuild = async (firstBase, firstLap = "first") => {
|
|
|
38714
38733
|
await packages();
|
|
38715
38734
|
const outcome = await buildTail(base, {
|
|
38716
38735
|
green: true,
|
|
38717
|
-
|
|
38736
|
+
lap
|
|
38718
38737
|
});
|
|
38719
38738
|
if (outcome.verdict === "signoff") return;
|
|
38720
38739
|
await reUnwind(outcome);
|
|
@@ -38751,7 +38770,7 @@ const featureFeedback = async (first, lensBase) => {
|
|
|
38751
38770
|
outcome = await buildTail(outcome.base, {
|
|
38752
38771
|
green: true,
|
|
38753
38772
|
lap: "feedback",
|
|
38754
|
-
|
|
38773
|
+
lensBase
|
|
38755
38774
|
});
|
|
38756
38775
|
}
|
|
38757
38776
|
};
|
package/package.json
CHANGED
|
@@ -49,7 +49,7 @@ export const planAndBuild = async (
|
|
|
49
49
|
await design(base)
|
|
50
50
|
await architecture()
|
|
51
51
|
await packages()
|
|
52
|
-
const outcome = await buildTail(base, { green: true,
|
|
52
|
+
const outcome = await buildTail(base, { green: true, lap })
|
|
53
53
|
if (outcome.verdict === "signoff") return
|
|
54
54
|
await reUnwind(outcome)
|
|
55
55
|
base = outcome.base
|
|
@@ -81,11 +81,7 @@ const featureFeedback = async (first: ReviewOutcome, lensBase?: string): Promise
|
|
|
81
81
|
await design(outcome.base)
|
|
82
82
|
const tier = riskTierOn() ? recordRisk() : undefined
|
|
83
83
|
await implement({ tier, amend: true })
|
|
84
|
-
outcome = await buildTail(outcome.base, {
|
|
85
|
-
green: true,
|
|
86
|
-
lap: "feedback",
|
|
87
|
-
...(lensBase === undefined ? {} : { lensBase }),
|
|
88
|
-
})
|
|
84
|
+
outcome = await buildTail(outcome.base, { green: true, lap: "feedback", lensBase })
|
|
89
85
|
}
|
|
90
86
|
}
|
|
91
87
|
|
|
@@ -5,7 +5,13 @@ export * from "./planning.js"
|
|
|
5
5
|
export * from "./packages.js"
|
|
6
6
|
export * from "./refactor.js"
|
|
7
7
|
export * from "./seam.js"
|
|
8
|
-
export {
|
|
8
|
+
export {
|
|
9
|
+
buildTail,
|
|
10
|
+
type BuildTailOptions,
|
|
11
|
+
type FeedbackTarget,
|
|
12
|
+
type ReviewOutcome,
|
|
13
|
+
} from "./review.js"
|
|
14
|
+
export type { ReviewRange } from "../shared/text.js"
|
|
9
15
|
export * from "./feature.js"
|
|
10
16
|
export * from "../shared/reserved.js"
|
|
11
17
|
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "./review.js"
|
|
18
18
|
import type { ReviewNote } from "../../steering/index.js"
|
|
19
19
|
import * as bundled from "../index.js"
|
|
20
|
-
import type
|
|
20
|
+
import type * as featureEntry from "./index.js"
|
|
21
21
|
import { fixtureContext } from "../shared/text.fixture.js"
|
|
22
22
|
import { toRequest } from "../../judges/index.js"
|
|
23
23
|
|
|
@@ -999,17 +999,38 @@ describe("BuildTailOptions.lensBase", () => {
|
|
|
999
999
|
expect(names).not.toEqual([])
|
|
1000
1000
|
})
|
|
1001
1001
|
|
|
1002
|
-
// pins `| undefined` on BuildTailOptions under exactOptionalPropertyTypes (typecheck)
|
|
1003
|
-
it("explicitly undefined
|
|
1002
|
+
// pins `| undefined` on every BuildTailOptions field under exactOptionalPropertyTypes (typecheck)
|
|
1003
|
+
it("explicitly undefined options default like omitted ones", async () => {
|
|
1004
1004
|
expect(
|
|
1005
|
-
await requestNames(() =>
|
|
1005
|
+
await requestNames(() =>
|
|
1006
|
+
buildTail("base", {
|
|
1007
|
+
fixFirst: undefined,
|
|
1008
|
+
green: undefined,
|
|
1009
|
+
guard: undefined,
|
|
1010
|
+
guardRests: undefined,
|
|
1011
|
+
lenses: undefined,
|
|
1012
|
+
feedback: undefined,
|
|
1013
|
+
fixAccess: undefined,
|
|
1014
|
+
lensBase: undefined,
|
|
1015
|
+
rules: undefined,
|
|
1016
|
+
lap: undefined,
|
|
1017
|
+
}),
|
|
1018
|
+
),
|
|
1006
1019
|
).toEqual(await requestNames(() => buildTail("base")))
|
|
1007
1020
|
})
|
|
1008
1021
|
})
|
|
1009
1022
|
})
|
|
1010
1023
|
|
|
1011
1024
|
// pins the public re-exports from the feature entry point and the published workflows entry
|
|
1012
|
-
export type _PublicBuildTailOptions = [
|
|
1025
|
+
export type _PublicBuildTailOptions = [
|
|
1026
|
+
featureEntry.BuildTailOptions,
|
|
1027
|
+
featureEntry.FeedbackTarget,
|
|
1028
|
+
featureEntry.ReviewRange,
|
|
1029
|
+
bundled.BuildTailOptions,
|
|
1030
|
+
bundled.FeedbackTarget,
|
|
1031
|
+
bundled.ReviewRange,
|
|
1032
|
+
bundled.TailOptions,
|
|
1033
|
+
]
|
|
1013
1034
|
|
|
1014
1035
|
describe("buildTail without rules", () => {
|
|
1015
1036
|
const RULES = "Change only these paths"
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "../../flows/index.js"
|
|
14
14
|
import type { ReviewNote } from "../../steering/index.js"
|
|
15
15
|
import * as kit from "../../workflow-kit/index.js"
|
|
16
|
-
import { guarded, healthy, type EscalationCount
|
|
16
|
+
import { guarded, healthy, type EscalationCount } from "../shared/health.js"
|
|
17
17
|
import {
|
|
18
18
|
awaitReview,
|
|
19
19
|
fixCheck,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
reviewing,
|
|
24
24
|
reviewMissing,
|
|
25
25
|
} from "../shared/steps.js"
|
|
26
|
-
import { closeReview, tail } from "../shared/tail.js"
|
|
26
|
+
import { closeReview, isOptionalObject, tail, type TailOptions } from "../shared/tail.js"
|
|
27
27
|
import * as t from "../shared/text.js"
|
|
28
28
|
import { answerReviewQuestions, collecting, deferNotes, fixNits } from "./steps.js"
|
|
29
29
|
|
|
@@ -169,7 +169,7 @@ export const isCode = (path: string): boolean => path !== ".gtd" && !path.starts
|
|
|
169
169
|
export const review = (
|
|
170
170
|
base: string,
|
|
171
171
|
escalations: EscalationCount = { rounds: 0 },
|
|
172
|
-
options: BuildTailOptions = {},
|
|
172
|
+
options: Omit<BuildTailOptions, "fixFirst" | "green"> = {},
|
|
173
173
|
): Promise<kit.ReviewOutcome> => {
|
|
174
174
|
const { guard, guardRests, fixAccess, rules } = options
|
|
175
175
|
const feedback: FeedbackTarget = options.feedback ?? {
|
|
@@ -255,26 +255,12 @@ export interface FeedbackTarget {
|
|
|
255
255
|
readonly dropped?: (requested: kit.Requested) => string
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
export interface BuildTailOptions {
|
|
259
|
-
|
|
260
|
-
readonly fixFirst?: boolean | undefined
|
|
261
|
-
/** Asserts the tree is already green, so the tail skips keep-green; default `false`. */
|
|
262
|
-
readonly green?: boolean | undefined
|
|
263
|
-
/** Runs after every code-changing turn: fix-check, quality fix, risk fix, nit fix, escalation describe. */
|
|
264
|
-
readonly guard?: Guard
|
|
265
|
-
/** `false`: `guard` skips the escalation's human rests. */
|
|
266
|
-
readonly guardRests?: boolean
|
|
267
|
-
/** Quality-lap lenses; default `lensesOf(vars.qualityReviews)`. */
|
|
268
|
-
readonly lenses?: readonly string[]
|
|
269
|
-
readonly feedback?: FeedbackTarget
|
|
258
|
+
export interface BuildTailOptions extends TailOptions {
|
|
259
|
+
readonly feedback?: FeedbackTarget | undefined
|
|
270
260
|
/** Write access for the nit and risk fix turns; absent: the bundled `access` export decides. */
|
|
271
|
-
readonly fixAccess?: ScopeAccess
|
|
272
|
-
/** The commit the quality lap reviews from; absent: `start()`. */
|
|
273
|
-
readonly lensBase?: string
|
|
274
|
-
/** Appended to every fix and describe prompt of the tail. */
|
|
275
|
-
readonly rules?: string
|
|
261
|
+
readonly fixAccess?: ScopeAccess | undefined
|
|
276
262
|
/** Absent: "first". */
|
|
277
|
-
readonly lap?: t.ReviewRange
|
|
263
|
+
readonly lap?: t.ReviewRange | undefined
|
|
278
264
|
}
|
|
279
265
|
|
|
280
266
|
/** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
|
|
@@ -301,26 +287,9 @@ export function buildTail(
|
|
|
301
287
|
)
|
|
302
288
|
}
|
|
303
289
|
const [base, form] = options
|
|
304
|
-
|
|
305
|
-
return tail(
|
|
306
|
-
fixFirst ?? false,
|
|
307
|
-
(escalations) => review(base, escalations, reviewOptions),
|
|
308
|
-
green ?? false,
|
|
309
|
-
form.guard,
|
|
310
|
-
form.lenses,
|
|
311
|
-
form.guardRests,
|
|
312
|
-
form.lensBase,
|
|
313
|
-
form.rules,
|
|
314
|
-
)
|
|
290
|
+
return tail((escalations) => review(base, escalations, form), form)
|
|
315
291
|
}
|
|
316
292
|
|
|
317
|
-
// null reads as absent; an un-awaited Promise is a mistake, not options.
|
|
318
|
-
const isOptionalObject = (value: unknown): value is BuildTailOptions | null | undefined =>
|
|
319
|
-
value == null ||
|
|
320
|
-
(typeof value === "object" &&
|
|
321
|
-
!Array.isArray(value) &&
|
|
322
|
-
typeof (value as { then?: unknown }).then !== "function")
|
|
323
|
-
|
|
324
293
|
const isOptionalBoolean = (value: unknown): value is boolean | null | undefined =>
|
|
325
294
|
value == null || typeof value === "boolean"
|
|
326
295
|
|
package/src/workflows/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { feature } from "./feature/feature.js"
|
|
|
6
6
|
export { threads, type ThreadInfo } from "../flows/index.js"
|
|
7
7
|
export * from "./shared/reserved.js"
|
|
8
8
|
export { FlagOptions, ShippedOptions } from "./shared/options.js"
|
|
9
|
-
export { agentWithSkills } from "./shared/text.js"
|
|
9
|
+
export { agentWithSkills, type ReviewRange } from "./shared/text.js"
|
|
10
10
|
export * from "./shared/steps.js"
|
|
11
11
|
export * from "./shared/health.js"
|
|
12
12
|
export {
|
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
qualityLap,
|
|
18
18
|
REVIEW_ORIG,
|
|
19
19
|
tail,
|
|
20
|
+
type TailOptions,
|
|
20
21
|
} from "./shared/tail.js"
|
|
21
22
|
export { lensesOf } from "../workflow-kit/index.js"
|
|
22
23
|
export * from "./feature/steps.js"
|
|
@@ -24,7 +25,12 @@ export * from "./feature/planning.js"
|
|
|
24
25
|
export * from "./feature/packages.js"
|
|
25
26
|
export * from "./feature/refactor.js"
|
|
26
27
|
export * from "./feature/seam.js"
|
|
27
|
-
export {
|
|
28
|
+
export {
|
|
29
|
+
buildTail,
|
|
30
|
+
type BuildTailOptions,
|
|
31
|
+
type FeedbackTarget,
|
|
32
|
+
type ReviewOutcome,
|
|
33
|
+
} from "./feature/review.js"
|
|
28
34
|
export * from "./feature/feature.js"
|
|
29
35
|
export { base, review, reviewFlow } from "./review/review.js"
|
|
30
36
|
export { fix, fixFlow, fixReviewNotes } from "./fix/fix.js"
|
|
@@ -260,32 +260,147 @@ describe("the quality lap's lenses", () => {
|
|
|
260
260
|
|
|
261
261
|
it("tail threads lensBase to every lens turn", async () => {
|
|
262
262
|
await lap(() =>
|
|
263
|
-
tail(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
["conventions", "correctness"],
|
|
269
|
-
true,
|
|
270
|
-
"BASE-SHA",
|
|
271
|
-
),
|
|
263
|
+
tail(() => Promise.resolve(), {
|
|
264
|
+
green: true,
|
|
265
|
+
lenses: ["conventions", "correctness"],
|
|
266
|
+
lensBase: "BASE-SHA",
|
|
267
|
+
}),
|
|
272
268
|
)
|
|
273
269
|
expect(prompts).toHaveLength(2)
|
|
274
270
|
for (const prompt of prompts) expect(prompt).toContain("from `BASE-SHA`")
|
|
275
271
|
})
|
|
276
272
|
|
|
273
|
+
it("the deprecated form threads lensBase from its seventh slot", async () => {
|
|
274
|
+
await lap(() =>
|
|
275
|
+
tail(false, () => Promise.resolve(), true, undefined, ["conventions"], true, "BASE-SHA", "R"),
|
|
276
|
+
)
|
|
277
|
+
expect(prompts).toHaveLength(1)
|
|
278
|
+
expect(prompts[0]).toContain("from `BASE-SHA`")
|
|
279
|
+
expect(prompts[0]).not.toContain("from `R`")
|
|
280
|
+
})
|
|
281
|
+
|
|
277
282
|
it("tail passes lenses to its lap and a guard after each lens turn", async () => {
|
|
278
283
|
let guards = 0
|
|
279
284
|
const log = await lap(() =>
|
|
280
|
-
tail(
|
|
281
|
-
|
|
282
|
-
() =>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
["conventions"],
|
|
286
|
-
),
|
|
285
|
+
tail(() => Promise.resolve(), {
|
|
286
|
+
green: true,
|
|
287
|
+
guard: () => void guards++,
|
|
288
|
+
lenses: ["conventions"],
|
|
289
|
+
}),
|
|
287
290
|
)
|
|
288
291
|
expect(log).toEqual(["quality.conventions.reviewing"])
|
|
289
292
|
expect(guards).toBe(1)
|
|
290
293
|
})
|
|
291
294
|
})
|
|
295
|
+
|
|
296
|
+
describe("tail's call forms", () => {
|
|
297
|
+
class Stop extends Error {}
|
|
298
|
+
const requestNames = async (
|
|
299
|
+
call: () => Promise<unknown>,
|
|
300
|
+
names: string[] = [],
|
|
301
|
+
): Promise<string[]> => {
|
|
302
|
+
installContext(
|
|
303
|
+
fixtureContext(
|
|
304
|
+
{ vars: { qualityReviews: "correctness" } },
|
|
305
|
+
{
|
|
306
|
+
step: (request: StepRequest) => {
|
|
307
|
+
if (request.kind === "restart") return Promise.resolve()
|
|
308
|
+
names.push(`${request.kind}:${request.name}`)
|
|
309
|
+
if (request.kind === "agent") prompts.push(request.prompt)
|
|
310
|
+
if (names.length >= 4) throw new Stop()
|
|
311
|
+
return Promise.resolve()
|
|
312
|
+
},
|
|
313
|
+
pushScope: () => undefined,
|
|
314
|
+
popScope: () => undefined,
|
|
315
|
+
start: () => "START-SHA",
|
|
316
|
+
},
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
await call().catch((error: unknown) => {
|
|
320
|
+
if (!(error instanceof Stop)) throw error
|
|
321
|
+
})
|
|
322
|
+
return names
|
|
323
|
+
}
|
|
324
|
+
const finish = () => Promise.resolve()
|
|
325
|
+
|
|
326
|
+
for (const [fixFirst, green] of [
|
|
327
|
+
[true, false],
|
|
328
|
+
[true, true],
|
|
329
|
+
[false, true],
|
|
330
|
+
[false, false],
|
|
331
|
+
] as const) {
|
|
332
|
+
it(`the deprecated positional form matches the options form for fixFirst ${fixFirst}, green ${green}`, async () => {
|
|
333
|
+
const modern = await requestNames(() => tail(finish, { fixFirst, green }))
|
|
334
|
+
const legacy = await requestNames(() => tail(fixFirst, finish, green))
|
|
335
|
+
expect(legacy).toEqual(modern)
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
it("the deprecated form passes every positional on", async () => {
|
|
340
|
+
let guards = 0
|
|
341
|
+
const guard = () => void guards++
|
|
342
|
+
const modern = await requestNames(() =>
|
|
343
|
+
tail(finish, { green: true, guard, lenses: ["conventions"], guardRests: false }),
|
|
344
|
+
)
|
|
345
|
+
const modernGuards = guards
|
|
346
|
+
guards = 0
|
|
347
|
+
const legacy = await requestNames(() =>
|
|
348
|
+
tail(false, finish, true, guard, ["conventions"], false),
|
|
349
|
+
)
|
|
350
|
+
expect(legacy).toEqual(modern)
|
|
351
|
+
expect(guards).toBe(modernGuards)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it("the deprecated form threads rules from its eighth slot", async () => {
|
|
355
|
+
prompts.length = 0
|
|
356
|
+
await requestNames(() =>
|
|
357
|
+
tail(true, finish, false, undefined, [], true, "BASE-SHA", "RULES-TEXT"),
|
|
358
|
+
)
|
|
359
|
+
expect(prompts.some((p) => p.includes("RULES-TEXT"))).toBe(true)
|
|
360
|
+
expect(prompts.some((p) => p.includes("BASE-SHA"))).toBe(false)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it("the deprecated form still runs untyped flags as truthy or falsy", async () => {
|
|
364
|
+
expect(await requestNames(() => tail(1 as never, finish, 0 as never))).toEqual(
|
|
365
|
+
await requestNames(() => tail(finish, { fixFirst: true })),
|
|
366
|
+
)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it("reads an explicit undefined in every option as absent", async () => {
|
|
370
|
+
const absent = await requestNames(() => tail(finish))
|
|
371
|
+
const explicit = await requestNames(() =>
|
|
372
|
+
tail(finish, {
|
|
373
|
+
fixFirst: undefined,
|
|
374
|
+
green: undefined,
|
|
375
|
+
guard: undefined,
|
|
376
|
+
lenses: undefined,
|
|
377
|
+
guardRests: undefined,
|
|
378
|
+
lensBase: undefined,
|
|
379
|
+
rules: undefined,
|
|
380
|
+
}),
|
|
381
|
+
)
|
|
382
|
+
expect(explicit).toEqual(absent)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
const fitsNeither: [string, () => Promise<unknown>][] = [
|
|
386
|
+
["no finish", () => (tail as (...args: unknown[]) => Promise<unknown>)(false)],
|
|
387
|
+
["a finish followed by a boolean", () => tail(finish, true as never)],
|
|
388
|
+
["a finish followed by a function", () => tail(finish, finish as never)],
|
|
389
|
+
["a finish followed by a Promise", () => tail(finish, Promise.resolve({}) as never)],
|
|
390
|
+
[
|
|
391
|
+
"a finish with a third argument",
|
|
392
|
+
() => (tail as (...args: unknown[]) => Promise<unknown>)(finish, {}, true),
|
|
393
|
+
],
|
|
394
|
+
]
|
|
395
|
+
for (const [label, call] of fitsNeither) {
|
|
396
|
+
it(`rejects ${label} before any step runs`, async () => {
|
|
397
|
+
const names: string[] = []
|
|
398
|
+
const rejected = expect(requestNames(call, names)).rejects
|
|
399
|
+
await rejected.toThrow("tail(finish, options?)")
|
|
400
|
+
await rejected.toThrow(
|
|
401
|
+
"tail(fixFirst, finish, green?, guard?, lenses?, guardRests?, lensBase?, rules?)",
|
|
402
|
+
)
|
|
403
|
+
expect(names).toEqual([])
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
})
|
|
@@ -70,23 +70,88 @@ export const refusePendingHandOff = (): void => {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export interface TailOptions {
|
|
74
|
+
/** Fix first when entered red; default `false`. */
|
|
75
|
+
readonly fixFirst?: boolean | undefined
|
|
76
|
+
/** Asserts the tree is already green, so the tail skips keep-green; default `false`. */
|
|
77
|
+
readonly green?: boolean | undefined
|
|
78
|
+
/** Runs after every code-changing turn: fix-check, quality fix, risk fix, nit fix, escalation describe. */
|
|
79
|
+
readonly guard?: Guard | undefined
|
|
80
|
+
/** Quality-lap lenses; default `lensesOf(vars.qualityReviews)`. */
|
|
81
|
+
readonly lenses?: readonly string[] | undefined
|
|
82
|
+
/** `false` leaves the escalation's human rests unguarded; default `true`. */
|
|
83
|
+
readonly guardRests?: boolean | undefined
|
|
84
|
+
/** The commit the quality lap reviews from; absent: `start()`. */
|
|
85
|
+
readonly lensBase?: string | undefined
|
|
86
|
+
/** Appended to every fix and describe prompt of the tail. */
|
|
87
|
+
readonly rules?: string | undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
73
90
|
/**
|
|
74
91
|
* The build tail: fix (when entered red), keep green, the quality lap, then
|
|
75
|
-
* `finish` — the human review, as the calling workflow runs it.
|
|
76
|
-
* caller just left the full suite green (the package loop), so no run opens it.
|
|
77
|
-
* `guardRests: false` leaves the escalation's human rests unguarded.
|
|
92
|
+
* `finish` — the human review, as the calling workflow runs it.
|
|
78
93
|
*/
|
|
79
|
-
export
|
|
94
|
+
export function tail<T>(
|
|
95
|
+
finish: (escalations: EscalationCount) => Promise<T>,
|
|
96
|
+
options?: TailOptions,
|
|
97
|
+
): Promise<T>
|
|
98
|
+
/** @deprecated Pass `tail(finish, { fixFirst, green, guard, lenses, guardRests, lensBase, rules })`. */
|
|
99
|
+
export function tail<T>(
|
|
80
100
|
fixFirst: boolean,
|
|
81
101
|
finish: (escalations: EscalationCount) => Promise<T>,
|
|
82
|
-
green
|
|
102
|
+
green?: boolean,
|
|
83
103
|
guard?: Guard,
|
|
84
104
|
lenses?: readonly string[],
|
|
85
|
-
guardRests
|
|
105
|
+
guardRests?: boolean,
|
|
86
106
|
lensBase?: string,
|
|
87
107
|
rules?: string,
|
|
88
|
-
): Promise<T>
|
|
108
|
+
): Promise<T>
|
|
109
|
+
export function tail<T>(first: unknown, second?: unknown, ...rest: unknown[]): Promise<T> {
|
|
110
|
+
const form = tailForm<T>(first, second, rest)
|
|
111
|
+
// Before any step runs, so a bad call never leaves a half-walked flow.
|
|
112
|
+
if (form === undefined) {
|
|
113
|
+
return Promise.reject(
|
|
114
|
+
new Error(
|
|
115
|
+
"tail takes tail(finish, options?) or the deprecated tail(fixFirst, finish, green?, guard?, lenses?, guardRests?, lensBase?, rules?); this call fits neither",
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
const [finish, options] = form
|
|
120
|
+
return runTail(finish, options)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type Finish<T> = (escalations: EscalationCount) => Promise<T>
|
|
124
|
+
|
|
125
|
+
// null reads as absent; an un-awaited Promise is a mistake, not options.
|
|
126
|
+
export const isOptionalObject = (value: unknown): value is object | null | undefined =>
|
|
127
|
+
value == null ||
|
|
128
|
+
(typeof value === "object" &&
|
|
129
|
+
!Array.isArray(value) &&
|
|
130
|
+
typeof (value as { then?: unknown }).then !== "function")
|
|
131
|
+
|
|
132
|
+
// Picks the form by shape only: the deprecated form never validated its
|
|
133
|
+
// positionals, and still runs whatever it ran before.
|
|
134
|
+
const tailForm = <T>(
|
|
135
|
+
first: unknown,
|
|
136
|
+
second: unknown,
|
|
137
|
+
rest: readonly unknown[],
|
|
138
|
+
): [Finish<T>, TailOptions] | undefined => {
|
|
139
|
+
// A function first is a misplaced finish, never a fixFirst.
|
|
140
|
+
if (typeof second === "function" && typeof first !== "function") {
|
|
141
|
+
const [green, guard, lenses, guardRests, lensBase, rules] = rest
|
|
142
|
+
const options = { fixFirst: first, green, guard, lenses, guardRests, lensBase, rules }
|
|
143
|
+
return [second as Finish<T>, options as TailOptions]
|
|
144
|
+
}
|
|
145
|
+
if (typeof first === "function" && isOptionalObject(second) && rest.every((v) => v == null))
|
|
146
|
+
return [first as Finish<T>, (second as TailOptions | null | undefined) ?? {}]
|
|
147
|
+
return undefined
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const runTail = <T>(finish: Finish<T>, options: TailOptions): Promise<T> =>
|
|
89
151
|
scope("build", async () => {
|
|
152
|
+
const { guard, lenses, lensBase, rules } = options
|
|
153
|
+
// Only undefined defaults: a null reaches every escalation as falsy, as it reaches review's.
|
|
154
|
+
const guardRests = options.guardRests === undefined ? true : options.guardRests
|
|
90
155
|
const escalations: EscalationCount = { rounds: 0 }
|
|
91
156
|
const fix = () => fixCheck(rules)
|
|
92
157
|
const repair = async () => {
|
|
@@ -94,8 +159,9 @@ export const tail = <T>(
|
|
|
94
159
|
await healthy(fix, { fixesSoFar: 1, escalations, guard, guardRests, rules })
|
|
95
160
|
}
|
|
96
161
|
// fixFirst's own fix + healthy below already is the full run.
|
|
97
|
-
|
|
98
|
-
|
|
162
|
+
// Truthy, not `true`: an untyped config's flags read as they always did.
|
|
163
|
+
if (options.fixFirst) await repair()
|
|
164
|
+
else if (!options.green) await healthy(fix, { escalations, guard, guardRests, rules })
|
|
99
165
|
// The lap runs once a tail: after its findings are fixed, review follows.
|
|
100
166
|
if ((await qualityLap(guard, lenses, lensBase)) === "findings") {
|
|
101
167
|
if (await fixQualityFindings(escalations, guard, guardRests, rules)) {
|
|
@@ -138,4 +204,4 @@ export const handOffReview = (
|
|
|
138
204
|
|
|
139
205
|
/** The tail the review and fix workflows run: ends at the hand-off review since `base`. */
|
|
140
206
|
export const handOffTail = (fixFirst: boolean, base: string, guard?: Guard): Promise<void> =>
|
|
141
|
-
tail(
|
|
207
|
+
tail((escalations) => handOffReview(base, escalations, guard), { fixFirst, guard })
|