@pmelab/gtd 32.1.0 → 33.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 +18 -15
- package/dist/chunks/{reserved-2fV0mj14.mjs → reserved-BU5Zi-nX.mjs} +67 -11
- package/dist/chunks/{tail-BynhCfuv.mjs → tail-DuXanjIY.mjs} +1 -1
- package/dist/testkit.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +156 -32
- 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 +220 -40
- package/package.json +1 -1
- package/src/workflows/feature/acceptance-criteria.feature +8 -0
- package/src/workflows/feature/fast-path.feature +8 -2
- package/src/workflows/feature/feature.test.ts +19 -9
- package/src/workflows/feature/naive-acceptance.feature +140 -0
- package/src/workflows/feature/one-step-land.feature +8 -2
- package/src/workflows/feature/refactor-absorbs.feature +6 -1
- package/src/workflows/feature/refactor-first.feature +9 -3
- package/src/workflows/feature/refactor-lens-models.feature +6 -1
- package/src/workflows/feature/refactor-review.feature +50 -5
- package/src/workflows/feature/refactor.ts +4 -3
- package/src/workflows/feature/risk-tier.feature +10 -0
- package/src/workflows/feature/seam-rejection.feature +19 -6
- package/src/workflows/feature/seam.feature +70 -16
- package/src/workflows/feature/seam.test.ts +173 -3
- package/src/workflows/feature/seam.ts +195 -30
- package/src/workflows/shared/text.test.ts +40 -0
- package/src/workflows/shared/text.ts +97 -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) => {
|
|
@@ -17999,24 +18000,47 @@ Print the closing message and stop — this writes nothing itself.
|
|
|
17999
18000
|
`;
|
|
18000
18001
|
};
|
|
18001
18002
|
const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
|
|
18003
|
+
const isRed = (retry) => "red" in retry;
|
|
18004
|
+
const isFault = (retry) => "fault" in retry;
|
|
18005
|
+
const pickUp = (hash) => `Pick it up — \`git diff --binary ${hash}^ ${hash} | git apply\``;
|
|
18002
18006
|
const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
|
|
18003
18007
|
the file, never from memory, and stop only when each one holds. They are
|
|
18004
18008
|
settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
|
|
18005
|
-
const
|
|
18009
|
+
const naiveRetry = (retry) => isFault(retry) ? `
|
|
18010
|
+
Your previous attempt's (\`${retry.hash}\`) tests fall short:
|
|
18011
|
+
${retry.fault}
|
|
18012
|
+
${pickUp(retry.hash)} — and fix its tests; do not start over.
|
|
18013
|
+
` : isRed(retry) ? `
|
|
18014
|
+
Your previous attempt (\`${retry.red}\`) left the suite red; \`.gtd/FEEDBACK.md\`
|
|
18015
|
+
holds the output. ${pickUp(retry.red)}
|
|
18016
|
+
— and fix it until the suite is green; do not start over.
|
|
18017
|
+
` : `
|
|
18018
|
+
Your previous attempt was rejected: the judge answered \`reject\` (p=${retry.p}),
|
|
18019
|
+
meaning the naive pass solved the task the wrong way — discard it and
|
|
18020
|
+
re-implement. It changed these files outside the expected set:
|
|
18021
|
+
${listed(retry.strays)}
|
|
18022
|
+
Start over and choose a different approach; do not rebuild the same diff.
|
|
18023
|
+
`;
|
|
18024
|
+
const naivePassPrompt = (retry) => `${stateFileRules}
|
|
18006
18025
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18007
18026
|
${criteriaBinding(false)}
|
|
18008
18027
|
- Implement them the quickest way that works — the naive version. Do not
|
|
18009
18028
|
restructure, abstract or refactor around the change; a later step judges
|
|
18010
18029
|
the shape of your diff and a reviewer makes it fit
|
|
18011
|
-
-
|
|
18030
|
+
- Work breadth-first: rough out every part of the change before finishing any
|
|
18031
|
+
one, so a run cut off midway still shows the whole shape
|
|
18032
|
+
- Write behaviour-level acceptance tests, at least one per criterion, each
|
|
18033
|
+
naming its ID in the test or scenario name (\`[AC-3] …\`). Test through code
|
|
18034
|
+
that existed before this pass — a public entry point, the CLI, an existing
|
|
18035
|
+
module's exports — never a file this pass adds: that is naive structure a
|
|
18036
|
+
later reset and refactor replaces. A pass that leaves a criterion's ID out
|
|
18037
|
+
of its tests, or whose test imports a file it added, is sent back
|
|
18038
|
+
- Run \`${env.testCommand}\` and finish only once it is green with every
|
|
18039
|
+
criterion's test passing: the next step runs it on your diff and sends a red
|
|
18040
|
+
pass back
|
|
18041
|
+
- Leave \`.gtd/\` untouched
|
|
18012
18042
|
- 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
|
-
`}`;
|
|
18043
|
+
${retry === void 0 ? "" : naiveRetry(retry)}`;
|
|
18020
18044
|
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
|
|
18021
18045
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18022
18046
|
${criteriaBinding(true)}
|
|
@@ -18041,8 +18065,41 @@ const implementFixPrompt = () => `${stateFileRules}
|
|
|
18041
18065
|
${fixFeedbackPrompt}
|
|
18042
18066
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
18043
18067
|
`;
|
|
18044
|
-
const
|
|
18045
|
-
|
|
18068
|
+
const steerChoices = (overrule) => `Two ways forward (then run \`gtd land\`):
|
|
18069
|
+
- **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
|
|
18070
|
+
pass runs again with a fresh retry budget
|
|
18071
|
+
- **Overrule** — ${overrule}
|
|
18072
|
+
|
|
18073
|
+
An edit together with an approval counts as steer. Only
|
|
18074
|
+
\`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
|
|
18075
|
+
branch unjudged.
|
|
18076
|
+
`;
|
|
18077
|
+
const seamSteerMessage = (retry) => isFault(retry) ? `The naive pass failed three times and the retries are spent. The last
|
|
18078
|
+
attempt's tests (\`git show ${retry.hash}\`) fall short:
|
|
18079
|
+
${retry.fault}
|
|
18080
|
+
|
|
18081
|
+
${steerChoices(`land a clean tree to go on with the last attempt as it is:
|
|
18082
|
+
\`seam.check\` runs its suite and the seam judge rules on its shape. Use it
|
|
18083
|
+
when the rule cannot fit this change (tests the globs do not see, or no
|
|
18084
|
+
entry point that existed before). Risk: a criterion may have no test that
|
|
18085
|
+
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
|
|
18086
|
+
attempt left the suite red: \`.gtd/FEEDBACK.md\` holds the last run's output; \`git show ${retry.red}\` shows
|
|
18087
|
+
the last attempt.
|
|
18088
|
+
|
|
18089
|
+
Two ways forward (then run \`gtd land\`):
|
|
18090
|
+
- **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
|
|
18091
|
+
pass runs again with a fresh retry budget
|
|
18092
|
+
- **Overrule** — land a clean tree to go on with the last attempt as it is:
|
|
18093
|
+
the seam judge rules on its shape, and the implementation's health loop
|
|
18094
|
+
must turn the suite green. Risk: the criteria's tests never passed on the
|
|
18095
|
+
naive pass — the choice is recorded in history.
|
|
18096
|
+
|
|
18097
|
+
An edit together with an approval counts as steer. Only
|
|
18098
|
+
\`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
|
|
18099
|
+
branch unjudged.
|
|
18100
|
+
` : seamRejectedMessage(retry);
|
|
18101
|
+
const seamRejectedMessage = (rejection) => `The naive pass failed three times and the retries are spent. The judge
|
|
18102
|
+
rejected the last attempt: it changed these files outside the expected set:
|
|
18046
18103
|
${listed(rejection.strays)}
|
|
18047
18104
|
|
|
18048
18105
|
Two ways forward (then run \`gtd land\`):
|
|
@@ -38473,7 +38530,7 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
38473
38530
|
};
|
|
38474
38531
|
//#endregion
|
|
38475
38532
|
//#region src/workflows/feature/seam.ts
|
|
38476
|
-
/** Naive-pass retries after a `reject
|
|
38533
|
+
/** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
|
|
38477
38534
|
const NAIVE_RETRIES = 2;
|
|
38478
38535
|
/** A change to an existing file outside the allowlist is a stray even when the task needs it: the judge decides. */
|
|
38479
38536
|
const judgeSeam = (tip) => judgeDiffShape("judge", {
|
|
@@ -38481,12 +38538,95 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
|
|
|
38481
38538
|
allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
|
|
38482
38539
|
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
|
|
38483
38540
|
});
|
|
38484
|
-
const naivePass = (
|
|
38485
|
-
label:
|
|
38541
|
+
const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
|
|
38542
|
+
label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
|
|
38486
38543
|
keep: true,
|
|
38487
38544
|
model: coder(),
|
|
38488
38545
|
system: builderSystem()
|
|
38489
38546
|
});
|
|
38547
|
+
const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
|
|
38548
|
+
const EXTENSION = /\.[\w-]+$/;
|
|
38549
|
+
const ALIAS = /^(?:[@~]\/|#)/;
|
|
38550
|
+
/** The specifiers an added `path` answers to: itself, without its extension, and its directory for an `index`. */
|
|
38551
|
+
const namesOf = (path) => {
|
|
38552
|
+
const bare = path.replace(EXTENSION, "");
|
|
38553
|
+
return posix.basename(bare) === "index" ? [
|
|
38554
|
+
path,
|
|
38555
|
+
bare,
|
|
38556
|
+
posix.dirname(bare)
|
|
38557
|
+
] : [path, bare];
|
|
38558
|
+
};
|
|
38559
|
+
/**
|
|
38560
|
+
* The repository paths `test` imports, each with and without an extension: a
|
|
38561
|
+
* relative specifier resolved from its directory, an aliased one (`@/`, `~/`,
|
|
38562
|
+
* `#`) as a path tail, anything else as written. Comments are skipped; a
|
|
38563
|
+
* computed import goes unseen.
|
|
38564
|
+
*/
|
|
38565
|
+
const importsOf = (test) => [...(test.after ?? "").replace(COMMENT, "").matchAll(IMPORTED)].flatMap(([, , spec]) => {
|
|
38566
|
+
const relative = /^\.\.?(\/|$)/.test(spec);
|
|
38567
|
+
const tail = ALIAS.test(spec);
|
|
38568
|
+
const path = relative ? posix.join(posix.dirname(test.path), spec) : spec.replace(ALIAS, "");
|
|
38569
|
+
return [path, path.replace(EXTENSION, "")].map((p) => ({
|
|
38570
|
+
path: p,
|
|
38571
|
+
tail
|
|
38572
|
+
}));
|
|
38573
|
+
});
|
|
38574
|
+
const reaches = (imports, added) => namesOf(added).some((name) => imports.some(({ path, tail }) => name === path || tail && name.endsWith(`/${path}`)));
|
|
38575
|
+
const named = (id, text) => new RegExp(`(?<![\\w-])${id}(?!\\d)`).test(text);
|
|
38576
|
+
/** The lines `change` added: an ID an earlier change left in the file does not count. */
|
|
38577
|
+
const addedLines = (change) => {
|
|
38578
|
+
const before = new Set((change.before ?? "").split("\n"));
|
|
38579
|
+
return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
|
|
38580
|
+
};
|
|
38581
|
+
/**
|
|
38582
|
+
* Why `tip`'s tests fall short, or `undefined`: a criterion ID of `file` no
|
|
38583
|
+
* test line of the pass names, or a test importing a file the pass added —
|
|
38584
|
+
* naive structure a reset and refactor replaces, which the tests must survive.
|
|
38585
|
+
*/
|
|
38586
|
+
const acceptanceFault = (file, tip) => {
|
|
38587
|
+
const ids = (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "");
|
|
38588
|
+
const tests = tip.changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
|
|
38589
|
+
const lines = tests.map(addedLines);
|
|
38590
|
+
const untested = ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
38591
|
+
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]}] …"`;
|
|
38592
|
+
const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
|
|
38593
|
+
const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
|
|
38594
|
+
const imports = importsOf(test);
|
|
38595
|
+
return added.filter((path) => reaches(imports, path)).map((path) => ` - ${test.path} → ${path}`);
|
|
38596
|
+
});
|
|
38597
|
+
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")}`;
|
|
38598
|
+
};
|
|
38599
|
+
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`;
|
|
38600
|
+
/**
|
|
38601
|
+
* Run the suite with `tip`'s diff applied, then take it out again; resolves
|
|
38602
|
+
* `true` when green. The pass's paths go back to HEAD before (an interrupted
|
|
38603
|
+
* run left them applied) and after; afterwards so does anything else the run
|
|
38604
|
+
* changed, judged against what was already dirty, which is left alone.
|
|
38605
|
+
*/
|
|
38606
|
+
const naiveGreen = (tip) => {
|
|
38607
|
+
const paths = tip.changes.map((c) => restore(c.path));
|
|
38608
|
+
return check("check", [
|
|
38609
|
+
...paths,
|
|
38610
|
+
"dirty=$(git -c core.quotePath=false ls-files -m -d -o --exclude-standard)",
|
|
38611
|
+
`if ! git diff --binary ${quote(`${tip.hash}^`)} ${quote(tip.hash)} -- | git apply; then`,
|
|
38612
|
+
" echo 'gtd: the kept naive pass no longer applies to HEAD; no test ran'",
|
|
38613
|
+
" exit 1",
|
|
38614
|
+
"fi",
|
|
38615
|
+
"(",
|
|
38616
|
+
env.testCommand ?? "",
|
|
38617
|
+
")",
|
|
38618
|
+
"code=$?",
|
|
38619
|
+
...paths,
|
|
38620
|
+
"git -c core.quotePath=false ls-files -m -d -o --exclude-standard | while IFS= read -r path; do",
|
|
38621
|
+
" printf '%s\\n' \"$dirty\" | grep -qxF -- \"$path\" && continue",
|
|
38622
|
+
" if git cat-file -e \"HEAD:$path\" 2>/dev/null; then git checkout -q HEAD -- \"$path\"; else rm -f -- \"$path\"; fi",
|
|
38623
|
+
"done",
|
|
38624
|
+
"exit \"$code\""
|
|
38625
|
+
].join("\n"), {
|
|
38626
|
+
report: FEEDBACK,
|
|
38627
|
+
label: "Running the naive pass's tests"
|
|
38628
|
+
});
|
|
38629
|
+
};
|
|
38490
38630
|
function implement(first, tier, amend) {
|
|
38491
38631
|
const options = typeof first === "object" && first !== null ? first : {
|
|
38492
38632
|
reference: first,
|
|
@@ -38513,36 +38653,76 @@ function implement(first, tier, amend) {
|
|
|
38513
38653
|
await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
|
|
38514
38654
|
});
|
|
38515
38655
|
}
|
|
38516
|
-
|
|
38517
|
-
|
|
38656
|
+
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
38657
|
+
const steer = async (retry) => {
|
|
38658
|
+
await human("steer", {
|
|
38659
|
+
message: seamSteerMessage(retry),
|
|
38660
|
+
label: "Steer or overrule the naive pass",
|
|
38661
|
+
file: REQUIREMENTS,
|
|
38662
|
+
acceptClean: true
|
|
38663
|
+
});
|
|
38664
|
+
if (changes().length === 0) return false;
|
|
38665
|
+
requireCriteria(REQUIREMENTS);
|
|
38666
|
+
return true;
|
|
38667
|
+
};
|
|
38668
|
+
/** Rejects, test faults and red checks share one budget; a steer or an overrule refills it. */
|
|
38669
|
+
const budget = () => {
|
|
38518
38670
|
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
|
-
};
|
|
38671
|
+
return async (next) => {
|
|
38532
38672
|
if (retries < NAIVE_RETRIES) {
|
|
38533
38673
|
retries++;
|
|
38534
|
-
|
|
38674
|
+
return {
|
|
38675
|
+
kind: "retry",
|
|
38676
|
+
retry: next
|
|
38677
|
+
};
|
|
38535
38678
|
}
|
|
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;
|
|
38679
|
+
const steered = await steer(next);
|
|
38545
38680
|
retries = 0;
|
|
38681
|
+
if (read(".gtd/FEEDBACK.md") !== void 0) await run("clear", removeScript([FEEDBACK]), { label: "Clearing the red check" });
|
|
38682
|
+
return { kind: steered ? "steered" : "overruled" };
|
|
38683
|
+
};
|
|
38684
|
+
};
|
|
38685
|
+
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
38686
|
+
const vetted = async (tip, vetting) => {
|
|
38687
|
+
const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
|
|
38688
|
+
if (fault !== void 0) {
|
|
38689
|
+
const verdict = await vetting.again({
|
|
38690
|
+
fault,
|
|
38691
|
+
hash: tip.hash
|
|
38692
|
+
});
|
|
38693
|
+
if (verdict.kind !== "overruled") return verdict;
|
|
38694
|
+
vetting.waived = true;
|
|
38695
|
+
}
|
|
38696
|
+
if (await naiveGreen(tip)) return void 0;
|
|
38697
|
+
const verdict = await vetting.again({ red: tip.hash });
|
|
38698
|
+
return verdict.kind === "overruled" ? void 0 : verdict;
|
|
38699
|
+
};
|
|
38700
|
+
/** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
|
|
38701
|
+
const ruled = async (tip, again) => {
|
|
38702
|
+
const shape = await judgeSeam(tip);
|
|
38703
|
+
if (shape.choice === "land") return { reference: tip.hash };
|
|
38704
|
+
if (shape.choice === "refactor-first") return {
|
|
38705
|
+
reference: tip.hash,
|
|
38706
|
+
shape
|
|
38707
|
+
};
|
|
38708
|
+
const next = await again({
|
|
38709
|
+
strays: shape.strays,
|
|
38710
|
+
p: shape.judgment?.answers.shape?.p ?? 0
|
|
38711
|
+
});
|
|
38712
|
+
return next.kind === "overruled" ? { reference: tip.hash } : next;
|
|
38713
|
+
};
|
|
38714
|
+
const decide = () => scope("seam", async () => {
|
|
38715
|
+
const vetting = {
|
|
38716
|
+
again: budget(),
|
|
38717
|
+
waived: false
|
|
38718
|
+
};
|
|
38719
|
+
let retry;
|
|
38720
|
+
for (;;) {
|
|
38721
|
+
await naivePass(retry);
|
|
38722
|
+
const tip = kept("naive");
|
|
38723
|
+
const outcome = await vetted(tip, vetting) ?? await ruled(tip, vetting.again);
|
|
38724
|
+
if (!("kind" in outcome)) return outcome;
|
|
38725
|
+
retry = outcome.kind === "retry" ? outcome.retry : void 0;
|
|
38546
38726
|
}
|
|
38547
38727
|
});
|
|
38548
38728
|
const seamBuild = async (base) => {
|
|
@@ -38823,4 +39003,4 @@ var workflows_default = feature;
|
|
|
38823
39003
|
*/
|
|
38824
39004
|
const doors = {};
|
|
38825
39005
|
//#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 };
|
|
39006
|
+
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
|
@@ -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"
|
|
@@ -57,7 +57,12 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
|
|
|
57
57
|
Then the process rests at "seam.naive"
|
|
58
58
|
And the last commit subject is "gtd(human): design.gate.answer → seam.naive"
|
|
59
59
|
|
|
60
|
-
Given step "seam.
|
|
60
|
+
Given step "seam.check" changes nothing
|
|
61
|
+
And step "seam.naive" writes "src/acceptance.test.ts" with:
|
|
62
|
+
"""
|
|
63
|
+
it("[AC-1] holds", () => {})
|
|
64
|
+
"""
|
|
65
|
+
And step "seam.naive" writes "src/greeting.ts" with:
|
|
61
66
|
"""
|
|
62
67
|
export const greeting = "Hello, world"
|
|
63
68
|
"""
|
|
@@ -99,7 +104,8 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
|
|
|
99
104
|
gtd(check): start-gate.check → design.triage
|
|
100
105
|
gtd(agent): design.triage → design.gate.answer
|
|
101
106
|
gtd(human): design.gate.answer → seam.naive
|
|
102
|
-
gtd(agent): seam.naive → seam.
|
|
107
|
+
gtd(agent): seam.naive → seam.check
|
|
108
|
+
gtd(check): seam.check → seam.judge
|
|
103
109
|
gtd(judge): seam.judge → implement.building
|
|
104
110
|
gtd(agent): implement.building → implement.health.check
|
|
105
111
|
gtd(check): implement.health.check → implement.closing
|
|
@@ -47,6 +47,8 @@ const review = (text?: string): string =>
|
|
|
47
47
|
|
|
48
48
|
const CALC = "export const add = (a: number, b: number) => a + b\n"
|
|
49
49
|
|
|
50
|
+
const ACCEPTANCE = 'it("[AC-1] done", () => {})\n'
|
|
51
|
+
|
|
50
52
|
const naivePlanned = () => {
|
|
51
53
|
const repo = testRepo({
|
|
52
54
|
workflow: CONFIG_SCAFFOLD,
|
|
@@ -77,7 +79,11 @@ describe("planAndBuild", () => {
|
|
|
77
79
|
"# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
78
80
|
),
|
|
79
81
|
"design.gate.answer": () => undefined,
|
|
80
|
-
"seam.naive": (_, r) =>
|
|
82
|
+
"seam.naive": (_, r) => {
|
|
83
|
+
r.write("src/calc.ts", SUBTRACT)
|
|
84
|
+
r.write("src/calc.test.ts", ACCEPTANCE)
|
|
85
|
+
},
|
|
86
|
+
"seam.check": () => undefined,
|
|
81
87
|
"seam.judge": () => '[{"id":"shape","answer":"refactor-first","p":0.9}]',
|
|
82
88
|
"refactor.deriving": runsItsScript,
|
|
83
89
|
"refactor.building": (_, r) => r.write("src/calc.ts", SUBTRACT),
|
|
@@ -112,7 +118,11 @@ describe("planAndBuild", () => {
|
|
|
112
118
|
"start-gate.check": () => undefined,
|
|
113
119
|
"design.triage": requirements,
|
|
114
120
|
"design.gate.answer": () => undefined,
|
|
115
|
-
"seam.naive": (_: unknown, r: typeof repo) =>
|
|
121
|
+
"seam.naive": (_: unknown, r: typeof repo) => {
|
|
122
|
+
r.write("src/calc.ts", `${CALC}// naive\n`)
|
|
123
|
+
r.write("src/calc.test.ts", ACCEPTANCE)
|
|
124
|
+
},
|
|
125
|
+
"seam.check": () => undefined,
|
|
116
126
|
"seam.judge": () => '[{"id":"shape","answer":"land","p":0.9}]',
|
|
117
127
|
"implement.building": (_: unknown, r: typeof repo) =>
|
|
118
128
|
r.write("src/calc.ts", `${CALC}// built\n`),
|
|
@@ -341,7 +351,10 @@ describe("an in-flight 28.0.0 naive-pass process", () => {
|
|
|
341
351
|
"# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
342
352
|
),
|
|
343
353
|
"design.gate.answer": () => undefined,
|
|
344
|
-
"seam.naive": (_, r) =>
|
|
354
|
+
"seam.naive": (_, r) => {
|
|
355
|
+
r.write("src/calc.ts", `${CALC}// naive\n`)
|
|
356
|
+
r.write("src/calc.test.ts", ACCEPTANCE)
|
|
357
|
+
},
|
|
345
358
|
})
|
|
346
359
|
const kept = /Gtd-Kept: (\w+)/.exec(repo.messages().at(-1)!)![1]!
|
|
347
360
|
const seen: Record<string, number> = {
|
|
@@ -366,16 +379,13 @@ describe("an in-flight 28.0.0 naive-pass process", () => {
|
|
|
366
379
|
return repo
|
|
367
380
|
}
|
|
368
381
|
|
|
382
|
+
// seam.check now follows every naive pass, so no rest past the first naive pass replays.
|
|
369
383
|
for (const [name, trail] of Object.entries(trails)) {
|
|
370
|
-
|
|
371
|
-
it(`${name}: ${replays ? "every rest replays to itself" : "every rest from the verdict on fails replay"}`, async () => {
|
|
384
|
+
it(`${name}: every rest past the naive pass fails replay`, async () => {
|
|
372
385
|
for (let upTo = 1; upTo <= trail.length; upTo++) {
|
|
373
386
|
const rest = trail[upTo - 1]![2]
|
|
374
387
|
const repo = await forged(trail, upTo)
|
|
375
|
-
|
|
376
|
-
else {
|
|
377
|
-
await expect(repo.next(), rest).rejects.toThrow("the workflow changed under this process")
|
|
378
|
-
}
|
|
388
|
+
await expect(repo.next(), rest).rejects.toThrow("the workflow changed under this process")
|
|
379
389
|
}
|
|
380
390
|
})
|
|
381
391
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a naive pass ends only when a test names every criterion and the suite is green
|
|
3
|
+
|
|
4
|
+
`seam.naive` writes behaviour-level acceptance tests, each naming its
|
|
5
|
+
criterion's ID. A pass whose tests leave an ID out or import a file the
|
|
6
|
+
pass added is sent back; `seam.check` then runs the suite with the kept
|
|
7
|
+
pass applied, and a red run sends it back too, before the judge sees it.
|
|
8
|
+
Three failed attempts rest at `seam.steer`.
|
|
9
|
+
|
|
10
|
+
Background:
|
|
11
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
12
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
13
|
+
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
14
|
+
And a file "src/calc.ts" with:
|
|
15
|
+
"""
|
|
16
|
+
export const add = (a: number, b: number) => a + b
|
|
17
|
+
"""
|
|
18
|
+
And the working tree is committed as "feat: add calculator"
|
|
19
|
+
And a file "TODO.md" with:
|
|
20
|
+
"""
|
|
21
|
+
# Add subtract
|
|
22
|
+
|
|
23
|
+
subtract(a, b) returns the difference.
|
|
24
|
+
"""
|
|
25
|
+
And step "unwind" changes nothing
|
|
26
|
+
And step "start-gate.check" changes nothing
|
|
27
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
28
|
+
"""
|
|
29
|
+
# Requirements
|
|
30
|
+
|
|
31
|
+
## Acceptance Criteria
|
|
32
|
+
|
|
33
|
+
- AC-1: `subtract(5, 3)` returns 2.
|
|
34
|
+
- AC-2: `add` still adds.
|
|
35
|
+
"""
|
|
36
|
+
And step "design.gate.answer" changes nothing
|
|
37
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
38
|
+
"""
|
|
39
|
+
export const add = (a: number, b: number) => a + b
|
|
40
|
+
export const subtract = (a: number, b: number) => a - b
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
Scenario: a pass whose tests leave a criterion's ID out is sent back, and an overrule runs the check and sticks
|
|
44
|
+
Given step "seam.naive" writes "tests/calc.test.ts" with:
|
|
45
|
+
"""
|
|
46
|
+
import { add, subtract } from "../src/calc.js"
|
|
47
|
+
it("[AC-1] subtracts", () => expect(subtract(5, 3)).toBe(2))
|
|
48
|
+
"""
|
|
49
|
+
When the process is driven
|
|
50
|
+
Then the process rests at "seam.steer"
|
|
51
|
+
And the resting step's content contains "acceptance-tests: no test of the naive pass names AC-2"
|
|
52
|
+
|
|
53
|
+
Given step "seam.check" changes nothing
|
|
54
|
+
When gtd lands
|
|
55
|
+
And the process is driven
|
|
56
|
+
Then the process rests at "seam.judge"
|
|
57
|
+
And the commit subjects end with:
|
|
58
|
+
"""
|
|
59
|
+
gtd(human): design.gate.answer → seam.naive
|
|
60
|
+
gtd(agent): seam.naive
|
|
61
|
+
gtd(agent): seam.naive
|
|
62
|
+
gtd(agent): seam.naive → seam.steer
|
|
63
|
+
gtd(human): seam.steer → seam.check
|
|
64
|
+
gtd(check): seam.check → seam.judge
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
Given judge step "seam.judge" answers:
|
|
68
|
+
"""
|
|
69
|
+
[{"id":"shape","answer":"reject","p":0.9}]
|
|
70
|
+
"""
|
|
71
|
+
When the process is driven
|
|
72
|
+
Then the process rests at "seam.steer"
|
|
73
|
+
And the resting step's content contains "rejected the last attempt"
|
|
74
|
+
And the commit subjects end with:
|
|
75
|
+
"""
|
|
76
|
+
gtd(check): seam.check → seam.judge
|
|
77
|
+
gtd(judge): seam.judge → seam.naive
|
|
78
|
+
gtd(agent): seam.naive → seam.check
|
|
79
|
+
gtd(check): seam.check → seam.judge
|
|
80
|
+
gtd(judge): seam.judge → seam.naive
|
|
81
|
+
gtd(agent): seam.naive → seam.check
|
|
82
|
+
gtd(check): seam.check → seam.judge
|
|
83
|
+
gtd(judge): seam.judge → seam.steer
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
Scenario: a test importing a file the pass added is sent back
|
|
87
|
+
Given step "seam.naive" writes "src/subtract.ts" with:
|
|
88
|
+
"""
|
|
89
|
+
export const subtract = (a: number, b: number) => a - b
|
|
90
|
+
"""
|
|
91
|
+
And step "seam.naive" writes "src/subtract.test.ts" with:
|
|
92
|
+
"""
|
|
93
|
+
import { subtract } from "./subtract.js"
|
|
94
|
+
it("[AC-1] subtracts", () => expect(subtract(5, 3)).toBe(2))
|
|
95
|
+
it("[AC-2] still adds", () => {})
|
|
96
|
+
"""
|
|
97
|
+
When the process is driven
|
|
98
|
+
Then the process rests at "seam.steer"
|
|
99
|
+
And the resting step's content contains "src/subtract.test.ts → src/subtract.ts"
|
|
100
|
+
|
|
101
|
+
Scenario: a red suite sends the pass back, and it moves on only once green
|
|
102
|
+
Given step "seam.naive" writes "tests/calc.test.ts" with:
|
|
103
|
+
"""
|
|
104
|
+
import { add, subtract } from "../src/calc.js"
|
|
105
|
+
it("[AC-1] subtracts", () => expect(subtract(5, 3)).toBe(2))
|
|
106
|
+
it("[AC-2] still adds", () => expect(add(1, 1)).toBe(2))
|
|
107
|
+
"""
|
|
108
|
+
When the process is driven
|
|
109
|
+
Then the process rests at "seam.check"
|
|
110
|
+
And the resting step's content contains "git checkout -q HEAD --"
|
|
111
|
+
And the resting step's content does not contain "'^'"
|
|
112
|
+
And the resting step's content contains "npm test"
|
|
113
|
+
|
|
114
|
+
Given step "seam.naive" is no longer scripted
|
|
115
|
+
And step "seam.check" writes ".gtd/FEEDBACK.md" with:
|
|
116
|
+
"""
|
|
117
|
+
FAIL tests/calc.test.ts > [AC-1] subtracts
|
|
118
|
+
"""
|
|
119
|
+
When the process is driven
|
|
120
|
+
Then the process rests at "seam.naive"
|
|
121
|
+
And the resting step's content contains "left the suite red"
|
|
122
|
+
And ".gtd/FEEDBACK.md" contains "[AC-1] subtracts"
|
|
123
|
+
|
|
124
|
+
Given step "seam.naive" writes "tests/calc.test.ts" with:
|
|
125
|
+
"""
|
|
126
|
+
import { add, subtract } from "../src/calc.js"
|
|
127
|
+
it("[AC-1] subtracts", () => expect(subtract(5, 3)).toBe(2))
|
|
128
|
+
it("[AC-2] still adds", () => expect(add(2, 2)).toBe(4))
|
|
129
|
+
"""
|
|
130
|
+
And step "seam.check" from now on changes nothing
|
|
131
|
+
When the process is driven
|
|
132
|
+
Then the process rests at "implement.building"
|
|
133
|
+
And the commit subjects end with:
|
|
134
|
+
"""
|
|
135
|
+
gtd(human): design.gate.answer → seam.naive
|
|
136
|
+
gtd(agent): seam.naive → seam.check
|
|
137
|
+
gtd(check): seam.check → seam.naive
|
|
138
|
+
gtd(agent): seam.naive → seam.check
|
|
139
|
+
gtd(check): seam.check → implement.building
|
|
140
|
+
"""
|
|
@@ -40,6 +40,11 @@ Feature: a naive pass the judge rules land goes straight to implement with no se
|
|
|
40
40
|
- AC-1: the change behaves as described.
|
|
41
41
|
"""
|
|
42
42
|
And step "design.gate.answer" changes nothing
|
|
43
|
+
And step "seam.check" changes nothing
|
|
44
|
+
And step "seam.naive" writes "src/acceptance.test.ts" with:
|
|
45
|
+
"""
|
|
46
|
+
it("[AC-1] holds", () => {})
|
|
47
|
+
"""
|
|
43
48
|
And step "seam.naive" writes "src/greeting.ts" with:
|
|
44
49
|
"""
|
|
45
50
|
export const greeting = "Hello, world"
|
|
@@ -93,7 +98,8 @@ Feature: a naive pass the judge rules land goes straight to implement with no se
|
|
|
93
98
|
gtd(check): start-gate.check → design.triage
|
|
94
99
|
gtd(agent): design.triage → design.gate.answer
|
|
95
100
|
gtd(human): design.gate.answer → seam.naive
|
|
96
|
-
gtd(agent): seam.naive → seam.
|
|
101
|
+
gtd(agent): seam.naive → seam.check
|
|
102
|
+
gtd(check): seam.check → seam.judge
|
|
97
103
|
gtd(judge): seam.judge → implement.building
|
|
98
104
|
gtd(agent): implement.building → implement.health.check
|
|
99
105
|
gtd(check): implement.health.check → implement.closing
|
|
@@ -104,7 +110,7 @@ Feature: a naive pass the judge rules land goes straight to implement with no se
|
|
|
104
110
|
gtd(check): build.review.closing → idle
|
|
105
111
|
"""
|
|
106
112
|
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
|
|
107
|
-
And the commit "gtd(agent): seam.naive → seam.
|
|
113
|
+
And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
|
|
108
114
|
And the commit "gtd(judge): seam.judge → implement.building" has a body containing "\"answer\":\"land\""
|
|
109
115
|
And no commit body contains "Gtd-Approved:"
|
|
110
116
|
And the output lines of gtd "slices" start with:
|
|
@@ -43,7 +43,12 @@ Feature: a refactor that carried the whole feature still gets a feature review
|
|
|
43
43
|
And step "design.gate.answer" changes nothing
|
|
44
44
|
|
|
45
45
|
Scenario: an empty implement turn after a refactor that did it all reaches review and idle
|
|
46
|
-
Given step "seam.
|
|
46
|
+
Given step "seam.check" changes nothing
|
|
47
|
+
And step "seam.naive" writes "src/acceptance.test.ts" with:
|
|
48
|
+
"""
|
|
49
|
+
it("[AC-1] holds", () => {})
|
|
50
|
+
"""
|
|
51
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
47
52
|
"""
|
|
48
53
|
export const add = (a: number, b: number) => a + b
|
|
49
54
|
export const subtract = (a: number, b: number) => a - b
|
|
@@ -42,7 +42,12 @@ Feature: a refactor-first judgment walks one fixed path to idle
|
|
|
42
42
|
And step "design.gate.answer" changes nothing
|
|
43
43
|
|
|
44
44
|
Scenario: a refactor-first judgment runs refactor, its review, implement and review to idle
|
|
45
|
-
Given step "seam.
|
|
45
|
+
Given step "seam.check" changes nothing
|
|
46
|
+
And step "seam.naive" writes "src/acceptance.test.ts" with:
|
|
47
|
+
"""
|
|
48
|
+
it("[AC-1] holds", () => {})
|
|
49
|
+
"""
|
|
50
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
46
51
|
"""
|
|
47
52
|
export const add = (a: number, b: number) => a + b
|
|
48
53
|
export const subtract = (a: number, b: number) => a - b
|
|
@@ -141,7 +146,8 @@ Feature: a refactor-first judgment walks one fixed path to idle
|
|
|
141
146
|
gtd(check): start-gate.check → design.triage
|
|
142
147
|
gtd(agent): design.triage → design.gate.answer
|
|
143
148
|
gtd(human): design.gate.answer → seam.naive
|
|
144
|
-
gtd(agent): seam.naive → seam.
|
|
149
|
+
gtd(agent): seam.naive → seam.check
|
|
150
|
+
gtd(check): seam.check → seam.judge
|
|
145
151
|
gtd(judge): seam.judge → refactor.deriving
|
|
146
152
|
gtd(check): refactor.deriving → refactor.building
|
|
147
153
|
gtd(agent): refactor.building → refactor.health.check
|
|
@@ -158,7 +164,7 @@ Feature: a refactor-first judgment walks one fixed path to idle
|
|
|
158
164
|
gtd(check): build.review.closing → idle
|
|
159
165
|
"""
|
|
160
166
|
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
|
|
161
|
-
And the commit "gtd(agent): seam.naive → seam.
|
|
167
|
+
And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
|
|
162
168
|
And the commit "gtd(judge): seam.judge → refactor.deriving" has a body containing "\"answer\":\"refactor-first\""
|
|
163
169
|
And no commit body contains "Gtd-Approved:"
|
|
164
170
|
And the output lines of gtd "slices" start with:
|