@pmelab/gtd 38.0.0 → 38.2.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 +36 -3
- package/dist/chunks/{Schema-DO5ttJaI.mjs → Schema-C6fLlQzx.mjs} +1 -1
- package/dist/chunks/{reserved-sfMdy7v7.mjs → reserved-C3PgP7XB.mjs} +56 -12
- package/dist/chunks/{tail-Cgca5Gp-.mjs → tail-Bkxki2nK.mjs} +1 -1
- package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
- package/dist/gtd.bundle.mjs +292 -53
- package/dist/testkit.mjs +257 -21
- package/dist/wire.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +158 -42
- 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 +230 -70
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +15 -3
- package/src/cli/Cli.ts +61 -28
- package/src/cli/History.ts +1 -19
- package/src/cli/Metrics.test.ts +224 -0
- package/src/cli/Metrics.ts +310 -0
- package/src/cli/Output.ts +13 -0
- package/src/cli/Program.test.ts +3 -1
- package/src/cli/Program.ts +4 -0
- package/src/testkit/steps.ts +4 -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/criteria.ts +11 -0
- package/src/workflows/feature/refactor.ts +64 -30
- package/src/workflows/feature/seam.ts +102 -30
- package/src/workflows/shared/continuation.ts +92 -0
- package/src/workflows/shared/options.ts +2 -0
- package/src/workflows/shared/steps.ts +1 -0
- package/src/workflows/shared/text.test.ts +6 -2
- package/src/workflows/shared/text.ts +104 -8
- package/src/workflows/shared/vars.ts +5 -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);
|
|
@@ -16937,7 +16937,9 @@ const defaults = {
|
|
|
16937
16937
|
sliceMaxCriteria: "8",
|
|
16938
16938
|
flag_risk_tier: "0",
|
|
16939
16939
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
|
|
16940
|
-
refactorQualityReviews: "correctness, ponytail-review, conventions"
|
|
16940
|
+
refactorQualityReviews: "correctness, ponytail-review, conventions",
|
|
16941
|
+
continuations: "3",
|
|
16942
|
+
continuationStall: "2"
|
|
16941
16943
|
};
|
|
16942
16944
|
/** Environment settings. */
|
|
16943
16945
|
const envDefaults = {
|
|
@@ -17455,7 +17457,7 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
|
17455
17457
|
- Introduce no new flag read and add no second read of an existing one:
|
|
17456
17458
|
every match of \`${pattern}\` may move, never increase in number`;
|
|
17457
17459
|
const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
|
|
17458
|
-
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false }) => `${stateFileRules}
|
|
17460
|
+
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false, from }) => `${stateFileRules}
|
|
17459
17461
|
- The package to implement is \`${pkg}\`; never delete it
|
|
17460
17462
|
${note ? `- The package's \`## Refactor note\` section names the seams to open and the
|
|
17461
17463
|
shape each takes, written from the whole naive pass: build exactly those
|
|
@@ -17470,8 +17472,9 @@ ${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${st
|
|
|
17470
17472
|
restructuring afresh here, minus the feature's own behaviour. A
|
|
17471
17473
|
refactor that touches none of it is wrong, however clean — never
|
|
17472
17474
|
swap in an unrelated tidy-up
|
|
17473
|
-
`}
|
|
17474
|
-
|
|
17475
|
+
`}${handoffRule()}
|
|
17476
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
17477
|
+
${from === void 0 ? "" : continuationLines(from)}`;
|
|
17475
17478
|
const refactorFixPrompt = (strays, pattern) => `${stateFileRules}
|
|
17476
17479
|
${fixFeedbackPrompt}
|
|
17477
17480
|
${refactorRules(strays, pattern)}
|
|
@@ -18039,7 +18042,7 @@ Print the closing message and stop — this writes nothing itself.
|
|
|
18039
18042
|
const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
|
|
18040
18043
|
const isRed = (retry) => "red" in retry;
|
|
18041
18044
|
const isFault = (retry) => "fault" in retry;
|
|
18042
|
-
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\``;
|
|
18043
18046
|
const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
|
|
18044
18047
|
the file, never from memory, and stop only when each one holds. They are
|
|
18045
18048
|
settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
|
|
@@ -18058,7 +18061,33 @@ re-implement. It changed these files outside the expected set:
|
|
|
18058
18061
|
${listed(retry.strays)}
|
|
18059
18062
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
18060
18063
|
`;
|
|
18061
|
-
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}
|
|
18062
18091
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18063
18092
|
${sliceLine(slice)}${criteriaBinding(false)}
|
|
18064
18093
|
- Implement them the quickest way that works — the naive version. Do not
|
|
@@ -18075,9 +18104,20 @@ ${sliceLine(slice)}${criteriaBinding(false)}
|
|
|
18075
18104
|
- Run \`${env.testCommand}\` and finish only once it is green with every
|
|
18076
18105
|
criterion's test passing: the next step runs it on your diff and sends a red
|
|
18077
18106
|
pass back
|
|
18078
|
-
- Leave \`.gtd/\` untouched
|
|
18107
|
+
- Leave \`.gtd/\` untouched, a handoff note excepted
|
|
18108
|
+
${handoffRule()}
|
|
18079
18109
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
18080
|
-
${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)}`;
|
|
18081
18121
|
const carries = (rebuild) => (rebuild.acceptance?.ids.length ?? 0) > 0;
|
|
18082
18122
|
const rebuildLines = ({ note, acceptance }) => `${note === void 0 ? "" : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
|
|
18083
18123
|
opened; plug the change into them:
|
|
@@ -18114,7 +18154,7 @@ const flagLine = (flagMode, tier, amend, sliced) => !flagMode ? "- Add no releas
|
|
|
18114
18154
|
change added; only when none did, behind a new one, following the
|
|
18115
18155
|
repository's flag conventions` : `- Put the new behaviour behind a new release flag, following the
|
|
18116
18156
|
repository's flag conventions`;
|
|
18117
|
-
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}, slice) => `${stateFileRules}
|
|
18157
|
+
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}, slice, from) => `${stateFileRules}
|
|
18118
18158
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18119
18159
|
${sliceLine(slice)}${criteriaBinding(true)}
|
|
18120
18160
|
${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
@@ -18129,9 +18169,10 @@ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `-
|
|
|
18129
18169
|
- Write the tests first, then the code that makes them pass. Name each
|
|
18130
18170
|
criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
|
|
18131
18171
|
green a judge checks every ID has a passing test, and sends uncovered ones back
|
|
18172
|
+
${handoffRule()}
|
|
18132
18173
|
- Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
|
|
18133
18174
|
finish your turn
|
|
18134
|
-
`;
|
|
18175
|
+
${from === void 0 ? "" : continuationLines(from)}`;
|
|
18135
18176
|
const implementFixPrompt = () => `${stateFileRules}
|
|
18136
18177
|
${fixFeedbackPrompt}
|
|
18137
18178
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
@@ -18236,6 +18277,7 @@ const REFACTOR = ".gtd/REFACTOR.md";
|
|
|
18236
18277
|
const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
|
|
18237
18278
|
const QUALITY = ".gtd/QUALITY.md";
|
|
18238
18279
|
const SLICES = ".gtd/SLICES.md";
|
|
18280
|
+
const HANDOFF = ".gtd/HANDOFF.md";
|
|
18239
18281
|
const planner = () => env.plannerModel ?? "";
|
|
18240
18282
|
const coder = () => env.coderModel ?? "";
|
|
18241
18283
|
const fixCheck = (rules) => agentWithSkills("fix", buildFixPrompt(rules), {
|
|
@@ -37738,21 +37780,23 @@ Struct({
|
|
|
37738
37780
|
});
|
|
37739
37781
|
//#endregion
|
|
37740
37782
|
//#region src/workflows/shared/options.ts
|
|
37741
|
-
const setting = optionalWith(String$, { exact: true });
|
|
37783
|
+
const setting$1 = optionalWith(String$, { exact: true });
|
|
37742
37784
|
const ShippedOptions = Struct({
|
|
37743
|
-
reviewBase: setting,
|
|
37744
|
-
judgeBudgetBytes: setting,
|
|
37745
|
-
judgeIdenticalMinP: setting,
|
|
37746
|
-
reviewNoteActionable: setting,
|
|
37747
|
-
reviewSeverityMinP: setting,
|
|
37748
|
-
refactorFlagPattern: setting,
|
|
37749
|
-
seamAllow: setting,
|
|
37750
|
-
sliceMaxCriteria: setting,
|
|
37751
|
-
qualityReviews: setting,
|
|
37752
|
-
refactorQualityReviews: setting,
|
|
37753
|
-
|
|
37754
|
-
|
|
37755
|
-
|
|
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,
|
|
37756
37800
|
flagMode: optionalWith(Boolean$, { exact: true }),
|
|
37757
37801
|
contextLimit: optionalWith(Struct({
|
|
37758
37802
|
soft: optionalWith(Number$, { exact: true }),
|
|
@@ -37783,7 +37827,7 @@ const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOp
|
|
|
37783
37827
|
}));
|
|
37784
37828
|
const FlagOptions = Struct({
|
|
37785
37829
|
...ShippedOptions.fields,
|
|
37786
|
-
flagRegistry: setting
|
|
37830
|
+
flagRegistry: setting$1
|
|
37787
37831
|
});
|
|
37788
37832
|
const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions, {
|
|
37789
37833
|
onExcessProperty: "error",
|
|
@@ -38056,6 +38100,13 @@ const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scop
|
|
|
38056
38100
|
});
|
|
38057
38101
|
return true;
|
|
38058
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
|
+
};
|
|
38059
38110
|
//#endregion
|
|
38060
38111
|
//#region src/workflows/feature/tier.ts
|
|
38061
38112
|
/** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
|
|
@@ -38404,6 +38455,53 @@ const reUnwind = async (feedback) => {
|
|
|
38404
38455
|
requireRevert(edited, base);
|
|
38405
38456
|
};
|
|
38406
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
|
|
38407
38505
|
//#region src/workflows/feature/refactor.ts
|
|
38408
38506
|
const REFACTOR_PACKAGE = REFACTOR;
|
|
38409
38507
|
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
@@ -38549,7 +38647,7 @@ const guardOf = ({ pattern, strays }, entry) => () => {
|
|
|
38549
38647
|
requireNoNewFlags(pattern, entry);
|
|
38550
38648
|
};
|
|
38551
38649
|
/** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
|
|
38552
|
-
const refactorLap = async (p, guard, feedback) => {
|
|
38650
|
+
const refactorLap = async (p, guard, refactor, feedback) => {
|
|
38553
38651
|
const { pattern, strays, reference, note } = p;
|
|
38554
38652
|
await run("deriving", deriveScript([
|
|
38555
38653
|
...packageLines(strays, pattern, reference),
|
|
@@ -38569,32 +38667,48 @@ const refactorLap = async (p, guard, feedback) => {
|
|
|
38569
38667
|
label: "Deriving the refactor package",
|
|
38570
38668
|
file: REFACTOR_PACKAGE
|
|
38571
38669
|
});
|
|
38572
|
-
|
|
38573
|
-
|
|
38574
|
-
|
|
38575
|
-
|
|
38576
|
-
|
|
38577
|
-
|
|
38578
|
-
|
|
38579
|
-
|
|
38580
|
-
|
|
38581
|
-
|
|
38582
|
-
|
|
38583
|
-
|
|
38584
|
-
guard();
|
|
38585
|
-
await healthy(async () => {
|
|
38586
|
-
await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
|
|
38587
|
-
label: "Fixing the check",
|
|
38588
|
-
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",
|
|
38589
38682
|
model: coder(),
|
|
38590
38683
|
system: builderSystem()
|
|
38591
|
-
});
|
|
38684
|
+
}));
|
|
38685
|
+
const left = read(HANDOFF);
|
|
38592
38686
|
guard();
|
|
38593
|
-
|
|
38594
|
-
|
|
38595
|
-
|
|
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
|
+
};
|
|
38596
38710
|
});
|
|
38597
|
-
await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
|
|
38711
|
+
await run("closing", removeScript([REFACTOR_PACKAGE, HANDOFF]), { label: "Closing out the refactor" });
|
|
38598
38712
|
};
|
|
38599
38713
|
const strayAccess = (strays) => ({ write: [
|
|
38600
38714
|
...strays,
|
|
@@ -38608,7 +38722,7 @@ const refactorFirst = async (shape, options = {}) => {
|
|
|
38608
38722
|
name: "refactor",
|
|
38609
38723
|
access: strayAccess(p.strays)
|
|
38610
38724
|
}, async () => {
|
|
38611
|
-
await refactorLap(p, guardOf(p, head$1()));
|
|
38725
|
+
await refactorLap(p, guardOf(p, head$1()), phase("refactor"));
|
|
38612
38726
|
return true;
|
|
38613
38727
|
});
|
|
38614
38728
|
};
|
|
@@ -38633,9 +38747,10 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
38633
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",
|
|
38634
38748
|
dropped: refactorDroppedRefusal
|
|
38635
38749
|
};
|
|
38750
|
+
const refactor = phase("refactor");
|
|
38636
38751
|
let collected;
|
|
38637
38752
|
for (;;) {
|
|
38638
|
-
await refactorLap(p, guard, collected);
|
|
38753
|
+
await refactorLap(p, guard, refactor, collected);
|
|
38639
38754
|
const outcome = await buildTail(entry, {
|
|
38640
38755
|
green: true,
|
|
38641
38756
|
guard,
|
|
@@ -38746,12 +38861,32 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
|
|
|
38746
38861
|
allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
|
|
38747
38862
|
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
|
|
38748
38863
|
});
|
|
38749
|
-
const naivePass = (retry, slice) => agentWithSkills("naive", naivePassPrompt(retry, sliceIds(slice)), {
|
|
38750
|
-
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",
|
|
38751
38866
|
keep: true,
|
|
38752
38867
|
model: coder(),
|
|
38753
38868
|
system: builderSystem()
|
|
38754
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
|
+
});
|
|
38755
38890
|
const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
|
|
38756
38891
|
const EXTENSION = /\.[\w-]+$/;
|
|
38757
38892
|
const ALIAS = /^(?:[@~]\/|#)/;
|
|
@@ -38879,6 +39014,32 @@ const applied = async (tip) => {
|
|
|
38879
39014
|
await run("applying", applyScript(tip, FEEDBACK, naiveApplyFailure(tip), [".gtd"]), { label: "Applying the naive pass" });
|
|
38880
39015
|
return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
|
|
38881
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
|
+
});
|
|
38882
39043
|
function implement(first, tier, amend) {
|
|
38883
39044
|
const options = typeof first === "object" && first !== null ? first : {
|
|
38884
39045
|
reference: first,
|
|
@@ -38889,14 +39050,12 @@ function implement(first, tier, amend) {
|
|
|
38889
39050
|
const since = head$1();
|
|
38890
39051
|
const acceptance = options.rebuild?.acceptance;
|
|
38891
39052
|
const built = options.naive === void 0 || !await applied(options.naive);
|
|
38892
|
-
if (built) await
|
|
38893
|
-
|
|
38894
|
-
|
|
38895
|
-
|
|
38896
|
-
|
|
38897
|
-
});
|
|
38898
|
-
requireCriteriaFrozen(REQUIREMENTS);
|
|
38899
|
-
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);
|
|
38900
39059
|
await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
|
|
38901
39060
|
await requireCoverage(options.base, options.slice);
|
|
38902
39061
|
await closing(options.more === true);
|
|
@@ -38905,10 +39064,11 @@ function implement(first, tier, amend) {
|
|
|
38905
39064
|
/** The last slice's closing, for a last slice a steer left with no criterion to build. */
|
|
38906
39065
|
const closeOut = () => scope("implement", () => closing(false));
|
|
38907
39066
|
/** 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] : [
|
|
39067
|
+
const closing = (more) => run("closing", removeScript(more ? [REFACTOR_NOTE, HANDOFF] : [
|
|
38909
39068
|
REQUIREMENTS,
|
|
38910
39069
|
REFACTOR_NOTE,
|
|
38911
|
-
SLICES
|
|
39070
|
+
SLICES,
|
|
39071
|
+
HANDOFF
|
|
38912
39072
|
]), { label: "Closing out the implementation" });
|
|
38913
39073
|
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
38914
39074
|
const steer = async (retry) => {
|
|
@@ -38980,10 +39140,10 @@ const decide = (slice) => scope("seam", async () => {
|
|
|
38980
39140
|
again: budget(),
|
|
38981
39141
|
waived: false
|
|
38982
39142
|
};
|
|
39143
|
+
const naive = phase("naive pass", requireCriteriaIfEdited);
|
|
38983
39144
|
let retry;
|
|
38984
39145
|
for (;;) {
|
|
38985
|
-
await
|
|
38986
|
-
const tip = kept("naive");
|
|
39146
|
+
const tip = await naiveContinued(retry, slice, naive);
|
|
38987
39147
|
const outcome = await vetted(tip, vetting, slice) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
|
|
38988
39148
|
reference: tip.hash,
|
|
38989
39149
|
tip
|
|
@@ -39418,4 +39578,4 @@ var workflows_default = feature;
|
|
|
39418
39578
|
*/
|
|
39419
39579
|
const doors = {};
|
|
39420
39580
|
//#endregion
|
|
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 };
|
|
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 };
|
package/package.json
CHANGED
package/src/cli/Cli.test.ts
CHANGED
|
@@ -1021,14 +1021,21 @@ describe("parseArgv — gtd history", () => {
|
|
|
1021
1021
|
expect(parse("history", "fetch", "upstream")).toMatchObject({
|
|
1022
1022
|
command: { kind: "historyFetch", remote: "upstream" },
|
|
1023
1023
|
})
|
|
1024
|
+
expect(parse("history", "metrics")).toMatchObject({
|
|
1025
|
+
command: { kind: "historyMetrics", refs: [] },
|
|
1026
|
+
})
|
|
1027
|
+
expect(parse("history", "metrics", "a", "feat/b")).toMatchObject({
|
|
1028
|
+
command: { kind: "historyMetrics", refs: ["a", "feat/b"] },
|
|
1029
|
+
})
|
|
1024
1030
|
})
|
|
1025
1031
|
|
|
1026
|
-
it("bare history and an unknown subcommand are usage errors naming list, show, fetch", () => {
|
|
1032
|
+
it("bare history and an unknown subcommand are usage errors naming list, show, metrics, fetch", () => {
|
|
1027
1033
|
for (const args of [["history"], ["history", "nope"]]) {
|
|
1028
1034
|
const message = usage(parse(...args))
|
|
1029
1035
|
expect(message).toContain("list")
|
|
1030
1036
|
expect(message).toContain("show")
|
|
1031
1037
|
expect(message).toContain("fetch")
|
|
1038
|
+
expect(message).toContain("metrics")
|
|
1032
1039
|
}
|
|
1033
1040
|
})
|
|
1034
1041
|
|
|
@@ -1039,19 +1046,23 @@ describe("parseArgv — gtd history", () => {
|
|
|
1039
1046
|
expect(usage(parse("history", "fetch", "a", "b"))).toContain("too many arguments")
|
|
1040
1047
|
})
|
|
1041
1048
|
|
|
1042
|
-
it("--json is accepted for list and
|
|
1049
|
+
it("--json is accepted for list, show and metrics, rejected for fetch", () => {
|
|
1043
1050
|
expect(parse("history", "list", "--json")).toMatchObject({ json: { kind: "document" } })
|
|
1044
1051
|
expect(parse("history", "show", "x", "--json=ended")).toMatchObject({
|
|
1045
1052
|
json: { kind: "select", path: "ended" },
|
|
1046
1053
|
})
|
|
1054
|
+
expect(parse("history", "metrics", "--json=total.refactorFirstRate")).toMatchObject({
|
|
1055
|
+
json: { kind: "select", path: "total.refactorFirstRate" },
|
|
1056
|
+
})
|
|
1047
1057
|
expect(parse("history", "fetch", "--json").kind).toBe("usage")
|
|
1048
1058
|
})
|
|
1049
1059
|
|
|
1050
|
-
it("help lists the
|
|
1060
|
+
it("help lists the four history rows", () => {
|
|
1051
1061
|
const help = renderHelp()
|
|
1052
1062
|
expect(help).toMatch(/^ {2}history list\b/m)
|
|
1053
1063
|
expect(help).toContain("history show <ref>")
|
|
1054
1064
|
expect(help).toContain("history fetch [remote]")
|
|
1065
|
+
expect(help).toContain("history metrics [ref...]")
|
|
1055
1066
|
})
|
|
1056
1067
|
})
|
|
1057
1068
|
|
|
@@ -1136,6 +1147,7 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
1136
1147
|
"slices",
|
|
1137
1148
|
"historyList",
|
|
1138
1149
|
"historyShow",
|
|
1150
|
+
"historyMetrics",
|
|
1139
1151
|
"historyFetch",
|
|
1140
1152
|
] as const) {
|
|
1141
1153
|
expect(needsOf(kind)).toBe("state")
|