@pmelab/gtd 40.0.0 → 41.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 +3 -2
- package/dist/chunks/{reserved-Qt5FM6Ia.mjs → reserved-Ch4LLrEj.mjs} +8 -4
- package/dist/chunks/{tail-tAHx_AHe.mjs → tail-lY1UmJrU.mjs} +1 -1
- package/dist/workflow-kit.mjs +29 -11
- 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 +37 -15
- package/package.json +1 -1
- package/src/workflow-kit/health.ts +42 -13
- package/src/workflows/shared/health.test.ts +30 -0
- package/src/workflows/shared/health.ts +12 -9
- package/src/workflows/shared/progress.feature +124 -0
package/README.md
CHANGED
|
@@ -506,8 +506,9 @@ noted in steps 3 and 4 below.
|
|
|
506
506
|
as-is, handing that document to the next fix turn as its instruction. Two
|
|
507
507
|
such rounds with no fix is the terminal stop: the last analysis stays on disk
|
|
508
508
|
instead of a third being written, and editing it is what gets the next
|
|
509
|
-
attempt anywhere new.
|
|
510
|
-
|
|
509
|
+
attempt anywhere new. A loop the retry judge still calls progress at the cap
|
|
510
|
+
gets one extra fix turn first. Before any escalation or red-baseline stop the
|
|
511
|
+
suite runs once more; a pass there continues without you and is recorded as
|
|
511
512
|
`Gtd-Mark: flaky=<check>` (see
|
|
512
513
|
[Re-check before a person](docs/configuration.md#re-check-before-a-person)).
|
|
513
514
|
|
|
@@ -1632,7 +1632,7 @@ const retryQuestion = (comparable) => comparable ? {
|
|
|
1632
1632
|
instructions: "state carries tailsNotComparable: one of the two check reports came back empty. There is nothing to compare here; answer from criteria alone. `identical` is FORBIDDEN here.",
|
|
1633
1633
|
criteria: "new-failure: a materially different failure than before. progress: still red, but closer to green — the default if genuinely unsure."
|
|
1634
1634
|
};
|
|
1635
|
-
const
|
|
1635
|
+
const compare = async (previous, current) => {
|
|
1636
1636
|
const comparable = current.trim() !== "" && previous.trim() !== "";
|
|
1637
1637
|
const { answers, truncated } = await judge("health.judge", {
|
|
1638
1638
|
questions: [retryQuestion(comparable)],
|
|
@@ -1643,7 +1643,10 @@ const sameFailure = async (previous, current) => {
|
|
|
1643
1643
|
message: healthJudgeMessage(),
|
|
1644
1644
|
label: "Judging the retry"
|
|
1645
1645
|
});
|
|
1646
|
-
|
|
1646
|
+
if (!comparable || truncated.length > 0) return void 0;
|
|
1647
|
+
const minP = numeric(vars.judgeIdenticalMinP, Infinity);
|
|
1648
|
+
if (answered(answers.verdict, "identical", minP)) return "identical";
|
|
1649
|
+
return answered(answers.verdict, "progress", minP) ? "progress" : void 0;
|
|
1647
1650
|
};
|
|
1648
1651
|
const healthSweep = (options) => [ESCALATION, ...options.sweepOnGreen ?? []];
|
|
1649
1652
|
const runSuite = (options) => check("health.check", env.testCommand ?? "", {
|
|
@@ -1654,7 +1657,8 @@ const runSuite = (options) => check("health.check", env.testCommand ?? "", {
|
|
|
1654
1657
|
/**
|
|
1655
1658
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
1656
1659
|
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
1657
|
-
* identical to the previous round's.
|
|
1660
|
+
* identical to the previous round's. A judge calling it progress at the cap
|
|
1661
|
+
* buys one extra fix per run of fixes. An escalation re-runs the suite once
|
|
1658
1662
|
* first; a green re-run ends the loop.
|
|
1659
1663
|
*/
|
|
1660
1664
|
const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
@@ -1662,7 +1666,7 @@ const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
|
1662
1666
|
report: FEEDBACK,
|
|
1663
1667
|
fixCap: 3,
|
|
1664
1668
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
1665
|
-
|
|
1669
|
+
compare,
|
|
1666
1670
|
escalations: options.escalations ?? { rounds: 0 },
|
|
1667
1671
|
recheck: () => recheck("health.recheck", "health.check", { sweepOnGreen: healthSweep(options) }),
|
|
1668
1672
|
escalation: guardedSteps(options.guard, options.describeGuard, options.guardRests, options.rules)
|
|
@@ -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-Ch4LLrEj.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/workflow-kit.mjs
CHANGED
|
@@ -22,13 +22,33 @@ const recheckReport = async (health) => {
|
|
|
22
22
|
if (health.recheck === void 0) return void 0;
|
|
23
23
|
return await health.recheck() || stripStamp(read(health.report) ?? "");
|
|
24
24
|
};
|
|
25
|
+
/** Escalate unless the re-check passes: `true` then, else the report the next round compares against. */
|
|
26
|
+
const escalated = async (health, current) => {
|
|
27
|
+
const rechecked = await recheckReport(health);
|
|
28
|
+
if (rechecked === true) {
|
|
29
|
+
health.escalations.rounds = 0;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
await escalation(health.escalations, health.escalation);
|
|
33
|
+
return rechecked ?? current;
|
|
34
|
+
};
|
|
35
|
+
const nextMove = async (health, fixes, cap, previous, current) => {
|
|
36
|
+
const capped = fixes >= cap;
|
|
37
|
+
if (capped && cap > health.fixCap) return "escalate";
|
|
38
|
+
const verdict = previous === void 0 ? void 0 : await health.compare(previous, current);
|
|
39
|
+
if (capped && verdict === "progress") return "extend";
|
|
40
|
+
return verdict === "identical" || capped ? "escalate" : void 0;
|
|
41
|
+
};
|
|
25
42
|
/**
|
|
26
43
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
27
|
-
* first once `fixCap` fixes are spent, or `
|
|
28
|
-
* identical to the previous round's, unless `recheck` passes.
|
|
44
|
+
* first once `fixCap` fixes are spent, or `compare` calls the failure
|
|
45
|
+
* identical to the previous round's, unless `recheck` passes. A round that
|
|
46
|
+
* reaches the cap while `compare` calls it progress raises the cap by one fix,
|
|
47
|
+
* once per run of fixes between escalations.
|
|
29
48
|
*/
|
|
30
49
|
const healthy = async (fix, health) => {
|
|
31
50
|
let fixes = health.fixesSoFar ?? 0;
|
|
51
|
+
let cap = health.fixCap;
|
|
32
52
|
let previous;
|
|
33
53
|
for (;;) {
|
|
34
54
|
if (await health.check()) {
|
|
@@ -36,17 +56,15 @@ const healthy = async (fix, health) => {
|
|
|
36
56
|
return;
|
|
37
57
|
}
|
|
38
58
|
const current = stripStamp(read(health.report) ?? "");
|
|
39
|
-
const
|
|
59
|
+
const move = await nextMove(health, fixes, cap, previous, current);
|
|
40
60
|
previous = current;
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
previous = rechecked ?? previous;
|
|
48
|
-
await escalation(health.escalations, health.escalation);
|
|
61
|
+
if (move === "extend") cap++;
|
|
62
|
+
else if (move === "escalate") {
|
|
63
|
+
const after = await escalated(health, current);
|
|
64
|
+
if (after === true) return;
|
|
65
|
+
previous = after;
|
|
49
66
|
fixes = 0;
|
|
67
|
+
cap = health.fixCap;
|
|
50
68
|
}
|
|
51
69
|
fixes++;
|
|
52
70
|
await fix();
|
|
@@ -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-Ch4LLrEj.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-lY1UmJrU.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-Ch4LLrEj.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-lY1UmJrU.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-Ch4LLrEj.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-lY1UmJrU.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-Ch4LLrEj.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-lY1UmJrU.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-Ch4LLrEj.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
|
@@ -456,13 +456,33 @@ const recheckReport = async (health) => {
|
|
|
456
456
|
if (health.recheck === void 0) return void 0;
|
|
457
457
|
return await health.recheck() || stripStamp(read(health.report) ?? "");
|
|
458
458
|
};
|
|
459
|
+
/** Escalate unless the re-check passes: `true` then, else the report the next round compares against. */
|
|
460
|
+
const escalated = async (health, current) => {
|
|
461
|
+
const rechecked = await recheckReport(health);
|
|
462
|
+
if (rechecked === true) {
|
|
463
|
+
health.escalations.rounds = 0;
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
await escalation$1(health.escalations, health.escalation);
|
|
467
|
+
return rechecked ?? current;
|
|
468
|
+
};
|
|
469
|
+
const nextMove = async (health, fixes, cap, previous, current) => {
|
|
470
|
+
const capped = fixes >= cap;
|
|
471
|
+
if (capped && cap > health.fixCap) return "escalate";
|
|
472
|
+
const verdict = previous === void 0 ? void 0 : await health.compare(previous, current);
|
|
473
|
+
if (capped && verdict === "progress") return "extend";
|
|
474
|
+
return verdict === "identical" || capped ? "escalate" : void 0;
|
|
475
|
+
};
|
|
459
476
|
/**
|
|
460
477
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
461
|
-
* first once `fixCap` fixes are spent, or `
|
|
462
|
-
* identical to the previous round's, unless `recheck` passes.
|
|
478
|
+
* first once `fixCap` fixes are spent, or `compare` calls the failure
|
|
479
|
+
* identical to the previous round's, unless `recheck` passes. A round that
|
|
480
|
+
* reaches the cap while `compare` calls it progress raises the cap by one fix,
|
|
481
|
+
* once per run of fixes between escalations.
|
|
463
482
|
*/
|
|
464
483
|
const healthy$1 = async (fix, health) => {
|
|
465
484
|
let fixes = health.fixesSoFar ?? 0;
|
|
485
|
+
let cap = health.fixCap;
|
|
466
486
|
let previous;
|
|
467
487
|
for (;;) {
|
|
468
488
|
if (await health.check()) {
|
|
@@ -470,17 +490,15 @@ const healthy$1 = async (fix, health) => {
|
|
|
470
490
|
return;
|
|
471
491
|
}
|
|
472
492
|
const current = stripStamp(read(health.report) ?? "");
|
|
473
|
-
const
|
|
493
|
+
const move = await nextMove(health, fixes, cap, previous, current);
|
|
474
494
|
previous = current;
|
|
475
|
-
if (
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
previous = rechecked ?? previous;
|
|
482
|
-
await escalation$1(health.escalations, health.escalation);
|
|
495
|
+
if (move === "extend") cap++;
|
|
496
|
+
else if (move === "escalate") {
|
|
497
|
+
const after = await escalated(health, current);
|
|
498
|
+
if (after === true) return;
|
|
499
|
+
previous = after;
|
|
483
500
|
fixes = 0;
|
|
501
|
+
cap = health.fixCap;
|
|
484
502
|
}
|
|
485
503
|
fixes++;
|
|
486
504
|
await fix();
|
|
@@ -18448,7 +18466,7 @@ const retryQuestion = (comparable) => comparable ? {
|
|
|
18448
18466
|
instructions: "state carries tailsNotComparable: one of the two check reports came back empty. There is nothing to compare here; answer from criteria alone. `identical` is FORBIDDEN here.",
|
|
18449
18467
|
criteria: "new-failure: a materially different failure than before. progress: still red, but closer to green — the default if genuinely unsure."
|
|
18450
18468
|
};
|
|
18451
|
-
const
|
|
18469
|
+
const compare$1 = async (previous, current) => {
|
|
18452
18470
|
const comparable = current.trim() !== "" && previous.trim() !== "";
|
|
18453
18471
|
const { answers, truncated } = await judge("health.judge", {
|
|
18454
18472
|
questions: [retryQuestion(comparable)],
|
|
@@ -18459,7 +18477,10 @@ const sameFailure = async (previous, current) => {
|
|
|
18459
18477
|
message: healthJudgeMessage(),
|
|
18460
18478
|
label: "Judging the retry"
|
|
18461
18479
|
});
|
|
18462
|
-
|
|
18480
|
+
if (!comparable || truncated.length > 0) return void 0;
|
|
18481
|
+
const minP = numeric(vars.judgeIdenticalMinP, Infinity);
|
|
18482
|
+
if (answered(answers.verdict, "identical", minP)) return "identical";
|
|
18483
|
+
return answered(answers.verdict, "progress", minP) ? "progress" : void 0;
|
|
18463
18484
|
};
|
|
18464
18485
|
const healthSweep = (options) => [ESCALATION, ...options.sweepOnGreen ?? []];
|
|
18465
18486
|
const runSuite = (options) => check("health.check", env.testCommand ?? "", {
|
|
@@ -18470,7 +18491,8 @@ const runSuite = (options) => check("health.check", env.testCommand ?? "", {
|
|
|
18470
18491
|
/**
|
|
18471
18492
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
18472
18493
|
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
18473
|
-
* identical to the previous round's.
|
|
18494
|
+
* identical to the previous round's. A judge calling it progress at the cap
|
|
18495
|
+
* buys one extra fix per run of fixes. An escalation re-runs the suite once
|
|
18474
18496
|
* first; a green re-run ends the loop.
|
|
18475
18497
|
*/
|
|
18476
18498
|
const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
@@ -18478,7 +18500,7 @@ const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
|
18478
18500
|
report: FEEDBACK,
|
|
18479
18501
|
fixCap: 3,
|
|
18480
18502
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
18481
|
-
|
|
18503
|
+
compare: compare$1,
|
|
18482
18504
|
escalations: options.escalations ?? { rounds: 0 },
|
|
18483
18505
|
recheck: () => recheck("health.recheck", "health.check", { sweepOnGreen: healthSweep(options) }),
|
|
18484
18506
|
escalation: guardedSteps(options.guard, options.describeGuard, options.guardRests, options.rules)
|
package/package.json
CHANGED
|
@@ -32,6 +32,8 @@ export const untilSettled = async (
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export type RetryVerdict = "identical" | "progress"
|
|
36
|
+
|
|
35
37
|
export interface Health {
|
|
36
38
|
/** One counted run of the suite; resolves `true` when green. */
|
|
37
39
|
readonly check: () => Promise<boolean>
|
|
@@ -39,8 +41,8 @@ export interface Health {
|
|
|
39
41
|
readonly report: string
|
|
40
42
|
readonly fixCap: number
|
|
41
43
|
readonly fixesSoFar?: number
|
|
42
|
-
/**
|
|
43
|
-
readonly
|
|
44
|
+
/** How a red report compares to the previous round's: `identical` escalates at once, `progress` at the cap earns one extra fix. */
|
|
45
|
+
readonly compare: (previous: string, current: string) => Promise<RetryVerdict | undefined>
|
|
44
46
|
/** Shared with other callers in the same run of red checks. */
|
|
45
47
|
readonly escalations: EscalationCount
|
|
46
48
|
readonly escalation: EscalationSteps
|
|
@@ -58,13 +60,42 @@ const recheckReport = async (health: Health): Promise<true | string | undefined>
|
|
|
58
60
|
return (await health.recheck()) || stripStamp(read(health.report) ?? "")
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/** Escalate unless the re-check passes: `true` then, else the report the next round compares against. */
|
|
64
|
+
const escalated = async (health: Health, current: string): Promise<true | string> => {
|
|
65
|
+
const rechecked = await recheckReport(health)
|
|
66
|
+
if (rechecked === true) {
|
|
67
|
+
health.escalations.rounds = 0
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
await escalation(health.escalations, health.escalation)
|
|
71
|
+
return rechecked ?? current
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const nextMove = async (
|
|
75
|
+
health: Health,
|
|
76
|
+
fixes: number,
|
|
77
|
+
cap: number,
|
|
78
|
+
previous: string | undefined,
|
|
79
|
+
current: string,
|
|
80
|
+
): Promise<"extend" | "escalate" | undefined> => {
|
|
81
|
+
const capped = fixes >= cap
|
|
82
|
+
// Past a raised cap the loop escalates whatever the judge would answer.
|
|
83
|
+
if (capped && cap > health.fixCap) return "escalate"
|
|
84
|
+
const verdict = previous === undefined ? undefined : await health.compare(previous, current)
|
|
85
|
+
if (capped && verdict === "progress") return "extend"
|
|
86
|
+
return verdict === "identical" || capped ? "escalate" : undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
/**
|
|
62
90
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
63
|
-
* first once `fixCap` fixes are spent, or `
|
|
64
|
-
* identical to the previous round's, unless `recheck` passes.
|
|
91
|
+
* first once `fixCap` fixes are spent, or `compare` calls the failure
|
|
92
|
+
* identical to the previous round's, unless `recheck` passes. A round that
|
|
93
|
+
* reaches the cap while `compare` calls it progress raises the cap by one fix,
|
|
94
|
+
* once per run of fixes between escalations.
|
|
65
95
|
*/
|
|
66
96
|
export const healthy = async (fix: () => Promise<void>, health: Health): Promise<void> => {
|
|
67
97
|
let fixes = health.fixesSoFar ?? 0
|
|
98
|
+
let cap = health.fixCap
|
|
68
99
|
let previous: string | undefined
|
|
69
100
|
for (;;) {
|
|
70
101
|
if (await health.check()) {
|
|
@@ -72,17 +103,15 @@ export const healthy = async (fix: () => Promise<void>, health: Health): Promise
|
|
|
72
103
|
return
|
|
73
104
|
}
|
|
74
105
|
const current = stripStamp(read(health.report) ?? "")
|
|
75
|
-
const
|
|
106
|
+
const move = await nextMove(health, fixes, cap, previous, current)
|
|
76
107
|
previous = current
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
previous = rechecked ?? previous
|
|
84
|
-
await escalation(health.escalations, health.escalation)
|
|
108
|
+
if (move === "extend") cap++
|
|
109
|
+
else if (move === "escalate") {
|
|
110
|
+
const after = await escalated(health, current)
|
|
111
|
+
if (after === true) return
|
|
112
|
+
previous = after
|
|
85
113
|
fixes = 0
|
|
114
|
+
cap = health.fixCap
|
|
86
115
|
}
|
|
87
116
|
fixes++
|
|
88
117
|
await fix()
|
|
@@ -116,3 +116,33 @@ describe("healthy describeGuard", () => {
|
|
|
116
116
|
).rejects.toThrow("guarded: nope")
|
|
117
117
|
})
|
|
118
118
|
})
|
|
119
|
+
|
|
120
|
+
describe("healthy at the fix cap", () => {
|
|
121
|
+
// Blank reports are not comparable; at an 8-byte budget the cut trims both.
|
|
122
|
+
it.each([
|
|
123
|
+
["could not compare", "", (n: number) => "\n".repeat(n)],
|
|
124
|
+
["saw only cut", `, judgeBudgetBytes: "8"`, (n: number) => `a long failing report ${n}\n`],
|
|
125
|
+
])("grants no extra turn on progress the judge %s", async (_, extra, report) => {
|
|
126
|
+
let runs = 0
|
|
127
|
+
const repo = testRepo({
|
|
128
|
+
workflow: [
|
|
129
|
+
`import { healthy } from "@pmelab/gtd/workflows"`,
|
|
130
|
+
`import { agent } from "@pmelab/gtd/flows"`,
|
|
131
|
+
`export const defaults = { judgeIdenticalMinP: "0.7"${extra} }`,
|
|
132
|
+
`export const held = async () => { await agent("first", "go"); await healthy(() => agent("fix", "go")) }`,
|
|
133
|
+
`export default held`,
|
|
134
|
+
].join("\n"),
|
|
135
|
+
env: { GTD_TEST_COMMAND: "true" },
|
|
136
|
+
})
|
|
137
|
+
await repo.start("held")
|
|
138
|
+
const rest = await repo.drive({
|
|
139
|
+
first: (_, r) => r.write("start", "1\n"),
|
|
140
|
+
"health.check": (_, r) => r.write(".gtd/FEEDBACK.md", report(++runs)),
|
|
141
|
+
"health.judge": () => '[{"id":"verdict","answer":"progress","p":0.9}]',
|
|
142
|
+
fix: (_, r) => r.write("f", `${runs}\n`),
|
|
143
|
+
"health.recheck": (_, r) => r.write(".gtd/FEEDBACK.md", "red again\n"),
|
|
144
|
+
})
|
|
145
|
+
expect(rest.state).toBe("health.describe")
|
|
146
|
+
expect(repo.subjects().filter((s) => s.startsWith("gtd(agent): fix")).length).toBe(3)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
@@ -105,7 +105,10 @@ export const retryQuestion = (comparable: boolean): JudgeQuestion =>
|
|
|
105
105
|
"new-failure: a materially different failure than before. progress: still red, but closer to green — the default if genuinely unsure.",
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
const
|
|
108
|
+
const compare = async (
|
|
109
|
+
previous: string,
|
|
110
|
+
current: string,
|
|
111
|
+
): Promise<kit.RetryVerdict | undefined> => {
|
|
109
112
|
const comparable = current.trim() !== "" && previous.trim() !== ""
|
|
110
113
|
const { answers, truncated } = await judge("health.judge", {
|
|
111
114
|
questions: [retryQuestion(comparable)],
|
|
@@ -113,12 +116,11 @@ const sameFailure = async (previous: string, current: string): Promise<boolean>
|
|
|
113
116
|
message: t.healthJudgeMessage(),
|
|
114
117
|
label: "Judging the retry",
|
|
115
118
|
})
|
|
116
|
-
// Evidence the budget cut can make two different reports look alike:
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
119
|
+
// Evidence the budget cut can make two different reports look alike: neither sameness nor progress.
|
|
120
|
+
if (!comparable || truncated.length > 0) return undefined
|
|
121
|
+
const minP = numeric(vars.judgeIdenticalMinP, Infinity)
|
|
122
|
+
if (answered(answers.verdict, "identical", minP)) return "identical"
|
|
123
|
+
return answered(answers.verdict, "progress", minP) ? "progress" : undefined
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
export interface HealthOptions {
|
|
@@ -153,7 +155,8 @@ const runSuite = (options: HealthOptions): Promise<boolean> =>
|
|
|
153
155
|
/**
|
|
154
156
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
155
157
|
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
156
|
-
* identical to the previous round's.
|
|
158
|
+
* identical to the previous round's. A judge calling it progress at the cap
|
|
159
|
+
* buys one extra fix per run of fixes. An escalation re-runs the suite once
|
|
157
160
|
* first; a green re-run ends the loop.
|
|
158
161
|
*/
|
|
159
162
|
export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}): Promise<void> =>
|
|
@@ -162,7 +165,7 @@ export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}):
|
|
|
162
165
|
report: FEEDBACK,
|
|
163
166
|
fixCap: FIX_CAP,
|
|
164
167
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
165
|
-
|
|
168
|
+
compare,
|
|
166
169
|
escalations: options.escalations ?? { rounds: 0 },
|
|
167
170
|
recheck: () =>
|
|
168
171
|
recheck("health.recheck", "health.check", { sweepOnGreen: healthSweep(options) }),
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a fix loop still making progress at the cap gets one extra turn
|
|
3
|
+
|
|
4
|
+
`healthy` escalates once three fix turns are spent. When `health.judge`
|
|
5
|
+
calls that round progress — still red, but closer to green — with at least
|
|
6
|
+
`judgeIdenticalMinP` confidence, one more fix runs first, once per run of
|
|
7
|
+
fixes. The round after it escalates without asking the judge. Any other
|
|
8
|
+
verdict escalates at the cap as before.
|
|
9
|
+
|
|
10
|
+
Background:
|
|
11
|
+
Given a gtd config with:
|
|
12
|
+
"""
|
|
13
|
+
import { healthy } from "@pmelab/gtd/workflows"
|
|
14
|
+
import { agent } from "@pmelab/gtd/flows"
|
|
15
|
+
|
|
16
|
+
export const defaults = { judgeIdenticalMinP: "0.7" }
|
|
17
|
+
export const held = async () => {
|
|
18
|
+
await agent("first", "go")
|
|
19
|
+
await healthy(() => agent("fix", "go"))
|
|
20
|
+
}
|
|
21
|
+
export default held
|
|
22
|
+
"""
|
|
23
|
+
And step "first" writes "src/a.ts" with:
|
|
24
|
+
"""
|
|
25
|
+
export const a = 1
|
|
26
|
+
"""
|
|
27
|
+
And step "health.check" writes ".gtd/FEEDBACK.md" with:
|
|
28
|
+
"""
|
|
29
|
+
tests failing
|
|
30
|
+
"""
|
|
31
|
+
And step "fix" deletes ".gtd/FEEDBACK.md"
|
|
32
|
+
And step "health.recheck" writes ".gtd/FEEDBACK.md" with:
|
|
33
|
+
"""
|
|
34
|
+
tests still failing
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
Scenario: progress at the cap buys exactly one extra fix turn
|
|
38
|
+
Given judge step "health.judge" answers:
|
|
39
|
+
"""
|
|
40
|
+
[{"id":"verdict","answer":"progress","p":0.9}]
|
|
41
|
+
"""
|
|
42
|
+
When gtd starts workflow "held"
|
|
43
|
+
And the process is driven
|
|
44
|
+
Then the process rests at "health.describe"
|
|
45
|
+
And the commit subjects end with:
|
|
46
|
+
"""
|
|
47
|
+
gtd(agent): first → health.check
|
|
48
|
+
gtd(check): health.check → fix
|
|
49
|
+
gtd(agent): fix → health.check
|
|
50
|
+
gtd(check): health.check → health.judge
|
|
51
|
+
gtd(judge): health.judge → fix
|
|
52
|
+
gtd(agent): fix → health.check
|
|
53
|
+
gtd(check): health.check → health.judge
|
|
54
|
+
gtd(judge): health.judge → fix
|
|
55
|
+
gtd(agent): fix → health.check
|
|
56
|
+
gtd(check): health.check → health.judge
|
|
57
|
+
gtd(judge): health.judge → fix
|
|
58
|
+
gtd(agent): fix → health.check
|
|
59
|
+
gtd(check): health.check → health.recheck
|
|
60
|
+
gtd(check): health.recheck → health.describe
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
Scenario Outline: <verdict> at <p> escalates at the cap without an extra turn
|
|
64
|
+
Given judge step "health.judge" answers:
|
|
65
|
+
"""
|
|
66
|
+
[{"id":"verdict","answer":"<verdict>","p":<p>}]
|
|
67
|
+
"""
|
|
68
|
+
When gtd starts workflow "held"
|
|
69
|
+
And the process is driven
|
|
70
|
+
Then the process rests at "health.describe"
|
|
71
|
+
And the commit subjects end with:
|
|
72
|
+
"""
|
|
73
|
+
gtd(agent): first → health.check
|
|
74
|
+
gtd(check): health.check → fix
|
|
75
|
+
gtd(agent): fix → health.check
|
|
76
|
+
gtd(check): health.check → health.judge
|
|
77
|
+
gtd(judge): health.judge → fix
|
|
78
|
+
gtd(agent): fix → health.check
|
|
79
|
+
gtd(check): health.check → health.judge
|
|
80
|
+
gtd(judge): health.judge → fix
|
|
81
|
+
gtd(agent): fix → health.check
|
|
82
|
+
gtd(check): health.check → health.judge
|
|
83
|
+
gtd(judge): health.judge → health.recheck
|
|
84
|
+
gtd(check): health.recheck → health.describe
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
| verdict | p |
|
|
89
|
+
| new-failure | 0.9 |
|
|
90
|
+
| progress | 0.5 |
|
|
91
|
+
|
|
92
|
+
Scenario: a person's escalation stop starts a new run of fixes with its own extra turn
|
|
93
|
+
Given judge step "health.judge" answers:
|
|
94
|
+
"""
|
|
95
|
+
[{"id":"verdict","answer":"progress","p":0.9}]
|
|
96
|
+
"""
|
|
97
|
+
And step "health.describe" writes ".gtd/ESCALATION.md" with:
|
|
98
|
+
"""
|
|
99
|
+
try something else
|
|
100
|
+
"""
|
|
101
|
+
And step "health.stop" writes "NOTES.md" with:
|
|
102
|
+
"""
|
|
103
|
+
narrowed
|
|
104
|
+
"""
|
|
105
|
+
When gtd starts workflow "held"
|
|
106
|
+
And the process is driven
|
|
107
|
+
Then the process rests at "health.exhausted"
|
|
108
|
+
And the commit subjects end with:
|
|
109
|
+
"""
|
|
110
|
+
gtd(agent): health.describe → health.stop
|
|
111
|
+
gtd(human): health.stop → fix
|
|
112
|
+
gtd(agent): fix → health.check
|
|
113
|
+
gtd(check): health.check → health.judge
|
|
114
|
+
gtd(judge): health.judge → fix
|
|
115
|
+
gtd(agent): fix → health.check
|
|
116
|
+
gtd(check): health.check → health.judge
|
|
117
|
+
gtd(judge): health.judge → fix
|
|
118
|
+
gtd(agent): fix → health.check
|
|
119
|
+
gtd(check): health.check → health.judge
|
|
120
|
+
gtd(judge): health.judge → fix
|
|
121
|
+
gtd(agent): fix → health.check
|
|
122
|
+
gtd(check): health.check → health.recheck
|
|
123
|
+
gtd(check): health.recheck → health.exhausted
|
|
124
|
+
"""
|