@pmelab/gtd 36.0.0 → 38.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 +30 -9
- package/claude/hooks/context.ts +66 -0
- package/claude/hooks/drive.ts +3 -0
- package/claude/hooks/register.tsx +69 -6
- package/dist/chunks/{reserved-BAv71I4i.mjs → reserved-sfMdy7v7.mjs} +83 -22
- package/dist/chunks/{tail-kar1zhOb.mjs → tail-Cgca5Gp-.mjs} +1 -1
- package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
- package/dist/gtd.bundle.mjs +498 -56
- package/dist/testkit.mjs +115 -3
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +277 -58
- 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 +355 -75
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +10 -3
- package/src/cli/Cli.ts +76 -1
- package/src/cli/Program.test.ts +1 -0
- package/src/cli/Program.ts +28 -0
- package/src/cli/ToolCommands.ts +3 -0
- package/src/edge/Edge.ts +11 -0
- package/src/flows/runtime.ts +11 -0
- package/src/replay/Replay.ts +51 -2
- package/src/testkit/Layers.ts +1 -0
- package/src/testkit/steps.ts +35 -0
- package/src/wire/BeatDocument.ts +12 -2
- package/src/wire/BeatStatus.ts +3 -0
- package/src/wire/ContextLimit.fixture.ts +30 -0
- package/src/wire/ContextLimit.test.ts +96 -0
- package/src/wire/ContextLimit.ts +169 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/types.ts +7 -0
- package/src/workflow/Workflow.ts +9 -1
- package/src/workflows/feature/context-limit.feature +37 -0
- package/src/workflows/feature/coverage.ts +22 -8
- package/src/workflows/feature/criteria.test.ts +17 -1
- package/src/workflows/feature/criteria.ts +26 -3
- package/src/workflows/feature/feature.ts +69 -16
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor-absorbs.feature +12 -5
- package/src/workflows/feature/refactor-first.feature +11 -2
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +269 -0
- package/src/workflows/feature/refactor-review.feature +45 -1
- package/src/workflows/feature/refactor.ts +9 -3
- package/src/workflows/feature/seam.test.ts +108 -3
- package/src/workflows/feature/seam.ts +169 -31
- 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/fences.ts +5 -0
- package/src/workflows/shared/options.test.ts +11 -2
- package/src/workflows/shared/options.ts +19 -4
- package/src/workflows/shared/steps.ts +2 -0
- package/src/workflows/shared/text.ts +129 -28
- package/src/workflows/shared/vars.ts +1 -0
- package/src/workflows/shared/workflows.steps.ts +13 -0
package/dist/workflows.mjs
CHANGED
|
@@ -16934,6 +16934,7 @@ 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
16940
|
refactorQualityReviews: "correctness, ponytail-review, conventions"
|
|
@@ -17393,6 +17394,24 @@ printf '%s\\n' ${[
|
|
|
17393
17394
|
].map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
|
|
17394
17395
|
`;
|
|
17395
17396
|
};
|
|
17397
|
+
const slicingPrompt = (cap) => `${stateFileRules}
|
|
17398
|
+
- Read \`.gtd/REQUIREMENTS.md\`: its \`## Acceptance Criteria\` are settled
|
|
17399
|
+
and more than ${cap}, too many to build in one slice
|
|
17400
|
+
- Group them into slices in \`.gtd/SLICES.md\`, one \`## <title>\` heading per
|
|
17401
|
+
slice over a \`- AC-<n>\` list item per criterion it builds. Put criteria of
|
|
17402
|
+
the same flow or subsystem together; every criterion in exactly one slice,
|
|
17403
|
+
at most ${cap} per slice
|
|
17404
|
+
- Keep it coarse: as few slices as the cap allows, never one per criterion
|
|
17405
|
+
- Order the slices so each builds on the ones before: they run in that order,
|
|
17406
|
+
each from where the previous one landed
|
|
17407
|
+
- Change nothing but \`.gtd/SLICES.md\` — leave the criteria as they are
|
|
17408
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
17409
|
+
`;
|
|
17410
|
+
/** The line telling a slice's turn which criteria are its own; empty for a single slice. */
|
|
17411
|
+
const sliceLine = (ids) => ids === void 0 ? "" : `- This slice builds ${ids.join(", ")} only: every rule here about the criteria
|
|
17412
|
+
means these. The others are for other slices, before or after it — leave
|
|
17413
|
+
them unbuilt and untested
|
|
17414
|
+
`;
|
|
17396
17415
|
const designSystem = () => `${designPersona}
|
|
17397
17416
|
|
|
17398
17417
|
${agentConduct}`;
|
|
@@ -17436,9 +17455,11 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
|
17436
17455
|
- Introduce no new flag read and add no second read of an existing one:
|
|
17437
17456
|
every match of \`${pattern}\` may move, never increase in number`;
|
|
17438
17457
|
const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
|
|
17439
|
-
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false }) => `${stateFileRules}
|
|
17458
|
+
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false }) => `${stateFileRules}
|
|
17440
17459
|
- The package to implement is \`${pkg}\`; never delete it
|
|
17441
|
-
${
|
|
17460
|
+
${note ? `- The package's \`## Refactor note\` section names the seams to open and the
|
|
17461
|
+
shape each takes, written from the whole naive pass: build exactly those
|
|
17462
|
+
` : ""}${feedback ? `- The package carries a \`## Review feedback\` section: a human's review of the
|
|
17442
17463
|
last refactor. Act on it, within the rules below
|
|
17443
17464
|
` : ""}${refactorRules(strays, pattern)}
|
|
17444
17465
|
${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
|
|
@@ -18037,9 +18058,9 @@ re-implement. It changed these files outside the expected set:
|
|
|
18037
18058
|
${listed(retry.strays)}
|
|
18038
18059
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
18039
18060
|
`;
|
|
18040
|
-
const naivePassPrompt = (retry) => `${stateFileRules}
|
|
18061
|
+
const naivePassPrompt = (retry, slice) => `${stateFileRules}
|
|
18041
18062
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18042
|
-
${criteriaBinding(false)}
|
|
18063
|
+
${sliceLine(slice)}${criteriaBinding(false)}
|
|
18043
18064
|
- Implement them the quickest way that works — the naive version. Do not
|
|
18044
18065
|
restructure, abstract or refactor around the change; it may land exactly as
|
|
18045
18066
|
you leave it, and a reviewer makes it fit
|
|
@@ -18057,22 +18078,54 @@ ${criteriaBinding(false)}
|
|
|
18057
18078
|
- Leave \`.gtd/\` untouched
|
|
18058
18079
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
18059
18080
|
${retry === void 0 ? "" : naiveRetry(retry)}`;
|
|
18060
|
-
const
|
|
18061
|
-
|
|
18062
|
-
|
|
18063
|
-
|
|
18081
|
+
const carries = (rebuild) => (rebuild.acceptance?.ids.length ?? 0) > 0;
|
|
18082
|
+
const rebuildLines = ({ note, acceptance }) => `${note === void 0 ? "" : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
|
|
18083
|
+
opened; plug the change into them:
|
|
18084
|
+
|
|
18085
|
+
${note.trimEnd().split("\n").map((line) => line === "" ? "" : ` > ${line}`).join("\n")}
|
|
18086
|
+
|
|
18087
|
+
`}${acceptance === void 0 || !carries({ acceptance }) ? "" : `- Carry over the naive pass's acceptance tests for ${acceptance.ids.join(", ")}, the
|
|
18088
|
+
one part of it you take as it is: \`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`.
|
|
18089
|
+
Copy each test with its \`[AC-<n>]\` name into the tree as the refactor left
|
|
18090
|
+
it — check a file out of the tip whole only where the refactor did not touch
|
|
18091
|
+
it — changing only what reaches the release flag. A turn whose changed tests
|
|
18092
|
+
leave an ID out is refused, and the suite must be green with them
|
|
18093
|
+
`}`;
|
|
18094
|
+
const refactorNotePrompt = (reference, strays) => `${stateFileRules}
|
|
18095
|
+
- The seam judge ruled the naive pass refactor-first: it is never applied.
|
|
18096
|
+
A behaviour-neutral refactor lands first, then the change is rebuilt on
|
|
18097
|
+
the refactored base. Both turns read the note you write
|
|
18098
|
+
- Read the whole naive pass: \`git show ${reference}\`. It changed these
|
|
18099
|
+
existing files outside the allowlist:
|
|
18100
|
+
${listed(strays)}
|
|
18101
|
+
- Write \`.gtd/REFACTOR-NOTE.md\`: which seams the refactor opens (the existing
|
|
18102
|
+
code to restructure, by path and symbol) and the shape each takes — the
|
|
18103
|
+
signatures and where the new behaviour will plug in. Name only the files
|
|
18104
|
+
listed above: the refactor may change no other path. Never the feature's own
|
|
18105
|
+
behaviour, and no flag read: the refactor adds neither
|
|
18106
|
+
- Change nothing else — a turn that does is refused
|
|
18107
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
18108
|
+
`;
|
|
18109
|
+
const afterRefactorLine = (rebuild) => `- A reviewed refactor has just landed and may already carry part of the change,
|
|
18064
18110
|
though never a release flag. Build only what is still missing, held to every
|
|
18065
|
-
rule below; if nothing is, change nothing and finish your turn
|
|
18066
|
-
|
|
18111
|
+
rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
|
|
18112
|
+
`;
|
|
18113
|
+
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
|
|
18114
|
+
change added; only when none did, behind a new one, following the
|
|
18115
|
+
repository's flag conventions` : `- Put the new behaviour behind a new release flag, following the
|
|
18116
|
+
repository's flag conventions`;
|
|
18117
|
+
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}, slice) => `${stateFileRules}
|
|
18118
|
+
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18119
|
+
${sliceLine(slice)}${criteriaBinding(true)}
|
|
18120
|
+
${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
18067
18121
|
the re-derived requirements instead of rewriting it
|
|
18068
18122
|
` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
|
|
18069
18123
|
` : `- Implement them the quickest way that works — the naive version. Do not
|
|
18070
18124
|
restructure, abstract or refactor around the change
|
|
18071
18125
|
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
18072
18126
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
18073
|
-
the change fresh
|
|
18074
|
-
`}${
|
|
18075
|
-
repository's flag conventions`}
|
|
18127
|
+
the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
|
|
18128
|
+
`}${rebuildLines(rebuild)}${flagLine(flagMode, tier, amend, slice !== void 0)}
|
|
18076
18129
|
- Write the tests first, then the code that makes them pass. Name each
|
|
18077
18130
|
criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
|
|
18078
18131
|
green a judge checks every ID has a passing test, and sends uncovered ones back
|
|
@@ -18153,9 +18206,9 @@ const coverageMessage = () => `Judging acceptance-criteria coverage. Run \`gtd j
|
|
|
18153
18206
|
\`yes\`/\`no\` verdict per criterion ID — or land without a verdict, which
|
|
18154
18207
|
counts every criterion as uncovered.
|
|
18155
18208
|
`;
|
|
18156
|
-
const coveringPrompt = (uncovered) => `${stateFileRules}
|
|
18209
|
+
const coveringPrompt = (uncovered, slice) => `${stateFileRules}
|
|
18157
18210
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18158
|
-
${criteriaBinding(true)}
|
|
18211
|
+
${sliceLine(slice)}${criteriaBinding(true)}
|
|
18159
18212
|
- A judge found no passing test in this change that covers ${uncovered.join(", ")}.
|
|
18160
18213
|
Write a behaviour-level test for each, naming its ID in the test or scenario
|
|
18161
18214
|
name (\`[${uncovered[0]}] …\`), and fix the code if the test shows it falls short
|
|
@@ -18180,7 +18233,9 @@ const ESCALATION = ".gtd/ESCALATION.md";
|
|
|
18180
18233
|
const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
|
|
18181
18234
|
const REVIEW = ".gtd/REVIEW.md";
|
|
18182
18235
|
const REFACTOR = ".gtd/REFACTOR.md";
|
|
18236
|
+
const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
|
|
18183
18237
|
const QUALITY = ".gtd/QUALITY.md";
|
|
18238
|
+
const SLICES = ".gtd/SLICES.md";
|
|
18184
18239
|
const planner = () => env.plannerModel ?? "";
|
|
18185
18240
|
const coder = () => env.coderModel ?? "";
|
|
18186
18241
|
const fixCheck = (rules) => agentWithSkills("fix", buildFixPrompt(rules), {
|
|
@@ -37692,12 +37747,17 @@ const ShippedOptions = Struct({
|
|
|
37692
37747
|
reviewSeverityMinP: setting,
|
|
37693
37748
|
refactorFlagPattern: setting,
|
|
37694
37749
|
seamAllow: setting,
|
|
37750
|
+
sliceMaxCriteria: setting,
|
|
37695
37751
|
qualityReviews: setting,
|
|
37696
37752
|
refactorQualityReviews: setting,
|
|
37697
37753
|
testCommand: setting,
|
|
37698
37754
|
plannerModel: setting,
|
|
37699
37755
|
coderModel: setting,
|
|
37700
|
-
flagMode: optionalWith(Boolean$, { exact: true })
|
|
37756
|
+
flagMode: optionalWith(Boolean$, { exact: true }),
|
|
37757
|
+
contextLimit: optionalWith(Struct({
|
|
37758
|
+
soft: optionalWith(Number$, { exact: true }),
|
|
37759
|
+
hard: optionalWith(Number$, { exact: true })
|
|
37760
|
+
}), { exact: true })
|
|
37701
37761
|
});
|
|
37702
37762
|
const over = (base, options) => ({
|
|
37703
37763
|
...base,
|
|
@@ -37709,12 +37769,13 @@ const flagModeOn = () => {
|
|
|
37709
37769
|
if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
|
|
37710
37770
|
return value === "true";
|
|
37711
37771
|
};
|
|
37772
|
+
const limited = (flow, context) => context === void 0 ? flow : () => scope({ context }, flow);
|
|
37712
37773
|
/** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
|
|
37713
37774
|
const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
|
|
37714
37775
|
onExcessProperty: "error",
|
|
37715
37776
|
errors: "all"
|
|
37716
|
-
}), (options) => ({
|
|
37717
|
-
flow,
|
|
37777
|
+
}), ({ contextLimit, ...options }) => ({
|
|
37778
|
+
flow: limited(flow, contextLimit),
|
|
37718
37779
|
defaults: over(defaults, options),
|
|
37719
37780
|
envDefaults: over(envDefaults, options),
|
|
37720
37781
|
doors,
|
|
@@ -37727,14 +37788,14 @@ const FlagOptions = Struct({
|
|
|
37727
37788
|
const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions, {
|
|
37728
37789
|
onExcessProperty: "error",
|
|
37729
37790
|
errors: "all"
|
|
37730
|
-
}), (options) => {
|
|
37791
|
+
}), ({ contextLimit, ...options }) => {
|
|
37731
37792
|
const flagDefaults = {
|
|
37732
37793
|
...defaults,
|
|
37733
37794
|
flag: "",
|
|
37734
37795
|
flagRegistry: defaultFlagRegistry
|
|
37735
37796
|
};
|
|
37736
37797
|
return {
|
|
37737
|
-
flow,
|
|
37798
|
+
flow: limited(flow, contextLimit),
|
|
37738
37799
|
defaults: over(flagDefaults, options),
|
|
37739
37800
|
envDefaults: over(envDefaults, options),
|
|
37740
37801
|
doors: { [name]: {
|
|
@@ -37867,13 +37928,15 @@ const markFenced = (text) => {
|
|
|
37867
37928
|
};
|
|
37868
37929
|
});
|
|
37869
37930
|
};
|
|
37931
|
+
/** A markdown heading: its `#` run, then its text. */
|
|
37932
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
37933
|
+
/** A list item, its checkbox dropped: its text. */
|
|
37934
|
+
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
37870
37935
|
//#endregion
|
|
37871
37936
|
//#region src/workflows/feature/criteria.ts
|
|
37872
37937
|
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
37873
37938
|
const CLEAR_MIN_P = .8;
|
|
37874
|
-
const
|
|
37875
|
-
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
37876
|
-
const ID = /^(AC-\d+):\s*(.*)$/;
|
|
37939
|
+
const ID$1 = /^(AC-\d+):\s*(.*)$/;
|
|
37877
37940
|
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
37878
37941
|
const listUnder = (text, title, level) => {
|
|
37879
37942
|
const lines = markFenced(text);
|
|
@@ -37901,7 +37964,7 @@ const listUnder = (text, title, level) => {
|
|
|
37901
37964
|
};
|
|
37902
37965
|
/** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
|
|
37903
37966
|
const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
|
|
37904
|
-
const id = ID.exec(item);
|
|
37967
|
+
const id = ID$1.exec(item);
|
|
37905
37968
|
return id === null ? {
|
|
37906
37969
|
id: "",
|
|
37907
37970
|
text: item
|
|
@@ -37910,16 +37973,20 @@ const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((
|
|
|
37910
37973
|
text: id[2]
|
|
37911
37974
|
};
|
|
37912
37975
|
});
|
|
37976
|
+
/** The ID'd criteria of `text`, only `slice`'s when given. */
|
|
37977
|
+
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));
|
|
37978
|
+
/** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
|
|
37979
|
+
const sliceIds = (slice) => slice === void 0 ? void 0 : criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice);
|
|
37913
37980
|
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
37914
37981
|
const numbered = (items) => {
|
|
37915
|
-
const ids = items.map((item) => ID.exec(item));
|
|
37982
|
+
const ids = items.map((item) => ID$1.exec(item));
|
|
37916
37983
|
const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
|
|
37917
37984
|
return items.map((item, i) => own ? {
|
|
37918
37985
|
id: ids[i][1],
|
|
37919
37986
|
text: ids[i][2]
|
|
37920
37987
|
} : {
|
|
37921
37988
|
id: `AC-${i + 1}`,
|
|
37922
|
-
text: item.replace(ID, "$2")
|
|
37989
|
+
text: item.replace(ID$1, "$2")
|
|
37923
37990
|
});
|
|
37924
37991
|
};
|
|
37925
37992
|
const STATED = /^acceptance criteria:?$/i;
|
|
@@ -38473,7 +38540,8 @@ const plan = (shape, options) => {
|
|
|
38473
38540
|
return {
|
|
38474
38541
|
pattern,
|
|
38475
38542
|
strays,
|
|
38476
|
-
reference: options.reference?.trim() ? options.reference : void 0
|
|
38543
|
+
reference: options.reference?.trim() ? options.reference : void 0,
|
|
38544
|
+
note: options.note?.trim() ? options.note : void 0
|
|
38477
38545
|
};
|
|
38478
38546
|
};
|
|
38479
38547
|
const guardOf = ({ pattern, strays }, entry) => () => {
|
|
@@ -38482,13 +38550,22 @@ const guardOf = ({ pattern, strays }, entry) => () => {
|
|
|
38482
38550
|
};
|
|
38483
38551
|
/** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
|
|
38484
38552
|
const refactorLap = async (p, guard, feedback) => {
|
|
38485
|
-
const { pattern, strays, reference } = p;
|
|
38486
|
-
await run("deriving", deriveScript([
|
|
38487
|
-
|
|
38488
|
-
|
|
38489
|
-
|
|
38490
|
-
|
|
38491
|
-
|
|
38553
|
+
const { pattern, strays, reference, note } = p;
|
|
38554
|
+
await run("deriving", deriveScript([
|
|
38555
|
+
...packageLines(strays, pattern, reference),
|
|
38556
|
+
...note === void 0 ? [] : [
|
|
38557
|
+
"## Refactor note",
|
|
38558
|
+
"",
|
|
38559
|
+
...demoteHeadings(note),
|
|
38560
|
+
""
|
|
38561
|
+
],
|
|
38562
|
+
...feedback === void 0 ? [] : [
|
|
38563
|
+
"## Review feedback",
|
|
38564
|
+
"",
|
|
38565
|
+
...demoteHeadings(feedback),
|
|
38566
|
+
""
|
|
38567
|
+
]
|
|
38568
|
+
]), {
|
|
38492
38569
|
label: "Deriving the refactor package",
|
|
38493
38570
|
file: REFACTOR_PACKAGE
|
|
38494
38571
|
});
|
|
@@ -38497,7 +38574,8 @@ const refactorLap = async (p, guard, feedback) => {
|
|
|
38497
38574
|
strays,
|
|
38498
38575
|
pattern,
|
|
38499
38576
|
reference,
|
|
38500
|
-
feedback: feedback !== void 0
|
|
38577
|
+
feedback: feedback !== void 0,
|
|
38578
|
+
note: note !== void 0
|
|
38501
38579
|
}), {
|
|
38502
38580
|
label: "Refactoring",
|
|
38503
38581
|
model: coder(),
|
|
@@ -38583,22 +38661,27 @@ const COVERED_MIN_P = .7;
|
|
|
38583
38661
|
/** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
|
|
38584
38662
|
const COVERING_RETRIES = 2;
|
|
38585
38663
|
const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
38586
|
-
const green = () => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
38664
|
+
const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
38587
38665
|
label: "Fixing the check",
|
|
38588
38666
|
file: FEEDBACK,
|
|
38589
38667
|
model: coder(),
|
|
38590
38668
|
system: builderSystem()
|
|
38591
38669
|
}), {
|
|
38592
|
-
guard:
|
|
38670
|
+
guard: () => {
|
|
38671
|
+
frozen();
|
|
38672
|
+
guard?.();
|
|
38673
|
+
},
|
|
38593
38674
|
guardRests: false
|
|
38594
38675
|
});
|
|
38595
38676
|
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");
|
|
38596
38677
|
/**
|
|
38597
38678
|
* The criterion IDs of `.gtd/REQUIREMENTS.md` — read afresh, never carried
|
|
38598
|
-
* over — no passing test since `base` covers, by the judge.
|
|
38679
|
+
* over, only `slice`'s when given — no passing test since `base` covers, by the judge.
|
|
38599
38680
|
*/
|
|
38600
|
-
const uncoveredSince = async (base) => {
|
|
38601
|
-
const
|
|
38681
|
+
const uncoveredSince = async (base, slice) => {
|
|
38682
|
+
const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
|
|
38683
|
+
const own = criterionIds(requirements, slice);
|
|
38684
|
+
const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
|
|
38602
38685
|
if (criteria.length === 0) return {
|
|
38603
38686
|
covered: [],
|
|
38604
38687
|
uncovered: []
|
|
@@ -38625,14 +38708,14 @@ const record = (covered, uncovered = []) => {
|
|
|
38625
38708
|
* sending uncovered IDs back to a covering turn; marks the IDs covered on the
|
|
38626
38709
|
* landing that ends it. A human may overrule once the retries are spent.
|
|
38627
38710
|
*/
|
|
38628
|
-
const requireCoverage = async (base = start()) => {
|
|
38711
|
+
const requireCoverage = async (base = start(), slice) => {
|
|
38629
38712
|
let retries = 0;
|
|
38630
38713
|
for (;;) {
|
|
38631
|
-
const { covered, uncovered } = await uncoveredSince(base);
|
|
38714
|
+
const { covered, uncovered } = await uncoveredSince(base, slice);
|
|
38632
38715
|
if (uncovered.length === 0) return record(covered);
|
|
38633
38716
|
if (retries < COVERING_RETRIES) {
|
|
38634
38717
|
retries++;
|
|
38635
|
-
await agentWithSkills("covering", coveringPrompt(uncovered), {
|
|
38718
|
+
await agentWithSkills("covering", coveringPrompt(uncovered, sliceIds(slice)), {
|
|
38636
38719
|
label: "Covering the acceptance criteria",
|
|
38637
38720
|
model: coder(),
|
|
38638
38721
|
system: builderSystem(),
|
|
@@ -38663,7 +38746,7 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
|
|
|
38663
38746
|
allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
|
|
38664
38747
|
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
|
|
38665
38748
|
});
|
|
38666
|
-
const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
|
|
38749
|
+
const naivePass = (retry, slice) => agentWithSkills("naive", naivePassPrompt(retry, sliceIds(slice)), {
|
|
38667
38750
|
label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
|
|
38668
38751
|
keep: true,
|
|
38669
38752
|
model: coder(),
|
|
@@ -38703,16 +38786,20 @@ const addedLines = (change) => {
|
|
|
38703
38786
|
const before = new Set((change.before ?? "").split("\n"));
|
|
38704
38787
|
return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
|
|
38705
38788
|
};
|
|
38789
|
+
const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
|
|
38790
|
+
const idsOf = (file, slice) => criterionIds(read(file) ?? "", slice);
|
|
38791
|
+
const unnamed = (ids, tests) => {
|
|
38792
|
+
const lines = tests.map(addedLines);
|
|
38793
|
+
return ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
38794
|
+
};
|
|
38706
38795
|
/**
|
|
38707
38796
|
* Why `tip`'s tests fall short, or `undefined`: a criterion ID of `file` no
|
|
38708
38797
|
* test line of the pass names, or a test importing a file the pass added —
|
|
38709
38798
|
* naive structure a reset and refactor replaces, which the tests must survive.
|
|
38710
38799
|
*/
|
|
38711
|
-
const acceptanceFault = (file, tip) => {
|
|
38712
|
-
const
|
|
38713
|
-
const
|
|
38714
|
-
const lines = tests.map(addedLines);
|
|
38715
|
-
const untested = ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
38800
|
+
const acceptanceFault = (file, tip, slice) => {
|
|
38801
|
+
const tests = acceptanceTests(tip.changes);
|
|
38802
|
+
const untested = unnamed(idsOf(file, slice), tests);
|
|
38716
38803
|
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]}] …"`;
|
|
38717
38804
|
const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
|
|
38718
38805
|
const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
|
|
@@ -38721,6 +38808,41 @@ const acceptanceFault = (file, tip) => {
|
|
|
38721
38808
|
});
|
|
38722
38809
|
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")}`;
|
|
38723
38810
|
};
|
|
38811
|
+
/** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
|
|
38812
|
+
const naiveAcceptance = (tip, slice) => {
|
|
38813
|
+
const ids = idsOf(REQUIREMENTS, slice);
|
|
38814
|
+
const perTest = acceptanceTests(tip.changes).map((c) => {
|
|
38815
|
+
const lines = addedLines(c);
|
|
38816
|
+
return {
|
|
38817
|
+
path: c.path,
|
|
38818
|
+
ids: ids.filter((id) => named(id, lines))
|
|
38819
|
+
};
|
|
38820
|
+
});
|
|
38821
|
+
return {
|
|
38822
|
+
hash: tip.hash,
|
|
38823
|
+
tests: perTest.filter((c) => c.ids.length > 0).map((c) => c.path),
|
|
38824
|
+
ids: ids.filter((id) => perTest.some((c) => c.ids.includes(id)))
|
|
38825
|
+
};
|
|
38826
|
+
};
|
|
38827
|
+
/**
|
|
38828
|
+
* Refuse a rebuild whose test changes since `since` leave out an ID the naive
|
|
38829
|
+
* pass's tests named. A skipped test still counts: the gate reads text, never runs it.
|
|
38830
|
+
*/
|
|
38831
|
+
const requireAcceptance = (acceptance, since) => {
|
|
38832
|
+
const missing = unnamed(acceptance.ids, acceptanceTests(changesSince(since)));
|
|
38833
|
+
if (missing.length === 0) return;
|
|
38834
|
+
refuse(`gtd land: rebuild-acceptance: the rebuild's tests leave out ${missing.join(", ")} — carry over the naive pass's acceptance tests (\`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`), each with its ID in the test or scenario name`);
|
|
38835
|
+
};
|
|
38836
|
+
/**
|
|
38837
|
+
* Refuse a fix turn that, on its own, drops an ID the naive pass's tests named
|
|
38838
|
+
* from a test file. Turn-local, so a person's edit at an escalation rest never
|
|
38839
|
+
* wedges later fix turns.
|
|
38840
|
+
*/
|
|
38841
|
+
const requireAcceptanceKept = (acceptance) => {
|
|
38842
|
+
const dropped = acceptance.ids.filter((id) => changes().filter((c) => isCode$1(c.path) && isTestFile(c.path)).some((c) => named(id, c.before ?? "") && !named(id, c.after ?? "")));
|
|
38843
|
+
if (dropped.length === 0) return;
|
|
38844
|
+
refuse(`gtd land: rebuild-acceptance: this turn drops ${dropped.join(", ")} from the tests — the naive pass's acceptance tests stay; fix the code, not them`);
|
|
38845
|
+
};
|
|
38724
38846
|
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`;
|
|
38725
38847
|
/**
|
|
38726
38848
|
* Run the suite with `tip`'s diff applied, then take it out again; resolves
|
|
@@ -38764,18 +38886,30 @@ function implement(first, tier, amend) {
|
|
|
38764
38886
|
amend
|
|
38765
38887
|
};
|
|
38766
38888
|
return scope("implement", async () => {
|
|
38767
|
-
|
|
38889
|
+
const since = head$1();
|
|
38890
|
+
const acceptance = options.rebuild?.acceptance;
|
|
38891
|
+
const built = options.naive === void 0 || !await applied(options.naive);
|
|
38892
|
+
if (built) await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild, sliceIds(options.slice)), {
|
|
38768
38893
|
label: "Implementing",
|
|
38769
38894
|
model: coder(),
|
|
38770
38895
|
system: builderSystem(),
|
|
38771
38896
|
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
38772
38897
|
});
|
|
38773
38898
|
requireCriteriaFrozen(REQUIREMENTS);
|
|
38774
|
-
|
|
38775
|
-
await
|
|
38776
|
-
await
|
|
38899
|
+
if (built && acceptance !== void 0) requireAcceptance(acceptance, since);
|
|
38900
|
+
await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
|
|
38901
|
+
await requireCoverage(options.base, options.slice);
|
|
38902
|
+
await closing(options.more === true);
|
|
38777
38903
|
});
|
|
38778
38904
|
}
|
|
38905
|
+
/** The last slice's closing, for a last slice a steer left with no criterion to build. */
|
|
38906
|
+
const closeOut = () => scope("implement", () => closing(false));
|
|
38907
|
+
/** A slice with `more` after it keeps the criteria and the slicing for the next one. */
|
|
38908
|
+
const closing = (more) => run("closing", removeScript(more ? [REFACTOR_NOTE] : [
|
|
38909
|
+
REQUIREMENTS,
|
|
38910
|
+
REFACTOR_NOTE,
|
|
38911
|
+
SLICES
|
|
38912
|
+
]), { label: "Closing out the implementation" });
|
|
38779
38913
|
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
38780
38914
|
const steer = async (retry) => {
|
|
38781
38915
|
await human("steer", {
|
|
@@ -38806,8 +38940,8 @@ const budget = () => {
|
|
|
38806
38940
|
};
|
|
38807
38941
|
};
|
|
38808
38942
|
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
38809
|
-
const vetted = async (tip, vetting) => {
|
|
38810
|
-
const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
|
|
38943
|
+
const vetted = async (tip, vetting, slice) => {
|
|
38944
|
+
const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip, slice);
|
|
38811
38945
|
if (fault !== void 0) {
|
|
38812
38946
|
const verdict = await vetting.again({
|
|
38813
38947
|
fault,
|
|
@@ -38823,33 +38957,56 @@ const vetted = async (tip, vetting) => {
|
|
|
38823
38957
|
/** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
|
|
38824
38958
|
const ruled = async (tip, again) => {
|
|
38825
38959
|
const shape = await judgeSeam(tip);
|
|
38826
|
-
if (shape.choice === "land") return {
|
|
38960
|
+
if (shape.choice === "land") return {
|
|
38961
|
+
reference: tip.hash,
|
|
38962
|
+
tip
|
|
38963
|
+
};
|
|
38827
38964
|
if (shape.choice === "refactor-first") return {
|
|
38828
38965
|
reference: tip.hash,
|
|
38966
|
+
tip,
|
|
38829
38967
|
shape
|
|
38830
38968
|
};
|
|
38831
38969
|
const next = await again({
|
|
38832
38970
|
strays: shape.strays,
|
|
38833
38971
|
p: shape.judgment?.answers.shape?.p ?? 0
|
|
38834
38972
|
});
|
|
38835
|
-
return next.kind === "overruled" ? {
|
|
38973
|
+
return next.kind === "overruled" ? {
|
|
38974
|
+
reference: tip.hash,
|
|
38975
|
+
tip
|
|
38976
|
+
} : next;
|
|
38836
38977
|
};
|
|
38837
|
-
const decide = () => scope("seam", async () => {
|
|
38978
|
+
const decide = (slice) => scope("seam", async () => {
|
|
38838
38979
|
const vetting = {
|
|
38839
38980
|
again: budget(),
|
|
38840
38981
|
waived: false
|
|
38841
38982
|
};
|
|
38842
38983
|
let retry;
|
|
38843
38984
|
for (;;) {
|
|
38844
|
-
await naivePass(retry);
|
|
38985
|
+
await naivePass(retry, slice);
|
|
38845
38986
|
const tip = kept("naive");
|
|
38846
|
-
const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
|
|
38987
|
+
const outcome = await vetted(tip, vetting, slice) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
|
|
38988
|
+
reference: tip.hash,
|
|
38989
|
+
tip
|
|
38990
|
+
});
|
|
38847
38991
|
if (!("kind" in outcome)) return outcome;
|
|
38848
38992
|
retry = outcome.kind === "retry" ? outcome.retry : void 0;
|
|
38849
38993
|
}
|
|
38850
38994
|
});
|
|
38851
|
-
|
|
38852
|
-
|
|
38995
|
+
/** Write the refactor note from the whole naive pass; a turn that changes anything else is refused. */
|
|
38996
|
+
const refactorNote = async (reference, strays) => {
|
|
38997
|
+
await agent("note", refactorNotePrompt(reference, strays), {
|
|
38998
|
+
label: "Writing the refactor note",
|
|
38999
|
+
file: REFACTOR_NOTE,
|
|
39000
|
+
model: coder(),
|
|
39001
|
+
system: builderSystem()
|
|
39002
|
+
});
|
|
39003
|
+
const others = changes().map((c) => c.path).filter((path) => path !== REFACTOR_NOTE);
|
|
39004
|
+
const note = read(".gtd/REFACTOR-NOTE.md") ?? "";
|
|
39005
|
+
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")}` : ""}`);
|
|
39006
|
+
return note;
|
|
39007
|
+
};
|
|
39008
|
+
const seamBuild = async (base, slice$1 = {}) => {
|
|
39009
|
+
const { reference, tip, shape } = await decide(slice$1.criteria);
|
|
38853
39010
|
if (shape === void 0) return {
|
|
38854
39011
|
reference,
|
|
38855
39012
|
base,
|
|
@@ -38863,42 +39020,164 @@ const seamBuild = async (base) => {
|
|
|
38863
39020
|
}));
|
|
38864
39021
|
return {
|
|
38865
39022
|
reference,
|
|
38866
|
-
base
|
|
39023
|
+
base,
|
|
39024
|
+
rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
|
|
38867
39025
|
};
|
|
38868
39026
|
}
|
|
38869
|
-
|
|
39027
|
+
const strays = refactorStrays(shape);
|
|
39028
|
+
if (strays.length === 0) return {
|
|
38870
39029
|
reference,
|
|
38871
|
-
base
|
|
39030
|
+
base,
|
|
39031
|
+
rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
|
|
38872
39032
|
};
|
|
38873
|
-
const
|
|
39033
|
+
const note = await scope("seam", () => refactorNote(reference, strays));
|
|
39034
|
+
const refactored = await slice(slice$1.refactor ?? "refactor", () => reviewedRefactor(shape, {
|
|
39035
|
+
reference,
|
|
39036
|
+
note
|
|
39037
|
+
}));
|
|
38874
39038
|
return {
|
|
38875
39039
|
reference,
|
|
38876
39040
|
base: head$1(),
|
|
38877
|
-
refactored
|
|
39041
|
+
refactored,
|
|
39042
|
+
rebuild: {
|
|
39043
|
+
note,
|
|
39044
|
+
acceptance: naiveAcceptance(tip, slice$1.criteria)
|
|
39045
|
+
}
|
|
38878
39046
|
};
|
|
38879
39047
|
};
|
|
38880
39048
|
//#endregion
|
|
39049
|
+
//#region src/workflows/feature/slicing.ts
|
|
39050
|
+
const ID = /^(AC-\d+)(?![\w-])/;
|
|
39051
|
+
/** The `sliceMaxCriteria` setting. Anything but a positive integer fails rather than guess a cap. */
|
|
39052
|
+
const sliceCap = () => {
|
|
39053
|
+
const value = vars.sliceMaxCriteria ?? defaults.sliceMaxCriteria;
|
|
39054
|
+
if (!/^[1-9]\d*$/.test(value)) refuse(`gtd: sliceMaxCriteria must be a positive integer, got ${JSON.stringify(value)}`);
|
|
39055
|
+
return Number(value);
|
|
39056
|
+
};
|
|
39057
|
+
/** Each `## ` heading of `text` with the list items under it; an item not led by an ID keeps an empty one. */
|
|
39058
|
+
const slicesOf = (text) => {
|
|
39059
|
+
const slices = [];
|
|
39060
|
+
let open = false;
|
|
39061
|
+
for (const { line, fenced } of markFenced(text)) {
|
|
39062
|
+
if (fenced) continue;
|
|
39063
|
+
const heading = HEADING.exec(line);
|
|
39064
|
+
if (heading !== null) {
|
|
39065
|
+
open = heading[1] === "##";
|
|
39066
|
+
if (open) slices.push({
|
|
39067
|
+
title: heading[2],
|
|
39068
|
+
ids: []
|
|
39069
|
+
});
|
|
39070
|
+
continue;
|
|
39071
|
+
}
|
|
39072
|
+
const item = ITEM.exec(line);
|
|
39073
|
+
if (item !== null && open) slices.at(-1).ids.push(ID.exec(item[1])?.[1] ?? "");
|
|
39074
|
+
}
|
|
39075
|
+
return slices;
|
|
39076
|
+
};
|
|
39077
|
+
const twice = (list) => [...new Set(list.filter((id, i) => list.indexOf(id) !== i))];
|
|
39078
|
+
/** Why `slices` fail to put each of `ids` in exactly one slice of at most `cap`, or `undefined`. */
|
|
39079
|
+
const sliceFault = (slices, ids, cap) => {
|
|
39080
|
+
const listed = slices.flatMap((s) => s.ids);
|
|
39081
|
+
if (slices.length === 0) return `no "## " slice`;
|
|
39082
|
+
const empty = slices.filter((s) => s.ids.length === 0).map((s) => s.title);
|
|
39083
|
+
if (empty.length > 0) return `a slice with no criterion: ${empty.join(", ")}`;
|
|
39084
|
+
if (listed.includes("")) return "a list item not led by a criterion ID";
|
|
39085
|
+
const over = slices.filter((s) => s.ids.length > cap).map((s) => s.title);
|
|
39086
|
+
if (over.length > 0) return `more than ${cap} criteria in: ${over.join(", ")}`;
|
|
39087
|
+
const repeated = twice(listed);
|
|
39088
|
+
if (repeated.length > 0) return `an ID in more than one slice: ${repeated.join(", ")}`;
|
|
39089
|
+
const unknown = listed.filter((id) => !ids.includes(id));
|
|
39090
|
+
if (unknown.length > 0) return `an ID ${REQUIREMENTS} does not list: ${unknown.join(", ")}`;
|
|
39091
|
+
const missing = ids.filter((id) => !listed.includes(id));
|
|
39092
|
+
if (missing.length > 0) return `no slice for: ${missing.join(", ")}`;
|
|
39093
|
+
};
|
|
39094
|
+
/** Refuse a turn whose `.gtd/SLICES.md` does not put each of `ids` in exactly one slice of at most `cap`. */
|
|
39095
|
+
const requireSlices = (ids, cap) => {
|
|
39096
|
+
const slices = slicesOf(read(".gtd/SLICES.md") ?? "");
|
|
39097
|
+
const why = sliceFault(slices, ids, cap);
|
|
39098
|
+
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`);
|
|
39099
|
+
return slices;
|
|
39100
|
+
};
|
|
39101
|
+
/**
|
|
39102
|
+
* The slices to build, in order. Criteria within `sliceMaxCriteria` are one
|
|
39103
|
+
* slice and no turn; above it `design.slicing` groups them.
|
|
39104
|
+
*/
|
|
39105
|
+
const slicing = async () => {
|
|
39106
|
+
const all = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
39107
|
+
const cap = sliceCap();
|
|
39108
|
+
if (all.length <= cap) return [{
|
|
39109
|
+
title: "",
|
|
39110
|
+
ids: all
|
|
39111
|
+
}];
|
|
39112
|
+
return scope("design", async () => {
|
|
39113
|
+
await agentWithSkills("slicing", slicingPrompt(cap), {
|
|
39114
|
+
label: "Slicing the change",
|
|
39115
|
+
file: SLICES,
|
|
39116
|
+
model: planner(),
|
|
39117
|
+
system: designSystem()
|
|
39118
|
+
});
|
|
39119
|
+
return requireSlices(all, cap);
|
|
39120
|
+
});
|
|
39121
|
+
};
|
|
39122
|
+
/** Slice `i`'s criteria; `undefined` when `slices` is the one slice of an unsliced change. */
|
|
39123
|
+
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) };
|
|
39124
|
+
//#endregion
|
|
38881
39125
|
//#region src/workflows/feature/feature.ts
|
|
38882
39126
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
38883
39127
|
const unwind = () => {
|
|
38884
39128
|
const commit = head$1();
|
|
38885
39129
|
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
38886
39130
|
};
|
|
38887
|
-
/**
|
|
39131
|
+
/**
|
|
39132
|
+
* The slices `design.slicing` grouped the criteria into, each built in turn
|
|
39133
|
+
* from where the previous one landed; design must have run since `firstBase`.
|
|
39134
|
+
*/
|
|
38888
39135
|
const buildFeature = async (firstBase) => {
|
|
38889
39136
|
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
38890
|
-
const
|
|
38891
|
-
|
|
39137
|
+
const slices = await slicing();
|
|
39138
|
+
const one = slices.length === 1;
|
|
39139
|
+
for (const i of slices.keys()) {
|
|
39140
|
+
const more = i < slices.length - 1;
|
|
39141
|
+
const criteria = sliceCriteria(slices, i);
|
|
39142
|
+
if (sliceIds(criteria)?.length === 0) {
|
|
39143
|
+
if (!more) await closeOut();
|
|
39144
|
+
continue;
|
|
39145
|
+
}
|
|
39146
|
+
const n = one ? "" : `-${i + 1}`;
|
|
39147
|
+
await buildSlice(i === 0 ? firstBase : head$1(), {
|
|
39148
|
+
tier,
|
|
39149
|
+
criteria,
|
|
39150
|
+
refactor: `refactor${n}`,
|
|
39151
|
+
feature: `feature${n}`,
|
|
39152
|
+
more
|
|
39153
|
+
});
|
|
39154
|
+
}
|
|
39155
|
+
};
|
|
39156
|
+
/** A naive pass (judged only with `flagMode` on), then implement and the quality lap. */
|
|
39157
|
+
const buildSlice = async (sliceBase, build) => {
|
|
39158
|
+
const seam = await seamBuild(sliceBase, {
|
|
39159
|
+
criteria: build.criteria,
|
|
39160
|
+
refactor: build.refactor
|
|
39161
|
+
});
|
|
39162
|
+
await slice(build.feature, async () => {
|
|
38892
39163
|
await implement({
|
|
38893
39164
|
reference: seam.reference,
|
|
38894
|
-
tier,
|
|
39165
|
+
tier: build.tier,
|
|
38895
39166
|
afterRefactor: seam.refactored,
|
|
38896
|
-
naive: seam.land === true ? seam.reference : void 0
|
|
39167
|
+
naive: seam.land === true ? seam.reference : void 0,
|
|
39168
|
+
rebuild: seam.rebuild,
|
|
39169
|
+
slice: build.criteria,
|
|
39170
|
+
base: build.criteria === void 0 ? void 0 : sliceBase,
|
|
39171
|
+
more: build.more
|
|
38897
39172
|
});
|
|
38898
|
-
const
|
|
39173
|
+
const carried = seam.rebuild?.acceptance?.tests ?? [];
|
|
39174
|
+
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? sliceBase : seam.base;
|
|
39175
|
+
const guard = build.more ? () => requireCriteriaFrozen(REQUIREMENTS) : void 0;
|
|
38899
39176
|
await tail(settleQuality, {
|
|
38900
39177
|
green: true,
|
|
38901
|
-
lensBase: base
|
|
39178
|
+
lensBase: base,
|
|
39179
|
+
guard,
|
|
39180
|
+
guardRests: false
|
|
38902
39181
|
});
|
|
38903
39182
|
});
|
|
38904
39183
|
};
|
|
@@ -38944,6 +39223,7 @@ const ordinaryStart = async () => {
|
|
|
38944
39223
|
});
|
|
38945
39224
|
refusePendingHandOff();
|
|
38946
39225
|
flagModeOn();
|
|
39226
|
+
sliceCap();
|
|
38947
39227
|
const stated = statedCriteria(changes(), head$1());
|
|
38948
39228
|
await unwind();
|
|
38949
39229
|
if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
|
|
@@ -39138,4 +39418,4 @@ var workflows_default = feature;
|
|
|
39138
39418
|
*/
|
|
39139
39419
|
const doors = {};
|
|
39140
39420
|
//#endregion
|
|
39141
|
-
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 };
|
|
39421
|
+
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, 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, 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 };
|