@pmelab/gtd 38.2.0 → 39.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 +11 -4
- package/dist/chunks/{reserved-C3PgP7XB.mjs → reserved-B6tTYjeb.mjs} +22 -5
- package/dist/chunks/{tail-Bkxki2nK.mjs → tail-_CZTP7Hu.mjs} +1 -1
- package/dist/gtd.bundle.mjs +14 -5
- package/dist/testkit.mjs +14 -5
- package/dist/workflow-kit.mjs +12 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +2 -2
- 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 +34 -6
- package/package.json +1 -1
- package/src/cli/Cli.ts +4 -3
- package/src/cli/Metrics.test.ts +31 -0
- package/src/cli/Metrics.ts +8 -0
- package/src/workflow-kit/health.ts +15 -1
- package/src/workflows/cleanup/cleanup.test.ts +2 -0
- package/src/workflows/feature/refactor.test.ts +4 -0
- package/src/workflows/rollout/rollout.feature +16 -0
- package/src/workflows/rollout/rollout.test.ts +2 -0
- package/src/workflows/shared/health.test.ts +2 -0
- package/src/workflows/shared/health.ts +34 -5
- package/src/workflows/shared/recheck.feature +60 -0
package/README.md
CHANGED
|
@@ -213,9 +213,10 @@ Label: Checking the baseline
|
|
|
213
213
|
```
|
|
214
214
|
|
|
215
215
|
A green suite leaves the tree clean, so the process moves on to triage. A red
|
|
216
|
-
suite instead writes `.gtd/FEEDBACK.md
|
|
217
|
-
`start-gate.
|
|
218
|
-
what the beat left in the
|
|
216
|
+
suite instead writes `.gtd/FEEDBACK.md`; the process runs it once more at
|
|
217
|
+
`start-gate.recheck`, and only a second red goes to `start-gate.blocked` — same
|
|
218
|
+
two commands, different outcome, decided entirely by what the beat left in the
|
|
219
|
+
tree.
|
|
219
220
|
|
|
220
221
|
Land the green one, and the beat after it belongs to an agent.
|
|
221
222
|
|
|
@@ -508,7 +509,10 @@ you open the `review` door.
|
|
|
508
509
|
as-is, handing that document to the next fix turn as its instruction. Two
|
|
509
510
|
such rounds with no fix is the terminal stop: the last analysis stays on disk
|
|
510
511
|
instead of a third being written, and editing it is what gets the next
|
|
511
|
-
attempt anywhere new.
|
|
512
|
+
attempt anywhere new. Before any escalation or red-baseline stop the suite
|
|
513
|
+
runs once more; a pass there continues without you and is recorded as
|
|
514
|
+
`Gtd-Mark: flaky=<check>` (see
|
|
515
|
+
[Re-check before a person](docs/configuration.md#re-check-before-a-person)).
|
|
512
516
|
|
|
513
517
|
4. **You review — the refactor, or on request.** The feature workflow asks for
|
|
514
518
|
one review only on `refactor-first`: of the refactor slice, before the
|
|
@@ -745,6 +749,9 @@ it never replays and a config changed since is no refusal:
|
|
|
745
749
|
- **findings** — the net count of `## ` findings each quality-lap lens added to
|
|
746
750
|
`.gtd/QUALITY.md`, keyed by the lap's lens path (`build.quality.correctness`,
|
|
747
751
|
`refactor.build.quality.correctness`, …)
|
|
752
|
+
- **flaky** — red checks that passed on their re-check (`Gtd-Mark: flaky=…`),
|
|
753
|
+
keyed by the re-check's full step name (`start-gate.recheck`,
|
|
754
|
+
`build.health.recheck`, …)
|
|
748
755
|
|
|
749
756
|
`--json` prints `{ processes, total }`; `--json=total.refactorFirstRate` prints
|
|
750
757
|
one value. Model, phase and lens keys may hold dots a path cannot reach: select
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, displayPath, env, head, human, judge, numeric, quote, read, refuse, scope, skillsFor, start, vars } from "../flows.mjs";
|
|
1
|
+
import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, displayPath, env, head, human, judge, mark, numeric, quote, read, refuse, scope, skillsFor, start, vars } from "../flows.mjs";
|
|
2
2
|
import { escalation as escalation$1, gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
|
|
3
3
|
import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct, n as Boolean$, s as Number$ } from "./Schema-C6fLlQzx.mjs";
|
|
4
4
|
//#region src/workflows/shared/registry.ts
|
|
@@ -1580,6 +1580,19 @@ const baseline = (name, label = "Checking the baseline") => check(name, env.test
|
|
|
1580
1580
|
report: FEEDBACK,
|
|
1581
1581
|
label
|
|
1582
1582
|
});
|
|
1583
|
+
/**
|
|
1584
|
+
* Re-run a red check as step `name` before a person sees it. Green records
|
|
1585
|
+
* `Gtd-Mark: flaky=<red>` on the re-check's landing and resolves `true`.
|
|
1586
|
+
*/
|
|
1587
|
+
const recheck = async (name, red, options = {}) => {
|
|
1588
|
+
const green = await check(name, env.testCommand ?? "", {
|
|
1589
|
+
...options,
|
|
1590
|
+
report: FEEDBACK,
|
|
1591
|
+
label: "Re-running checks"
|
|
1592
|
+
});
|
|
1593
|
+
if (green) mark("flaky", red);
|
|
1594
|
+
return green;
|
|
1595
|
+
};
|
|
1583
1596
|
const escalationSteps = (rules) => ({
|
|
1584
1597
|
limit: 2,
|
|
1585
1598
|
describe: () => describeEscalation(rules),
|
|
@@ -1630,15 +1643,17 @@ const sameFailure = async (previous, current) => {
|
|
|
1630
1643
|
});
|
|
1631
1644
|
return comparable && truncated.length === 0 && answered(answers.verdict, "identical", numeric(vars.judgeIdenticalMinP, Infinity));
|
|
1632
1645
|
};
|
|
1646
|
+
const healthSweep = (options) => [ESCALATION, ...options.sweepOnGreen ?? []];
|
|
1633
1647
|
const runSuite = (options) => check("health.check", env.testCommand ?? "", {
|
|
1634
1648
|
report: FEEDBACK,
|
|
1635
1649
|
label: "Running checks",
|
|
1636
|
-
sweepOnGreen:
|
|
1650
|
+
sweepOnGreen: healthSweep(options)
|
|
1637
1651
|
});
|
|
1638
1652
|
/**
|
|
1639
1653
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
1640
1654
|
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
1641
|
-
* identical to the previous round's.
|
|
1655
|
+
* identical to the previous round's. An escalation re-runs the suite once
|
|
1656
|
+
* first; a green re-run ends the loop.
|
|
1642
1657
|
*/
|
|
1643
1658
|
const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
1644
1659
|
check: () => runSuite(options),
|
|
@@ -1647,14 +1662,16 @@ const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
|
1647
1662
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
1648
1663
|
sameFailure,
|
|
1649
1664
|
escalations: options.escalations ?? { rounds: 0 },
|
|
1665
|
+
recheck: () => recheck("health.recheck", "health.check", { sweepOnGreen: healthSweep(options) }),
|
|
1650
1666
|
escalation: guardedSteps(options.guard, options.describeGuard, options.guardRests, options.rules)
|
|
1651
1667
|
});
|
|
1652
1668
|
/**
|
|
1653
|
-
* Hold the process at `<name>.blocked` until the suite is green
|
|
1669
|
+
* Hold the process at `<name>.blocked` until the suite is green, re-running a
|
|
1670
|
+
* red check once as `<name>.recheck` before resting. `guard` runs
|
|
1654
1671
|
* after each `blocked` landing only — never after a check, whose refusal would
|
|
1655
1672
|
* re-run the same script forever.
|
|
1656
1673
|
*/
|
|
1657
|
-
const gate = (name, message, guard) => gate$1(name, () => baseline("check"), guarded(() => human("blocked", {
|
|
1674
|
+
const gate = (name, message, guard) => gate$1(name, async () => await baseline("check") || recheck("recheck", `${name}.check`), guarded(() => human("blocked", {
|
|
1658
1675
|
message,
|
|
1659
1676
|
label: "Baseline is red",
|
|
1660
1677
|
file: FEEDBACK
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
|
|
2
2
|
import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import { C as fixCheck, D as reviewMissing, O as reviewQuality, T as fixRisks, Y as handOffAwaitReviewMessage, d as guarded, f as healthy, h as QUALITY, k as reviewing, l as escalation, w as fixQuality, x as awaitReview, y as REVIEW } from "./reserved-
|
|
3
|
+
import { C as fixCheck, D as reviewMissing, O as reviewQuality, T as fixRisks, Y as handOffAwaitReviewMessage, d as guarded, f as healthy, h as QUALITY, k as reviewing, l as escalation, w as fixQuality, x as awaitReview, y as REVIEW } from "./reserved-B6tTYjeb.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";
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -41406,7 +41406,8 @@ const emptyTally = () => ({
|
|
|
41406
41406
|
naive: 0,
|
|
41407
41407
|
rebuild: 0
|
|
41408
41408
|
},
|
|
41409
|
-
byLens: record()
|
|
41409
|
+
byLens: record(),
|
|
41410
|
+
flaky: record()
|
|
41410
41411
|
});
|
|
41411
41412
|
const sum = (record) => Object.values(record).reduce((a, b) => a + b, 0);
|
|
41412
41413
|
const finish = (t) => ({
|
|
@@ -41426,6 +41427,10 @@ const finish = (t) => ({
|
|
|
41426
41427
|
findings: {
|
|
41427
41428
|
total: sum(t.byLens),
|
|
41428
41429
|
byLens: t.byLens
|
|
41430
|
+
},
|
|
41431
|
+
flaky: {
|
|
41432
|
+
total: sum(t.flaky),
|
|
41433
|
+
byStep: t.flaky
|
|
41429
41434
|
}
|
|
41430
41435
|
});
|
|
41431
41436
|
const tallyCost = (t, open, costs, phase) => {
|
|
@@ -41482,6 +41487,7 @@ const processMetrics = (commits) => {
|
|
|
41482
41487
|
if (subject.actor === "agent" && CONTINUE.test(at)) add(t.continuations, at.replace(CONTINUE, "$1"), 1);
|
|
41483
41488
|
const lens = LENS.exec(at)?.[1];
|
|
41484
41489
|
if (lens !== void 0 && commit.findings > 0) add(t.byLens, lens, commit.findings);
|
|
41490
|
+
if (message.marks.some((m) => m.name === "flaky")) add(t.flaky, at, 1);
|
|
41485
41491
|
tallyDecision(t, open, at, subject.to);
|
|
41486
41492
|
}
|
|
41487
41493
|
return finish(t);
|
|
@@ -41496,6 +41502,7 @@ const totalMetrics = (all) => {
|
|
|
41496
41502
|
t.doubleWork.naive += m.cost.doubleWork.naive;
|
|
41497
41503
|
t.doubleWork.rebuild += m.cost.doubleWork.rebuild;
|
|
41498
41504
|
for (const [k, v] of Object.entries(m.findings.byLens)) add(t.byLens, k, v);
|
|
41505
|
+
for (const [k, v] of Object.entries(m.flaky.byStep)) add(t.flaky, k, v);
|
|
41499
41506
|
}
|
|
41500
41507
|
return {
|
|
41501
41508
|
processes: all.length,
|
|
@@ -41533,7 +41540,8 @@ const line = (label, m) => {
|
|
|
41533
41540
|
`${label}: ${slices.decided} slices, ${slices.rebuilt} rebuilt (refactor-first ${percent}), ${slices.refactored} refactored`,
|
|
41534
41541
|
`${continued} continuations${continued > 0 ? ` (${listed(m.continuations)})` : ""}`,
|
|
41535
41542
|
`cost ${cost.total} (${listed(cost.byPhase)}; by model ${listed(cost.byModel) || "-"}), double work ${cost.doubleWork.total}`,
|
|
41536
|
-
`${m.findings.total} findings${m.findings.total > 0 ? ` (${listed(m.findings.byLens)})` : ""}
|
|
41543
|
+
`${m.findings.total} findings${m.findings.total > 0 ? ` (${listed(m.findings.byLens)})` : ""}`,
|
|
41544
|
+
`${m.flaky.total} flaky${m.flaky.total > 0 ? ` (${listed(m.flaky.byStep)})` : ""}`
|
|
41537
41545
|
].join("; ");
|
|
41538
41546
|
};
|
|
41539
41547
|
const runHistoryMetricsCommand = (refs, json, out) => gen(function* () {
|
|
@@ -42943,9 +42951,10 @@ const COMMAND_ROWS = [
|
|
|
42943
42951
|
"Report how the kept processes went: slices whose naive pass",
|
|
42944
42952
|
"landed or was rebuilt (the refactor-first rate), continuations",
|
|
42945
42953
|
"per phase, Gtd-Cost by model and phase with the double work of",
|
|
42946
|
-
"a rebuild,
|
|
42947
|
-
"
|
|
42948
|
-
"
|
|
42954
|
+
"a rebuild, quality-lap findings per lens, and red checks that",
|
|
42955
|
+
"passed on their re-check — per process and in total. Each <ref>",
|
|
42956
|
+
"is read as by history show, but a branch name means every",
|
|
42957
|
+
"process kept from it; none means every ref.",
|
|
42949
42958
|
"Reads commit subjects and trailers, never replays, so a",
|
|
42950
42959
|
"changed config is no refusal. --json prints { processes, total",
|
|
42951
42960
|
"} as one document; writes nothing"
|
package/dist/testkit.mjs
CHANGED
|
@@ -10327,7 +10327,8 @@ const emptyTally = () => ({
|
|
|
10327
10327
|
naive: 0,
|
|
10328
10328
|
rebuild: 0
|
|
10329
10329
|
},
|
|
10330
|
-
byLens: record()
|
|
10330
|
+
byLens: record(),
|
|
10331
|
+
flaky: record()
|
|
10331
10332
|
});
|
|
10332
10333
|
const sum = (record) => Object.values(record).reduce((a, b) => a + b, 0);
|
|
10333
10334
|
const finish = (t) => ({
|
|
@@ -10347,6 +10348,10 @@ const finish = (t) => ({
|
|
|
10347
10348
|
findings: {
|
|
10348
10349
|
total: sum(t.byLens),
|
|
10349
10350
|
byLens: t.byLens
|
|
10351
|
+
},
|
|
10352
|
+
flaky: {
|
|
10353
|
+
total: sum(t.flaky),
|
|
10354
|
+
byStep: t.flaky
|
|
10350
10355
|
}
|
|
10351
10356
|
});
|
|
10352
10357
|
const tallyCost = (t, open, costs, phase) => {
|
|
@@ -10403,6 +10408,7 @@ const processMetrics = (commits) => {
|
|
|
10403
10408
|
if (subject.actor === "agent" && CONTINUE.test(at)) add(t.continuations, at.replace(CONTINUE, "$1"), 1);
|
|
10404
10409
|
const lens = LENS.exec(at)?.[1];
|
|
10405
10410
|
if (lens !== void 0 && commit.findings > 0) add(t.byLens, lens, commit.findings);
|
|
10411
|
+
if (message.marks.some((m) => m.name === "flaky")) add(t.flaky, at, 1);
|
|
10406
10412
|
tallyDecision(t, open, at, subject.to);
|
|
10407
10413
|
}
|
|
10408
10414
|
return finish(t);
|
|
@@ -10417,6 +10423,7 @@ const totalMetrics = (all) => {
|
|
|
10417
10423
|
t.doubleWork.naive += m.cost.doubleWork.naive;
|
|
10418
10424
|
t.doubleWork.rebuild += m.cost.doubleWork.rebuild;
|
|
10419
10425
|
for (const [k, v] of Object.entries(m.findings.byLens)) add(t.byLens, k, v);
|
|
10426
|
+
for (const [k, v] of Object.entries(m.flaky.byStep)) add(t.flaky, k, v);
|
|
10420
10427
|
}
|
|
10421
10428
|
return {
|
|
10422
10429
|
processes: all.length,
|
|
@@ -10454,7 +10461,8 @@ const line = (label, m) => {
|
|
|
10454
10461
|
`${label}: ${slices.decided} slices, ${slices.rebuilt} rebuilt (refactor-first ${percent}), ${slices.refactored} refactored`,
|
|
10455
10462
|
`${continued} continuations${continued > 0 ? ` (${listed(m.continuations)})` : ""}`,
|
|
10456
10463
|
`cost ${cost.total} (${listed(cost.byPhase)}; by model ${listed(cost.byModel) || "-"}), double work ${cost.doubleWork.total}`,
|
|
10457
|
-
`${m.findings.total} findings${m.findings.total > 0 ? ` (${listed(m.findings.byLens)})` : ""}
|
|
10464
|
+
`${m.findings.total} findings${m.findings.total > 0 ? ` (${listed(m.findings.byLens)})` : ""}`,
|
|
10465
|
+
`${m.flaky.total} flaky${m.flaky.total > 0 ? ` (${listed(m.flaky.byStep)})` : ""}`
|
|
10458
10466
|
].join("; ");
|
|
10459
10467
|
};
|
|
10460
10468
|
const runHistoryMetricsCommand = (refs, json, out) => gen(function* () {
|
|
@@ -11855,9 +11863,10 @@ const COMMAND_ROWS = [
|
|
|
11855
11863
|
"Report how the kept processes went: slices whose naive pass",
|
|
11856
11864
|
"landed or was rebuilt (the refactor-first rate), continuations",
|
|
11857
11865
|
"per phase, Gtd-Cost by model and phase with the double work of",
|
|
11858
|
-
"a rebuild,
|
|
11859
|
-
"
|
|
11860
|
-
"
|
|
11866
|
+
"a rebuild, quality-lap findings per lens, and red checks that",
|
|
11867
|
+
"passed on their re-check — per process and in total. Each <ref>",
|
|
11868
|
+
"is read as by history show, but a branch name means every",
|
|
11869
|
+
"process kept from it; none means every ref.",
|
|
11861
11870
|
"Reads commit subjects and trailers, never replays, so a",
|
|
11862
11871
|
"changed config is no refusal. --json prints { processes, total",
|
|
11863
11872
|
"} as one document; writes nothing"
|
package/dist/workflow-kit.mjs
CHANGED
|
@@ -17,10 +17,15 @@ const untilSettled = async (check, pending) => {
|
|
|
17
17
|
};
|
|
18
18
|
const STAMP = /\n<!-- gtd check [0-9a-f]+ -->\n?$/;
|
|
19
19
|
const stripStamp = (text) => text.replace(STAMP, "\n");
|
|
20
|
+
/** `true` when the re-check passed, else the red re-check's report — the one the escalation describes. */
|
|
21
|
+
const recheckReport = async (health) => {
|
|
22
|
+
if (health.recheck === void 0) return void 0;
|
|
23
|
+
return await health.recheck() || stripStamp(read(health.report) ?? "");
|
|
24
|
+
};
|
|
20
25
|
/**
|
|
21
26
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
22
27
|
* first once `fixCap` fixes are spent, or `sameFailure` calls the failure
|
|
23
|
-
* identical to the previous round's.
|
|
28
|
+
* identical to the previous round's, unless `recheck` passes.
|
|
24
29
|
*/
|
|
25
30
|
const healthy = async (fix, health) => {
|
|
26
31
|
let fixes = health.fixesSoFar ?? 0;
|
|
@@ -34,6 +39,12 @@ const healthy = async (fix, health) => {
|
|
|
34
39
|
const stuck = previous !== void 0 && await health.sameFailure(previous, current);
|
|
35
40
|
previous = current;
|
|
36
41
|
if (stuck || fixes >= health.fixCap) {
|
|
42
|
+
const rechecked = await recheckReport(health);
|
|
43
|
+
if (rechecked === true) {
|
|
44
|
+
health.escalations.rounds = 0;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
previous = rechecked ?? previous;
|
|
37
48
|
await escalation(health.escalations, health.escalation);
|
|
38
49
|
fixes = 0;
|
|
39
50
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import { Dt as registryEntry, Ot as requireFlag, S as coder, St as envDefaults, Tt as flagReferences, i as skills, n as summary, o as flagShipped, r as access, t as steering, xt as defaults } from "../chunks/reserved-
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { Dt as registryEntry, Ot as requireFlag, S as coder, St as envDefaults, Tt as flagReferences, i as skills, n as summary, o as flagShipped, r as access, t as steering, xt as defaults } from "../chunks/reserved-B6tTYjeb.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-_CZTP7Hu.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 { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, 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, wrote } from "../flows.mjs";
|
|
2
2
|
import { lensEntriesOf, review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as naiveApplyFailure, A as agentWithSkills, B as coverageQuestion, C as fixCheck, Ct as testFileGlobs, D as reviewMissing, E as planner, F as buildReviewTriageMessage, G as designSystem, H as criterionLine, I as builderSystem, K as designTriagePrompt, L as clearCriteriaMessage, M as buildReviewAwaitAfterDeferMessage, N as buildReviewCollectingPrompt, P as buildReviewFixNitsPrompt, Q as implementFixPrompt, R as clearCriteriaQuestion, S as coder, St as envDefaults, T as fixRisks, U as deferNotesScript, V as coveringPrompt, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REFACTOR_NOTE, _t as startGateBlockedMessage, a as flagModeOn, at as refactorDroppedRefusal, b as SLICES, bt as unwindFailure, ct as refactorRules, d as guarded, et as naivePassPrompt, f as healthy, ft as reviewNotesCapture, g as REFACTOR, gt as slicingPrompt, h as QUALITY, ht as seamSteerMessage, i as skills, it as refactorBuildingPrompt, j as buildReviewAnswerQuestionsPrompt, k as reviewing, lt as reviewEditNotesCapture, m as HANDOFF, mt as seamByHandMessage, n as summary, nt as oversizedMessage, ot as refactorFixPrompt, p as FEEDBACK, pt as reviewerSystem, r as access, rt as qualityUnresolvedMessage, s as shipped, st as refactorNotePrompt, t as steering, tt as normaliseCriteriaScript, u as gate, ut as reviewEditsCapture, v as REQUIREMENTS, vt as uncoveredMessage, x as awaitReview, xt as defaults, y as REVIEW, yt as unwindFailedMessage, z as coverageMessage } from "../chunks/reserved-
|
|
4
|
-
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-
|
|
3
|
+
import { $ as naiveApplyFailure, A as agentWithSkills, B as coverageQuestion, C as fixCheck, Ct as testFileGlobs, D as reviewMissing, E as planner, F as buildReviewTriageMessage, G as designSystem, H as criterionLine, I as builderSystem, K as designTriagePrompt, L as clearCriteriaMessage, M as buildReviewAwaitAfterDeferMessage, N as buildReviewCollectingPrompt, P as buildReviewFixNitsPrompt, Q as implementFixPrompt, R as clearCriteriaQuestion, S as coder, St as envDefaults, T as fixRisks, U as deferNotesScript, V as coveringPrompt, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REFACTOR_NOTE, _t as startGateBlockedMessage, a as flagModeOn, at as refactorDroppedRefusal, b as SLICES, bt as unwindFailure, ct as refactorRules, d as guarded, et as naivePassPrompt, f as healthy, ft as reviewNotesCapture, g as REFACTOR, gt as slicingPrompt, h as QUALITY, ht as seamSteerMessage, i as skills, it as refactorBuildingPrompt, j as buildReviewAnswerQuestionsPrompt, k as reviewing, lt as reviewEditNotesCapture, m as HANDOFF, mt as seamByHandMessage, n as summary, nt as oversizedMessage, ot as refactorFixPrompt, p as FEEDBACK, pt as reviewerSystem, r as access, rt as qualityUnresolvedMessage, s as shipped, st as refactorNotePrompt, t as steering, tt as normaliseCriteriaScript, u as gate, ut as reviewEditsCapture, v as REQUIREMENTS, vt as uncoveredMessage, x as awaitReview, xt as defaults, y as REVIEW, yt as unwindFailedMessage, z as coverageMessage } from "../chunks/reserved-B6tTYjeb.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-_CZTP7Hu.mjs";
|
|
5
5
|
import { posix } from "node:path";
|
|
6
6
|
//#region src/workflows/shared/fences.ts
|
|
7
7
|
const markFenced = (text) => {
|
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 { A as agentWithSkills, J as fixReviewNotesPrompt, S as coder, St as envDefaults, c as baseline, i as skills, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, xt as defaults, y as REVIEW } from "../chunks/reserved-
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { A as agentWithSkills, J as fixReviewNotesPrompt, S as coder, St as envDefaults, c as baseline, i as skills, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, xt as defaults, y as REVIEW } from "../chunks/reserved-B6tTYjeb.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-_CZTP7Hu.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 { St as envDefaults, dt as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, u as gate, xt as defaults } from "../chunks/reserved-
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { St as envDefaults, dt as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, u as gate, xt as defaults } from "../chunks/reserved-B6tTYjeb.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-_CZTP7Hu.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 { Et as flipProblems, Ot as requireFlag, S as coder, St as envDefaults, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as flagMentions, xt as defaults } from "../chunks/reserved-
|
|
2
|
+
import { Et as flipProblems, Ot as requireFlag, S as coder, St as envDefaults, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as flagMentions, xt as defaults } from "../chunks/reserved-B6tTYjeb.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
|
@@ -451,10 +451,15 @@ const escalation$1 = async (count, steps) => {
|
|
|
451
451
|
};
|
|
452
452
|
const STAMP = /\n<!-- gtd check [0-9a-f]+ -->\n?$/;
|
|
453
453
|
const stripStamp = (text) => text.replace(STAMP, "\n");
|
|
454
|
+
/** `true` when the re-check passed, else the red re-check's report — the one the escalation describes. */
|
|
455
|
+
const recheckReport = async (health) => {
|
|
456
|
+
if (health.recheck === void 0) return void 0;
|
|
457
|
+
return await health.recheck() || stripStamp(read(health.report) ?? "");
|
|
458
|
+
};
|
|
454
459
|
/**
|
|
455
460
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
456
461
|
* first once `fixCap` fixes are spent, or `sameFailure` calls the failure
|
|
457
|
-
* identical to the previous round's.
|
|
462
|
+
* identical to the previous round's, unless `recheck` passes.
|
|
458
463
|
*/
|
|
459
464
|
const healthy$1 = async (fix, health) => {
|
|
460
465
|
let fixes = health.fixesSoFar ?? 0;
|
|
@@ -468,6 +473,12 @@ const healthy$1 = async (fix, health) => {
|
|
|
468
473
|
const stuck = previous !== void 0 && await health.sameFailure(previous, current);
|
|
469
474
|
previous = current;
|
|
470
475
|
if (stuck || fixes >= health.fixCap) {
|
|
476
|
+
const rechecked = await recheckReport(health);
|
|
477
|
+
if (rechecked === true) {
|
|
478
|
+
health.escalations.rounds = 0;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
previous = rechecked ?? previous;
|
|
471
482
|
await escalation$1(health.escalations, health.escalation);
|
|
472
483
|
fixes = 0;
|
|
473
484
|
}
|
|
@@ -18385,6 +18396,19 @@ const baseline = (name, label = "Checking the baseline") => check(name, env.test
|
|
|
18385
18396
|
report: FEEDBACK,
|
|
18386
18397
|
label
|
|
18387
18398
|
});
|
|
18399
|
+
/**
|
|
18400
|
+
* Re-run a red check as step `name` before a person sees it. Green records
|
|
18401
|
+
* `Gtd-Mark: flaky=<red>` on the re-check's landing and resolves `true`.
|
|
18402
|
+
*/
|
|
18403
|
+
const recheck = async (name, red, options = {}) => {
|
|
18404
|
+
const green = await check(name, env.testCommand ?? "", {
|
|
18405
|
+
...options,
|
|
18406
|
+
report: FEEDBACK,
|
|
18407
|
+
label: "Re-running checks"
|
|
18408
|
+
});
|
|
18409
|
+
if (green) mark("flaky", red);
|
|
18410
|
+
return green;
|
|
18411
|
+
};
|
|
18388
18412
|
const escalationSteps = (rules) => ({
|
|
18389
18413
|
limit: 2,
|
|
18390
18414
|
describe: () => describeEscalation(rules),
|
|
@@ -18435,15 +18459,17 @@ const sameFailure = async (previous, current) => {
|
|
|
18435
18459
|
});
|
|
18436
18460
|
return comparable && truncated.length === 0 && answered(answers.verdict, "identical", numeric(vars.judgeIdenticalMinP, Infinity));
|
|
18437
18461
|
};
|
|
18462
|
+
const healthSweep = (options) => [ESCALATION, ...options.sweepOnGreen ?? []];
|
|
18438
18463
|
const runSuite = (options) => check("health.check", env.testCommand ?? "", {
|
|
18439
18464
|
report: FEEDBACK,
|
|
18440
18465
|
label: "Running checks",
|
|
18441
|
-
sweepOnGreen:
|
|
18466
|
+
sweepOnGreen: healthSweep(options)
|
|
18442
18467
|
});
|
|
18443
18468
|
/**
|
|
18444
18469
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
18445
18470
|
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
18446
|
-
* identical to the previous round's.
|
|
18471
|
+
* identical to the previous round's. An escalation re-runs the suite once
|
|
18472
|
+
* first; a green re-run ends the loop.
|
|
18447
18473
|
*/
|
|
18448
18474
|
const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
18449
18475
|
check: () => runSuite(options),
|
|
@@ -18452,14 +18478,16 @@ const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
|
18452
18478
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
18453
18479
|
sameFailure,
|
|
18454
18480
|
escalations: options.escalations ?? { rounds: 0 },
|
|
18481
|
+
recheck: () => recheck("health.recheck", "health.check", { sweepOnGreen: healthSweep(options) }),
|
|
18455
18482
|
escalation: guardedSteps(options.guard, options.describeGuard, options.guardRests, options.rules)
|
|
18456
18483
|
});
|
|
18457
18484
|
/**
|
|
18458
|
-
* Hold the process at `<name>.blocked` until the suite is green
|
|
18485
|
+
* Hold the process at `<name>.blocked` until the suite is green, re-running a
|
|
18486
|
+
* red check once as `<name>.recheck` before resting. `guard` runs
|
|
18459
18487
|
* after each `blocked` landing only — never after a check, whose refusal would
|
|
18460
18488
|
* re-run the same script forever.
|
|
18461
18489
|
*/
|
|
18462
|
-
const gate = (name, message, guard) => gate$1(name, () => baseline("check"), guarded(() => human("blocked", {
|
|
18490
|
+
const gate = (name, message, guard) => gate$1(name, async () => await baseline("check") || recheck("recheck", `${name}.check`), guarded(() => human("blocked", {
|
|
18463
18491
|
message,
|
|
18464
18492
|
label: "Baseline is red",
|
|
18465
18493
|
file: FEEDBACK
|
|
@@ -39578,4 +39606,4 @@ var workflows_default = feature;
|
|
|
39578
39606
|
*/
|
|
39579
39607
|
const doors = {};
|
|
39580
39608
|
//#endregion
|
|
39581
|
-
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unwind };
|
|
39609
|
+
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unwind };
|
package/package.json
CHANGED
package/src/cli/Cli.ts
CHANGED
|
@@ -838,9 +838,10 @@ const COMMAND_ROWS: readonly CommandRow[] = [
|
|
|
838
838
|
"Report how the kept processes went: slices whose naive pass",
|
|
839
839
|
"landed or was rebuilt (the refactor-first rate), continuations",
|
|
840
840
|
"per phase, Gtd-Cost by model and phase with the double work of",
|
|
841
|
-
"a rebuild,
|
|
842
|
-
"
|
|
843
|
-
"
|
|
841
|
+
"a rebuild, quality-lap findings per lens, and red checks that",
|
|
842
|
+
"passed on their re-check — per process and in total. Each <ref>",
|
|
843
|
+
"is read as by history show, but a branch name means every",
|
|
844
|
+
"process kept from it; none means every ref.",
|
|
844
845
|
"Reads commit subjects and trailers, never replays, so a",
|
|
845
846
|
"changed config is no refusal. --json prints { processes, total",
|
|
846
847
|
"} as one document; writes nothing",
|
package/src/cli/Metrics.test.ts
CHANGED
|
@@ -163,6 +163,37 @@ describe("processMetrics", () => {
|
|
|
163
163
|
})
|
|
164
164
|
})
|
|
165
165
|
|
|
166
|
+
describe("flaky re-checks", () => {
|
|
167
|
+
it("counts each flaky re-check under its own full step name", () => {
|
|
168
|
+
const m = processMetrics([
|
|
169
|
+
{
|
|
170
|
+
message:
|
|
171
|
+
"gtd(check): start-gate.recheck → design.triage\n\nGtd-Mark: flaky=start-gate.check",
|
|
172
|
+
findings: 0,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
message: "gtd(check): build.health.recheck → idle\n\nGtd-Mark: flaky=health.check",
|
|
176
|
+
findings: 0,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
message:
|
|
180
|
+
"gtd(check): implement.health.recheck → implement.closing\n\nGtd-Mark: flaky=health.check",
|
|
181
|
+
findings: 0,
|
|
182
|
+
},
|
|
183
|
+
{ message: "gtd(agent): seam.naive → seam.check\n\nGtd-Mark: covers=AC-1", findings: 0 },
|
|
184
|
+
{ message: "fix: hand-made\n\nGtd-Mark: flaky=x.check", findings: 0 },
|
|
185
|
+
])
|
|
186
|
+
expect(m.flaky).toEqual({
|
|
187
|
+
total: 3,
|
|
188
|
+
byStep: { "start-gate.recheck": 1, "build.health.recheck": 1, "implement.health.recheck": 1 },
|
|
189
|
+
})
|
|
190
|
+
expect(totalMetrics([m, m]).flaky).toEqual({
|
|
191
|
+
total: 6,
|
|
192
|
+
byStep: { "start-gate.recheck": 2, "build.health.recheck": 2, "implement.health.recheck": 2 },
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
166
197
|
describe("totalMetrics", () => {
|
|
167
198
|
it("adds processes and recomputes the rate over every decision", () => {
|
|
168
199
|
const a = processMetrics([
|
package/src/cli/Metrics.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface Metrics {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
readonly findings: { readonly total: number; readonly byLens: Readonly<Record<string, number>> }
|
|
48
|
+
/** Keyed by the re-check's full step name, from its subject: the mark's value is scope-relative. */
|
|
49
|
+
readonly flaky: { readonly total: number; readonly byStep: Readonly<Record<string, number>> }
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
type Phase = keyof Metrics["cost"]["byPhase"]
|
|
@@ -87,6 +89,7 @@ interface Tally {
|
|
|
87
89
|
readonly byPhase: Record<Phase, number>
|
|
88
90
|
readonly doubleWork: { naive: number; rebuild: number }
|
|
89
91
|
readonly byLens: Record<string, number>
|
|
92
|
+
readonly flaky: Record<string, number>
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
const emptyTally = (): Tally => ({
|
|
@@ -96,6 +99,7 @@ const emptyTally = (): Tally => ({
|
|
|
96
99
|
byPhase: { naive: 0, refactor: 0, implement: 0, other: 0 },
|
|
97
100
|
doubleWork: { naive: 0, rebuild: 0 },
|
|
98
101
|
byLens: record(),
|
|
102
|
+
flaky: record(),
|
|
99
103
|
})
|
|
100
104
|
|
|
101
105
|
const sum = (record: Readonly<Record<string, number>>): number =>
|
|
@@ -116,6 +120,7 @@ const finish = (t: Tally): Metrics => ({
|
|
|
116
120
|
},
|
|
117
121
|
},
|
|
118
122
|
findings: { total: sum(t.byLens), byLens: t.byLens },
|
|
123
|
+
flaky: { total: sum(t.flaky), byStep: t.flaky },
|
|
119
124
|
})
|
|
120
125
|
|
|
121
126
|
/** The open slice: its naive cost so far, and whether its implement turns rebuild it. */
|
|
@@ -186,6 +191,7 @@ export const processMetrics = (commits: readonly MetricCommit[]): Metrics => {
|
|
|
186
191
|
add(t.continuations, at.replace(CONTINUE, "$1"), 1)
|
|
187
192
|
const lens = LENS.exec(at)?.[1]
|
|
188
193
|
if (lens !== undefined && commit.findings > 0) add(t.byLens, lens, commit.findings)
|
|
194
|
+
if (message.marks.some((m) => m.name === "flaky")) add(t.flaky, at, 1)
|
|
189
195
|
tallyDecision(t, open, at, subject.to)
|
|
190
196
|
}
|
|
191
197
|
return finish(t)
|
|
@@ -202,6 +208,7 @@ export const totalMetrics = (all: readonly Metrics[]): Metrics & { readonly proc
|
|
|
202
208
|
t.doubleWork.naive += m.cost.doubleWork.naive
|
|
203
209
|
t.doubleWork.rebuild += m.cost.doubleWork.rebuild
|
|
204
210
|
for (const [k, v] of Object.entries(m.findings.byLens)) add(t.byLens, k, v)
|
|
211
|
+
for (const [k, v] of Object.entries(m.flaky.byStep)) add(t.flaky, k, v)
|
|
205
212
|
}
|
|
206
213
|
return { processes: all.length, ...finish(t) }
|
|
207
214
|
}
|
|
@@ -257,6 +264,7 @@ const line = (label: string, m: Metrics): string => {
|
|
|
257
264
|
`${continued} continuations${continued > 0 ? ` (${listed(m.continuations)})` : ""}`,
|
|
258
265
|
`cost ${cost.total} (${listed(cost.byPhase)}; by model ${listed(cost.byModel) || "-"}), double work ${cost.doubleWork.total}`,
|
|
259
266
|
`${m.findings.total} findings${m.findings.total > 0 ? ` (${listed(m.findings.byLens)})` : ""}`,
|
|
267
|
+
`${m.flaky.total} flaky${m.flaky.total > 0 ? ` (${listed(m.flaky.byStep)})` : ""}`,
|
|
260
268
|
].join("; ")
|
|
261
269
|
}
|
|
262
270
|
|
|
@@ -44,16 +44,24 @@ export interface Health {
|
|
|
44
44
|
/** Shared with other callers in the same run of red checks. */
|
|
45
45
|
readonly escalations: EscalationCount
|
|
46
46
|
readonly escalation: EscalationSteps
|
|
47
|
+
/** Runs the suite once more before a person is asked; `true` ends the loop as if `check` had passed. */
|
|
48
|
+
readonly recheck?: () => Promise<boolean>
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
// check() stamps the report with the commit it ran at, so equal failures differ there.
|
|
50
52
|
const STAMP = /\n<!-- gtd check [0-9a-f]+ -->\n?$/
|
|
51
53
|
const stripStamp = (text: string): string => text.replace(STAMP, "\n")
|
|
52
54
|
|
|
55
|
+
/** `true` when the re-check passed, else the red re-check's report — the one the escalation describes. */
|
|
56
|
+
const recheckReport = async (health: Health): Promise<true | string | undefined> => {
|
|
57
|
+
if (health.recheck === undefined) return undefined
|
|
58
|
+
return (await health.recheck()) || stripStamp(read(health.report) ?? "")
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
/**
|
|
54
62
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
55
63
|
* first once `fixCap` fixes are spent, or `sameFailure` calls the failure
|
|
56
|
-
* identical to the previous round's.
|
|
64
|
+
* identical to the previous round's, unless `recheck` passes.
|
|
57
65
|
*/
|
|
58
66
|
export const healthy = async (fix: () => Promise<void>, health: Health): Promise<void> => {
|
|
59
67
|
let fixes = health.fixesSoFar ?? 0
|
|
@@ -67,6 +75,12 @@ export const healthy = async (fix: () => Promise<void>, health: Health): Promise
|
|
|
67
75
|
const stuck = previous !== undefined && (await health.sameFailure(previous, current))
|
|
68
76
|
previous = current
|
|
69
77
|
if (stuck || fixes >= health.fixCap) {
|
|
78
|
+
const rechecked = await recheckReport(health)
|
|
79
|
+
if (rechecked === true) {
|
|
80
|
+
health.escalations.rounds = 0
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
previous = rechecked ?? previous
|
|
70
84
|
await escalation(health.escalations, health.escalation)
|
|
71
85
|
fixes = 0
|
|
72
86
|
}
|
|
@@ -160,6 +160,7 @@ describe("the cleanup guard over every later agent turn", () => {
|
|
|
160
160
|
"build.health.check": (_, r) => {
|
|
161
161
|
if (!stopped) r.write(FEEDBACK, `failing ${runs++}\n`)
|
|
162
162
|
},
|
|
163
|
+
"build.health.recheck": (_, r) => r.write(FEEDBACK, `failing ${runs++}\n`),
|
|
163
164
|
"build.health.describe": () => undefined,
|
|
164
165
|
"build.health.judge": () => '[{"id":"verdict","answer":"progress","p":0.9}]',
|
|
165
166
|
"build.fix": (_, r) => r.write("src/other.ts", `export const n = ${runs}\n`),
|
|
@@ -176,6 +177,7 @@ describe("the cleanup guard over every later agent turn", () => {
|
|
|
176
177
|
await expect(
|
|
177
178
|
driven(guarded("", NO_ACCESS), {
|
|
178
179
|
"build.health.check": (_, r) => r.write(FEEDBACK, `failing ${runs++}\n`),
|
|
180
|
+
"build.health.recheck": (_, r) => r.write(FEEDBACK, `failing ${runs++}\n`),
|
|
179
181
|
"build.health.judge": () => '[{"id":"verdict","answer":"progress","p":0.9}]',
|
|
180
182
|
"build.fix": (_, r) => r.write("src/other.ts", `export const n = ${runs}\n`),
|
|
181
183
|
"build.health.describe": (_, r) => r.write("src/b.ts", NAMED),
|
|
@@ -436,6 +436,8 @@ const drive = async (
|
|
|
436
436
|
checks++
|
|
437
437
|
phase = "check"
|
|
438
438
|
}
|
|
439
|
+
// Re-runs the red check it follows, so it reads red as well.
|
|
440
|
+
if (request.kind === "run" && name === "health.recheck") phase = "check"
|
|
439
441
|
return Promise.resolve(
|
|
440
442
|
request.kind === "judge" ? { answers: {}, truncated: [] } : undefined,
|
|
441
443
|
)
|
|
@@ -806,6 +808,8 @@ const driveReviewed = async (
|
|
|
806
808
|
// Red checks apply to the tail's own runs, never the refactor lap's.
|
|
807
809
|
checking = request.name === "health.check" && stack.includes("build")
|
|
808
810
|
if (checking && reds > 0) reds--
|
|
811
|
+
// Re-runs the red check it follows, so it reads red as well.
|
|
812
|
+
if (request.name === "health.recheck" && stack.includes("build")) checking = true
|
|
809
813
|
}
|
|
810
814
|
const lensEffects = ["correctness", "ponytail-review", "conventions"].map(
|
|
811
815
|
(lens): [string, () => void] => [
|
|
@@ -157,6 +157,10 @@ Feature: rollout turns one release flag default-on and touches nothing else
|
|
|
157
157
|
"""
|
|
158
158
|
1 test failing
|
|
159
159
|
"""
|
|
160
|
+
And step "rollout.suite.recheck" writes ".gtd/FEEDBACK.md" with:
|
|
161
|
+
"""
|
|
162
|
+
1 test still failing
|
|
163
|
+
"""
|
|
160
164
|
When gtd starts workflow "rollout" with "--var flag=alpha"
|
|
161
165
|
And the process is driven
|
|
162
166
|
Then the process rests at "rollout.suite.blocked"
|
|
@@ -176,6 +180,10 @@ Feature: rollout turns one release flag default-on and touches nothing else
|
|
|
176
180
|
"""
|
|
177
181
|
1 test failing
|
|
178
182
|
"""
|
|
183
|
+
And step "rollout.suite.recheck" writes ".gtd/FEEDBACK.md" with:
|
|
184
|
+
"""
|
|
185
|
+
1 test still failing
|
|
186
|
+
"""
|
|
179
187
|
When gtd starts workflow "rollout" with "--var flag=alpha"
|
|
180
188
|
And the process is driven
|
|
181
189
|
Then the process rests at "rollout.suite.blocked"
|
|
@@ -200,6 +208,10 @@ Feature: rollout turns one release flag default-on and touches nothing else
|
|
|
200
208
|
"""
|
|
201
209
|
1 test failing
|
|
202
210
|
"""
|
|
211
|
+
And step "rollout.suite.recheck" writes ".gtd/FEEDBACK.md" with:
|
|
212
|
+
"""
|
|
213
|
+
1 test still failing
|
|
214
|
+
"""
|
|
203
215
|
When gtd starts workflow "rollout" with "--var flag=alpha"
|
|
204
216
|
And the process is driven
|
|
205
217
|
Then the process rests at "rollout.suite.blocked"
|
|
@@ -229,6 +241,10 @@ Feature: rollout turns one release flag default-on and touches nothing else
|
|
|
229
241
|
"""
|
|
230
242
|
1 test failing
|
|
231
243
|
"""
|
|
244
|
+
And step "rollout.suite.recheck" writes ".gtd/FEEDBACK.md" with:
|
|
245
|
+
"""
|
|
246
|
+
1 test still failing
|
|
247
|
+
"""
|
|
232
248
|
When gtd starts workflow "rollout" with "--var flag=alpha"
|
|
233
249
|
And the process is driven
|
|
234
250
|
Then the process rests at "rollout.suite.blocked"
|
|
@@ -114,6 +114,7 @@ describe("the rollout workflow", () => {
|
|
|
114
114
|
const rest = await repo.drive({
|
|
115
115
|
"rollout.flipping": (_, r) => r.write("src/flags/index.ts", FLIPPED),
|
|
116
116
|
"rollout.suite.check": (_, r) => r.write(".gtd/FEEDBACK.md", "1 failing\n"),
|
|
117
|
+
"rollout.suite.recheck": (_, r) => r.write(".gtd/FEEDBACK.md", "still 1 failing\n"),
|
|
117
118
|
})
|
|
118
119
|
expect(rest).toMatchObject({ state: "rollout.suite.blocked", actor: "human" })
|
|
119
120
|
})
|
|
@@ -125,6 +126,7 @@ describe("the rollout workflow", () => {
|
|
|
125
126
|
await repo.drive({
|
|
126
127
|
"rollout.flipping": (_, r) => r.write("src/flags/index.ts", FLIPPED),
|
|
127
128
|
"rollout.suite.check": (_, r) => r.write(".gtd/FEEDBACK.md", "1 failing\n"),
|
|
129
|
+
"rollout.suite.recheck": (_, r) => r.write(".gtd/FEEDBACK.md", "still 1 failing\n"),
|
|
128
130
|
})
|
|
129
131
|
return repo
|
|
130
132
|
}
|
|
@@ -27,6 +27,7 @@ describe("gate guard", () => {
|
|
|
27
27
|
const red = {
|
|
28
28
|
first: (_: unknown, r: W) => r.write("f", "1\n"),
|
|
29
29
|
"suite.check": (_: unknown, r: W) => r.write(".gtd/FEEDBACK.md", "red\n"),
|
|
30
|
+
"suite.recheck": (_: unknown, r: W) => r.write(".gtd/FEEDBACK.md", "red again\n"),
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
it("runs the guard after a blocked landing and lets its refusal stop it", async () => {
|
|
@@ -66,6 +67,7 @@ describe("healthy describeGuard", () => {
|
|
|
66
67
|
let runs = 0
|
|
67
68
|
const handlers = (extra: Record<string, unknown>) => ({
|
|
68
69
|
"health.check": (_: unknown, r: W) => r.write(".gtd/FEEDBACK.md", `red ${runs++}\n`),
|
|
70
|
+
"health.recheck": (_: unknown, r: W) => r.write(".gtd/FEEDBACK.md", `red ${runs++}\n`),
|
|
69
71
|
"health.judge": () => '[{"id":"verdict","answer":"progress","p":0.9}]',
|
|
70
72
|
fix: (_: unknown, r: W) => r.write("f", `${runs}\n`),
|
|
71
73
|
first: (_: unknown, r: W) => r.write("start", "1\n"),
|
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
env,
|
|
5
5
|
human,
|
|
6
6
|
judge,
|
|
7
|
+
mark,
|
|
7
8
|
numeric,
|
|
8
9
|
vars,
|
|
10
|
+
type CheckOptions,
|
|
9
11
|
type JudgeQuestion,
|
|
10
12
|
} from "../../flows/index.js"
|
|
11
13
|
import * as kit from "../../workflow-kit/index.js"
|
|
@@ -19,6 +21,24 @@ export const FIX_CAP = 3
|
|
|
19
21
|
export const baseline = (name: string, label = "Checking the baseline"): Promise<boolean> =>
|
|
20
22
|
check(name, env.testCommand ?? "", { report: FEEDBACK, label })
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Re-run a red check as step `name` before a person sees it. Green records
|
|
26
|
+
* `Gtd-Mark: flaky=<red>` on the re-check's landing and resolves `true`.
|
|
27
|
+
*/
|
|
28
|
+
export const recheck = async (
|
|
29
|
+
name: string,
|
|
30
|
+
red: string,
|
|
31
|
+
options: Pick<CheckOptions, "sweepOnGreen"> = {},
|
|
32
|
+
): Promise<boolean> => {
|
|
33
|
+
const green = await check(name, env.testCommand ?? "", {
|
|
34
|
+
...options,
|
|
35
|
+
report: FEEDBACK,
|
|
36
|
+
label: "Re-running checks",
|
|
37
|
+
})
|
|
38
|
+
if (green) mark("flaky", red)
|
|
39
|
+
return green
|
|
40
|
+
}
|
|
41
|
+
|
|
22
42
|
export type { EscalationCount } from "../../workflow-kit/index.js"
|
|
23
43
|
|
|
24
44
|
const escalationSteps = (rules?: string): kit.EscalationSteps => ({
|
|
@@ -117,18 +137,24 @@ export interface HealthOptions {
|
|
|
117
137
|
readonly rules?: string | undefined
|
|
118
138
|
}
|
|
119
139
|
|
|
140
|
+
// Swept only on green: an unresolved analysis survives every retry.
|
|
141
|
+
const healthSweep = (options: HealthOptions): string[] => [
|
|
142
|
+
ESCALATION,
|
|
143
|
+
...(options.sweepOnGreen ?? []),
|
|
144
|
+
]
|
|
145
|
+
|
|
120
146
|
const runSuite = (options: HealthOptions): Promise<boolean> =>
|
|
121
147
|
check("health.check", env.testCommand ?? "", {
|
|
122
148
|
report: FEEDBACK,
|
|
123
149
|
label: "Running checks",
|
|
124
|
-
|
|
125
|
-
sweepOnGreen: [ESCALATION, ...(options.sweepOnGreen ?? [])],
|
|
150
|
+
sweepOnGreen: healthSweep(options),
|
|
126
151
|
})
|
|
127
152
|
|
|
128
153
|
/**
|
|
129
154
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
130
155
|
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
131
|
-
* identical to the previous round's.
|
|
156
|
+
* identical to the previous round's. An escalation re-runs the suite once
|
|
157
|
+
* first; a green re-run ends the loop.
|
|
132
158
|
*/
|
|
133
159
|
export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}): Promise<void> =>
|
|
134
160
|
kit.healthy(guarded(fix, options.guard), {
|
|
@@ -138,6 +164,8 @@ export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}):
|
|
|
138
164
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
139
165
|
sameFailure,
|
|
140
166
|
escalations: options.escalations ?? { rounds: 0 },
|
|
167
|
+
recheck: () =>
|
|
168
|
+
recheck("health.recheck", "health.check", { sweepOnGreen: healthSweep(options) }),
|
|
141
169
|
escalation: guardedSteps(
|
|
142
170
|
options.guard,
|
|
143
171
|
options.describeGuard,
|
|
@@ -147,13 +175,14 @@ export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}):
|
|
|
147
175
|
})
|
|
148
176
|
|
|
149
177
|
/**
|
|
150
|
-
* Hold the process at `<name>.blocked` until the suite is green
|
|
178
|
+
* Hold the process at `<name>.blocked` until the suite is green, re-running a
|
|
179
|
+
* red check once as `<name>.recheck` before resting. `guard` runs
|
|
151
180
|
* after each `blocked` landing only — never after a check, whose refusal would
|
|
152
181
|
* re-run the same script forever.
|
|
153
182
|
*/
|
|
154
183
|
export const gate = (name: string, message: string, guard?: Guard): Promise<void> =>
|
|
155
184
|
kit.gate(
|
|
156
185
|
name,
|
|
157
|
-
() => baseline("check"),
|
|
186
|
+
async () => (await baseline("check")) || recheck("recheck", `${name}.check`),
|
|
158
187
|
guarded(() => human("blocked", { message, label: "Baseline is red", file: FEEDBACK }), guard),
|
|
159
188
|
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a red check is re-run once before it reaches a person
|
|
3
|
+
|
|
4
|
+
Before the fix loop escalates, `healthy` re-runs the suite as
|
|
5
|
+
`health.recheck`. Green there ends the loop as if the check had passed, and
|
|
6
|
+
the re-check's landing records `Gtd-Mark: flaky=health.check`. Red again
|
|
7
|
+
escalates as before.
|
|
8
|
+
|
|
9
|
+
Background:
|
|
10
|
+
Given a gtd config with:
|
|
11
|
+
"""
|
|
12
|
+
import { healthy } from "@pmelab/gtd/workflows"
|
|
13
|
+
import { agent, human } from "@pmelab/gtd/flows"
|
|
14
|
+
|
|
15
|
+
export const held = async () => {
|
|
16
|
+
await agent("first", "go")
|
|
17
|
+
await healthy(() => agent("fix", "go"))
|
|
18
|
+
await human("done")
|
|
19
|
+
}
|
|
20
|
+
export default held
|
|
21
|
+
"""
|
|
22
|
+
And step "first" writes "src/a.ts" with:
|
|
23
|
+
"""
|
|
24
|
+
export const a = 1
|
|
25
|
+
"""
|
|
26
|
+
And step "health.check" writes ".gtd/FEEDBACK.md" with:
|
|
27
|
+
"""
|
|
28
|
+
1 test failing
|
|
29
|
+
"""
|
|
30
|
+
And step "fix" deletes ".gtd/FEEDBACK.md"
|
|
31
|
+
And judge step "health.judge" answers:
|
|
32
|
+
"""
|
|
33
|
+
[{"id":"verdict","answer":"progress","p":0.9}]
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
Scenario: a re-check that passes ends the loop without a person, recorded as flaky
|
|
37
|
+
Given step "health.recheck" deletes ".gtd/FEEDBACK.md"
|
|
38
|
+
When gtd starts workflow "held"
|
|
39
|
+
And the process is driven
|
|
40
|
+
Then the process rests at "done"
|
|
41
|
+
And the commit subjects end with:
|
|
42
|
+
"""
|
|
43
|
+
gtd(agent): fix → health.check
|
|
44
|
+
gtd(check): health.check → health.judge
|
|
45
|
+
gtd(judge): health.judge → health.recheck
|
|
46
|
+
gtd(check): health.recheck → done
|
|
47
|
+
"""
|
|
48
|
+
And the last commit body contains "Gtd-Mark: flaky=health.check"
|
|
49
|
+
And ".gtd/FEEDBACK.md" does not exist
|
|
50
|
+
|
|
51
|
+
Scenario: a re-check that stays red escalates, with nothing recorded as flaky
|
|
52
|
+
Given step "health.recheck" writes ".gtd/FEEDBACK.md" with:
|
|
53
|
+
"""
|
|
54
|
+
1 test still failing
|
|
55
|
+
"""
|
|
56
|
+
When gtd starts workflow "held"
|
|
57
|
+
And the process is driven
|
|
58
|
+
Then the process rests at "health.describe"
|
|
59
|
+
And the last commit subject is "gtd(check): health.recheck → health.describe"
|
|
60
|
+
And no commit body contains "Gtd-Mark: flaky="
|