@pmelab/gtd 23.1.0 → 24.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +13 -6
- package/dist/gtd.bundle.mjs +795 -1051
- package/dist/workflows.mjs +1952 -1725
- package/package.json +2 -1
- package/skills/authoring/SKILL.md +5 -4
- package/src/flows/factory.ts +5 -1
- package/src/flows/runtime.ts +3 -0
- package/src/workflow-kit/health.test.ts +30 -0
- package/src/workflow-kit/health.ts +86 -0
- package/src/workflow-kit/index.ts +7 -0
- package/src/workflow-kit/quality.test.ts +17 -0
- package/src/workflow-kit/quality.ts +45 -0
- package/src/workflow-kit/review.ts +324 -0
- package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
- package/src/workflows/feature/feature.test.ts +38 -0
- package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
- package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
- package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
- package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
- package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
- package/src/workflows/feature/review.ts +106 -0
- package/src/workflows/feature/steps.test.ts +82 -0
- package/src/workflows/feature/steps.ts +92 -0
- package/src/workflows/fix/fix.test.ts +94 -0
- package/src/workflows/fix/fix.ts +52 -0
- package/src/workflows/index.ts +44 -2
- package/src/workflows/review/review.test.ts +43 -0
- package/src/workflows/review/review.ts +31 -0
- package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
- package/src/workflows/{access.ts → shared/access.ts} +3 -5
- package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
- package/src/workflows/{health.ts → shared/health.ts} +38 -55
- package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
- package/src/workflows/{options.ts → shared/options.ts} +14 -2
- package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
- package/src/workflows/shared/scenarios.ts +35 -0
- package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
- package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
- package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
- package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
- package/src/workflows/shared/tail.test.ts +205 -0
- package/src/workflows/shared/tail.ts +135 -0
- package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
- package/src/workflows/{text.ts → shared/text.ts} +48 -7
- package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
- package/src/workflows/review.ts +0 -362
- /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
- /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
- /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
package/src/workflows/review.ts
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
answered,
|
|
3
|
-
changes,
|
|
4
|
-
changesSince,
|
|
5
|
-
hasThreadFor,
|
|
6
|
-
head,
|
|
7
|
-
judge,
|
|
8
|
-
numeric,
|
|
9
|
-
read,
|
|
10
|
-
refuse,
|
|
11
|
-
removeScript,
|
|
12
|
-
requireReplies,
|
|
13
|
-
requireThreadsClosed,
|
|
14
|
-
run,
|
|
15
|
-
scope,
|
|
16
|
-
vars,
|
|
17
|
-
type Change,
|
|
18
|
-
type JudgeQuestion,
|
|
19
|
-
} from "../flows/index.js"
|
|
20
|
-
import { reviewNotes, reviewRisks, stripCodeThreads, type ReviewNote } from "../steering/index.js"
|
|
21
|
-
import { escalation, FIX_CAP, healthy, type EscalationCount } from "./health.js"
|
|
22
|
-
import type { BuiltPlan } from "./packages.js"
|
|
23
|
-
import { guarded, type FrozenScenarios } from "./scenarios.js"
|
|
24
|
-
import {
|
|
25
|
-
answerReviewQuestions,
|
|
26
|
-
ARCHITECTURE,
|
|
27
|
-
awaitReview,
|
|
28
|
-
collecting,
|
|
29
|
-
fixCheck,
|
|
30
|
-
fixNits,
|
|
31
|
-
fixQuality,
|
|
32
|
-
fixRisks,
|
|
33
|
-
QUALITY,
|
|
34
|
-
REQUIREMENTS,
|
|
35
|
-
REVIEW,
|
|
36
|
-
reviewing,
|
|
37
|
-
reviewMissing,
|
|
38
|
-
reviewQuality,
|
|
39
|
-
} from "./steps.js"
|
|
40
|
-
import * as t from "./text.js"
|
|
41
|
-
|
|
42
|
-
/** Split a `qualityReviews` value into its lenses: on `,`, trimmed, blanks dropped. */
|
|
43
|
-
export const lensesOf = (value: string): readonly string[] =>
|
|
44
|
-
value
|
|
45
|
-
.split(",")
|
|
46
|
-
.map((lens) => lens.trim())
|
|
47
|
-
.filter((lens) => lens.length > 0)
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* One review turn per lens over the whole change, each appending every
|
|
51
|
-
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
52
|
-
* has any.
|
|
53
|
-
*/
|
|
54
|
-
export const qualityLap = async (): Promise<"clean" | "findings"> => {
|
|
55
|
-
for (const lens of lensesOf(vars.qualityReviews ?? "")) await reviewQuality(lens)
|
|
56
|
-
return (read(QUALITY) ?? "").length > 0 ? "findings" : "clean"
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
60
|
-
export const fixQualityFindings = async (
|
|
61
|
-
escalations: EscalationCount,
|
|
62
|
-
frozen?: FrozenScenarios,
|
|
63
|
-
): Promise<boolean> => {
|
|
64
|
-
for (let turns = 0; turns < FIX_CAP; turns++) {
|
|
65
|
-
await guarded(frozen, fixQuality)()
|
|
66
|
-
// Not `changes()`: a wording gate after the fix would be the last step.
|
|
67
|
-
if (read(QUALITY) === undefined) return true
|
|
68
|
-
}
|
|
69
|
-
await escalation(escalations)
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** How a review round ended. `feedback` carries what the human edited, for the re-unwind to undo. */
|
|
74
|
-
export type ReviewOutcome =
|
|
75
|
-
| { readonly verdict: "signoff" }
|
|
76
|
-
| {
|
|
77
|
-
readonly verdict: "feedback"
|
|
78
|
-
/** The last round's commit, the re-plan base. */
|
|
79
|
-
readonly base: string
|
|
80
|
-
/** The commit `.gtd/REVIEW.md` was written at; edits are restored from it. */
|
|
81
|
-
readonly restoreFrom: string
|
|
82
|
-
/** The code paths the rounds changed, with their content before. */
|
|
83
|
-
readonly edited: readonly Change[]
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
type Verdict = "edit" | "question" | "nit" | "praise"
|
|
87
|
-
|
|
88
|
-
const VERDICTS: readonly Verdict[] = ["edit", "question", "nit", "praise"]
|
|
89
|
-
|
|
90
|
-
/** One four-way choice per note the human added to `.gtd/REVIEW.md`. */
|
|
91
|
-
const verdictQuestion = (note: ReviewNote): JudgeQuestion => ({
|
|
92
|
-
id: note.id,
|
|
93
|
-
primitive: "choice",
|
|
94
|
-
instructions: `Classify the human's note on review item "${note.anchor}" (in .gtd/REVIEW.md). Its evidence holds the anchor, the reviewer's text and the human's text.`,
|
|
95
|
-
criteria:
|
|
96
|
-
"edit: a request to change behaviour, design or scope — anything needing a plan, or any note you are unsure about. question: asks something and wants an answer, with no change requested. nit: a small, local, unambiguous fix (naming, typo, formatting, comment wording) needing no re-plan. praise: an approving remark with nothing to do.",
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* One judge rest, one verdict per note. Dismissing a note is the risky
|
|
101
|
-
* direction, so only a confident non-`edit` verdict on uncut evidence counts;
|
|
102
|
-
* a cut, unanswered or below-floor note is an `edit`, and a blank floor makes
|
|
103
|
-
* every note one.
|
|
104
|
-
*/
|
|
105
|
-
const triage = async (notes: readonly ReviewNote[]): Promise<ReadonlyMap<string, Verdict>> => {
|
|
106
|
-
const { answers, truncated } = await judge("review.triage", {
|
|
107
|
-
questions: notes.map(verdictQuestion),
|
|
108
|
-
evidence: Object.fromEntries(
|
|
109
|
-
notes.map((n) => [
|
|
110
|
-
n.id,
|
|
111
|
-
`Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`,
|
|
112
|
-
]),
|
|
113
|
-
),
|
|
114
|
-
message: t.buildReviewTriageMessage(),
|
|
115
|
-
label: "Judging each note",
|
|
116
|
-
})
|
|
117
|
-
const minP = numeric(vars.reviewNoteActionable, Infinity)
|
|
118
|
-
return new Map(
|
|
119
|
-
notes.map((n): [string, Verdict] => {
|
|
120
|
-
const verdict = VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP))
|
|
121
|
-
return [n.id, truncated.includes(n.id) || verdict === undefined ? "edit" : verdict]
|
|
122
|
-
}),
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
|
|
127
|
-
|
|
128
|
-
/** A code edit, not a thread-only change: lines that only add, answer or remove code threads don't count. */
|
|
129
|
-
const isCodeEdit = (c: Change): boolean =>
|
|
130
|
-
isCode(c.path) &&
|
|
131
|
-
stripCodeThreads(c.path, c.before ?? "") !== stripCodeThreads(c.path, c.after ?? "")
|
|
132
|
-
|
|
133
|
-
const collect = async (capture: string): Promise<"signoff" | "feedback"> => {
|
|
134
|
-
await collecting(capture)
|
|
135
|
-
if (read(REQUIREMENTS) !== undefined) return "feedback"
|
|
136
|
-
if (changes().length === 0) return "signoff"
|
|
137
|
-
return refuse(
|
|
138
|
-
"gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing",
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** A review with ticks, footnote markers and thread definitions removed: what is left is line notes. */
|
|
143
|
-
const notesOf = (text: string): string =>
|
|
144
|
-
text
|
|
145
|
-
.replace(/^\[\^[^\]]+\]:[^\n]*(\n(?: {2,}|\t)[^\n]*|\n(?=\s*\n(?: {2,}|\t)))*/gm, "")
|
|
146
|
-
.replace(/\[\^[^\]]+\]/g, "")
|
|
147
|
-
.replace(/\[x\]/gi, "[ ]")
|
|
148
|
-
.replace(/\s+/g, " ")
|
|
149
|
-
.trim()
|
|
150
|
-
|
|
151
|
-
/** Whether anything but thread edits and ticks landed in `.gtd/REVIEW.md` since `commit`. */
|
|
152
|
-
const notedSince = (commit: string): boolean =>
|
|
153
|
-
changesSince(commit).some(
|
|
154
|
-
(c) => c.path === REVIEW && notesOf(c.before ?? "") !== notesOf(read(REVIEW) ?? ""),
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* The human gate and its reply rounds: a question gets an answer inside
|
|
159
|
-
* `.gtd/REVIEW.md` and rests at the gate again, without a lap. Resolves the
|
|
160
|
-
* commit of the last `review.collecting` turn, `"missing"` when the round
|
|
161
|
-
* left no review, or `undefined` when no such turn ran.
|
|
162
|
-
*/
|
|
163
|
-
const converse = async (base: string, collectedBefore?: string): Promise<string | undefined> => {
|
|
164
|
-
let collectedAt = collectedBefore
|
|
165
|
-
for (;;) {
|
|
166
|
-
await awaitReview(base)
|
|
167
|
-
if (changes(REVIEW).some((c) => c.status === "deleted")) {
|
|
168
|
-
refuse(
|
|
169
|
-
"gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
if (read(REVIEW) === undefined) {
|
|
173
|
-
await reviewMissing(head())
|
|
174
|
-
return "missing"
|
|
175
|
-
}
|
|
176
|
-
requireThreadsClosed(REVIEW)
|
|
177
|
-
if (!hasThreadFor("agent", REVIEW)) return collectedAt
|
|
178
|
-
await collecting(t.reviewEditsCapture(head()))
|
|
179
|
-
collectedAt = head()
|
|
180
|
-
requireReplies(REVIEW)
|
|
181
|
-
if (!hasThreadFor("human", REVIEW)) return collectedAt
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* What the in-loop `review.collecting` turn left: `folded` when it wrote
|
|
187
|
-
* REQUIREMENTS.md (those folds need their lap whatever is judged later),
|
|
188
|
-
* `settled` when only thread edits and ticks landed after it.
|
|
189
|
-
*/
|
|
190
|
-
const afterCollect = (collectedAt: string | undefined): { folded: boolean; settled: boolean } =>
|
|
191
|
-
// While the collecting turn is still pending its commit has no hash, so no
|
|
192
|
-
// range can start there; the flow rests at `review.closing` before it matters.
|
|
193
|
-
collectedAt === "" ? { folded: false, settled: false } : settledAfter(collectedAt)
|
|
194
|
-
|
|
195
|
-
const settledAfter = (collectedAt: string | undefined): { folded: boolean; settled: boolean } => ({
|
|
196
|
-
folded: collectedAt !== undefined && read(REQUIREMENTS) !== undefined,
|
|
197
|
-
settled:
|
|
198
|
-
collectedAt !== undefined &&
|
|
199
|
-
changesSince(collectedAt).every((c) => !isCodeEdit(c)) &&
|
|
200
|
-
!notedSince(collectedAt),
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
/** What `finish` hands back to `review`: an outcome, or another lap at the gate. */
|
|
204
|
-
type Finish =
|
|
205
|
-
| ReviewOutcome
|
|
206
|
-
| { readonly next: "await"; readonly reviewed: string }
|
|
207
|
-
| { readonly next: "rereview"; readonly carry: string | undefined }
|
|
208
|
-
|
|
209
|
-
/** What a round's routing needs from `finish`: its closing step and the ways it ends. */
|
|
210
|
-
interface Round {
|
|
211
|
-
readonly round: string
|
|
212
|
-
readonly escalations: EscalationCount
|
|
213
|
-
readonly frozen: FrozenScenarios | undefined
|
|
214
|
-
readonly close: () => Promise<void>
|
|
215
|
-
readonly outcome: (verdict: "signoff" | "feedback") => ReviewOutcome
|
|
216
|
-
readonly unfolded: () => Promise<Finish>
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** Judge every note, then run each verdict's route: answers, then nit fixes, then the edits' lap. */
|
|
220
|
-
const routeNotes = async (notes: readonly ReviewNote[], r: Round): Promise<Finish> => {
|
|
221
|
-
const verdicts = await triage(notes)
|
|
222
|
-
const of = (v: Verdict): ReviewNote[] => notes.filter((n) => verdicts.get(n.id) === v)
|
|
223
|
-
const [edits, questions, nits] = [of("edit"), of("question"), of("nit")]
|
|
224
|
-
if (edits.length + questions.length + nits.length === 0) return r.unfolded()
|
|
225
|
-
|
|
226
|
-
let answeredAt: string | undefined
|
|
227
|
-
if (questions.length > 0) {
|
|
228
|
-
await answerReviewQuestions(questions)
|
|
229
|
-
answeredAt = head()
|
|
230
|
-
}
|
|
231
|
-
if (nits.length > 0) {
|
|
232
|
-
await guarded(r.frozen, () => fixNits(nits), "review")()
|
|
233
|
-
await healthy(guarded(r.frozen, fixCheck, "review"), { escalations: r.escalations })
|
|
234
|
-
}
|
|
235
|
-
if (edits.length > 0) {
|
|
236
|
-
await r.close()
|
|
237
|
-
return r.outcome(await collect(t.reviewEditNotesCapture(r.round, edits, answeredAt)))
|
|
238
|
-
}
|
|
239
|
-
if (nits.length > 0) {
|
|
240
|
-
await r.close()
|
|
241
|
-
return { next: "rereview", carry: answeredAt }
|
|
242
|
-
}
|
|
243
|
-
return { next: "await", reviewed: answeredAt! }
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const finish = async (
|
|
247
|
-
reviewed: string,
|
|
248
|
-
collectedAt: string | undefined,
|
|
249
|
-
escalations: EscalationCount,
|
|
250
|
-
frozen: FrozenScenarios | undefined,
|
|
251
|
-
): Promise<Finish> => {
|
|
252
|
-
// Everything the human did is read before any agent turn runs, so a nit fix
|
|
253
|
-
// is never counted as a hand-edit.
|
|
254
|
-
const round = head()
|
|
255
|
-
const since = changesSince(reviewed)
|
|
256
|
-
const edited = since.filter(isCodeEdit)
|
|
257
|
-
const baselineText = since.get(REVIEW)?.before ?? ""
|
|
258
|
-
const record = read(REVIEW) ?? ""
|
|
259
|
-
const { folded, settled } = afterCollect(collectedAt)
|
|
260
|
-
// The review gate clears every tick before its commit, so a changed
|
|
261
|
-
// REVIEW.md is a note, never a tick.
|
|
262
|
-
const noted = since.some((c) => c.path === REVIEW)
|
|
263
|
-
const close = (): Promise<void> =>
|
|
264
|
-
run("review.closing", removeScript([REVIEW]), { label: "Closing the review", base: round })
|
|
265
|
-
const outcome = (verdict: "signoff" | "feedback"): ReviewOutcome =>
|
|
266
|
-
verdict === "signoff" ? { verdict } : { verdict, base: round, restoreFrom: reviewed, edited }
|
|
267
|
-
const unfolded = async (): Promise<Finish> => {
|
|
268
|
-
await close()
|
|
269
|
-
return outcome(folded ? "feedback" : "signoff")
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const collectWith = async (capture: string): Promise<Finish> => {
|
|
273
|
-
await close()
|
|
274
|
-
return outcome(await collect(capture))
|
|
275
|
-
}
|
|
276
|
-
if (settled || (edited.length === 0 && !noted)) return unfolded()
|
|
277
|
-
if (edited.length > 0) return collectWith(t.reviewEditsCapture(round))
|
|
278
|
-
const notes = reviewNotes(baselineText, record)
|
|
279
|
-
if (notes.length === 0) return collectWith(t.reviewNotesCapture(round))
|
|
280
|
-
return routeNotes(notes, { round, escalations, frozen, close, outcome, unfolded })
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
|
|
284
|
-
const reviewOnce = async (
|
|
285
|
-
base: string,
|
|
286
|
-
carry: string | undefined,
|
|
287
|
-
escalations: EscalationCount,
|
|
288
|
-
frozen: FrozenScenarios | undefined,
|
|
289
|
-
): Promise<void> => {
|
|
290
|
-
await reviewing(base, carry)
|
|
291
|
-
const risks = reviewRisks(read(REVIEW) ?? "")
|
|
292
|
-
if (risks.length === 0) return
|
|
293
|
-
await guarded(frozen, () => fixRisks(risks), "review")()
|
|
294
|
-
await healthy(guarded(frozen, fixCheck, "review"), { escalations })
|
|
295
|
-
await reviewing(base, carry)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
|
|
300
|
-
* reviews and signs off or comments, and each note is judged: edits go to
|
|
301
|
-
* another planning lap, questions are answered in place, nits fixed in one
|
|
302
|
-
* batch, praise dropped.
|
|
303
|
-
*/
|
|
304
|
-
export const review = async (
|
|
305
|
-
base: string,
|
|
306
|
-
escalations: EscalationCount = { rounds: 0 },
|
|
307
|
-
frozen?: FrozenScenarios,
|
|
308
|
-
): Promise<ReviewOutcome> => {
|
|
309
|
-
let carry: string | undefined
|
|
310
|
-
for (;;) {
|
|
311
|
-
await reviewOnce(base, carry, escalations, frozen)
|
|
312
|
-
carry = undefined
|
|
313
|
-
let reviewed = head()
|
|
314
|
-
let collectedAt: string | undefined
|
|
315
|
-
for (;;) {
|
|
316
|
-
collectedAt = await converse(base, collectedAt)
|
|
317
|
-
if (collectedAt === "missing") break
|
|
318
|
-
const result = await finish(reviewed, collectedAt, escalations, frozen)
|
|
319
|
-
if (!("next" in result)) return result
|
|
320
|
-
if (result.next === "rereview") {
|
|
321
|
-
carry = result.carry
|
|
322
|
-
break
|
|
323
|
-
}
|
|
324
|
-
reviewed = result.reviewed
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/** The build tail: fix (when entered red), keep green, the quality lap, then human review since `base`. */
|
|
330
|
-
export const buildTail = (
|
|
331
|
-
fixFirst: boolean,
|
|
332
|
-
base: string,
|
|
333
|
-
built?: BuiltPlan,
|
|
334
|
-
): Promise<ReviewOutcome> =>
|
|
335
|
-
scope("build", async () => {
|
|
336
|
-
const frozen = built?.frozen
|
|
337
|
-
const escalations: EscalationCount = { rounds: 0 }
|
|
338
|
-
const guardedFix = guarded(frozen, () => fixCheck())
|
|
339
|
-
let redFirst = fixFirst
|
|
340
|
-
// fixFirst's own fix + healthy below already is the full run.
|
|
341
|
-
if (!fixFirst) {
|
|
342
|
-
await healthy(
|
|
343
|
-
guarded(frozen, () => fixCheck(built)),
|
|
344
|
-
{ escalations, sweepOnGreen: [ARCHITECTURE] },
|
|
345
|
-
)
|
|
346
|
-
}
|
|
347
|
-
// The lap runs once a tail: after its findings are fixed, review follows.
|
|
348
|
-
let lapped = false
|
|
349
|
-
for (;;) {
|
|
350
|
-
if (redFirst) {
|
|
351
|
-
await guardedFix()
|
|
352
|
-
await healthy(guardedFix, { fixesSoFar: 1, escalations })
|
|
353
|
-
redFirst = false
|
|
354
|
-
}
|
|
355
|
-
const lap = lapped ? "clean" : await qualityLap()
|
|
356
|
-
lapped = true
|
|
357
|
-
if (lap === "clean") return review(base, escalations, frozen)
|
|
358
|
-
if (await fixQualityFindings(escalations, frozen)) {
|
|
359
|
-
await healthy(guardedFix, { escalations })
|
|
360
|
-
} else redFirst = true
|
|
361
|
-
}
|
|
362
|
-
})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|