@pmelab/gtd 37.0.0 → 38.1.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 +15 -0
- package/dist/chunks/{reserved-zAfoJtR7.mjs → reserved-CfQ8eurM.mjs} +85 -17
- package/dist/chunks/{tail-BRC4DGdV.mjs → tail-C7yaWvaS.mjs} +1 -1
- package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
- package/dist/gtd.bundle.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +318 -76
- 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 +419 -109
- package/package.json +1 -1
- package/src/testkit/steps.ts +39 -0
- package/src/wire/ContextLimit.ts +1 -1
- package/src/workflows/feature/continuation-cap.feature +65 -0
- package/src/workflows/feature/continuation-rebuild.feature +88 -0
- package/src/workflows/feature/continuation-slices.feature +64 -0
- package/src/workflows/feature/continuation.feature +205 -0
- package/src/workflows/feature/coverage.ts +14 -6
- package/src/workflows/feature/criteria.test.ts +17 -1
- package/src/workflows/feature/criteria.ts +37 -3
- package/src/workflows/feature/feature.ts +64 -15
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor.ts +64 -30
- package/src/workflows/feature/seam.ts +157 -48
- package/src/workflows/feature/slicing-cap.feature +14 -0
- package/src/workflows/feature/slicing.feature +225 -0
- package/src/workflows/feature/slicing.test.ts +49 -0
- package/src/workflows/feature/slicing.ts +105 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/continuation.ts +92 -0
- package/src/workflows/shared/fences.ts +5 -0
- package/src/workflows/shared/options.ts +3 -0
- package/src/workflows/shared/steps.ts +2 -0
- package/src/workflows/shared/text.test.ts +6 -2
- package/src/workflows/shared/text.ts +153 -22
- package/src/workflows/shared/vars.ts +6 -0
- package/src/workflows/shared/workflows.steps.ts +13 -0
package/dist/workflows.mjs
CHANGED
|
@@ -2784,7 +2784,7 @@ const characterEntities = {
|
|
|
2784
2784
|
};
|
|
2785
2785
|
//#endregion
|
|
2786
2786
|
//#region node_modules/decode-named-character-reference/index.js
|
|
2787
|
-
const own$
|
|
2787
|
+
const own$2 = {}.hasOwnProperty;
|
|
2788
2788
|
/**
|
|
2789
2789
|
* Decode a single character reference (without the `&` or `;`).
|
|
2790
2790
|
* You probably only need this when you’re building parsers yourself that follow
|
|
@@ -2797,7 +2797,7 @@ const own$1 = {}.hasOwnProperty;
|
|
|
2797
2797
|
* Decoded reference.
|
|
2798
2798
|
*/
|
|
2799
2799
|
function decodeNamedCharacterReference(value) {
|
|
2800
|
-
return own$
|
|
2800
|
+
return own$2.call(characterEntities, value) ? characterEntities[value] : false;
|
|
2801
2801
|
}
|
|
2802
2802
|
//#endregion
|
|
2803
2803
|
//#region node_modules/micromark-util-chunked/index.js
|
|
@@ -10261,7 +10261,7 @@ function index(value) {
|
|
|
10261
10261
|
* Options
|
|
10262
10262
|
* } from './types.js'
|
|
10263
10263
|
*/
|
|
10264
|
-
const own = {}.hasOwnProperty;
|
|
10264
|
+
const own$1 = {}.hasOwnProperty;
|
|
10265
10265
|
/**
|
|
10266
10266
|
* Turn markdown into a syntax tree.
|
|
10267
10267
|
*
|
|
@@ -10440,7 +10440,7 @@ function compiler(options) {
|
|
|
10440
10440
|
index = -1;
|
|
10441
10441
|
while (++index < events.length) {
|
|
10442
10442
|
const handler = config[events[index][0]];
|
|
10443
|
-
if (own.call(handler, events[index][1].type)) handler[events[index][1].type].call(Object.assign({ sliceSerialize: events[index][2].sliceSerialize }, context), events[index][1]);
|
|
10443
|
+
if (own$1.call(handler, events[index][1].type)) handler[events[index][1].type].call(Object.assign({ sliceSerialize: events[index][2].sliceSerialize }, context), events[index][1]);
|
|
10444
10444
|
}
|
|
10445
10445
|
if (context.tokenStack.length > 0) {
|
|
10446
10446
|
const tail = context.tokenStack[context.tokenStack.length - 1];
|
|
@@ -11152,7 +11152,7 @@ function configure(combined, extensions) {
|
|
|
11152
11152
|
function extension(combined, extension) {
|
|
11153
11153
|
/** @type {keyof Extension} */
|
|
11154
11154
|
let key;
|
|
11155
|
-
for (key in extension) if (own.call(extension, key)) switch (key) {
|
|
11155
|
+
for (key in extension) if (own$1.call(extension, key)) switch (key) {
|
|
11156
11156
|
case "canContainEols": {
|
|
11157
11157
|
const right = extension[key];
|
|
11158
11158
|
if (right) combined[key].push(...right);
|
|
@@ -16934,9 +16934,12 @@ const defaults = {
|
|
|
16934
16934
|
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
16935
16935
|
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
16936
16936
|
flagMode: "true",
|
|
16937
|
+
sliceMaxCriteria: "8",
|
|
16937
16938
|
flag_risk_tier: "0",
|
|
16938
16939
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
|
|
16939
|
-
refactorQualityReviews: "correctness, ponytail-review, conventions"
|
|
16940
|
+
refactorQualityReviews: "correctness, ponytail-review, conventions",
|
|
16941
|
+
continuations: "3",
|
|
16942
|
+
continuationStall: "2"
|
|
16940
16943
|
};
|
|
16941
16944
|
/** Environment settings. */
|
|
16942
16945
|
const envDefaults = {
|
|
@@ -17393,6 +17396,24 @@ printf '%s\\n' ${[
|
|
|
17393
17396
|
].map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
|
|
17394
17397
|
`;
|
|
17395
17398
|
};
|
|
17399
|
+
const slicingPrompt = (cap) => `${stateFileRules}
|
|
17400
|
+
- Read \`.gtd/REQUIREMENTS.md\`: its \`## Acceptance Criteria\` are settled
|
|
17401
|
+
and more than ${cap}, too many to build in one slice
|
|
17402
|
+
- Group them into slices in \`.gtd/SLICES.md\`, one \`## <title>\` heading per
|
|
17403
|
+
slice over a \`- AC-<n>\` list item per criterion it builds. Put criteria of
|
|
17404
|
+
the same flow or subsystem together; every criterion in exactly one slice,
|
|
17405
|
+
at most ${cap} per slice
|
|
17406
|
+
- Keep it coarse: as few slices as the cap allows, never one per criterion
|
|
17407
|
+
- Order the slices so each builds on the ones before: they run in that order,
|
|
17408
|
+
each from where the previous one landed
|
|
17409
|
+
- Change nothing but \`.gtd/SLICES.md\` — leave the criteria as they are
|
|
17410
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
17411
|
+
`;
|
|
17412
|
+
/** The line telling a slice's turn which criteria are its own; empty for a single slice. */
|
|
17413
|
+
const sliceLine = (ids) => ids === void 0 ? "" : `- This slice builds ${ids.join(", ")} only: every rule here about the criteria
|
|
17414
|
+
means these. The others are for other slices, before or after it — leave
|
|
17415
|
+
them unbuilt and untested
|
|
17416
|
+
`;
|
|
17396
17417
|
const designSystem = () => `${designPersona}
|
|
17397
17418
|
|
|
17398
17419
|
${agentConduct}`;
|
|
@@ -17436,7 +17457,7 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
|
17436
17457
|
- Introduce no new flag read and add no second read of an existing one:
|
|
17437
17458
|
every match of \`${pattern}\` may move, never increase in number`;
|
|
17438
17459
|
const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
|
|
17439
|
-
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false }) => `${stateFileRules}
|
|
17460
|
+
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false, from }) => `${stateFileRules}
|
|
17440
17461
|
- The package to implement is \`${pkg}\`; never delete it
|
|
17441
17462
|
${note ? `- The package's \`## Refactor note\` section names the seams to open and the
|
|
17442
17463
|
shape each takes, written from the whole naive pass: build exactly those
|
|
@@ -17451,8 +17472,9 @@ ${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${st
|
|
|
17451
17472
|
restructuring afresh here, minus the feature's own behaviour. A
|
|
17452
17473
|
refactor that touches none of it is wrong, however clean — never
|
|
17453
17474
|
swap in an unrelated tidy-up
|
|
17454
|
-
`}
|
|
17455
|
-
|
|
17475
|
+
`}${handoffRule()}
|
|
17476
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
17477
|
+
${from === void 0 ? "" : continuationLines(from)}`;
|
|
17456
17478
|
const refactorFixPrompt = (strays, pattern) => `${stateFileRules}
|
|
17457
17479
|
${fixFeedbackPrompt}
|
|
17458
17480
|
${refactorRules(strays, pattern)}
|
|
@@ -18020,7 +18042,7 @@ Print the closing message and stop — this writes nothing itself.
|
|
|
18020
18042
|
const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
|
|
18021
18043
|
const isRed = (retry) => "red" in retry;
|
|
18022
18044
|
const isFault = (retry) => "fault" in retry;
|
|
18023
|
-
const pickUp = (hash) => `Pick it up — \`git diff --binary ${hash}^ ${hash} | git apply\``;
|
|
18045
|
+
const pickUp = (hash, exclude) => `Pick it up — \`git diff --binary --no-color --no-ext-diff --no-textconv ${hash}^ ${hash}${exclude === void 0 ? "" : ` -- ${quote(":(top)")} ${quote(`:(top,exclude)${exclude}`)}`} | git apply\``;
|
|
18024
18046
|
const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
|
|
18025
18047
|
the file, never from memory, and stop only when each one holds. They are
|
|
18026
18048
|
settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
|
|
@@ -18039,9 +18061,35 @@ re-implement. It changed these files outside the expected set:
|
|
|
18039
18061
|
${listed(retry.strays)}
|
|
18040
18062
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
18041
18063
|
`;
|
|
18042
|
-
const
|
|
18064
|
+
const quoted = (text) => text.trimEnd().split("\n").map((line) => line === "" ? "" : ` > ${line}`).join("\n");
|
|
18065
|
+
/** Every turn a continuation may pick up tells its agent where the note goes. */
|
|
18066
|
+
const handoffRule = () => `- Should you be told to stop at a context limit, write the handoff note to
|
|
18067
|
+
\`.gtd/HANDOFF.md\` — what you touched, what was next — and finish your turn:
|
|
18068
|
+
a fresh turn picks up from what you leave, broken or not. That file is the one
|
|
18069
|
+
exception to any path rule here; write it at no other time`;
|
|
18070
|
+
/** `pickUp`: how the snapshot gets into the tree — absent when it already is. */
|
|
18071
|
+
const continuationLines = (from, pickUp) => `
|
|
18072
|
+
Your previous turn stopped at its context limit; this fresh turn picks up from
|
|
18073
|
+
its snapshot (\`${from.snapshot}\`). ${pickUp === void 0 ? "Its work is already in the tree." : `${pickUp}.`} Its handoff note:
|
|
18074
|
+
|
|
18075
|
+
${quoted(from.note === "" ? "(empty)" : from.note)}
|
|
18076
|
+
|
|
18077
|
+
Still open: ${from.open.length === 0 ? "nothing the workflow can count — finish what the note says is next" : from.open.join(", ")}.
|
|
18078
|
+
${pickUp === void 0 ? "Delete `.gtd/HANDOFF.md` once you are done; rewrite it should you be stopped again.\n" : ""}`;
|
|
18079
|
+
const oversizedMessage = (phase, from, why, narrowable = true) => `The ${phase} keeps outgrowing its context: ${"cap" in why ? why.cap === 0 ? "continuations are off (`continuations` is 0) and the turn stopped at its context limit" : `${why.cap} continuation${why.cap === 1 ? "" : "s"} ran and the last still stopped at its context limit` : `the last ${why.stall} continuation${why.stall === 1 ? "" : "s"} closed nothing still open`}. The last snapshot is \`${from.snapshot}\`; still open: ${from.open.length === 0 ? "nothing countable" : from.open.join(", ")}.
|
|
18080
|
+
|
|
18081
|
+
Its handoff note:
|
|
18082
|
+
|
|
18083
|
+
${quoted(from.note === "" ? "(empty)" : from.note)}
|
|
18084
|
+
|
|
18085
|
+
Land to go on: a fresh round of continuations picks up from that snapshot${narrowable ? ` — narrow
|
|
18086
|
+
\`.gtd/REQUIREMENTS.md\` first if the change is too big, or raise the context limit` : ` — raise
|
|
18087
|
+
the context limit first if the refactor is too big`}. \`gtd abandon\` gives the change up.
|
|
18088
|
+
`;
|
|
18089
|
+
/** `from`: a continuation of a pass that stopped at its context limit, in place of `retry`'s pick-up. */
|
|
18090
|
+
const naivePassPrompt = (retry, slice, from) => `${stateFileRules}
|
|
18043
18091
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18044
|
-
${criteriaBinding(false)}
|
|
18092
|
+
${sliceLine(slice)}${criteriaBinding(false)}
|
|
18045
18093
|
- Implement them the quickest way that works — the naive version. Do not
|
|
18046
18094
|
restructure, abstract or refactor around the change; it may land exactly as
|
|
18047
18095
|
you leave it, and a reviewer makes it fit
|
|
@@ -18056,9 +18104,20 @@ ${criteriaBinding(false)}
|
|
|
18056
18104
|
- Run \`${env.testCommand}\` and finish only once it is green with every
|
|
18057
18105
|
criterion's test passing: the next step runs it on your diff and sends a red
|
|
18058
18106
|
pass back
|
|
18059
|
-
- Leave \`.gtd/\` untouched
|
|
18107
|
+
- Leave \`.gtd/\` untouched, a handoff note excepted
|
|
18108
|
+
${handoffRule()}
|
|
18060
18109
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
18061
|
-
${retry === void 0 ? "" :
|
|
18110
|
+
${from !== void 0 ? `${continuationLines(from, pickUp(from.snapshot, ".gtd/HANDOFF.md"))}${retry === void 0 ? "" : isFault(retry) ? `
|
|
18111
|
+
The pass this continues was a retry: its tests fell short, and still must not:
|
|
18112
|
+
${retry.fault}
|
|
18113
|
+
` : isRed(retry) ? `
|
|
18114
|
+
The pass this continues was a retry after a red suite (\`.gtd/FEEDBACK.md\`
|
|
18115
|
+
holds the output): finish only once the suite is green.
|
|
18116
|
+
` : `
|
|
18117
|
+
The pass this continues was a retry after a \`reject\`: keep away from the
|
|
18118
|
+
approach that changed these files outside the expected set:
|
|
18119
|
+
${listed(retry.strays)}
|
|
18120
|
+
`}` : retry === void 0 ? "" : naiveRetry(retry)}`;
|
|
18062
18121
|
const carries = (rebuild) => (rebuild.acceptance?.ids.length ?? 0) > 0;
|
|
18063
18122
|
const rebuildLines = ({ note, acceptance }) => `${note === void 0 ? "" : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
|
|
18064
18123
|
opened; plug the change into them:
|
|
@@ -18091,9 +18150,13 @@ const afterRefactorLine = (rebuild) => `- A reviewed refactor has just landed an
|
|
|
18091
18150
|
though never a release flag. Build only what is still missing, held to every
|
|
18092
18151
|
rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
|
|
18093
18152
|
`;
|
|
18094
|
-
const
|
|
18153
|
+
const flagLine = (flagMode, tier, amend, sliced) => !flagMode ? "- Add no release flag — this project ships without release flags" : tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : sliced ? `- Put the new behaviour behind the release flag an earlier slice of this
|
|
18154
|
+
change added; only when none did, behind a new one, following the
|
|
18155
|
+
repository's flag conventions` : `- Put the new behaviour behind a new release flag, following the
|
|
18156
|
+
repository's flag conventions`;
|
|
18157
|
+
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}, slice, from) => `${stateFileRules}
|
|
18095
18158
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18096
|
-
${criteriaBinding(true)}
|
|
18159
|
+
${sliceLine(slice)}${criteriaBinding(true)}
|
|
18097
18160
|
${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
18098
18161
|
the re-derived requirements instead of rewriting it
|
|
18099
18162
|
` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
|
|
@@ -18102,14 +18165,14 @@ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `-
|
|
|
18102
18165
|
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
18103
18166
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
18104
18167
|
the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
|
|
18105
|
-
`}${rebuildLines(rebuild)}${
|
|
18106
|
-
repository's flag conventions`}
|
|
18168
|
+
`}${rebuildLines(rebuild)}${flagLine(flagMode, tier, amend, slice !== void 0)}
|
|
18107
18169
|
- Write the tests first, then the code that makes them pass. Name each
|
|
18108
18170
|
criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
|
|
18109
18171
|
green a judge checks every ID has a passing test, and sends uncovered ones back
|
|
18172
|
+
${handoffRule()}
|
|
18110
18173
|
- Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
|
|
18111
18174
|
finish your turn
|
|
18112
|
-
`;
|
|
18175
|
+
${from === void 0 ? "" : continuationLines(from)}`;
|
|
18113
18176
|
const implementFixPrompt = () => `${stateFileRules}
|
|
18114
18177
|
${fixFeedbackPrompt}
|
|
18115
18178
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
@@ -18184,9 +18247,9 @@ const coverageMessage = () => `Judging acceptance-criteria coverage. Run \`gtd j
|
|
|
18184
18247
|
\`yes\`/\`no\` verdict per criterion ID — or land without a verdict, which
|
|
18185
18248
|
counts every criterion as uncovered.
|
|
18186
18249
|
`;
|
|
18187
|
-
const coveringPrompt = (uncovered) => `${stateFileRules}
|
|
18250
|
+
const coveringPrompt = (uncovered, slice) => `${stateFileRules}
|
|
18188
18251
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18189
|
-
${criteriaBinding(true)}
|
|
18252
|
+
${sliceLine(slice)}${criteriaBinding(true)}
|
|
18190
18253
|
- A judge found no passing test in this change that covers ${uncovered.join(", ")}.
|
|
18191
18254
|
Write a behaviour-level test for each, naming its ID in the test or scenario
|
|
18192
18255
|
name (\`[${uncovered[0]}] …\`), and fix the code if the test shows it falls short
|
|
@@ -18213,6 +18276,8 @@ const REVIEW = ".gtd/REVIEW.md";
|
|
|
18213
18276
|
const REFACTOR = ".gtd/REFACTOR.md";
|
|
18214
18277
|
const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
|
|
18215
18278
|
const QUALITY = ".gtd/QUALITY.md";
|
|
18279
|
+
const SLICES = ".gtd/SLICES.md";
|
|
18280
|
+
const HANDOFF = ".gtd/HANDOFF.md";
|
|
18216
18281
|
const planner = () => env.plannerModel ?? "";
|
|
18217
18282
|
const coder = () => env.coderModel ?? "";
|
|
18218
18283
|
const fixCheck = (rules) => agentWithSkills("fix", buildFixPrompt(rules), {
|
|
@@ -37715,20 +37780,23 @@ Struct({
|
|
|
37715
37780
|
});
|
|
37716
37781
|
//#endregion
|
|
37717
37782
|
//#region src/workflows/shared/options.ts
|
|
37718
|
-
const setting = optionalWith(String$, { exact: true });
|
|
37783
|
+
const setting$1 = optionalWith(String$, { exact: true });
|
|
37719
37784
|
const ShippedOptions = Struct({
|
|
37720
|
-
reviewBase: setting,
|
|
37721
|
-
judgeBudgetBytes: setting,
|
|
37722
|
-
judgeIdenticalMinP: setting,
|
|
37723
|
-
reviewNoteActionable: setting,
|
|
37724
|
-
reviewSeverityMinP: setting,
|
|
37725
|
-
refactorFlagPattern: setting,
|
|
37726
|
-
seamAllow: setting,
|
|
37727
|
-
|
|
37728
|
-
|
|
37729
|
-
|
|
37730
|
-
|
|
37731
|
-
|
|
37785
|
+
reviewBase: setting$1,
|
|
37786
|
+
judgeBudgetBytes: setting$1,
|
|
37787
|
+
judgeIdenticalMinP: setting$1,
|
|
37788
|
+
reviewNoteActionable: setting$1,
|
|
37789
|
+
reviewSeverityMinP: setting$1,
|
|
37790
|
+
refactorFlagPattern: setting$1,
|
|
37791
|
+
seamAllow: setting$1,
|
|
37792
|
+
sliceMaxCriteria: setting$1,
|
|
37793
|
+
qualityReviews: setting$1,
|
|
37794
|
+
refactorQualityReviews: setting$1,
|
|
37795
|
+
continuations: setting$1,
|
|
37796
|
+
continuationStall: setting$1,
|
|
37797
|
+
testCommand: setting$1,
|
|
37798
|
+
plannerModel: setting$1,
|
|
37799
|
+
coderModel: setting$1,
|
|
37732
37800
|
flagMode: optionalWith(Boolean$, { exact: true }),
|
|
37733
37801
|
contextLimit: optionalWith(Struct({
|
|
37734
37802
|
soft: optionalWith(Number$, { exact: true }),
|
|
@@ -37759,7 +37827,7 @@ const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOp
|
|
|
37759
37827
|
}));
|
|
37760
37828
|
const FlagOptions = Struct({
|
|
37761
37829
|
...ShippedOptions.fields,
|
|
37762
|
-
flagRegistry: setting
|
|
37830
|
+
flagRegistry: setting$1
|
|
37763
37831
|
});
|
|
37764
37832
|
const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions, {
|
|
37765
37833
|
onExcessProperty: "error",
|
|
@@ -37904,13 +37972,15 @@ const markFenced = (text) => {
|
|
|
37904
37972
|
};
|
|
37905
37973
|
});
|
|
37906
37974
|
};
|
|
37975
|
+
/** A markdown heading: its `#` run, then its text. */
|
|
37976
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
37977
|
+
/** A list item, its checkbox dropped: its text. */
|
|
37978
|
+
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
37907
37979
|
//#endregion
|
|
37908
37980
|
//#region src/workflows/feature/criteria.ts
|
|
37909
37981
|
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
37910
37982
|
const CLEAR_MIN_P = .8;
|
|
37911
|
-
const
|
|
37912
|
-
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
37913
|
-
const ID = /^(AC-\d+):\s*(.*)$/;
|
|
37983
|
+
const ID$1 = /^(AC-\d+):\s*(.*)$/;
|
|
37914
37984
|
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
37915
37985
|
const listUnder = (text, title, level) => {
|
|
37916
37986
|
const lines = markFenced(text);
|
|
@@ -37938,7 +38008,7 @@ const listUnder = (text, title, level) => {
|
|
|
37938
38008
|
};
|
|
37939
38009
|
/** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
|
|
37940
38010
|
const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
|
|
37941
|
-
const id = ID.exec(item);
|
|
38011
|
+
const id = ID$1.exec(item);
|
|
37942
38012
|
return id === null ? {
|
|
37943
38013
|
id: "",
|
|
37944
38014
|
text: item
|
|
@@ -37947,16 +38017,20 @@ const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((
|
|
|
37947
38017
|
text: id[2]
|
|
37948
38018
|
};
|
|
37949
38019
|
});
|
|
38020
|
+
/** The ID'd criteria of `text`, only `slice`'s when given. */
|
|
38021
|
+
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));
|
|
38022
|
+
/** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
|
|
38023
|
+
const sliceIds = (slice) => slice === void 0 ? void 0 : criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice);
|
|
37950
38024
|
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
37951
38025
|
const numbered = (items) => {
|
|
37952
|
-
const ids = items.map((item) => ID.exec(item));
|
|
38026
|
+
const ids = items.map((item) => ID$1.exec(item));
|
|
37953
38027
|
const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
|
|
37954
38028
|
return items.map((item, i) => own ? {
|
|
37955
38029
|
id: ids[i][1],
|
|
37956
38030
|
text: ids[i][2]
|
|
37957
38031
|
} : {
|
|
37958
38032
|
id: `AC-${i + 1}`,
|
|
37959
|
-
text: item.replace(ID, "$2")
|
|
38033
|
+
text: item.replace(ID$1, "$2")
|
|
37960
38034
|
});
|
|
37961
38035
|
};
|
|
37962
38036
|
const STATED = /^acceptance criteria:?$/i;
|
|
@@ -38026,6 +38100,13 @@ const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scop
|
|
|
38026
38100
|
});
|
|
38027
38101
|
return true;
|
|
38028
38102
|
});
|
|
38103
|
+
/** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
|
|
38104
|
+
const requireCriteriaIfEdited = () => {
|
|
38105
|
+
const change = changes().get(REQUIREMENTS);
|
|
38106
|
+
if (change === void 0) return;
|
|
38107
|
+
if (change.status === "deleted") refuse(`gtd land: criteria-missing: ${REQUIREMENTS} must stay — narrow its criteria, never delete it`);
|
|
38108
|
+
requireCriteria(REQUIREMENTS);
|
|
38109
|
+
};
|
|
38029
38110
|
//#endregion
|
|
38030
38111
|
//#region src/workflows/feature/tier.ts
|
|
38031
38112
|
/** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
|
|
@@ -38374,6 +38455,53 @@ const reUnwind = async (feedback) => {
|
|
|
38374
38455
|
requireRevert(edited, base);
|
|
38375
38456
|
};
|
|
38376
38457
|
//#endregion
|
|
38458
|
+
//#region src/workflows/shared/continuation.ts
|
|
38459
|
+
const phase = (name, steered) => ({
|
|
38460
|
+
name,
|
|
38461
|
+
spent: false,
|
|
38462
|
+
count: 0,
|
|
38463
|
+
stalled: 0,
|
|
38464
|
+
steered
|
|
38465
|
+
});
|
|
38466
|
+
const own = (fn) => fn();
|
|
38467
|
+
const continued = (fn) => scope("continue", fn);
|
|
38468
|
+
const afresh = (fn) => scope("fresh", fn);
|
|
38469
|
+
const setting = (name, least) => {
|
|
38470
|
+
const raw = vars[name] ?? defaults[name];
|
|
38471
|
+
const n = Number(raw);
|
|
38472
|
+
if (raw.trim() === "" || !Number.isInteger(n) || n < least) refuse(`gtd: ${name} must be a whole number of at least ${least}, got ${JSON.stringify(raw)}`);
|
|
38473
|
+
return n;
|
|
38474
|
+
};
|
|
38475
|
+
/**
|
|
38476
|
+
* Run `turn` until one ends without a handoff note: each stopped turn is picked
|
|
38477
|
+
* up by a continuation in a fresh conversation. Past `continuations` of them in
|
|
38478
|
+
* the phase, or after `continuationStall` in a row that close nothing still
|
|
38479
|
+
* open, the phase rests at `oversized` for a person; landing there grants a new round.
|
|
38480
|
+
*/
|
|
38481
|
+
const continuing = async (phase, turn) => {
|
|
38482
|
+
const cap = setting("continuations", 0);
|
|
38483
|
+
const stall = setting("continuationStall", 1);
|
|
38484
|
+
let last = await turn(void 0, phase.spent ? afresh : own);
|
|
38485
|
+
while (last.handoff !== void 0) {
|
|
38486
|
+
phase.spent = true;
|
|
38487
|
+
const from = last.handoff;
|
|
38488
|
+
if (phase.count >= cap || phase.stalled >= stall) {
|
|
38489
|
+
await human("oversized", {
|
|
38490
|
+
message: oversizedMessage(phase.name, from, phase.count >= cap ? { cap } : { stall }, phase.steered !== void 0),
|
|
38491
|
+
label: "Too big for its context",
|
|
38492
|
+
acceptClean: true
|
|
38493
|
+
});
|
|
38494
|
+
phase.steered?.();
|
|
38495
|
+
phase.count = 0;
|
|
38496
|
+
phase.stalled = 0;
|
|
38497
|
+
}
|
|
38498
|
+
last = await turn(from, continued);
|
|
38499
|
+
phase.count++;
|
|
38500
|
+
phase.stalled = from.open.length === 0 || from.open.some((item) => !(last.handoff?.open ?? []).includes(item)) ? 0 : phase.stalled + 1;
|
|
38501
|
+
}
|
|
38502
|
+
return last.result;
|
|
38503
|
+
};
|
|
38504
|
+
//#endregion
|
|
38377
38505
|
//#region src/workflows/feature/refactor.ts
|
|
38378
38506
|
const REFACTOR_PACKAGE = REFACTOR;
|
|
38379
38507
|
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
@@ -38519,7 +38647,7 @@ const guardOf = ({ pattern, strays }, entry) => () => {
|
|
|
38519
38647
|
requireNoNewFlags(pattern, entry);
|
|
38520
38648
|
};
|
|
38521
38649
|
/** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
|
|
38522
|
-
const refactorLap = async (p, guard, feedback) => {
|
|
38650
|
+
const refactorLap = async (p, guard, refactor, feedback) => {
|
|
38523
38651
|
const { pattern, strays, reference, note } = p;
|
|
38524
38652
|
await run("deriving", deriveScript([
|
|
38525
38653
|
...packageLines(strays, pattern, reference),
|
|
@@ -38539,32 +38667,48 @@ const refactorLap = async (p, guard, feedback) => {
|
|
|
38539
38667
|
label: "Deriving the refactor package",
|
|
38540
38668
|
file: REFACTOR_PACKAGE
|
|
38541
38669
|
});
|
|
38542
|
-
|
|
38543
|
-
|
|
38544
|
-
|
|
38545
|
-
|
|
38546
|
-
|
|
38547
|
-
|
|
38548
|
-
|
|
38549
|
-
|
|
38550
|
-
|
|
38551
|
-
|
|
38552
|
-
|
|
38553
|
-
|
|
38554
|
-
guard();
|
|
38555
|
-
await healthy(async () => {
|
|
38556
|
-
await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
|
|
38557
|
-
label: "Fixing the check",
|
|
38558
|
-
file: FEEDBACK,
|
|
38670
|
+
const reached = /* @__PURE__ */ new Set();
|
|
38671
|
+
await continuing(refactor, async (from, fresh) => {
|
|
38672
|
+
await fresh(() => agentWithSkills("building", refactorBuildingPrompt({
|
|
38673
|
+
pkg: REFACTOR_PACKAGE,
|
|
38674
|
+
strays,
|
|
38675
|
+
pattern,
|
|
38676
|
+
reference,
|
|
38677
|
+
feedback: feedback !== void 0,
|
|
38678
|
+
note: note !== void 0,
|
|
38679
|
+
from
|
|
38680
|
+
}), {
|
|
38681
|
+
label: from === void 0 ? "Refactoring" : "Continuing the refactor",
|
|
38559
38682
|
model: coder(),
|
|
38560
38683
|
system: builderSystem()
|
|
38561
|
-
});
|
|
38684
|
+
}));
|
|
38685
|
+
const left = read(HANDOFF);
|
|
38562
38686
|
guard();
|
|
38563
|
-
|
|
38564
|
-
|
|
38565
|
-
|
|
38687
|
+
const snapshot = head$1();
|
|
38688
|
+
for (const c of changes()) reached.add(c.path);
|
|
38689
|
+
await healthy(async () => {
|
|
38690
|
+
await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
|
|
38691
|
+
label: "Fixing the check",
|
|
38692
|
+
file: FEEDBACK,
|
|
38693
|
+
model: coder(),
|
|
38694
|
+
system: builderSystem()
|
|
38695
|
+
});
|
|
38696
|
+
guard();
|
|
38697
|
+
}, {
|
|
38698
|
+
describeGuard: guard,
|
|
38699
|
+
rules: refactorRules(strays, pattern)
|
|
38700
|
+
});
|
|
38701
|
+
if (left === void 0) return { result: void 0 };
|
|
38702
|
+
return {
|
|
38703
|
+
result: void 0,
|
|
38704
|
+
handoff: {
|
|
38705
|
+
snapshot,
|
|
38706
|
+
note: left.trim(),
|
|
38707
|
+
open: strays.filter((path) => !reached.has(path))
|
|
38708
|
+
}
|
|
38709
|
+
};
|
|
38566
38710
|
});
|
|
38567
|
-
await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
|
|
38711
|
+
await run("closing", removeScript([REFACTOR_PACKAGE, HANDOFF]), { label: "Closing out the refactor" });
|
|
38568
38712
|
};
|
|
38569
38713
|
const strayAccess = (strays) => ({ write: [
|
|
38570
38714
|
...strays,
|
|
@@ -38578,7 +38722,7 @@ const refactorFirst = async (shape, options = {}) => {
|
|
|
38578
38722
|
name: "refactor",
|
|
38579
38723
|
access: strayAccess(p.strays)
|
|
38580
38724
|
}, async () => {
|
|
38581
|
-
await refactorLap(p, guardOf(p, head$1()));
|
|
38725
|
+
await refactorLap(p, guardOf(p, head$1()), phase("refactor"));
|
|
38582
38726
|
return true;
|
|
38583
38727
|
});
|
|
38584
38728
|
};
|
|
@@ -38603,9 +38747,10 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
38603
38747
|
unmatched: "gtd land: refactor-feedback: the review feedback changed files but wrote no .gtd/REFACTOR.md — a refactor cannot act on it; revert the change, or `gtd abandon` and restart",
|
|
38604
38748
|
dropped: refactorDroppedRefusal
|
|
38605
38749
|
};
|
|
38750
|
+
const refactor = phase("refactor");
|
|
38606
38751
|
let collected;
|
|
38607
38752
|
for (;;) {
|
|
38608
|
-
await refactorLap(p, guard, collected);
|
|
38753
|
+
await refactorLap(p, guard, refactor, collected);
|
|
38609
38754
|
const outcome = await buildTail(entry, {
|
|
38610
38755
|
green: true,
|
|
38611
38756
|
guard,
|
|
@@ -38646,10 +38791,12 @@ const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", imple
|
|
|
38646
38791
|
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");
|
|
38647
38792
|
/**
|
|
38648
38793
|
* The criterion IDs of `.gtd/REQUIREMENTS.md` — read afresh, never carried
|
|
38649
|
-
* over — no passing test since `base` covers, by the judge.
|
|
38794
|
+
* over, only `slice`'s when given — no passing test since `base` covers, by the judge.
|
|
38650
38795
|
*/
|
|
38651
|
-
const uncoveredSince = async (base) => {
|
|
38652
|
-
const
|
|
38796
|
+
const uncoveredSince = async (base, slice) => {
|
|
38797
|
+
const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
|
|
38798
|
+
const own = criterionIds(requirements, slice);
|
|
38799
|
+
const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
|
|
38653
38800
|
if (criteria.length === 0) return {
|
|
38654
38801
|
covered: [],
|
|
38655
38802
|
uncovered: []
|
|
@@ -38676,14 +38823,14 @@ const record = (covered, uncovered = []) => {
|
|
|
38676
38823
|
* sending uncovered IDs back to a covering turn; marks the IDs covered on the
|
|
38677
38824
|
* landing that ends it. A human may overrule once the retries are spent.
|
|
38678
38825
|
*/
|
|
38679
|
-
const requireCoverage = async (base = start()) => {
|
|
38826
|
+
const requireCoverage = async (base = start(), slice) => {
|
|
38680
38827
|
let retries = 0;
|
|
38681
38828
|
for (;;) {
|
|
38682
|
-
const { covered, uncovered } = await uncoveredSince(base);
|
|
38829
|
+
const { covered, uncovered } = await uncoveredSince(base, slice);
|
|
38683
38830
|
if (uncovered.length === 0) return record(covered);
|
|
38684
38831
|
if (retries < COVERING_RETRIES) {
|
|
38685
38832
|
retries++;
|
|
38686
|
-
await agentWithSkills("covering", coveringPrompt(uncovered), {
|
|
38833
|
+
await agentWithSkills("covering", coveringPrompt(uncovered, sliceIds(slice)), {
|
|
38687
38834
|
label: "Covering the acceptance criteria",
|
|
38688
38835
|
model: coder(),
|
|
38689
38836
|
system: builderSystem(),
|
|
@@ -38714,12 +38861,32 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
|
|
|
38714
38861
|
allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
|
|
38715
38862
|
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
|
|
38716
38863
|
});
|
|
38717
|
-
const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
|
|
38718
|
-
label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
|
|
38864
|
+
const naivePass = (retry, slice, from) => agentWithSkills("naive", naivePassPrompt(retry, sliceIds(slice), from), {
|
|
38865
|
+
label: from !== void 0 ? "Continuing the naive pass" : retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
|
|
38719
38866
|
keep: true,
|
|
38720
38867
|
model: coder(),
|
|
38721
38868
|
system: builderSystem()
|
|
38722
38869
|
});
|
|
38870
|
+
const handoffOf = (tip, slice) => {
|
|
38871
|
+
const note = tip.changes.find((c) => c.path === ".gtd/HANDOFF.md" && c.after !== void 0);
|
|
38872
|
+
if (note === void 0) return void 0;
|
|
38873
|
+
return {
|
|
38874
|
+
snapshot: tip.hash,
|
|
38875
|
+
note: note.after.trim(),
|
|
38876
|
+
open: unnamed(idsOf(REQUIREMENTS, slice), acceptanceTests(tip.changes))
|
|
38877
|
+
};
|
|
38878
|
+
};
|
|
38879
|
+
/** A naive pass, continued until a pass ends without a handoff note: the snapshot is its kept tip. */
|
|
38880
|
+
const naiveContinued = (retry, slice, naive) => continuing(naive, async (from, fresh) => {
|
|
38881
|
+
const tip = await fresh(async () => {
|
|
38882
|
+
await naivePass(retry, slice, from);
|
|
38883
|
+
return kept("naive");
|
|
38884
|
+
});
|
|
38885
|
+
return {
|
|
38886
|
+
result: tip,
|
|
38887
|
+
handoff: handoffOf(tip, slice)
|
|
38888
|
+
};
|
|
38889
|
+
});
|
|
38723
38890
|
const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
|
|
38724
38891
|
const EXTENSION = /\.[\w-]+$/;
|
|
38725
38892
|
const ALIAS = /^(?:[@~]\/|#)/;
|
|
@@ -38755,7 +38922,7 @@ const addedLines = (change) => {
|
|
|
38755
38922
|
return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
|
|
38756
38923
|
};
|
|
38757
38924
|
const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
|
|
38758
|
-
const idsOf = (file) => (
|
|
38925
|
+
const idsOf = (file, slice) => criterionIds(read(file) ?? "", slice);
|
|
38759
38926
|
const unnamed = (ids, tests) => {
|
|
38760
38927
|
const lines = tests.map(addedLines);
|
|
38761
38928
|
return ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
@@ -38765,9 +38932,9 @@ const unnamed = (ids, tests) => {
|
|
|
38765
38932
|
* test line of the pass names, or a test importing a file the pass added —
|
|
38766
38933
|
* naive structure a reset and refactor replaces, which the tests must survive.
|
|
38767
38934
|
*/
|
|
38768
|
-
const acceptanceFault = (file, tip) => {
|
|
38935
|
+
const acceptanceFault = (file, tip, slice) => {
|
|
38769
38936
|
const tests = acceptanceTests(tip.changes);
|
|
38770
|
-
const untested = unnamed(idsOf(file), tests);
|
|
38937
|
+
const untested = unnamed(idsOf(file, slice), tests);
|
|
38771
38938
|
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]}] …"`;
|
|
38772
38939
|
const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
|
|
38773
38940
|
const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
|
|
@@ -38777,8 +38944,8 @@ const acceptanceFault = (file, tip) => {
|
|
|
38777
38944
|
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")}`;
|
|
38778
38945
|
};
|
|
38779
38946
|
/** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
|
|
38780
|
-
const naiveAcceptance = (tip) => {
|
|
38781
|
-
const ids = idsOf(REQUIREMENTS);
|
|
38947
|
+
const naiveAcceptance = (tip, slice) => {
|
|
38948
|
+
const ids = idsOf(REQUIREMENTS, slice);
|
|
38782
38949
|
const perTest = acceptanceTests(tip.changes).map((c) => {
|
|
38783
38950
|
const lines = addedLines(c);
|
|
38784
38951
|
return {
|
|
@@ -38847,6 +39014,32 @@ const applied = async (tip) => {
|
|
|
38847
39014
|
await run("applying", applyScript(tip, FEEDBACK, naiveApplyFailure(tip), [".gtd"]), { label: "Applying the naive pass" });
|
|
38848
39015
|
return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
|
|
38849
39016
|
};
|
|
39017
|
+
/** `carried` limited to the criteria `.gtd/REQUIREMENTS.md` lists now; a file listing none narrows nothing. */
|
|
39018
|
+
const settled = (carried, slice) => {
|
|
39019
|
+
const listed = idsOf(REQUIREMENTS, slice);
|
|
39020
|
+
if (carried === void 0) return listed;
|
|
39021
|
+
return listed.length === 0 ? carried : carried.filter((id) => listed.includes(id));
|
|
39022
|
+
};
|
|
39023
|
+
const build = (options) => continuing(phase("build", requireCriteriaIfEdited), async (from, fresh) => {
|
|
39024
|
+
await fresh(() => agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild, sliceIds(options.slice), from), {
|
|
39025
|
+
label: from === void 0 ? "Implementing" : "Continuing the implementation",
|
|
39026
|
+
model: coder(),
|
|
39027
|
+
system: builderSystem(),
|
|
39028
|
+
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
39029
|
+
}));
|
|
39030
|
+
requireCriteriaFrozen(REQUIREMENTS);
|
|
39031
|
+
const note = read(HANDOFF);
|
|
39032
|
+
if (note === void 0) return { result: void 0 };
|
|
39033
|
+
const ids = settled(options.rebuild?.acceptance?.ids, options.slice);
|
|
39034
|
+
return {
|
|
39035
|
+
result: void 0,
|
|
39036
|
+
handoff: {
|
|
39037
|
+
snapshot: head$1(),
|
|
39038
|
+
note: note.trim(),
|
|
39039
|
+
open: unnamed(ids, acceptanceTests(changesSince(start())))
|
|
39040
|
+
}
|
|
39041
|
+
};
|
|
39042
|
+
});
|
|
38850
39043
|
function implement(first, tier, amend) {
|
|
38851
39044
|
const options = typeof first === "object" && first !== null ? first : {
|
|
38852
39045
|
reference: first,
|
|
@@ -38857,19 +39050,26 @@ function implement(first, tier, amend) {
|
|
|
38857
39050
|
const since = head$1();
|
|
38858
39051
|
const acceptance = options.rebuild?.acceptance;
|
|
38859
39052
|
const built = options.naive === void 0 || !await applied(options.naive);
|
|
38860
|
-
if (built) await
|
|
38861
|
-
|
|
38862
|
-
|
|
38863
|
-
|
|
38864
|
-
|
|
38865
|
-
});
|
|
38866
|
-
requireCriteriaFrozen(REQUIREMENTS);
|
|
38867
|
-
if (built && acceptance !== void 0) requireAcceptance(acceptance, since);
|
|
39053
|
+
if (built) await build(options);
|
|
39054
|
+
else requireCriteriaFrozen(REQUIREMENTS);
|
|
39055
|
+
if (built && acceptance !== void 0) requireAcceptance({
|
|
39056
|
+
...acceptance,
|
|
39057
|
+
ids: settled(acceptance.ids, options.slice)
|
|
39058
|
+
}, since);
|
|
38868
39059
|
await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
|
|
38869
|
-
await requireCoverage();
|
|
38870
|
-
await
|
|
39060
|
+
await requireCoverage(options.base, options.slice);
|
|
39061
|
+
await closing(options.more === true);
|
|
38871
39062
|
});
|
|
38872
39063
|
}
|
|
39064
|
+
/** The last slice's closing, for a last slice a steer left with no criterion to build. */
|
|
39065
|
+
const closeOut = () => scope("implement", () => closing(false));
|
|
39066
|
+
/** A slice with `more` after it keeps the criteria and the slicing for the next one. */
|
|
39067
|
+
const closing = (more) => run("closing", removeScript(more ? [REFACTOR_NOTE, HANDOFF] : [
|
|
39068
|
+
REQUIREMENTS,
|
|
39069
|
+
REFACTOR_NOTE,
|
|
39070
|
+
SLICES,
|
|
39071
|
+
HANDOFF
|
|
39072
|
+
]), { label: "Closing out the implementation" });
|
|
38873
39073
|
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
38874
39074
|
const steer = async (retry) => {
|
|
38875
39075
|
await human("steer", {
|
|
@@ -38900,8 +39100,8 @@ const budget = () => {
|
|
|
38900
39100
|
};
|
|
38901
39101
|
};
|
|
38902
39102
|
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
38903
|
-
const vetted = async (tip, vetting) => {
|
|
38904
|
-
const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
|
|
39103
|
+
const vetted = async (tip, vetting, slice) => {
|
|
39104
|
+
const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip, slice);
|
|
38905
39105
|
if (fault !== void 0) {
|
|
38906
39106
|
const verdict = await vetting.again({
|
|
38907
39107
|
fault,
|
|
@@ -38935,16 +39135,16 @@ const ruled = async (tip, again) => {
|
|
|
38935
39135
|
tip
|
|
38936
39136
|
} : next;
|
|
38937
39137
|
};
|
|
38938
|
-
const decide = () => scope("seam", async () => {
|
|
39138
|
+
const decide = (slice) => scope("seam", async () => {
|
|
38939
39139
|
const vetting = {
|
|
38940
39140
|
again: budget(),
|
|
38941
39141
|
waived: false
|
|
38942
39142
|
};
|
|
39143
|
+
const naive = phase("naive pass", requireCriteriaIfEdited);
|
|
38943
39144
|
let retry;
|
|
38944
39145
|
for (;;) {
|
|
38945
|
-
await
|
|
38946
|
-
const
|
|
38947
|
-
const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
|
|
39146
|
+
const tip = await naiveContinued(retry, slice, naive);
|
|
39147
|
+
const outcome = await vetted(tip, vetting, slice) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
|
|
38948
39148
|
reference: tip.hash,
|
|
38949
39149
|
tip
|
|
38950
39150
|
});
|
|
@@ -38965,8 +39165,8 @@ const refactorNote = async (reference, strays) => {
|
|
|
38965
39165
|
if (others.length > 0 || note.trim() === "") refuse(`gtd land: refactor-note: write ${REFACTOR_NOTE} and change nothing else${others.length > 0 ? ` — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}` : ""}`);
|
|
38966
39166
|
return note;
|
|
38967
39167
|
};
|
|
38968
|
-
const seamBuild = async (base) => {
|
|
38969
|
-
const { reference, tip, shape } = await decide();
|
|
39168
|
+
const seamBuild = async (base, slice$1 = {}) => {
|
|
39169
|
+
const { reference, tip, shape } = await decide(slice$1.criteria);
|
|
38970
39170
|
if (shape === void 0) return {
|
|
38971
39171
|
reference,
|
|
38972
39172
|
base,
|
|
@@ -38981,17 +39181,17 @@ const seamBuild = async (base) => {
|
|
|
38981
39181
|
return {
|
|
38982
39182
|
reference,
|
|
38983
39183
|
base,
|
|
38984
|
-
rebuild: { acceptance: naiveAcceptance(tip) }
|
|
39184
|
+
rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
|
|
38985
39185
|
};
|
|
38986
39186
|
}
|
|
38987
39187
|
const strays = refactorStrays(shape);
|
|
38988
39188
|
if (strays.length === 0) return {
|
|
38989
39189
|
reference,
|
|
38990
39190
|
base,
|
|
38991
|
-
rebuild: { acceptance: naiveAcceptance(tip) }
|
|
39191
|
+
rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
|
|
38992
39192
|
};
|
|
38993
39193
|
const note = await scope("seam", () => refactorNote(reference, strays));
|
|
38994
|
-
const refactored = await slice("refactor", () => reviewedRefactor(shape, {
|
|
39194
|
+
const refactored = await slice(slice$1.refactor ?? "refactor", () => reviewedRefactor(shape, {
|
|
38995
39195
|
reference,
|
|
38996
39196
|
note
|
|
38997
39197
|
}));
|
|
@@ -39001,34 +39201,143 @@ const seamBuild = async (base) => {
|
|
|
39001
39201
|
refactored,
|
|
39002
39202
|
rebuild: {
|
|
39003
39203
|
note,
|
|
39004
|
-
acceptance: naiveAcceptance(tip)
|
|
39204
|
+
acceptance: naiveAcceptance(tip, slice$1.criteria)
|
|
39005
39205
|
}
|
|
39006
39206
|
};
|
|
39007
39207
|
};
|
|
39008
39208
|
//#endregion
|
|
39209
|
+
//#region src/workflows/feature/slicing.ts
|
|
39210
|
+
const ID = /^(AC-\d+)(?![\w-])/;
|
|
39211
|
+
/** The `sliceMaxCriteria` setting. Anything but a positive integer fails rather than guess a cap. */
|
|
39212
|
+
const sliceCap = () => {
|
|
39213
|
+
const value = vars.sliceMaxCriteria ?? defaults.sliceMaxCriteria;
|
|
39214
|
+
if (!/^[1-9]\d*$/.test(value)) refuse(`gtd: sliceMaxCriteria must be a positive integer, got ${JSON.stringify(value)}`);
|
|
39215
|
+
return Number(value);
|
|
39216
|
+
};
|
|
39217
|
+
/** Each `## ` heading of `text` with the list items under it; an item not led by an ID keeps an empty one. */
|
|
39218
|
+
const slicesOf = (text) => {
|
|
39219
|
+
const slices = [];
|
|
39220
|
+
let open = false;
|
|
39221
|
+
for (const { line, fenced } of markFenced(text)) {
|
|
39222
|
+
if (fenced) continue;
|
|
39223
|
+
const heading = HEADING.exec(line);
|
|
39224
|
+
if (heading !== null) {
|
|
39225
|
+
open = heading[1] === "##";
|
|
39226
|
+
if (open) slices.push({
|
|
39227
|
+
title: heading[2],
|
|
39228
|
+
ids: []
|
|
39229
|
+
});
|
|
39230
|
+
continue;
|
|
39231
|
+
}
|
|
39232
|
+
const item = ITEM.exec(line);
|
|
39233
|
+
if (item !== null && open) slices.at(-1).ids.push(ID.exec(item[1])?.[1] ?? "");
|
|
39234
|
+
}
|
|
39235
|
+
return slices;
|
|
39236
|
+
};
|
|
39237
|
+
const twice = (list) => [...new Set(list.filter((id, i) => list.indexOf(id) !== i))];
|
|
39238
|
+
/** Why `slices` fail to put each of `ids` in exactly one slice of at most `cap`, or `undefined`. */
|
|
39239
|
+
const sliceFault = (slices, ids, cap) => {
|
|
39240
|
+
const listed = slices.flatMap((s) => s.ids);
|
|
39241
|
+
if (slices.length === 0) return `no "## " slice`;
|
|
39242
|
+
const empty = slices.filter((s) => s.ids.length === 0).map((s) => s.title);
|
|
39243
|
+
if (empty.length > 0) return `a slice with no criterion: ${empty.join(", ")}`;
|
|
39244
|
+
if (listed.includes("")) return "a list item not led by a criterion ID";
|
|
39245
|
+
const over = slices.filter((s) => s.ids.length > cap).map((s) => s.title);
|
|
39246
|
+
if (over.length > 0) return `more than ${cap} criteria in: ${over.join(", ")}`;
|
|
39247
|
+
const repeated = twice(listed);
|
|
39248
|
+
if (repeated.length > 0) return `an ID in more than one slice: ${repeated.join(", ")}`;
|
|
39249
|
+
const unknown = listed.filter((id) => !ids.includes(id));
|
|
39250
|
+
if (unknown.length > 0) return `an ID ${REQUIREMENTS} does not list: ${unknown.join(", ")}`;
|
|
39251
|
+
const missing = ids.filter((id) => !listed.includes(id));
|
|
39252
|
+
if (missing.length > 0) return `no slice for: ${missing.join(", ")}`;
|
|
39253
|
+
};
|
|
39254
|
+
/** Refuse a turn whose `.gtd/SLICES.md` does not put each of `ids` in exactly one slice of at most `cap`. */
|
|
39255
|
+
const requireSlices = (ids, cap) => {
|
|
39256
|
+
const slices = slicesOf(read(".gtd/SLICES.md") ?? "");
|
|
39257
|
+
const why = sliceFault(slices, ids, cap);
|
|
39258
|
+
if (why !== void 0) refuse(`gtd land: slices: ${SLICES} has ${why} — list each slice as a "## <title>" heading over "- AC-<n>" items, every criterion of ${REQUIREMENTS} in exactly one slice, at most ${cap} per slice`);
|
|
39259
|
+
return slices;
|
|
39260
|
+
};
|
|
39261
|
+
/**
|
|
39262
|
+
* The slices to build, in order. Criteria within `sliceMaxCriteria` are one
|
|
39263
|
+
* slice and no turn; above it `design.slicing` groups them.
|
|
39264
|
+
*/
|
|
39265
|
+
const slicing = async () => {
|
|
39266
|
+
const all = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
39267
|
+
const cap = sliceCap();
|
|
39268
|
+
if (all.length <= cap) return [{
|
|
39269
|
+
title: "",
|
|
39270
|
+
ids: all
|
|
39271
|
+
}];
|
|
39272
|
+
return scope("design", async () => {
|
|
39273
|
+
await agentWithSkills("slicing", slicingPrompt(cap), {
|
|
39274
|
+
label: "Slicing the change",
|
|
39275
|
+
file: SLICES,
|
|
39276
|
+
model: planner(),
|
|
39277
|
+
system: designSystem()
|
|
39278
|
+
});
|
|
39279
|
+
return requireSlices(all, cap);
|
|
39280
|
+
});
|
|
39281
|
+
};
|
|
39282
|
+
/** Slice `i`'s criteria; `undefined` when `slices` is the one slice of an unsliced change. */
|
|
39283
|
+
const sliceCriteria = (slices, i) => slices.length === 1 ? void 0 : i < slices.length - 1 ? { only: slices[i].ids } : { except: slices.slice(0, -1).flatMap((s) => s.ids) };
|
|
39284
|
+
//#endregion
|
|
39009
39285
|
//#region src/workflows/feature/feature.ts
|
|
39010
39286
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
39011
39287
|
const unwind = () => {
|
|
39012
39288
|
const commit = head$1();
|
|
39013
39289
|
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
39014
39290
|
};
|
|
39015
|
-
/**
|
|
39291
|
+
/**
|
|
39292
|
+
* The slices `design.slicing` grouped the criteria into, each built in turn
|
|
39293
|
+
* from where the previous one landed; design must have run since `firstBase`.
|
|
39294
|
+
*/
|
|
39016
39295
|
const buildFeature = async (firstBase) => {
|
|
39017
39296
|
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
39018
|
-
const
|
|
39019
|
-
|
|
39297
|
+
const slices = await slicing();
|
|
39298
|
+
const one = slices.length === 1;
|
|
39299
|
+
for (const i of slices.keys()) {
|
|
39300
|
+
const more = i < slices.length - 1;
|
|
39301
|
+
const criteria = sliceCriteria(slices, i);
|
|
39302
|
+
if (sliceIds(criteria)?.length === 0) {
|
|
39303
|
+
if (!more) await closeOut();
|
|
39304
|
+
continue;
|
|
39305
|
+
}
|
|
39306
|
+
const n = one ? "" : `-${i + 1}`;
|
|
39307
|
+
await buildSlice(i === 0 ? firstBase : head$1(), {
|
|
39308
|
+
tier,
|
|
39309
|
+
criteria,
|
|
39310
|
+
refactor: `refactor${n}`,
|
|
39311
|
+
feature: `feature${n}`,
|
|
39312
|
+
more
|
|
39313
|
+
});
|
|
39314
|
+
}
|
|
39315
|
+
};
|
|
39316
|
+
/** A naive pass (judged only with `flagMode` on), then implement and the quality lap. */
|
|
39317
|
+
const buildSlice = async (sliceBase, build) => {
|
|
39318
|
+
const seam = await seamBuild(sliceBase, {
|
|
39319
|
+
criteria: build.criteria,
|
|
39320
|
+
refactor: build.refactor
|
|
39321
|
+
});
|
|
39322
|
+
await slice(build.feature, async () => {
|
|
39020
39323
|
await implement({
|
|
39021
39324
|
reference: seam.reference,
|
|
39022
|
-
tier,
|
|
39325
|
+
tier: build.tier,
|
|
39023
39326
|
afterRefactor: seam.refactored,
|
|
39024
39327
|
naive: seam.land === true ? seam.reference : void 0,
|
|
39025
|
-
rebuild: seam.rebuild
|
|
39328
|
+
rebuild: seam.rebuild,
|
|
39329
|
+
slice: build.criteria,
|
|
39330
|
+
base: build.criteria === void 0 ? void 0 : sliceBase,
|
|
39331
|
+
more: build.more
|
|
39026
39332
|
});
|
|
39027
39333
|
const carried = seam.rebuild?.acceptance?.tests ?? [];
|
|
39028
|
-
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ?
|
|
39334
|
+
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? sliceBase : seam.base;
|
|
39335
|
+
const guard = build.more ? () => requireCriteriaFrozen(REQUIREMENTS) : void 0;
|
|
39029
39336
|
await tail(settleQuality, {
|
|
39030
39337
|
green: true,
|
|
39031
|
-
lensBase: base
|
|
39338
|
+
lensBase: base,
|
|
39339
|
+
guard,
|
|
39340
|
+
guardRests: false
|
|
39032
39341
|
});
|
|
39033
39342
|
});
|
|
39034
39343
|
};
|
|
@@ -39074,6 +39383,7 @@ const ordinaryStart = async () => {
|
|
|
39074
39383
|
});
|
|
39075
39384
|
refusePendingHandOff();
|
|
39076
39385
|
flagModeOn();
|
|
39386
|
+
sliceCap();
|
|
39077
39387
|
const stated = statedCriteria(changes(), head$1());
|
|
39078
39388
|
await unwind();
|
|
39079
39389
|
if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
|
|
@@ -39268,4 +39578,4 @@ var workflows_default = feature;
|
|
|
39268
39578
|
*/
|
|
39269
39579
|
const doors = {};
|
|
39270
39580
|
//#endregion
|
|
39271
|
-
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_NOTE, 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, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, 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 };
|
|
39581
|
+
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unwind };
|