@pmelab/gtd 32.1.0 → 34.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 +66 -53
- package/dist/chunks/{reserved-2fV0mj14.mjs → reserved-BcJgt5_R.mjs} +79 -12
- package/dist/chunks/{tail-BynhCfuv.mjs → tail-CM_fa93v.mjs} +1 -1
- package/dist/testkit.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +174 -40
- 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 +249 -48
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +2 -2
- package/src/testkit/steps.ts +4 -0
- package/src/workflows/feature/acceptance-criteria.feature +8 -0
- package/src/workflows/feature/fast-path.feature +11 -35
- package/src/workflows/feature/feature.test.ts +9 -100
- package/src/workflows/feature/feature.ts +29 -16
- package/src/workflows/feature/flag-mode.feature +2 -91
- package/src/workflows/feature/naive-acceptance.feature +140 -0
- package/src/workflows/feature/one-step-land.feature +11 -34
- package/src/workflows/feature/quality-unresolved.feature +96 -0
- package/src/workflows/feature/refactor-absorbs.feature +17 -25
- package/src/workflows/feature/refactor-first.feature +11 -35
- package/src/workflows/feature/refactor-lens-models.feature +6 -1
- package/src/workflows/feature/refactor-review.feature +67 -360
- package/src/workflows/feature/refactor.ts +4 -3
- package/src/workflows/feature/risk-tier.feature +10 -0
- package/src/workflows/feature/seam-rejection.feature +20 -37
- package/src/workflows/feature/seam.feature +66 -113
- package/src/workflows/feature/seam.test.ts +173 -3
- package/src/workflows/feature/seam.ts +196 -31
- package/src/workflows/review/review-door.feature +108 -0
- package/src/workflows/shared/tail.ts +2 -2
- package/src/workflows/shared/text.test.ts +40 -0
- package/src/workflows/shared/text.ts +110 -16
package/dist/workflows.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
1
2
|
//#region \0rolldown/runtime.js
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -17212,6 +17213,17 @@ carries it.
|
|
|
17212
17213
|
What each change does next (then run \`gtd land\`):
|
|
17213
17214
|
- **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
|
|
17214
17215
|
`;
|
|
17216
|
+
const qualityUnresolvedMessage = () => `The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
|
|
17217
|
+
holds findings. Nothing reviews the change after this, so the process
|
|
17218
|
+
will not end with them open.
|
|
17219
|
+
|
|
17220
|
+
Fix what matters by hand, then delete or empty \`.gtd/QUALITY.md\` —
|
|
17221
|
+
dropping a finding is your call to make.
|
|
17222
|
+
|
|
17223
|
+
What each change does next (then run \`gtd land\`):
|
|
17224
|
+
- **Delete or empty \`.gtd/QUALITY.md\`** — the suite runs on your edits, then the process ends (**idle**).
|
|
17225
|
+
- **Anything else** — rests here again.
|
|
17226
|
+
`;
|
|
17215
17227
|
const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
|
|
17216
17228
|
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
17217
17229
|
|
|
@@ -17999,24 +18011,47 @@ Print the closing message and stop — this writes nothing itself.
|
|
|
17999
18011
|
`;
|
|
18000
18012
|
};
|
|
18001
18013
|
const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
|
|
18014
|
+
const isRed = (retry) => "red" in retry;
|
|
18015
|
+
const isFault = (retry) => "fault" in retry;
|
|
18016
|
+
const pickUp = (hash) => `Pick it up — \`git diff --binary ${hash}^ ${hash} | git apply\``;
|
|
18002
18017
|
const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
|
|
18003
18018
|
the file, never from memory, and stop only when each one holds. They are
|
|
18004
18019
|
settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
|
|
18005
|
-
const
|
|
18020
|
+
const naiveRetry = (retry) => isFault(retry) ? `
|
|
18021
|
+
Your previous attempt's (\`${retry.hash}\`) tests fall short:
|
|
18022
|
+
${retry.fault}
|
|
18023
|
+
${pickUp(retry.hash)} — and fix its tests; do not start over.
|
|
18024
|
+
` : isRed(retry) ? `
|
|
18025
|
+
Your previous attempt (\`${retry.red}\`) left the suite red; \`.gtd/FEEDBACK.md\`
|
|
18026
|
+
holds the output. ${pickUp(retry.red)}
|
|
18027
|
+
— and fix it until the suite is green; do not start over.
|
|
18028
|
+
` : `
|
|
18029
|
+
Your previous attempt was rejected: the judge answered \`reject\` (p=${retry.p}),
|
|
18030
|
+
meaning the naive pass solved the task the wrong way — discard it and
|
|
18031
|
+
re-implement. It changed these files outside the expected set:
|
|
18032
|
+
${listed(retry.strays)}
|
|
18033
|
+
Start over and choose a different approach; do not rebuild the same diff.
|
|
18034
|
+
`;
|
|
18035
|
+
const naivePassPrompt = (retry) => `${stateFileRules}
|
|
18006
18036
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18007
18037
|
${criteriaBinding(false)}
|
|
18008
18038
|
- Implement them the quickest way that works — the naive version. Do not
|
|
18009
18039
|
restructure, abstract or refactor around the change; a later step judges
|
|
18010
18040
|
the shape of your diff and a reviewer makes it fit
|
|
18011
|
-
-
|
|
18041
|
+
- Work breadth-first: rough out every part of the change before finishing any
|
|
18042
|
+
one, so a run cut off midway still shows the whole shape
|
|
18043
|
+
- Write behaviour-level acceptance tests, at least one per criterion, each
|
|
18044
|
+
naming its ID in the test or scenario name (\`[AC-3] …\`). Test through code
|
|
18045
|
+
that existed before this pass — a public entry point, the CLI, an existing
|
|
18046
|
+
module's exports — never a file this pass adds: that is naive structure a
|
|
18047
|
+
later reset and refactor replaces. A pass that leaves a criterion's ID out
|
|
18048
|
+
of its tests, or whose test imports a file it added, is sent back
|
|
18049
|
+
- Run \`${env.testCommand}\` and finish only once it is green with every
|
|
18050
|
+
criterion's test passing: the next step runs it on your diff and sends a red
|
|
18051
|
+
pass back
|
|
18052
|
+
- Leave \`.gtd/\` untouched
|
|
18012
18053
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
18013
|
-
${
|
|
18014
|
-
Your previous attempt was rejected: the judge answered \`reject\` (p=${rejection.p}),
|
|
18015
|
-
meaning the naive pass solved the task the wrong way — discard it and
|
|
18016
|
-
re-implement. It changed these files outside the expected set:
|
|
18017
|
-
${listed(rejection.strays)}
|
|
18018
|
-
Start over and choose a different approach; do not rebuild the same diff.
|
|
18019
|
-
`}`;
|
|
18054
|
+
${retry === void 0 ? "" : naiveRetry(retry)}`;
|
|
18020
18055
|
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
|
|
18021
18056
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18022
18057
|
${criteriaBinding(true)}
|
|
@@ -18041,8 +18076,41 @@ const implementFixPrompt = () => `${stateFileRules}
|
|
|
18041
18076
|
${fixFeedbackPrompt}
|
|
18042
18077
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
18043
18078
|
`;
|
|
18044
|
-
const
|
|
18045
|
-
|
|
18079
|
+
const steerChoices = (overrule) => `Two ways forward (then run \`gtd land\`):
|
|
18080
|
+
- **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
|
|
18081
|
+
pass runs again with a fresh retry budget
|
|
18082
|
+
- **Overrule** — ${overrule}
|
|
18083
|
+
|
|
18084
|
+
An edit together with an approval counts as steer. Only
|
|
18085
|
+
\`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
|
|
18086
|
+
branch unjudged.
|
|
18087
|
+
`;
|
|
18088
|
+
const seamSteerMessage = (retry) => isFault(retry) ? `The naive pass failed three times and the retries are spent. The last
|
|
18089
|
+
attempt's tests (\`git show ${retry.hash}\`) fall short:
|
|
18090
|
+
${retry.fault}
|
|
18091
|
+
|
|
18092
|
+
${steerChoices(`land a clean tree to go on with the last attempt as it is:
|
|
18093
|
+
\`seam.check\` runs its suite and the seam judge rules on its shape. Use it
|
|
18094
|
+
when the rule cannot fit this change (tests the globs do not see, or no
|
|
18095
|
+
entry point that existed before). Risk: a criterion may have no test that
|
|
18096
|
+
survives a later refactor — the choice is recorded in history.`)}` : isRed(retry) ? `The naive pass failed three times and the retries are spent. The last
|
|
18097
|
+
attempt left the suite red: \`.gtd/FEEDBACK.md\` holds the last run's output; \`git show ${retry.red}\` shows
|
|
18098
|
+
the last attempt.
|
|
18099
|
+
|
|
18100
|
+
Two ways forward (then run \`gtd land\`):
|
|
18101
|
+
- **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
|
|
18102
|
+
pass runs again with a fresh retry budget
|
|
18103
|
+
- **Overrule** — land a clean tree to go on with the last attempt as it is:
|
|
18104
|
+
the seam judge rules on its shape, and the implementation's health loop
|
|
18105
|
+
must turn the suite green. Risk: the criteria's tests never passed on the
|
|
18106
|
+
naive pass — the choice is recorded in history.
|
|
18107
|
+
|
|
18108
|
+
An edit together with an approval counts as steer. Only
|
|
18109
|
+
\`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
|
|
18110
|
+
branch unjudged.
|
|
18111
|
+
` : seamRejectedMessage(retry);
|
|
18112
|
+
const seamRejectedMessage = (rejection) => `The naive pass failed three times and the retries are spent. The judge
|
|
18113
|
+
rejected the last attempt: it changed these files outside the expected set:
|
|
18046
18114
|
${listed(rejection.strays)}
|
|
18047
18115
|
|
|
18048
18116
|
Two ways forward (then run \`gtd land\`):
|
|
@@ -38473,7 +38541,7 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
38473
38541
|
};
|
|
38474
38542
|
//#endregion
|
|
38475
38543
|
//#region src/workflows/feature/seam.ts
|
|
38476
|
-
/** Naive-pass retries after a `reject
|
|
38544
|
+
/** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
|
|
38477
38545
|
const NAIVE_RETRIES = 2;
|
|
38478
38546
|
/** A change to an existing file outside the allowlist is a stray even when the task needs it: the judge decides. */
|
|
38479
38547
|
const judgeSeam = (tip) => judgeDiffShape("judge", {
|
|
@@ -38481,12 +38549,95 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
|
|
|
38481
38549
|
allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
|
|
38482
38550
|
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
|
|
38483
38551
|
});
|
|
38484
|
-
const naivePass = (
|
|
38485
|
-
label:
|
|
38552
|
+
const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
|
|
38553
|
+
label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
|
|
38486
38554
|
keep: true,
|
|
38487
38555
|
model: coder(),
|
|
38488
38556
|
system: builderSystem()
|
|
38489
38557
|
});
|
|
38558
|
+
const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
|
|
38559
|
+
const EXTENSION = /\.[\w-]+$/;
|
|
38560
|
+
const ALIAS = /^(?:[@~]\/|#)/;
|
|
38561
|
+
/** The specifiers an added `path` answers to: itself, without its extension, and its directory for an `index`. */
|
|
38562
|
+
const namesOf = (path) => {
|
|
38563
|
+
const bare = path.replace(EXTENSION, "");
|
|
38564
|
+
return posix.basename(bare) === "index" ? [
|
|
38565
|
+
path,
|
|
38566
|
+
bare,
|
|
38567
|
+
posix.dirname(bare)
|
|
38568
|
+
] : [path, bare];
|
|
38569
|
+
};
|
|
38570
|
+
/**
|
|
38571
|
+
* The repository paths `test` imports, each with and without an extension: a
|
|
38572
|
+
* relative specifier resolved from its directory, an aliased one (`@/`, `~/`,
|
|
38573
|
+
* `#`) as a path tail, anything else as written. Comments are skipped; a
|
|
38574
|
+
* computed import goes unseen.
|
|
38575
|
+
*/
|
|
38576
|
+
const importsOf = (test) => [...(test.after ?? "").replace(COMMENT, "").matchAll(IMPORTED)].flatMap(([, , spec]) => {
|
|
38577
|
+
const relative = /^\.\.?(\/|$)/.test(spec);
|
|
38578
|
+
const tail = ALIAS.test(spec);
|
|
38579
|
+
const path = relative ? posix.join(posix.dirname(test.path), spec) : spec.replace(ALIAS, "");
|
|
38580
|
+
return [path, path.replace(EXTENSION, "")].map((p) => ({
|
|
38581
|
+
path: p,
|
|
38582
|
+
tail
|
|
38583
|
+
}));
|
|
38584
|
+
});
|
|
38585
|
+
const reaches = (imports, added) => namesOf(added).some((name) => imports.some(({ path, tail }) => name === path || tail && name.endsWith(`/${path}`)));
|
|
38586
|
+
const named = (id, text) => new RegExp(`(?<![\\w-])${id}(?!\\d)`).test(text);
|
|
38587
|
+
/** The lines `change` added: an ID an earlier change left in the file does not count. */
|
|
38588
|
+
const addedLines = (change) => {
|
|
38589
|
+
const before = new Set((change.before ?? "").split("\n"));
|
|
38590
|
+
return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
|
|
38591
|
+
};
|
|
38592
|
+
/**
|
|
38593
|
+
* Why `tip`'s tests fall short, or `undefined`: a criterion ID of `file` no
|
|
38594
|
+
* test line of the pass names, or a test importing a file the pass added —
|
|
38595
|
+
* naive structure a reset and refactor replaces, which the tests must survive.
|
|
38596
|
+
*/
|
|
38597
|
+
const acceptanceFault = (file, tip) => {
|
|
38598
|
+
const ids = (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "");
|
|
38599
|
+
const tests = tip.changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
|
|
38600
|
+
const lines = tests.map(addedLines);
|
|
38601
|
+
const untested = ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
38602
|
+
if (untested.length > 0) return `acceptance-tests: no test of the naive pass names ${untested.join(", ")} — write a behaviour-level test for each criterion with its ID in the test or scenario name, e.g. "[${untested[0]}] …"`;
|
|
38603
|
+
const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
|
|
38604
|
+
const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
|
|
38605
|
+
const imports = importsOf(test);
|
|
38606
|
+
return added.filter((path) => reaches(imports, path)).map((path) => ` - ${test.path} → ${path}`);
|
|
38607
|
+
});
|
|
38608
|
+
if (reaching.length > 0) return `acceptance-internals: these tests import files the naive pass added — test through code that existed before it (a public entry point, an existing module's exports):\n${reaching.join("\n")}`;
|
|
38609
|
+
};
|
|
38610
|
+
const restore = (path) => `if git cat-file -e ${quote(`HEAD:${path}`)} 2>/dev/null; then git checkout -q HEAD -- ${quote(path)}; else rm -f -- ${quote(path)}; fi`;
|
|
38611
|
+
/**
|
|
38612
|
+
* Run the suite with `tip`'s diff applied, then take it out again; resolves
|
|
38613
|
+
* `true` when green. The pass's paths go back to HEAD before (an interrupted
|
|
38614
|
+
* run left them applied) and after; afterwards so does anything else the run
|
|
38615
|
+
* changed, judged against what was already dirty, which is left alone.
|
|
38616
|
+
*/
|
|
38617
|
+
const naiveGreen = (tip) => {
|
|
38618
|
+
const paths = tip.changes.map((c) => restore(c.path));
|
|
38619
|
+
return check("check", [
|
|
38620
|
+
...paths,
|
|
38621
|
+
"dirty=$(git -c core.quotePath=false ls-files -m -d -o --exclude-standard)",
|
|
38622
|
+
`if ! git diff --binary ${quote(`${tip.hash}^`)} ${quote(tip.hash)} -- | git apply; then`,
|
|
38623
|
+
" echo 'gtd: the kept naive pass no longer applies to HEAD; no test ran'",
|
|
38624
|
+
" exit 1",
|
|
38625
|
+
"fi",
|
|
38626
|
+
"(",
|
|
38627
|
+
env.testCommand ?? "",
|
|
38628
|
+
")",
|
|
38629
|
+
"code=$?",
|
|
38630
|
+
...paths,
|
|
38631
|
+
"git -c core.quotePath=false ls-files -m -d -o --exclude-standard | while IFS= read -r path; do",
|
|
38632
|
+
" printf '%s\\n' \"$dirty\" | grep -qxF -- \"$path\" && continue",
|
|
38633
|
+
" if git cat-file -e \"HEAD:$path\" 2>/dev/null; then git checkout -q HEAD -- \"$path\"; else rm -f -- \"$path\"; fi",
|
|
38634
|
+
"done",
|
|
38635
|
+
"exit \"$code\""
|
|
38636
|
+
].join("\n"), {
|
|
38637
|
+
report: FEEDBACK,
|
|
38638
|
+
label: "Running the naive pass's tests"
|
|
38639
|
+
});
|
|
38640
|
+
};
|
|
38490
38641
|
function implement(first, tier, amend) {
|
|
38491
38642
|
const options = typeof first === "object" && first !== null ? first : {
|
|
38492
38643
|
reference: first,
|
|
@@ -38513,36 +38664,76 @@ function implement(first, tier, amend) {
|
|
|
38513
38664
|
await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
|
|
38514
38665
|
});
|
|
38515
38666
|
}
|
|
38516
|
-
|
|
38517
|
-
|
|
38667
|
+
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
38668
|
+
const steer = async (retry) => {
|
|
38669
|
+
await human("steer", {
|
|
38670
|
+
message: seamSteerMessage(retry),
|
|
38671
|
+
label: "Steer or overrule the naive pass",
|
|
38672
|
+
file: REQUIREMENTS,
|
|
38673
|
+
acceptClean: true
|
|
38674
|
+
});
|
|
38675
|
+
if (changes().length === 0) return false;
|
|
38676
|
+
requireCriteria(REQUIREMENTS);
|
|
38677
|
+
return true;
|
|
38678
|
+
};
|
|
38679
|
+
/** Rejects, test faults and red checks share one budget; a steer or an overrule refills it. */
|
|
38680
|
+
const budget = () => {
|
|
38518
38681
|
let retries = 0;
|
|
38519
|
-
|
|
38520
|
-
await naivePass(rejection);
|
|
38521
|
-
const tip = kept("naive");
|
|
38522
|
-
const shape = await judgeSeam(tip);
|
|
38523
|
-
if (shape.choice === "land") return { reference: tip.hash };
|
|
38524
|
-
if (shape.choice === "refactor-first") return {
|
|
38525
|
-
reference: tip.hash,
|
|
38526
|
-
shape
|
|
38527
|
-
};
|
|
38528
|
-
rejection = {
|
|
38529
|
-
strays: shape.strays,
|
|
38530
|
-
p: shape.judgment?.answers.shape?.p ?? 0
|
|
38531
|
-
};
|
|
38682
|
+
return async (next) => {
|
|
38532
38683
|
if (retries < NAIVE_RETRIES) {
|
|
38533
38684
|
retries++;
|
|
38534
|
-
|
|
38685
|
+
return {
|
|
38686
|
+
kind: "retry",
|
|
38687
|
+
retry: next
|
|
38688
|
+
};
|
|
38535
38689
|
}
|
|
38536
|
-
await
|
|
38537
|
-
message: seamSteerMessage(rejection),
|
|
38538
|
-
label: "Steer or overrule the rejected naive pass",
|
|
38539
|
-
file: REQUIREMENTS,
|
|
38540
|
-
acceptClean: true
|
|
38541
|
-
});
|
|
38542
|
-
if (changes().length === 0) return { reference: tip.hash };
|
|
38543
|
-
requireCriteria(REQUIREMENTS);
|
|
38544
|
-
rejection = void 0;
|
|
38690
|
+
const steered = await steer(next);
|
|
38545
38691
|
retries = 0;
|
|
38692
|
+
if (read(".gtd/FEEDBACK.md") !== void 0) await run("clear", removeScript([FEEDBACK]), { label: "Clearing the red check" });
|
|
38693
|
+
return { kind: steered ? "steered" : "overruled" };
|
|
38694
|
+
};
|
|
38695
|
+
};
|
|
38696
|
+
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
38697
|
+
const vetted = async (tip, vetting) => {
|
|
38698
|
+
const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
|
|
38699
|
+
if (fault !== void 0) {
|
|
38700
|
+
const verdict = await vetting.again({
|
|
38701
|
+
fault,
|
|
38702
|
+
hash: tip.hash
|
|
38703
|
+
});
|
|
38704
|
+
if (verdict.kind !== "overruled") return verdict;
|
|
38705
|
+
vetting.waived = true;
|
|
38706
|
+
}
|
|
38707
|
+
if (await naiveGreen(tip)) return void 0;
|
|
38708
|
+
const verdict = await vetting.again({ red: tip.hash });
|
|
38709
|
+
return verdict.kind === "overruled" ? void 0 : verdict;
|
|
38710
|
+
};
|
|
38711
|
+
/** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
|
|
38712
|
+
const ruled = async (tip, again) => {
|
|
38713
|
+
const shape = await judgeSeam(tip);
|
|
38714
|
+
if (shape.choice === "land") return { reference: tip.hash };
|
|
38715
|
+
if (shape.choice === "refactor-first") return {
|
|
38716
|
+
reference: tip.hash,
|
|
38717
|
+
shape
|
|
38718
|
+
};
|
|
38719
|
+
const next = await again({
|
|
38720
|
+
strays: shape.strays,
|
|
38721
|
+
p: shape.judgment?.answers.shape?.p ?? 0
|
|
38722
|
+
});
|
|
38723
|
+
return next.kind === "overruled" ? { reference: tip.hash } : next;
|
|
38724
|
+
};
|
|
38725
|
+
const decide = () => scope("seam", async () => {
|
|
38726
|
+
const vetting = {
|
|
38727
|
+
again: budget(),
|
|
38728
|
+
waived: false
|
|
38729
|
+
};
|
|
38730
|
+
let retry;
|
|
38731
|
+
for (;;) {
|
|
38732
|
+
await naivePass(retry);
|
|
38733
|
+
const tip = kept("naive");
|
|
38734
|
+
const outcome = await vetted(tip, vetting) ?? await ruled(tip, vetting.again);
|
|
38735
|
+
if (!("kind" in outcome)) return outcome;
|
|
38736
|
+
retry = outcome.kind === "retry" ? outcome.retry : void 0;
|
|
38546
38737
|
}
|
|
38547
38738
|
});
|
|
38548
38739
|
const seamBuild = async (base) => {
|
|
@@ -38580,7 +38771,7 @@ const unwind = () => {
|
|
|
38580
38771
|
const commit = head$1();
|
|
38581
38772
|
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
38582
38773
|
};
|
|
38583
|
-
/** A judged naive pass (none with `flagMode` off), then implement and
|
|
38774
|
+
/** A judged naive pass (none with `flagMode` off), then implement and the quality lap; design must have run since `firstBase`. */
|
|
38584
38775
|
const buildFeature = async (firstBase) => {
|
|
38585
38776
|
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
38586
38777
|
const seam = flagModeOn() ? await seamBuild(firstBase) : { base: firstBase };
|
|
@@ -38591,18 +38782,31 @@ const buildFeature = async (firstBase) => {
|
|
|
38591
38782
|
afterRefactor: seam.refactored
|
|
38592
38783
|
});
|
|
38593
38784
|
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path)) ? firstBase : seam.base;
|
|
38594
|
-
await
|
|
38785
|
+
await tail(settleQuality, {
|
|
38595
38786
|
green: true,
|
|
38596
38787
|
lensBase: base
|
|
38597
|
-
})
|
|
38788
|
+
});
|
|
38598
38789
|
});
|
|
38599
38790
|
};
|
|
38791
|
+
const settleQuality = async () => {
|
|
38792
|
+
let rested = false;
|
|
38793
|
+
while ((read(".gtd/QUALITY.md") ?? "").length > 0) {
|
|
38794
|
+
rested = true;
|
|
38795
|
+
await human("quality-unresolved", {
|
|
38796
|
+
message: qualityUnresolvedMessage(),
|
|
38797
|
+
label: "Quality findings left unresolved",
|
|
38798
|
+
file: QUALITY
|
|
38799
|
+
});
|
|
38800
|
+
}
|
|
38801
|
+
if (rested) await healthy(() => fixCheck());
|
|
38802
|
+
};
|
|
38600
38803
|
/** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
|
|
38601
38804
|
const planAndBuild = async (firstBase, stated) => {
|
|
38602
38805
|
if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
|
|
38603
38806
|
await buildFeature(firstBase);
|
|
38604
38807
|
};
|
|
38605
|
-
|
|
38808
|
+
/** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
|
|
38809
|
+
const afterTail = async (first) => {
|
|
38606
38810
|
let outcome = first;
|
|
38607
38811
|
while (outcome.verdict === "feedback") {
|
|
38608
38812
|
await reUnwind(outcome);
|
|
@@ -38613,13 +38817,10 @@ const featureFeedback = async (first, lensBase) => {
|
|
|
38613
38817
|
});
|
|
38614
38818
|
outcome = await buildTail(outcome.base, {
|
|
38615
38819
|
green: true,
|
|
38616
|
-
lap: "feedback"
|
|
38617
|
-
lensBase
|
|
38820
|
+
lap: "feedback"
|
|
38618
38821
|
});
|
|
38619
38822
|
}
|
|
38620
38823
|
};
|
|
38621
|
-
/** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
|
|
38622
|
-
const afterTail = (outcome) => featureFeedback(outcome);
|
|
38623
38824
|
/** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
|
|
38624
38825
|
const ordinaryStart = async () => {
|
|
38625
38826
|
await human("idle", {
|
|
@@ -38823,4 +39024,4 @@ var workflows_default = feature;
|
|
|
38823
39024
|
*/
|
|
38824
39025
|
const doors = {};
|
|
38825
39026
|
//#endregion
|
|
38826
|
-
export { ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, 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, implement, judgeSeam, lensEntriesOf, lensOf, lensesOf, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unwind };
|
|
39027
|
+
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, 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, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unwind };
|
package/package.json
CHANGED
|
@@ -224,8 +224,8 @@ workflow.
|
|
|
224
224
|
|
|
225
225
|
In the bundled default, `planAndBuild` runs the design phase, then
|
|
226
226
|
`buildFeature`: the naive pass and its seam judge, then the implementation and
|
|
227
|
-
review. Add a human sign-off before the
|
|
228
|
-
|
|
227
|
+
the quality lap, ending with no human review. Add a human sign-off before the
|
|
228
|
+
build:
|
|
229
229
|
|
|
230
230
|
```ts
|
|
231
231
|
const planAndBuild = async (firstBase: string): Promise<void> => {
|
package/src/testkit/steps.ts
CHANGED
|
@@ -108,6 +108,10 @@ Given("a file {string} with:", (world: TestkitWorld, path: string, content: stri
|
|
|
108
108
|
world.repo().write(path, withNewline(content))
|
|
109
109
|
})
|
|
110
110
|
|
|
111
|
+
Given("the file {string} is deleted", (world: TestkitWorld, path: string) => {
|
|
112
|
+
world.repo().remove(path)
|
|
113
|
+
})
|
|
114
|
+
|
|
111
115
|
Given("the working tree is committed as {string}", (world: TestkitWorld, message: string) => {
|
|
112
116
|
world.repo().commit(message)
|
|
113
117
|
})
|
|
@@ -86,6 +86,14 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
86
86
|
- AC-1: `greeting` reads "Hello, world".
|
|
87
87
|
"""
|
|
88
88
|
And step "design.gate.answer" changes nothing
|
|
89
|
+
And step "seam.check" changes nothing
|
|
90
|
+
And step "seam.naive" writes "src/acceptance.test.ts" with:
|
|
91
|
+
"""
|
|
92
|
+
it("[AC-1] holds", () => {})
|
|
93
|
+
it("[AC-2] holds", () => {})
|
|
94
|
+
it("[AC-3] holds", () => {})
|
|
95
|
+
it("[AC-7] holds", () => {})
|
|
96
|
+
"""
|
|
89
97
|
And step "seam.naive" writes "src/greeting.ts" with:
|
|
90
98
|
"""
|
|
91
99
|
export const greeting = "Hello, world"
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Feature: a trivial change takes the same few beats at either risk tier, flag-free at low
|
|
3
3
|
|
|
4
4
|
With GTD_FLAG_RISK_TIER on, a one-line copy fix goes
|
|
5
|
-
from idle through the naive pass, the seam judge's land, implement and
|
|
6
|
-
|
|
5
|
+
from idle through the naive pass, the seam judge's land, implement and the
|
|
6
|
+
quality lap back to idle. Low tier walks exactly the steps high does; only the
|
|
7
7
|
implementation prompt and the summary differ.
|
|
8
8
|
|
|
9
9
|
Background:
|
|
@@ -27,18 +27,6 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
|
|
|
27
27
|
And step "implement.health.check" changes nothing
|
|
28
28
|
And step "implement.closing" runs its script
|
|
29
29
|
And step "build.quality.correctness.reviewing" changes nothing
|
|
30
|
-
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
31
|
-
"""
|
|
32
|
-
# Review: abc1234
|
|
33
|
-
|
|
34
|
-
<!-- base: 0000000 -->
|
|
35
|
-
|
|
36
|
-
## greeting
|
|
37
|
-
|
|
38
|
-
- [ ] ./src/greeting.ts#1-1
|
|
39
|
-
typo fixed
|
|
40
|
-
"""
|
|
41
|
-
And step "build.review.closing" runs its script
|
|
42
30
|
|
|
43
31
|
Scenario Outline: a one-line fix runs idle to idle with the same beats at either tier
|
|
44
32
|
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
@@ -57,7 +45,12 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
|
|
|
57
45
|
Then the process rests at "seam.naive"
|
|
58
46
|
And the last commit subject is "gtd(human): design.gate.answer → seam.naive"
|
|
59
47
|
|
|
60
|
-
Given step "seam.
|
|
48
|
+
Given step "seam.check" changes nothing
|
|
49
|
+
And step "seam.naive" writes "src/acceptance.test.ts" with:
|
|
50
|
+
"""
|
|
51
|
+
it("[AC-1] holds", () => {})
|
|
52
|
+
"""
|
|
53
|
+
And step "seam.naive" writes "src/greeting.ts" with:
|
|
61
54
|
"""
|
|
62
55
|
export const greeting = "Hello, world"
|
|
63
56
|
"""
|
|
@@ -75,21 +68,6 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
|
|
|
75
68
|
export const greeting = "Hello, world!"
|
|
76
69
|
"""
|
|
77
70
|
When the process is driven
|
|
78
|
-
Then the process rests at "build.review.await-review"
|
|
79
|
-
|
|
80
|
-
Given a file ".gtd/REVIEW.md" with:
|
|
81
|
-
"""
|
|
82
|
-
# Review: abc1234
|
|
83
|
-
|
|
84
|
-
<!-- base: 0000000 -->
|
|
85
|
-
|
|
86
|
-
## greeting
|
|
87
|
-
|
|
88
|
-
- [x] ./src/greeting.ts#1-1
|
|
89
|
-
typo fixed
|
|
90
|
-
"""
|
|
91
|
-
When gtd lands
|
|
92
|
-
And the process is driven
|
|
93
71
|
Then the process rests at "idle"
|
|
94
72
|
And "src/greeting.ts" contains "Hello, world!"
|
|
95
73
|
And the commit subjects end with:
|
|
@@ -99,15 +77,13 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
|
|
|
99
77
|
gtd(check): start-gate.check → design.triage
|
|
100
78
|
gtd(agent): design.triage → design.gate.answer
|
|
101
79
|
gtd(human): design.gate.answer → seam.naive
|
|
102
|
-
gtd(agent): seam.naive → seam.
|
|
80
|
+
gtd(agent): seam.naive → seam.check
|
|
81
|
+
gtd(check): seam.check → seam.judge
|
|
103
82
|
gtd(judge): seam.judge → implement.building
|
|
104
83
|
gtd(agent): implement.building → implement.health.check
|
|
105
84
|
gtd(check): implement.health.check → implement.closing
|
|
106
85
|
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
107
|
-
gtd(agent): build.quality.correctness.reviewing →
|
|
108
|
-
gtd(agent): build.review.reviewing → build.review.await-review
|
|
109
|
-
gtd(human): build.review.await-review → build.review.closing
|
|
110
|
-
gtd(check): build.review.closing → idle
|
|
86
|
+
gtd(agent): build.quality.correctness.reviewing → idle
|
|
111
87
|
"""
|
|
112
88
|
And the output of gtd "summary" <summary> "needs a human reviewer"
|
|
113
89
|
|