@pmelab/gtd 51.0.0 → 52.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 +41 -38
- package/dist/chunks/{reserved-CUkk9XgL.mjs → reserved-D0MKVFFa.mjs} +28 -34
- package/dist/chunks/{review-BaCXlIT2.mjs → review-ZzUQH1KZ.mjs} +11 -9
- package/dist/chunks/{steering-DtUVUz9l.mjs → steering-CLhu8C7q.mjs} +262 -52
- package/dist/chunks/{tail-CX6Pdsql.mjs → tail-I1-vVq0P.mjs} +1 -1
- package/dist/gtd.bundle.mjs +224 -68
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +27 -26
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +510 -147
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +181 -222
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +478 -313
- package/package.json +1 -1
- package/src/edge/Edge.ts +1 -30
- package/src/replay/Replay.test.ts +4 -8
- package/src/replay/Replay.ts +35 -3
- package/src/replay/index.ts +7 -1
- package/src/steering/FORMATS.md +4 -1
- package/src/steering/SteeringFormat.ts +7 -1
- package/src/steering/contract.test.ts +2 -1
- package/src/steering/fixtures/review/sectioned.md +31 -0
- package/src/steering/fixtures/review/sectioned.read.json +119 -0
- package/src/steering/index.ts +4 -0
- package/src/steering/qa.ts +1 -1
- package/src/steering/review.test.ts +270 -4
- package/src/steering/review.ts +328 -69
- package/src/workflows/feature/criteria.test.ts +39 -1
- package/src/workflows/feature/criteria.ts +21 -7
- package/src/workflows/feature/fast-path.feature +1 -6
- package/src/workflows/feature/feature.ts +1 -2
- package/src/workflows/feature/flag-mode.feature +1 -6
- package/src/workflows/feature/green.ts +22 -0
- package/src/workflows/feature/hands-on-skip.feature +344 -0
- package/src/workflows/feature/lens-models.feature +0 -4
- package/src/workflows/feature/lens-review.feature +6 -8
- package/src/workflows/feature/one-step-land.feature +1 -6
- package/src/workflows/feature/prototype-rounds.feature +1 -1
- package/src/workflows/feature/prototype.feature +10 -9
- package/src/workflows/feature/prototype.ts +48 -17
- package/src/workflows/feature/refactor-absorbs.feature +3 -8
- package/src/workflows/feature/refactor-first.feature +3 -8
- package/src/workflows/feature/refactor-rebuild.feature +7 -12
- package/src/workflows/feature/refactor-review.feature +52 -72
- package/src/workflows/feature/review-answers.feature +90 -5
- package/src/workflows/feature/review-gate.feature +11 -53
- package/src/workflows/feature/reviewer-walkthrough.feature +29 -12
- package/src/workflows/feature/seam-rejection.feature +1 -6
- package/src/workflows/feature/seam.feature +1 -6
- package/src/workflows/feature/seam.test.ts +1 -1
- package/src/workflows/feature/seam.ts +2 -3
- package/src/workflows/feature/severity-budget.feature +7 -7
- package/src/workflows/feature/severity-sorted-review.feature +50 -54
- package/src/workflows/feature/slice-findings.feature +5 -15
- package/src/workflows/feature/slicing.feature +4 -24
- package/src/workflows/feature/standards.feature +0 -4
- package/src/workflows/shared/review.ts +26 -6
- package/src/workflows/shared/text.test.ts +45 -11
- package/src/workflows/shared/text.ts +45 -38
- package/src/workflows/feature/coverage.feature +0 -165
- package/src/workflows/feature/coverage.ts +0 -120
package/dist/workflow-kit.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { changes, changesSince, hasThreadFor, head, mark, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { S as unansweredNotes, _ as reviewNotes, c as stripCodeThreads, g as reviewDecisions, k as parseThreads, v as reviewRisks } from "./chunks/steering-CLhu8C7q.mjs";
|
|
3
3
|
//#region src/workflow-kit/health.ts
|
|
4
4
|
/** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
|
|
5
5
|
const escalation = async (count, steps) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import { C as coder,
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { C as coder, Ft as envDefaults, Pt as defaults, Xt as requireFlag, Yt as registryEntry, i as skills, n as summary, o as flagShipped, qt as flagReferences, r as access, t as steering } from "../chunks/reserved-D0MKVFFa.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-I1-vVq0P.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,8 +1,8 @@
|
|
|
1
1
|
import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
|
|
2
2
|
import { lensEntriesOf } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as
|
|
4
|
-
import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-
|
|
5
|
-
import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-
|
|
3
|
+
import { $ as normaliseCriteriaScript, At as unwindFailure, C as coder, Ct as seamSteerMessage, E as fixNits, Et as testerSystem, Ft as envDefaults, H as designGateAnswerMessage, I as builderSystem, It as testFileGlobs, J as idleMessage, N as agentWithSkills, P as buildReviewCollectingPrompt, Pt as defaults, Q as naivePassPrompt, R as clearCriteriaMessage, St as seamByHandMessage, Tt as startGateBlockedMessage, U as designSystem, V as criterionLine, W as designTriagePrompt, X as implementFixPrompt, Y as implementBuildingPrompt, Z as naiveApplyFailure, _ as REFACTOR_NOTE, a as flagModeOn, at as prototypeTestingPrompt, b as SLICES, d as healthy, dt as refactorDroppedRefusal, et as observableMessage, f as FEEDBACK, ft as refactorFixPrompt, g as REFACTOR, i as skills, it as prototypeOpenMessage, k as planner, kt as unwindFailedMessage, l as gate, m as PROTOTYPE, mt as refactorRules, n as summary, nt as oversizedMessage, ot as prototypeUnguidedMessage, p as HANDOFF, pt as refactorNotePrompt, r as access, rt as prototypeFixingPrompt, s as shipped, t as steering, tt as observableQuestion, ut as refactorBuildingPrompt, v as REQUIREMENTS, w as collecting, wt as slicingPrompt, x as answerReviewQuestions, xt as reviewerSystem, z as clearCriteriaQuestion } from "../chunks/reserved-D0MKVFFa.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-I1-vVq0P.mjs";
|
|
5
|
+
import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-ZzUQH1KZ.mjs";
|
|
6
6
|
import { posix } from "node:path";
|
|
7
7
|
//#region src/workflows/shared/fences.ts
|
|
8
8
|
const markFenced = (text) => {
|
|
@@ -26,7 +26,12 @@ const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
|
26
26
|
//#region src/workflows/feature/criteria.ts
|
|
27
27
|
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
28
28
|
const CLEAR_MIN_P = .8;
|
|
29
|
-
const ID$1 = /^(AC-\d+)
|
|
29
|
+
const ID$1 = /^(AC-\d+)(\s*\(no hands-on\))?:\s*(.*)$/;
|
|
30
|
+
const criterion = (id, as = id[1]) => ({
|
|
31
|
+
id: as,
|
|
32
|
+
text: id[3],
|
|
33
|
+
...id[2] === void 0 ? {} : { noHandsOn: true }
|
|
34
|
+
});
|
|
30
35
|
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
31
36
|
const listUnder = (text, title, level) => {
|
|
32
37
|
const lines = markFenced(text);
|
|
@@ -58,25 +63,28 @@ const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((
|
|
|
58
63
|
return id === null ? {
|
|
59
64
|
id: "",
|
|
60
65
|
text: item
|
|
61
|
-
} :
|
|
62
|
-
id: id[1],
|
|
63
|
-
text: id[2]
|
|
64
|
-
};
|
|
66
|
+
} : criterion(id);
|
|
65
67
|
});
|
|
66
68
|
/** The ID'd criteria of `text`, only `slice`'s when given. */
|
|
67
69
|
const criterionIds = (text, slice) => (criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "" && (slice?.only === void 0 || slice.only.includes(id)) && !(slice?.except ?? []).includes(id));
|
|
70
|
+
/** The ID'd criteria of `text` (`slice`'s when given) a person can try by hand: none tagged `(no hands-on)`. */
|
|
71
|
+
const handsOnIds = (text, slice) => {
|
|
72
|
+
const tagged = (criteriaOf(text) ?? []).filter((c) => c.noHandsOn).map((c) => c.id);
|
|
73
|
+
return criterionIds(text, slice).filter((id) => !tagged.includes(id));
|
|
74
|
+
};
|
|
68
75
|
/** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
|
|
69
76
|
const sliceIds = (slice) => slice === void 0 ? void 0 : criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice);
|
|
70
77
|
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
71
78
|
const numbered = (items) => {
|
|
72
79
|
const ids = items.map((item) => ID$1.exec(item));
|
|
73
80
|
const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
|
|
74
|
-
return items.map((item, i) =>
|
|
75
|
-
id
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
return items.map((item, i) => {
|
|
82
|
+
const id = ids[i];
|
|
83
|
+
if (own) return criterion(id);
|
|
84
|
+
return id === null ? {
|
|
85
|
+
id: `AC-${i + 1}`,
|
|
86
|
+
text: item
|
|
87
|
+
} : criterion(id, `AC-${i + 1}`);
|
|
80
88
|
});
|
|
81
89
|
};
|
|
82
90
|
const STATED = /^acceptance criteria:?$/i;
|
|
@@ -257,6 +265,164 @@ const design = (base, guard) => scope("design", async () => {
|
|
|
257
265
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
258
266
|
});
|
|
259
267
|
//#endregion
|
|
268
|
+
//#region src/workflows/feature/green.ts
|
|
269
|
+
const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
270
|
+
label: "Fixing the check",
|
|
271
|
+
file: FEEDBACK,
|
|
272
|
+
model: coder(),
|
|
273
|
+
system: builderSystem()
|
|
274
|
+
}), {
|
|
275
|
+
guard: () => {
|
|
276
|
+
requireCriteriaFrozen(REQUIREMENTS);
|
|
277
|
+
guard?.();
|
|
278
|
+
},
|
|
279
|
+
guardRests: false
|
|
280
|
+
});
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/workflows/feature/prototype.ts
|
|
283
|
+
/** Only a `no` at this or above skips a criterion: no verdict, or an unsure one, sends it to the hands-on test. */
|
|
284
|
+
const UNOBSERVABLE_MIN_P = .5;
|
|
285
|
+
/** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
|
|
286
|
+
const prototypeRounds = () => {
|
|
287
|
+
const value = vars.prototypeRounds ?? defaults.prototypeRounds;
|
|
288
|
+
if (!/^(0|[1-9]\d*)$/.test(value)) refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`);
|
|
289
|
+
return Number(value);
|
|
290
|
+
};
|
|
291
|
+
const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i;
|
|
292
|
+
const UNGUIDED = /^no testing instructions$/i;
|
|
293
|
+
/** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
|
|
294
|
+
const reportOf = (text) => {
|
|
295
|
+
let unguided = false;
|
|
296
|
+
const items = [];
|
|
297
|
+
const malformed = [];
|
|
298
|
+
for (const { line, fenced } of markFenced(text)) {
|
|
299
|
+
const heading = fenced ? null : HEADING.exec(line);
|
|
300
|
+
if (heading?.[1] !== "##") continue;
|
|
301
|
+
if (UNGUIDED.test(heading[2])) unguided = true;
|
|
302
|
+
const chunk = CHUNK.exec(heading[2]);
|
|
303
|
+
if (chunk === null && /^(ac-\d|problem)/i.test(heading[2])) malformed.push(heading[2]);
|
|
304
|
+
if (chunk !== null) items.push({
|
|
305
|
+
subject: /^ac-/i.test(chunk[1]) ? chunk[1].toUpperCase() : chunk[1],
|
|
306
|
+
verdict: chunk[2].toLowerCase()
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
unguided,
|
|
311
|
+
items,
|
|
312
|
+
malformed
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* The report the testing turn left, refused unless it is the only change and
|
|
317
|
+
* names each of `ids` once, with its open items: any other criterion is not
|
|
318
|
+
* this test's to fix.
|
|
319
|
+
*/
|
|
320
|
+
const requireReport = (ids, retest) => {
|
|
321
|
+
const others = changes().map((c) => c.path).filter((path) => path !== PROTOTYPE);
|
|
322
|
+
if (others.length > 0) refuse(`gtd land: prototype-report: test in a disposable place and change nothing but ${PROTOTYPE} — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}`);
|
|
323
|
+
const text = read(PROTOTYPE);
|
|
324
|
+
if (text === void 0) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`);
|
|
325
|
+
const report = reportOf(text);
|
|
326
|
+
if (report.malformed.length > 0) refuse(`gtd land: prototype-report: head each chunk \`## <ID>: pass|fail|blocked\` or \`## Problem <title>: pass|fail|blocked\` — these miss it:\n${report.malformed.map((h) => ` - ${h}`).join("\n")}`);
|
|
327
|
+
if (report.unguided && report.items.length === 0 && !retest) return {
|
|
328
|
+
unguided: true,
|
|
329
|
+
open: []
|
|
330
|
+
};
|
|
331
|
+
const elsewhere = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "").filter((id) => !ids.includes(id));
|
|
332
|
+
const count = (id) => report.items.filter((i) => i.subject === id).length;
|
|
333
|
+
const wrong = ids.filter((id) => count(id) !== 1);
|
|
334
|
+
if (wrong.length > 0) refuse(`gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`);
|
|
335
|
+
return {
|
|
336
|
+
unguided: false,
|
|
337
|
+
open: report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* The slice's criteria a person can observe by using the change, by the
|
|
342
|
+
* judge: those tagged `(no hands-on)` are never asked about.
|
|
343
|
+
*/
|
|
344
|
+
const observable = async (slice) => {
|
|
345
|
+
const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
|
|
346
|
+
const own = handsOnIds(requirements, slice);
|
|
347
|
+
const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
|
|
348
|
+
if (criteria.length === 0) return [];
|
|
349
|
+
const { answers } = await judge("observable", {
|
|
350
|
+
questions: criteria.map(observableQuestion),
|
|
351
|
+
evidence: { requirements },
|
|
352
|
+
message: observableMessage(),
|
|
353
|
+
label: "Judging which criteria to test by hand"
|
|
354
|
+
});
|
|
355
|
+
return own.filter((id) => !answered(answers[id], "no", UNOBSERVABLE_MIN_P));
|
|
356
|
+
};
|
|
357
|
+
const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
358
|
+
const testing = async (slice, ids, retest) => {
|
|
359
|
+
const skipped = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice).filter((id) => !ids.includes(id));
|
|
360
|
+
await agentWithSkills("testing", prototypeTestingPrompt(sliceIds(slice), skipped, retest), {
|
|
361
|
+
label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
|
|
362
|
+
file: PROTOTYPE,
|
|
363
|
+
model: coder(),
|
|
364
|
+
system: testerSystem(),
|
|
365
|
+
allowEmpty: true
|
|
366
|
+
});
|
|
367
|
+
return requireReport(ids, retest);
|
|
368
|
+
};
|
|
369
|
+
const fixing = async (slice, guard) => {
|
|
370
|
+
await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(sliceIds(slice)), {
|
|
371
|
+
label: "Fixing what the hands-on test found",
|
|
372
|
+
file: PROTOTYPE,
|
|
373
|
+
model: coder(),
|
|
374
|
+
system: builderSystem(),
|
|
375
|
+
allowEmpty: true
|
|
376
|
+
}));
|
|
377
|
+
if (changes().some((c) => c.path === ".gtd/PROTOTYPE.md")) refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`);
|
|
378
|
+
frozen();
|
|
379
|
+
guard?.();
|
|
380
|
+
};
|
|
381
|
+
/** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
|
|
382
|
+
const askPerson = async (name, message, label) => {
|
|
383
|
+
await human(name, {
|
|
384
|
+
message,
|
|
385
|
+
label,
|
|
386
|
+
file: PROTOTYPE,
|
|
387
|
+
acceptClean: true
|
|
388
|
+
});
|
|
389
|
+
if (!changes().some((c) => c.path !== ".gtd/PROTOTYPE.md")) return false;
|
|
390
|
+
frozen();
|
|
391
|
+
return true;
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Use the slice by hand: a testing turn reports on every criterion a person
|
|
395
|
+
* can observe, a fix turn takes each failure, and the slice is tested again —
|
|
396
|
+
* until the report is clean, or `prototypeRounds` fixes are spent and a person
|
|
397
|
+
* decides. A slice with no such criterion is not tested by hand. `guard` holds
|
|
398
|
+
* every fix turn, as it holds the implementation's.
|
|
399
|
+
*/
|
|
400
|
+
const prototypeTest = async (slice, guard) => {
|
|
401
|
+
const cap = prototypeRounds();
|
|
402
|
+
if (cap === 0) return;
|
|
403
|
+
await scope("prototype", async () => {
|
|
404
|
+
const ids = await observable(slice);
|
|
405
|
+
if (ids.length === 0) return;
|
|
406
|
+
let fixes = 0;
|
|
407
|
+
let retest = false;
|
|
408
|
+
for (;;) {
|
|
409
|
+
const { unguided, open } = await testing(slice, ids, retest);
|
|
410
|
+
retest = !unguided;
|
|
411
|
+
if (unguided) {
|
|
412
|
+
if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
|
|
413
|
+
} else if (open.length === 0) return;
|
|
414
|
+
else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
|
|
415
|
+
fixes++;
|
|
416
|
+
await fixing(slice, guard);
|
|
417
|
+
} else {
|
|
418
|
+
if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
|
|
419
|
+
fixes = 0;
|
|
420
|
+
}
|
|
421
|
+
await green(guard);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
};
|
|
425
|
+
//#endregion
|
|
260
426
|
//#region src/workflows/shared/continuation.ts
|
|
261
427
|
const phase = (name, steered) => ({
|
|
262
428
|
name,
|
|
@@ -573,211 +739,6 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
573
739
|
});
|
|
574
740
|
};
|
|
575
741
|
//#endregion
|
|
576
|
-
//#region src/workflows/feature/coverage.ts
|
|
577
|
-
/** Below this the judge's `yes` is not trusted: a wrongly covered ID ends the feature without its test. */
|
|
578
|
-
const COVERED_MIN_P = .7;
|
|
579
|
-
/** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
|
|
580
|
-
const COVERING_RETRIES = 2;
|
|
581
|
-
const frozen$1 = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
582
|
-
const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
583
|
-
label: "Fixing the check",
|
|
584
|
-
file: FEEDBACK,
|
|
585
|
-
model: coder(),
|
|
586
|
-
system: builderSystem()
|
|
587
|
-
}), {
|
|
588
|
-
guard: () => {
|
|
589
|
-
frozen$1();
|
|
590
|
-
guard?.();
|
|
591
|
-
},
|
|
592
|
-
guardRests: false
|
|
593
|
-
});
|
|
594
|
-
const testsSince = (base) => changesSince(base).filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path)).sort((a, b) => Number(/\bAC-\d/.test(a.after)) - Number(/\bAC-\d/.test(b.after))).map((c) => `=== ${c.path} (${c.status})\n${c.after}`).join("\n");
|
|
595
|
-
/**
|
|
596
|
-
* The criterion IDs of `.gtd/REQUIREMENTS.md` — read afresh, never carried
|
|
597
|
-
* over, only `slice`'s when given — no passing test since `base` covers, by the judge.
|
|
598
|
-
*/
|
|
599
|
-
const uncoveredSince = async (base, slice) => {
|
|
600
|
-
const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
|
|
601
|
-
const own = criterionIds(requirements, slice);
|
|
602
|
-
const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
|
|
603
|
-
if (criteria.length === 0) return {
|
|
604
|
-
covered: [],
|
|
605
|
-
uncovered: []
|
|
606
|
-
};
|
|
607
|
-
const { answers } = await judge("coverage", {
|
|
608
|
-
questions: criteria.map(coverageQuestion),
|
|
609
|
-
evidence: { tests: testsSince(base) },
|
|
610
|
-
message: coverageMessage(),
|
|
611
|
-
label: "Judging acceptance-criteria coverage"
|
|
612
|
-
});
|
|
613
|
-
const covers = (id) => answered(answers[id], "yes", COVERED_MIN_P);
|
|
614
|
-
const ids = criteria.map((c) => c.id);
|
|
615
|
-
return {
|
|
616
|
-
covered: ids.filter(covers),
|
|
617
|
-
uncovered: ids.filter((id) => !covers(id))
|
|
618
|
-
};
|
|
619
|
-
};
|
|
620
|
-
const record = (covered, uncovered = []) => {
|
|
621
|
-
for (const id of covered) mark("covers", id);
|
|
622
|
-
for (const id of uncovered) mark("uncovered", id);
|
|
623
|
-
};
|
|
624
|
-
/**
|
|
625
|
-
* Hold a green change until every acceptance criterion has a passing test,
|
|
626
|
-
* sending uncovered IDs back to a covering turn; marks the IDs covered on the
|
|
627
|
-
* landing that ends it. A human may overrule once the retries are spent.
|
|
628
|
-
*/
|
|
629
|
-
const requireCoverage = async (base = start(), slice) => {
|
|
630
|
-
let retries = 0;
|
|
631
|
-
for (;;) {
|
|
632
|
-
const { covered, uncovered } = await uncoveredSince(base, slice);
|
|
633
|
-
if (uncovered.length === 0) return record(covered);
|
|
634
|
-
if (retries < COVERING_RETRIES) {
|
|
635
|
-
retries++;
|
|
636
|
-
await agentWithSkills("covering", coveringPrompt(uncovered, sliceIds(slice)), {
|
|
637
|
-
label: "Covering the acceptance criteria",
|
|
638
|
-
model: coder(),
|
|
639
|
-
system: builderSystem(),
|
|
640
|
-
allowEmpty: true
|
|
641
|
-
});
|
|
642
|
-
frozen$1();
|
|
643
|
-
} else {
|
|
644
|
-
await human("uncovered", {
|
|
645
|
-
message: uncoveredMessage(uncovered),
|
|
646
|
-
label: "Acceptance criteria without a test",
|
|
647
|
-
file: REQUIREMENTS,
|
|
648
|
-
acceptClean: true
|
|
649
|
-
});
|
|
650
|
-
if (!changes().some((c) => isCode$1(c.path))) return record(covered, uncovered);
|
|
651
|
-
frozen$1();
|
|
652
|
-
retries = 0;
|
|
653
|
-
}
|
|
654
|
-
await green();
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
//#endregion
|
|
658
|
-
//#region src/workflows/feature/prototype.ts
|
|
659
|
-
/** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
|
|
660
|
-
const prototypeRounds = () => {
|
|
661
|
-
const value = vars.prototypeRounds ?? defaults.prototypeRounds;
|
|
662
|
-
if (!/^(0|[1-9]\d*)$/.test(value)) refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`);
|
|
663
|
-
return Number(value);
|
|
664
|
-
};
|
|
665
|
-
const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i;
|
|
666
|
-
const UNGUIDED = /^no testing instructions$/i;
|
|
667
|
-
/** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
|
|
668
|
-
const reportOf = (text) => {
|
|
669
|
-
let unguided = false;
|
|
670
|
-
const items = [];
|
|
671
|
-
const malformed = [];
|
|
672
|
-
for (const { line, fenced } of markFenced(text)) {
|
|
673
|
-
const heading = fenced ? null : HEADING.exec(line);
|
|
674
|
-
if (heading?.[1] !== "##") continue;
|
|
675
|
-
if (UNGUIDED.test(heading[2])) unguided = true;
|
|
676
|
-
const chunk = CHUNK.exec(heading[2]);
|
|
677
|
-
if (chunk === null && /^(ac-\d|problem)/i.test(heading[2])) malformed.push(heading[2]);
|
|
678
|
-
if (chunk !== null) items.push({
|
|
679
|
-
subject: /^ac-/i.test(chunk[1]) ? chunk[1].toUpperCase() : chunk[1],
|
|
680
|
-
verdict: chunk[2].toLowerCase()
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
return {
|
|
684
|
-
unguided,
|
|
685
|
-
items,
|
|
686
|
-
malformed
|
|
687
|
-
};
|
|
688
|
-
};
|
|
689
|
-
/**
|
|
690
|
-
* The report the testing turn left, refused unless it is the only change and
|
|
691
|
-
* names every criterion once, with its open items: another slice's criteria
|
|
692
|
-
* are not this slice's to fix.
|
|
693
|
-
*/
|
|
694
|
-
const requireReport = (slice, retest) => {
|
|
695
|
-
const others = changes().map((c) => c.path).filter((path) => path !== PROTOTYPE);
|
|
696
|
-
if (others.length > 0) refuse(`gtd land: prototype-report: test in a disposable place and change nothing but ${PROTOTYPE} — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}`);
|
|
697
|
-
const text = read(PROTOTYPE);
|
|
698
|
-
if (text === void 0) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`);
|
|
699
|
-
const report = reportOf(text);
|
|
700
|
-
if (report.malformed.length > 0) refuse(`gtd land: prototype-report: head each chunk \`## <ID>: pass|fail|blocked\` or \`## Problem <title>: pass|fail|blocked\` — these miss it:\n${report.malformed.map((h) => ` - ${h}`).join("\n")}`);
|
|
701
|
-
if (report.unguided && report.items.length === 0 && !retest) return {
|
|
702
|
-
unguided: true,
|
|
703
|
-
open: []
|
|
704
|
-
};
|
|
705
|
-
const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
|
|
706
|
-
const ids = criterionIds(requirements, slice);
|
|
707
|
-
const elsewhere = criterionIds(requirements).filter((id) => !ids.includes(id));
|
|
708
|
-
const count = (id) => report.items.filter((i) => i.subject === id).length;
|
|
709
|
-
const wrong = ids.filter((id) => count(id) !== 1);
|
|
710
|
-
if (wrong.length > 0) refuse(`gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`);
|
|
711
|
-
return {
|
|
712
|
-
unguided: false,
|
|
713
|
-
open: report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
|
|
714
|
-
};
|
|
715
|
-
};
|
|
716
|
-
const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
717
|
-
const testing = async (slice, retest) => {
|
|
718
|
-
await agentWithSkills("testing", prototypeTestingPrompt(sliceIds(slice), retest), {
|
|
719
|
-
label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
|
|
720
|
-
file: PROTOTYPE,
|
|
721
|
-
model: coder(),
|
|
722
|
-
system: testerSystem(),
|
|
723
|
-
allowEmpty: true
|
|
724
|
-
});
|
|
725
|
-
return requireReport(slice, retest);
|
|
726
|
-
};
|
|
727
|
-
const fixing = async (slice, guard) => {
|
|
728
|
-
await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(sliceIds(slice)), {
|
|
729
|
-
label: "Fixing what the hands-on test found",
|
|
730
|
-
file: PROTOTYPE,
|
|
731
|
-
model: coder(),
|
|
732
|
-
system: builderSystem(),
|
|
733
|
-
allowEmpty: true
|
|
734
|
-
}));
|
|
735
|
-
if (changes().some((c) => c.path === ".gtd/PROTOTYPE.md")) refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`);
|
|
736
|
-
frozen();
|
|
737
|
-
guard?.();
|
|
738
|
-
};
|
|
739
|
-
/** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
|
|
740
|
-
const askPerson = async (name, message, label) => {
|
|
741
|
-
await human(name, {
|
|
742
|
-
message,
|
|
743
|
-
label,
|
|
744
|
-
file: PROTOTYPE,
|
|
745
|
-
acceptClean: true
|
|
746
|
-
});
|
|
747
|
-
if (!changes().some((c) => c.path !== ".gtd/PROTOTYPE.md")) return false;
|
|
748
|
-
frozen();
|
|
749
|
-
return true;
|
|
750
|
-
};
|
|
751
|
-
/**
|
|
752
|
-
* Use the slice by hand: a testing turn reports on every criterion, a fix turn
|
|
753
|
-
* takes each failure, and the slice is tested again — until the report is
|
|
754
|
-
* clean, or `prototypeRounds` fixes are spent and a person decides. `guard`
|
|
755
|
-
* holds every fix turn, as it holds the implementation's.
|
|
756
|
-
*/
|
|
757
|
-
const prototypeTest = async (slice, guard) => {
|
|
758
|
-
const cap = prototypeRounds();
|
|
759
|
-
if (cap === 0) return;
|
|
760
|
-
await scope("prototype", async () => {
|
|
761
|
-
let fixes = 0;
|
|
762
|
-
let retest = false;
|
|
763
|
-
for (;;) {
|
|
764
|
-
const { unguided, open } = await testing(slice, retest);
|
|
765
|
-
retest = !unguided;
|
|
766
|
-
if (unguided) {
|
|
767
|
-
if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
|
|
768
|
-
} else if (open.length === 0) return;
|
|
769
|
-
else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
|
|
770
|
-
fixes++;
|
|
771
|
-
await fixing(slice, guard);
|
|
772
|
-
} else {
|
|
773
|
-
if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
|
|
774
|
-
fixes = 0;
|
|
775
|
-
}
|
|
776
|
-
await green(guard);
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
};
|
|
780
|
-
//#endregion
|
|
781
742
|
//#region src/workflows/feature/seam.ts
|
|
782
743
|
/** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
|
|
783
744
|
const NAIVE_RETRIES = 2;
|
|
@@ -986,7 +947,6 @@ const implementing = (options) => scope("implement", async () => {
|
|
|
986
947
|
}, since);
|
|
987
948
|
const keepsTests = built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0;
|
|
988
949
|
await green(keepsTests);
|
|
989
|
-
await requireCoverage(options.base, options.slice);
|
|
990
950
|
await prototypeTest(options.slice, keepsTests);
|
|
991
951
|
const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
992
952
|
await closing(moreToBuild(options.more));
|
|
@@ -1272,7 +1232,6 @@ const buildSlice = async (sliceBase, build) => {
|
|
|
1272
1232
|
naive: seam.land === true ? seam.reference : void 0,
|
|
1273
1233
|
rebuild: seam.rebuild,
|
|
1274
1234
|
slice: build.criteria,
|
|
1275
|
-
base: build.criteria === void 0 ? void 0 : sliceBase,
|
|
1276
1235
|
more: build.more
|
|
1277
1236
|
});
|
|
1278
1237
|
const more = build.more();
|
|
@@ -1386,4 +1345,4 @@ const feature = shipped(ordinaryStart);
|
|
|
1386
1345
|
//#region src/workflows/feature/index.ts
|
|
1387
1346
|
var feature_default = feature;
|
|
1388
1347
|
//#endregion
|
|
1389
|
-
export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, triage, unstableIds, unwind };
|
|
1348
|
+
export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, handsOnIds, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, triage, unstableIds, unwind };
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
|
-
import { _ as
|
|
3
|
-
import { C as coder,
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
2
|
+
import { _ as reviewNotes, v as reviewRisks } from "../chunks/steering-CLhu8C7q.mjs";
|
|
3
|
+
import { C as coder, Ft as envDefaults, G as finisherSystem, K as fixReviewNotesPrompt, N as agentWithSkills, Pt as defaults, c as baseline, i as skills, n as summary, r as access, s as shipped, t as steering, y as REVIEW } from "../chunks/reserved-D0MKVFFa.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-I1-vVq0P.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,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, start } from "../flows.mjs";
|
|
2
|
-
import { C as coder,
|
|
3
|
-
import { a as refusePendingHandOff } from "../chunks/tail-
|
|
4
|
-
import { i as reUnwind, t as buildTail } from "../chunks/review-
|
|
2
|
+
import { C as coder, Ft as envDefaults, G as finisherSystem, N as agentWithSkills, Pt as defaults, _t as reviewFeedbackFixPrompt, i as skills, l as gate, n as summary, r as access, s as shipped, t as steering, v as REQUIREMENTS, vt as reviewGateBlockedMessage } from "../chunks/reserved-D0MKVFFa.mjs";
|
|
3
|
+
import { a as refusePendingHandOff } from "../chunks/tail-I1-vVq0P.mjs";
|
|
4
|
+
import { i as reUnwind, t as buildTail } from "../chunks/review-ZzUQH1KZ.mjs";
|
|
5
5
|
//#region src/workflows/review/review.ts
|
|
6
6
|
const fixFeedback = async () => {
|
|
7
7
|
await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import { C as coder,
|
|
2
|
+
import { C as coder, Ft as envDefaults, Jt as flipProblems, Kt as flagMentions, Pt as defaults, Xt as requireFlag, f as FEEDBACK, i as skills, l as gate, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-D0MKVFFa.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.";
|