@pmelab/gtd 29.2.4 → 29.2.5
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 +5 -3
- package/dist/chunks/{reserved-BduvUQxQ.mjs → reserved-C7JrDBnQ.mjs} +5 -2
- package/dist/chunks/{tail-pgq1Fbbv.mjs → tail-JAC-v_TW.mjs} +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +33 -19
- 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 +36 -19
- package/package.json +1 -1
- package/src/workflows/feature/feature.test.ts +46 -2
- package/src/workflows/feature/feature.ts +19 -7
- package/src/workflows/feature/refactor-absorbs.feature +125 -0
- package/src/workflows/feature/review.test.ts +77 -0
- package/src/workflows/feature/review.ts +51 -14
- package/src/workflows/feature/seam.test.ts +18 -1
- package/src/workflows/feature/seam.ts +12 -3
- package/src/workflows/shared/text.ts +12 -4
package/README.md
CHANGED
|
@@ -587,9 +587,11 @@ directly. A judge checks whether it changed anything outside the expected set
|
|
|
587
587
|
change is refactored first, in its own slice with its own quality lap
|
|
588
588
|
(`refactorQualityReviews`) and review, before the feature is implemented and
|
|
589
589
|
reviewed; a wrong approach is retried twice, then rests for you to steer or
|
|
590
|
-
overrule it. `gtd slices` lists the `refactor` and `feature` slices.
|
|
591
|
-
|
|
592
|
-
|
|
590
|
+
overrule it. `gtd slices` lists the `refactor` and `feature` slices. A refactor
|
|
591
|
+
that already carried the whole feature leaves the implementation nothing to do:
|
|
592
|
+
its empty turn passes, and the feature review covers the whole change. Feedback
|
|
593
|
+
at the feature review re-runs design and the implementation, never the naive
|
|
594
|
+
pass; feedback at the refactor review re-runs the refactor, never design; a
|
|
593
595
|
refactor-review change the refactor cannot build is refused, and the only way
|
|
594
596
|
out is `gtd abandon`. A process from 27.5.0 or later whose judge ruled
|
|
595
597
|
`refactor-first` must `gtd abandon` too. Each slice's quality lap reviews only
|
|
@@ -1365,9 +1365,12 @@ re-implement. It changed these files outside the expected set:
|
|
|
1365
1365
|
${listed(rejection.strays)}
|
|
1366
1366
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
1367
1367
|
`}`;
|
|
1368
|
-
const implementBuildingPrompt = (reference, tier, amend) => `${stateFileRules}
|
|
1368
|
+
const implementBuildingPrompt = (reference, tier, amend, afterRefactor) => `${stateFileRules}
|
|
1369
1369
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1370
|
-
${
|
|
1370
|
+
${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
|
|
1371
|
+
though never a release flag. Build only what is still missing, held to every
|
|
1372
|
+
rule below; if nothing is, change nothing and finish your turn
|
|
1373
|
+
` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
1371
1374
|
the re-derived requirements instead of rewriting it
|
|
1372
1375
|
` : `- Implement them properly, fitting the codebase's seams and conventions
|
|
1373
1376
|
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
|
|
2
2
|
import { fixFindings, lensesOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import { E as reviewing, S as fixRisks, T as reviewQuality, W as handOffAwaitReviewMessage, _ as REVIEW, b as fixCheck, c as escalation, d as healthy, m as QUALITY, u as guarded, v as awaitReview, w as reviewMissing, x as fixQuality } from "./reserved-
|
|
3
|
+
import { E as reviewing, S as fixRisks, T as reviewQuality, W as handOffAwaitReviewMessage, _ as REVIEW, b as fixCheck, c as escalation, d as healthy, m as QUALITY, u as guarded, v as awaitReview, w as reviewMissing, x as fixQuality } from "./reserved-C7JrDBnQ.mjs";
|
|
4
4
|
//#region src/workflows/shared/tail.ts
|
|
5
5
|
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
6
6
|
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import { _t as registryEntry, a as flagShipped, dt as defaults, ft as envDefaults, ht as flagReferences, i as skills, n as summary, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-
|
|
3
|
-
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { _t as registryEntry, a as flagShipped, dt as defaults, ft as envDefaults, ht as flagReferences, i as skills, n as summary, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-C7JrDBnQ.mjs";
|
|
3
|
+
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-JAC-v_TW.mjs";
|
|
4
4
|
//#region src/workflows/cleanup/cleanup.ts
|
|
5
5
|
const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
|
|
6
6
|
const cleanupFlow = async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars } from "../flows.mjs";
|
|
2
2
|
import { lensesOf, review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as refactorFixPrompt, A as architectureDecomposePrompt, B as designSystem, C as planner, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as idleMessage, I as buildReviewTriageMessage, J as naivePassPrompt, K as implementBuildingPrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as refactorDroppedRefusal, R as deferNotesScript, S as fixRisks, V as designTriagePrompt, X as packagesItemFixSuitePrompt, Y as packagesItemBuildingPrompt, Z as refactorBuildingPrompt, _ as REVIEW, at as reviewerSystem, b as fixCheck, ct as startGateBlockedMessage, d as healthy, dt as defaults, et as refactorRules, f as ARCHITECTURE, ft as envDefaults, g as REQUIREMENTS, h as REFACTOR, i as skills, it as reviewNotesCapture, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as gate, lt as unwindFailedMessage, n as summary, nt as reviewEditsCapture, o as shipped, ot as seamByHandMessage, p as FEEDBACK, pt as testFileGlobs, q as implementFixPrompt, r as access, st as seamSteerMessage, t as steering, tt as reviewEditNotesCapture, u as guarded, ut as unwindFailure, v as awaitReview, w as reviewMissing, y as coder, z as designGateAnswerMessage } from "../chunks/reserved-
|
|
4
|
-
import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-
|
|
3
|
+
import { $ as refactorFixPrompt, A as architectureDecomposePrompt, B as designSystem, C as planner, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as idleMessage, I as buildReviewTriageMessage, J as naivePassPrompt, K as implementBuildingPrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as refactorDroppedRefusal, R as deferNotesScript, S as fixRisks, V as designTriagePrompt, X as packagesItemFixSuitePrompt, Y as packagesItemBuildingPrompt, Z as refactorBuildingPrompt, _ as REVIEW, at as reviewerSystem, b as fixCheck, ct as startGateBlockedMessage, d as healthy, dt as defaults, et as refactorRules, f as ARCHITECTURE, ft as envDefaults, g as REQUIREMENTS, h as REFACTOR, i as skills, it as reviewNotesCapture, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as gate, lt as unwindFailedMessage, n as summary, nt as reviewEditsCapture, o as shipped, ot as seamByHandMessage, p as FEEDBACK, pt as testFileGlobs, q as implementFixPrompt, r as access, st as seamSteerMessage, t as steering, tt as reviewEditNotesCapture, u as guarded, ut as unwindFailure, v as awaitReview, w as reviewMissing, y as coder, z as designGateAnswerMessage } from "../chunks/reserved-C7JrDBnQ.mjs";
|
|
4
|
+
import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-JAC-v_TW.mjs";
|
|
5
5
|
//#region src/workflows/shared/fences.ts
|
|
6
6
|
const markFenced = (text) => {
|
|
7
7
|
let fence;
|
|
@@ -475,16 +475,26 @@ const review = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
475
475
|
}
|
|
476
476
|
});
|
|
477
477
|
};
|
|
478
|
-
function buildTail(first, second,
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}];
|
|
485
|
-
const { fixFirst, green, ...reviewOptions } = options;
|
|
486
|
-
return tail(fixFirst ?? false, (escalations) => review(base, escalations, reviewOptions), green ?? false, options.guard, options.lenses, options.guardRests, options.lensBase, options.rules);
|
|
478
|
+
function buildTail(first, second, ...rest) {
|
|
479
|
+
const options = callForm(first, second, rest);
|
|
480
|
+
if (options === void 0) return Promise.reject(/* @__PURE__ */ new Error("buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither"));
|
|
481
|
+
const [base, form] = options;
|
|
482
|
+
const { fixFirst, green, ...reviewOptions } = form;
|
|
483
|
+
return tail(fixFirst ?? false, (escalations) => review(base, escalations, reviewOptions), green ?? false, form.guard, form.lenses, form.guardRests, form.lensBase, form.rules);
|
|
487
484
|
}
|
|
485
|
+
const isOptionalObject = (value) => value == null || typeof value === "object" && !Array.isArray(value) && typeof value.then !== "function";
|
|
486
|
+
const isOptionalBoolean = (value) => value == null || typeof value === "boolean";
|
|
487
|
+
const legacyForm = (fixFirst, base, green, options) => isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options) ? [base, {
|
|
488
|
+
...options,
|
|
489
|
+
fixFirst: fixFirst ?? void 0,
|
|
490
|
+
green: green ?? void 0
|
|
491
|
+
}] : void 0;
|
|
492
|
+
const currentForm = (base, options, third, fourth) => isOptionalObject(options) && third == null && fourth == null ? [base, options ?? {}] : void 0;
|
|
493
|
+
const callForm = (first, second, [third, fourth, ...extra]) => {
|
|
494
|
+
if (extra.some((value) => value != null)) return void 0;
|
|
495
|
+
if (typeof second === "string") return legacyForm(first, second, third, fourth);
|
|
496
|
+
if (typeof first === "string") return currentForm(first, second, third, fourth);
|
|
497
|
+
};
|
|
488
498
|
/**
|
|
489
499
|
* Undo the human's review-round code edits. A path changed since the human
|
|
490
500
|
* left it is not overwritten; requireRevert names it instead.
|
|
@@ -764,10 +774,11 @@ function implement(first, tier, amend) {
|
|
|
764
774
|
amend
|
|
765
775
|
};
|
|
766
776
|
return scope("implement", async () => {
|
|
767
|
-
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend), {
|
|
777
|
+
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor), {
|
|
768
778
|
label: "Implementing",
|
|
769
779
|
model: coder(),
|
|
770
|
-
system: builderSystem()
|
|
780
|
+
system: builderSystem(),
|
|
781
|
+
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
771
782
|
});
|
|
772
783
|
await healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
773
784
|
label: "Fixing the check",
|
|
@@ -830,10 +841,11 @@ const seamBuild = async (base) => {
|
|
|
830
841
|
reference,
|
|
831
842
|
base
|
|
832
843
|
};
|
|
833
|
-
await slice("refactor", () => reviewedRefactor(shape, { reference }));
|
|
844
|
+
const refactored = await slice("refactor", () => reviewedRefactor(shape, { reference }));
|
|
834
845
|
return {
|
|
835
846
|
reference,
|
|
836
|
-
base: head()
|
|
847
|
+
base: head(),
|
|
848
|
+
refactored
|
|
837
849
|
};
|
|
838
850
|
};
|
|
839
851
|
//#endregion
|
|
@@ -869,12 +881,14 @@ const naivePlanAndBuild = async (firstBase) => {
|
|
|
869
881
|
await slice("feature", async () => {
|
|
870
882
|
await implement({
|
|
871
883
|
reference: seam.reference,
|
|
872
|
-
tier
|
|
884
|
+
tier,
|
|
885
|
+
afterRefactor: seam.refactored
|
|
873
886
|
});
|
|
874
|
-
|
|
887
|
+
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path)) ? firstBase : seam.base;
|
|
888
|
+
await featureFeedback(await buildTail(base, {
|
|
875
889
|
green: true,
|
|
876
|
-
lensBase:
|
|
877
|
-
}),
|
|
890
|
+
lensBase: base
|
|
891
|
+
}), base);
|
|
878
892
|
});
|
|
879
893
|
};
|
|
880
894
|
const featureFeedback = async (first, lensBase) => {
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
|
|
3
|
-
import { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, _ as REVIEW, dt as defaults, ft as envDefaults, i as skills, n as summary, o as shipped, r as access, s as baseline, t as steering, y as coder } from "../chunks/reserved-
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, _ as REVIEW, dt as defaults, ft as envDefaults, i as skills, n as summary, o as shipped, r as access, s as baseline, t as steering, y as coder } from "../chunks/reserved-C7JrDBnQ.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-JAC-v_TW.mjs";
|
|
5
5
|
//#region src/workflows/fix/fix.ts
|
|
6
6
|
/**
|
|
7
7
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import { dt as defaults, ft as envDefaults, i as skills, l as gate, n as summary, o as shipped, r as access, rt as reviewGateBlockedMessage, t as steering } from "../chunks/reserved-
|
|
3
|
-
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { dt as defaults, ft as envDefaults, i as skills, l as gate, n as summary, o as shipped, r as access, rt as reviewGateBlockedMessage, t as steering } from "../chunks/reserved-C7JrDBnQ.mjs";
|
|
3
|
+
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-JAC-v_TW.mjs";
|
|
4
4
|
//#region src/workflows/review/review.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import { a as flagShipped, dt as defaults, ft as envDefaults, gt as flipProblems, i as skills, l as gate, mt as flagMentions, n as summary, p as FEEDBACK, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-
|
|
2
|
+
import { a as flagShipped, dt as defaults, ft as envDefaults, gt as flipProblems, i as skills, l as gate, mt as flagMentions, n as summary, p as FEEDBACK, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-C7JrDBnQ.mjs";
|
|
3
3
|
//#region src/workflows/rollout/rollout.ts
|
|
4
4
|
const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
|
|
5
5
|
const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
|
package/dist/workflows.mjs
CHANGED
|
@@ -18149,9 +18149,12 @@ re-implement. It changed these files outside the expected set:
|
|
|
18149
18149
|
${listed(rejection.strays)}
|
|
18150
18150
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
18151
18151
|
`}`;
|
|
18152
|
-
const implementBuildingPrompt = (reference, tier, amend) => `${stateFileRules}
|
|
18152
|
+
const implementBuildingPrompt = (reference, tier, amend, afterRefactor) => `${stateFileRules}
|
|
18153
18153
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18154
|
-
${
|
|
18154
|
+
${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
|
|
18155
|
+
though never a release flag. Build only what is still missing, held to every
|
|
18156
|
+
rule below; if nothing is, change nothing and finish your turn
|
|
18157
|
+
` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
18155
18158
|
the re-derived requirements instead of rewriting it
|
|
18156
18159
|
` : `- Implement them properly, fitting the codebase's seams and conventions
|
|
18157
18160
|
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
@@ -38312,16 +38315,26 @@ const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
38312
38315
|
}
|
|
38313
38316
|
});
|
|
38314
38317
|
};
|
|
38315
|
-
function buildTail(first, second,
|
|
38316
|
-
|
|
38317
|
-
|
|
38318
|
-
|
|
38319
|
-
|
|
38320
|
-
|
|
38321
|
-
}];
|
|
38322
|
-
const { fixFirst, green, ...reviewOptions } = options;
|
|
38323
|
-
return tail(fixFirst ?? false, (escalations) => review$1(base, escalations, reviewOptions), green ?? false, options.guard, options.lenses, options.guardRests, options.lensBase, options.rules);
|
|
38318
|
+
function buildTail(first, second, ...rest) {
|
|
38319
|
+
const options = callForm(first, second, rest);
|
|
38320
|
+
if (options === void 0) return Promise.reject(/* @__PURE__ */ new Error("buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither"));
|
|
38321
|
+
const [base, form] = options;
|
|
38322
|
+
const { fixFirst, green, ...reviewOptions } = form;
|
|
38323
|
+
return tail(fixFirst ?? false, (escalations) => review$1(base, escalations, reviewOptions), green ?? false, form.guard, form.lenses, form.guardRests, form.lensBase, form.rules);
|
|
38324
38324
|
}
|
|
38325
|
+
const isOptionalObject = (value) => value == null || typeof value === "object" && !Array.isArray(value) && typeof value.then !== "function";
|
|
38326
|
+
const isOptionalBoolean = (value) => value == null || typeof value === "boolean";
|
|
38327
|
+
const legacyForm = (fixFirst, base, green, options) => isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options) ? [base, {
|
|
38328
|
+
...options,
|
|
38329
|
+
fixFirst: fixFirst ?? void 0,
|
|
38330
|
+
green: green ?? void 0
|
|
38331
|
+
}] : void 0;
|
|
38332
|
+
const currentForm = (base, options, third, fourth) => isOptionalObject(options) && third == null && fourth == null ? [base, options ?? {}] : void 0;
|
|
38333
|
+
const callForm = (first, second, [third, fourth, ...extra]) => {
|
|
38334
|
+
if (extra.some((value) => value != null)) return void 0;
|
|
38335
|
+
if (typeof second === "string") return legacyForm(first, second, third, fourth);
|
|
38336
|
+
if (typeof first === "string") return currentForm(first, second, third, fourth);
|
|
38337
|
+
};
|
|
38325
38338
|
/**
|
|
38326
38339
|
* Undo the human's review-round code edits. A path changed since the human
|
|
38327
38340
|
* left it is not overwritten; requireRevert names it instead.
|
|
@@ -38601,10 +38614,11 @@ function implement(first, tier, amend) {
|
|
|
38601
38614
|
amend
|
|
38602
38615
|
};
|
|
38603
38616
|
return scope("implement", async () => {
|
|
38604
|
-
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend), {
|
|
38617
|
+
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor), {
|
|
38605
38618
|
label: "Implementing",
|
|
38606
38619
|
model: coder(),
|
|
38607
|
-
system: builderSystem()
|
|
38620
|
+
system: builderSystem(),
|
|
38621
|
+
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
38608
38622
|
});
|
|
38609
38623
|
await healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
38610
38624
|
label: "Fixing the check",
|
|
@@ -38667,10 +38681,11 @@ const seamBuild = async (base) => {
|
|
|
38667
38681
|
reference,
|
|
38668
38682
|
base
|
|
38669
38683
|
};
|
|
38670
|
-
await slice("refactor", () => reviewedRefactor(shape, { reference }));
|
|
38684
|
+
const refactored = await slice("refactor", () => reviewedRefactor(shape, { reference }));
|
|
38671
38685
|
return {
|
|
38672
38686
|
reference,
|
|
38673
|
-
base: head$1()
|
|
38687
|
+
base: head$1(),
|
|
38688
|
+
refactored
|
|
38674
38689
|
};
|
|
38675
38690
|
};
|
|
38676
38691
|
//#endregion
|
|
@@ -38706,12 +38721,14 @@ const naivePlanAndBuild = async (firstBase) => {
|
|
|
38706
38721
|
await slice("feature", async () => {
|
|
38707
38722
|
await implement({
|
|
38708
38723
|
reference: seam.reference,
|
|
38709
|
-
tier
|
|
38724
|
+
tier,
|
|
38725
|
+
afterRefactor: seam.refactored
|
|
38710
38726
|
});
|
|
38711
|
-
|
|
38727
|
+
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path)) ? firstBase : seam.base;
|
|
38728
|
+
await featureFeedback(await buildTail(base, {
|
|
38712
38729
|
green: true,
|
|
38713
|
-
lensBase:
|
|
38714
|
-
}),
|
|
38730
|
+
lensBase: base
|
|
38731
|
+
}), base);
|
|
38715
38732
|
});
|
|
38716
38733
|
};
|
|
38717
38734
|
const featureFeedback = async (first, lensBase) => {
|
package/package.json
CHANGED
|
@@ -50,7 +50,12 @@ const CALC = "export const add = (a: number, b: number) => a + b\n"
|
|
|
50
50
|
const naivePlanned = (flag: "0" | "1") => {
|
|
51
51
|
const repo = testRepo({
|
|
52
52
|
workflow: CONFIG_SCAFFOLD,
|
|
53
|
-
env: {
|
|
53
|
+
env: {
|
|
54
|
+
GTD_QUALITYREVIEWS: "",
|
|
55
|
+
GTD_REFACTORQUALITYREVIEWS: "",
|
|
56
|
+
GTD_FLAG_NAIVE_PASS: flag,
|
|
57
|
+
GTD_FLAG_RISK_TIER: "0",
|
|
58
|
+
},
|
|
54
59
|
})
|
|
55
60
|
repo.write("src/calc.ts", CALC)
|
|
56
61
|
repo.commit("feat: add calculator")
|
|
@@ -59,6 +64,40 @@ const naivePlanned = (flag: "0" | "1") => {
|
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
describe("planAndBuild on the naive pass", () => {
|
|
67
|
+
it("a refactor that carried the whole feature: the empty implement turn passes and the review covers the whole change", async () => {
|
|
68
|
+
const repo = naivePlanned("1")
|
|
69
|
+
const SUBTRACT = `${CALC}export const subtract = (a: number, b: number) => a - b\n`
|
|
70
|
+
expect(
|
|
71
|
+
(
|
|
72
|
+
await repo.drive({
|
|
73
|
+
unwind: () => undefined,
|
|
74
|
+
"start-gate.check": () => undefined,
|
|
75
|
+
"design.triage": (_, r) =>
|
|
76
|
+
r.write(".gtd/REQUIREMENTS.md", "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n"),
|
|
77
|
+
"design.gate.answer": () => undefined,
|
|
78
|
+
"seam.naive": (_, r) => r.write("src/calc.ts", SUBTRACT),
|
|
79
|
+
"seam.judge": () => '[{"id":"shape","answer":"refactor-first","p":0.9}]',
|
|
80
|
+
"refactor.deriving": runsItsScript,
|
|
81
|
+
"refactor.building": (_, r) => r.write("src/calc.ts", SUBTRACT),
|
|
82
|
+
"refactor.health.check": () => undefined,
|
|
83
|
+
"refactor.closing": runsItsScript,
|
|
84
|
+
"refactor.build.review.reviewing": (_, r) => r.write(".gtd/REVIEW.md", review()),
|
|
85
|
+
})
|
|
86
|
+
).state,
|
|
87
|
+
).toBe("refactor.build.review.await-review")
|
|
88
|
+
await repo.land()
|
|
89
|
+
const rest = await repo.drive({
|
|
90
|
+
"refactor.build.review.closing": runsItsScript,
|
|
91
|
+
"implement.building": () => undefined,
|
|
92
|
+
"implement.health.check": () => undefined,
|
|
93
|
+
"implement.closing": runsItsScript,
|
|
94
|
+
"build.review.reviewing": (_, r) => r.write(".gtd/REVIEW.md", review()),
|
|
95
|
+
})
|
|
96
|
+
expect(rest.state).toBe("build.review.await-review")
|
|
97
|
+
const base = (await repo.gtd("base")).stdout.trim()
|
|
98
|
+
expect(repo.show(base, "src/calc.ts")).toBe(CALC)
|
|
99
|
+
})
|
|
100
|
+
|
|
62
101
|
it("after land, feedback rests next at design.triage, then implement.building, never seam.naive", async () => {
|
|
63
102
|
const repo = naivePlanned("1")
|
|
64
103
|
const requirements = (_: unknown, r: typeof repo) =>
|
|
@@ -141,7 +180,12 @@ describe("afterTail feedback lap", () => {
|
|
|
141
180
|
prompts.length = 0
|
|
142
181
|
const repo = testRepo({
|
|
143
182
|
workflow: TAIL,
|
|
144
|
-
env: {
|
|
183
|
+
env: {
|
|
184
|
+
GTD_QUALITYREVIEWS: "",
|
|
185
|
+
GTD_REFACTORQUALITYREVIEWS: "",
|
|
186
|
+
GTD_FLAG_NAIVE_PASS: flag,
|
|
187
|
+
GTD_FLAG_RISK_TIER: "0",
|
|
188
|
+
},
|
|
145
189
|
})
|
|
146
190
|
repo.write("src/calc.ts", CALC)
|
|
147
191
|
repo.commit("feat: add calculator")
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
changes,
|
|
3
|
+
changesSince,
|
|
4
|
+
head,
|
|
5
|
+
human,
|
|
6
|
+
revertScript,
|
|
7
|
+
run,
|
|
8
|
+
slice,
|
|
9
|
+
start,
|
|
10
|
+
vars,
|
|
11
|
+
} from "../../flows/index.js"
|
|
2
12
|
import { gate } from "../shared/health.js"
|
|
3
13
|
import { shipped } from "../shared/options.js"
|
|
4
14
|
import { FEEDBACK } from "../shared/steps.js"
|
|
@@ -6,7 +16,7 @@ import { refusePendingHandOff } from "../shared/tail.js"
|
|
|
6
16
|
import * as t from "../shared/text.js"
|
|
7
17
|
import { packages } from "./packages.js"
|
|
8
18
|
import { architecture, design } from "./planning.js"
|
|
9
|
-
import { buildTail, reUnwind, type ReviewOutcome } from "./review.js"
|
|
19
|
+
import { buildTail, isCode, reUnwind, type ReviewOutcome } from "./review.js"
|
|
10
20
|
import { implement, seamBuild } from "./seam.js"
|
|
11
21
|
import { recordRisk, riskTierOn } from "./tier.js"
|
|
12
22
|
|
|
@@ -54,11 +64,13 @@ const naivePlanAndBuild = async (firstBase: string): Promise<void> => {
|
|
|
54
64
|
const tier = riskTierOn() ? recordRisk() : undefined
|
|
55
65
|
const seam = await seamBuild(firstBase)
|
|
56
66
|
await slice("feature", async () => {
|
|
57
|
-
await implement({ reference: seam.reference, tier })
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
seam.base
|
|
61
|
-
|
|
67
|
+
await implement({ reference: seam.reference, tier, afterRefactor: seam.refactored })
|
|
68
|
+
// An empty feature slice: the refactor carried the feature, so review the whole change.
|
|
69
|
+
const base =
|
|
70
|
+
seam.refactored === true && !changesSince(seam.base).some((c) => isCode(c.path))
|
|
71
|
+
? firstBase
|
|
72
|
+
: seam.base
|
|
73
|
+
await featureFeedback(await buildTail(base, { green: true, lensBase: base }), base)
|
|
62
74
|
})
|
|
63
75
|
}
|
|
64
76
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# One SDLC path per file, so a changed path reds exactly one fixture — do not merge.
|
|
2
|
+
@inmem
|
|
3
|
+
Feature: a refactor that carried the whole feature still gets a feature review
|
|
4
|
+
|
|
5
|
+
With GTD_FLAG_NAIVE_PASS on and GTD_FLAG_RISK_TIER off, a refactor-first
|
|
6
|
+
refactor can land everything the requirements ask for. The feature slice's
|
|
7
|
+
implement turn then has nothing to author: its empty turn completes the
|
|
8
|
+
step, and with no code changed since the refactor the feature review covers
|
|
9
|
+
the whole change, refactor included.
|
|
10
|
+
|
|
11
|
+
Background:
|
|
12
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
13
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
14
|
+
And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
|
|
15
|
+
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
16
|
+
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
17
|
+
And a file "src/calc.ts" with:
|
|
18
|
+
"""
|
|
19
|
+
export const add = (a: number, b: number) => a + b
|
|
20
|
+
"""
|
|
21
|
+
And the working tree is committed as "feat: add calculator"
|
|
22
|
+
And a file "TODO.md" with:
|
|
23
|
+
"""
|
|
24
|
+
# Add subtract
|
|
25
|
+
|
|
26
|
+
subtract(a, b) returns the difference.
|
|
27
|
+
"""
|
|
28
|
+
And step "unwind" changes nothing
|
|
29
|
+
And step "start-gate.check" changes nothing
|
|
30
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
31
|
+
"""
|
|
32
|
+
# Requirements
|
|
33
|
+
|
|
34
|
+
## Concerns
|
|
35
|
+
|
|
36
|
+
### Subtract
|
|
37
|
+
|
|
38
|
+
- `subtract(a, b)` returns `a - b`.
|
|
39
|
+
"""
|
|
40
|
+
And step "design.gate.answer" changes nothing
|
|
41
|
+
|
|
42
|
+
Scenario: an empty implement turn after a refactor that did it all reaches review and idle
|
|
43
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
44
|
+
"""
|
|
45
|
+
export const add = (a: number, b: number) => a + b
|
|
46
|
+
export const subtract = (a: number, b: number) => a - b
|
|
47
|
+
"""
|
|
48
|
+
And judge step "seam.judge" answers:
|
|
49
|
+
"""
|
|
50
|
+
[{"id":"shape","answer":"refactor-first","p":0.9}]
|
|
51
|
+
"""
|
|
52
|
+
And step "refactor.deriving" runs its script
|
|
53
|
+
And step "refactor.building" writes "src/calc.ts" with:
|
|
54
|
+
"""
|
|
55
|
+
export const add = (a: number, b: number) => a + b
|
|
56
|
+
export const subtract = (a: number, b: number) => a - b
|
|
57
|
+
"""
|
|
58
|
+
And step "refactor.health.check" changes nothing
|
|
59
|
+
And step "refactor.closing" runs its script
|
|
60
|
+
And step "refactor.build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
61
|
+
"""
|
|
62
|
+
# Review: abc1234
|
|
63
|
+
|
|
64
|
+
<!-- base: 0000000 -->
|
|
65
|
+
|
|
66
|
+
## calc
|
|
67
|
+
|
|
68
|
+
- [ ] ./src/calc.ts#1-2
|
|
69
|
+
subtract next to add
|
|
70
|
+
"""
|
|
71
|
+
And step "refactor.build.review.closing" runs its script
|
|
72
|
+
When the process is driven
|
|
73
|
+
Then the process rests at "refactor.build.review.await-review"
|
|
74
|
+
|
|
75
|
+
Given a file ".gtd/REVIEW.md" with:
|
|
76
|
+
"""
|
|
77
|
+
# Review: abc1234
|
|
78
|
+
|
|
79
|
+
<!-- base: 0000000 -->
|
|
80
|
+
|
|
81
|
+
## calc
|
|
82
|
+
|
|
83
|
+
- [x] ./src/calc.ts#1-2
|
|
84
|
+
subtract next to add
|
|
85
|
+
"""
|
|
86
|
+
And step "implement.building" changes nothing
|
|
87
|
+
And step "implement.health.check" changes nothing
|
|
88
|
+
And step "implement.closing" runs its script
|
|
89
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
90
|
+
"""
|
|
91
|
+
# Review: abc1234
|
|
92
|
+
|
|
93
|
+
<!-- base: 0000000 -->
|
|
94
|
+
|
|
95
|
+
## calc
|
|
96
|
+
|
|
97
|
+
- [ ] ./src/calc.ts#1-2
|
|
98
|
+
subtract next to add
|
|
99
|
+
"""
|
|
100
|
+
When gtd lands
|
|
101
|
+
And the process is driven
|
|
102
|
+
Then the process rests at "build.review.await-review"
|
|
103
|
+
And ".gtd/REQUIREMENTS.md" does not exist
|
|
104
|
+
|
|
105
|
+
Given step "build.review.closing" runs its script
|
|
106
|
+
When gtd lands
|
|
107
|
+
And the process is driven
|
|
108
|
+
Then the process rests at "idle"
|
|
109
|
+
And "src/calc.ts" contains "subtract"
|
|
110
|
+
And the commit subjects end with:
|
|
111
|
+
"""
|
|
112
|
+
gtd(human): refactor.build.review.await-review → refactor.build.review.closing
|
|
113
|
+
gtd(check): refactor.build.review.closing → implement.building
|
|
114
|
+
gtd(agent): implement.building → implement.health.check
|
|
115
|
+
gtd(check): implement.health.check → implement.closing
|
|
116
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
117
|
+
gtd(agent): build.review.reviewing → build.review.await-review
|
|
118
|
+
gtd(human): build.review.await-review → build.review.closing
|
|
119
|
+
gtd(check): build.review.closing → idle
|
|
120
|
+
"""
|
|
121
|
+
And the output lines of gtd "slices" start with:
|
|
122
|
+
"""
|
|
123
|
+
refactor
|
|
124
|
+
feature
|
|
125
|
+
"""
|
|
@@ -916,6 +916,83 @@ describe("BuildTailOptions.lensBase", () => {
|
|
|
916
916
|
expect(names).toEqual([])
|
|
917
917
|
})
|
|
918
918
|
|
|
919
|
+
const fitsNeither: [string, () => Promise<unknown>][] = [
|
|
920
|
+
["a boolean with no base", () => buildTail(true as never)],
|
|
921
|
+
["false followed by an options object", () => buildTail(false as never, {} as never)],
|
|
922
|
+
["a base followed by a boolean", () => buildTail("base", true as never)],
|
|
923
|
+
["a base followed by a string array", () => buildTail("base", ["x"] as never)],
|
|
924
|
+
[
|
|
925
|
+
"a base with a third argument",
|
|
926
|
+
() => (buildTail as (...args: unknown[]) => Promise<unknown>)("base", {}, true),
|
|
927
|
+
],
|
|
928
|
+
["a number followed by a base", () => buildTail(1 as never, "base")],
|
|
929
|
+
["a string fixFirst followed by a base", () => buildTail("x" as never, "base")],
|
|
930
|
+
["a legacy call with a non-boolean green", () => buildTail(false, "base", 1 as never)],
|
|
931
|
+
["a legacy call with non-object options", () => buildTail(false, "base", true, 1 as never)],
|
|
932
|
+
["a base followed by a Promise", () => buildTail("base", Promise.resolve({}) as never)],
|
|
933
|
+
[
|
|
934
|
+
"a base with a fifth argument",
|
|
935
|
+
() =>
|
|
936
|
+
(buildTail as (...args: unknown[]) => Promise<unknown>)(
|
|
937
|
+
"base",
|
|
938
|
+
{},
|
|
939
|
+
undefined,
|
|
940
|
+
undefined,
|
|
941
|
+
{},
|
|
942
|
+
),
|
|
943
|
+
],
|
|
944
|
+
[
|
|
945
|
+
"a legacy call with a fifth argument",
|
|
946
|
+
() => (buildTail as (...args: unknown[]) => Promise<unknown>)(false, "base", true, {}, {}),
|
|
947
|
+
],
|
|
948
|
+
]
|
|
949
|
+
for (const [label, call] of fitsNeither) {
|
|
950
|
+
it(`rejects ${label} before any step runs`, async () => {
|
|
951
|
+
const names: string[] = []
|
|
952
|
+
const rejected = expect(requestNames(call, names)).rejects
|
|
953
|
+
await rejected.toThrow("buildTail(base, options?)")
|
|
954
|
+
await rejected.toThrow("buildTail(fixFirst, base, green?, options?)")
|
|
955
|
+
expect(names).toEqual([])
|
|
956
|
+
})
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
it("reads null and trailing undefined arguments as absent", async () => {
|
|
960
|
+
const omitted = await requestNames(() => buildTail("base"))
|
|
961
|
+
expect(await requestNames(() => buildTail(null as never, "base", null as never))).toEqual(
|
|
962
|
+
omitted,
|
|
963
|
+
)
|
|
964
|
+
expect(
|
|
965
|
+
await requestNames(() =>
|
|
966
|
+
(buildTail as (...args: unknown[]) => Promise<unknown>)(
|
|
967
|
+
"base",
|
|
968
|
+
{},
|
|
969
|
+
null,
|
|
970
|
+
undefined,
|
|
971
|
+
null,
|
|
972
|
+
),
|
|
973
|
+
),
|
|
974
|
+
).toEqual(omitted)
|
|
975
|
+
expect(await requestNames(() => buildTail("base", null as never))).toEqual(omitted)
|
|
976
|
+
expect(await requestNames(() => buildTail(false, "base", false, null as never))).toEqual(
|
|
977
|
+
omitted,
|
|
978
|
+
)
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
it("accepts a class instance as options", async () => {
|
|
982
|
+
class Options {
|
|
983
|
+
green = true
|
|
984
|
+
}
|
|
985
|
+
expect(await requestNames(() => buildTail("base", new Options()))).toEqual(
|
|
986
|
+
await requestNames(() => buildTail("base", { green: true })),
|
|
987
|
+
)
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
it("accepts a null-prototype options object", async () => {
|
|
991
|
+
expect(await requestNames(() => buildTail("base", Object.create(null) as never))).toEqual(
|
|
992
|
+
await requestNames(() => buildTail("base")),
|
|
993
|
+
)
|
|
994
|
+
})
|
|
995
|
+
|
|
919
996
|
it("still runs buildTail(undefined, base) as a legacy call", async () => {
|
|
920
997
|
const names: string[] = []
|
|
921
998
|
await requestNames(() => buildTail(undefined as never, "base"), names)
|
|
@@ -158,7 +158,7 @@ const triage =
|
|
|
158
158
|
)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
|
|
161
|
+
export const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
164
|
* A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
|
|
@@ -289,34 +289,71 @@ export function buildTail(
|
|
|
289
289
|
export function buildTail(
|
|
290
290
|
first: boolean | string,
|
|
291
291
|
second?: string | BuildTailOptions,
|
|
292
|
-
|
|
293
|
-
fourth: Omit<BuildTailOptions, "fixFirst" | "green"> = {},
|
|
292
|
+
...rest: unknown[]
|
|
294
293
|
): Promise<kit.ReviewOutcome> {
|
|
294
|
+
const options = callForm(first, second, rest)
|
|
295
295
|
// Before any step runs, so a bad call never leaves a half-walked flow.
|
|
296
|
-
if (
|
|
296
|
+
if (options === undefined) {
|
|
297
297
|
return Promise.reject(
|
|
298
298
|
new Error(
|
|
299
299
|
"buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither",
|
|
300
300
|
),
|
|
301
301
|
)
|
|
302
302
|
}
|
|
303
|
-
const [base,
|
|
304
|
-
|
|
305
|
-
? [first, (second as BuildTailOptions | undefined) ?? {}]
|
|
306
|
-
: [second as string, { ...fourth, fixFirst: first, green: third }]
|
|
307
|
-
const { fixFirst, green, ...reviewOptions } = options
|
|
303
|
+
const [base, form] = options
|
|
304
|
+
const { fixFirst, green, ...reviewOptions } = form
|
|
308
305
|
return tail(
|
|
309
306
|
fixFirst ?? false,
|
|
310
307
|
(escalations) => review(base, escalations, reviewOptions),
|
|
311
308
|
green ?? false,
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
309
|
+
form.guard,
|
|
310
|
+
form.lenses,
|
|
311
|
+
form.guardRests,
|
|
312
|
+
form.lensBase,
|
|
313
|
+
form.rules,
|
|
317
314
|
)
|
|
318
315
|
}
|
|
319
316
|
|
|
317
|
+
// null reads as absent; an un-awaited Promise is a mistake, not options.
|
|
318
|
+
const isOptionalObject = (value: unknown): value is BuildTailOptions | null | undefined =>
|
|
319
|
+
value == null ||
|
|
320
|
+
(typeof value === "object" &&
|
|
321
|
+
!Array.isArray(value) &&
|
|
322
|
+
typeof (value as { then?: unknown }).then !== "function")
|
|
323
|
+
|
|
324
|
+
const isOptionalBoolean = (value: unknown): value is boolean | null | undefined =>
|
|
325
|
+
value == null || typeof value === "boolean"
|
|
326
|
+
|
|
327
|
+
const legacyForm = (
|
|
328
|
+
fixFirst: unknown,
|
|
329
|
+
base: string,
|
|
330
|
+
green: unknown,
|
|
331
|
+
options: unknown,
|
|
332
|
+
): [string, BuildTailOptions] | undefined =>
|
|
333
|
+
isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options)
|
|
334
|
+
? [base, { ...options, fixFirst: fixFirst ?? undefined, green: green ?? undefined }]
|
|
335
|
+
: undefined
|
|
336
|
+
|
|
337
|
+
const currentForm = (
|
|
338
|
+
base: string,
|
|
339
|
+
options: unknown,
|
|
340
|
+
third: unknown,
|
|
341
|
+
fourth: unknown,
|
|
342
|
+
): [string, BuildTailOptions] | undefined =>
|
|
343
|
+
isOptionalObject(options) && third == null && fourth == null ? [base, options ?? {}] : undefined
|
|
344
|
+
|
|
345
|
+
// gtd.config.ts is untyped at runtime, so every argument is checked against the form chosen.
|
|
346
|
+
const callForm = (
|
|
347
|
+
first: unknown,
|
|
348
|
+
second: unknown,
|
|
349
|
+
[third, fourth, ...extra]: readonly unknown[],
|
|
350
|
+
): [string, BuildTailOptions] | undefined => {
|
|
351
|
+
if (extra.some((value) => value != null)) return undefined
|
|
352
|
+
if (typeof second === "string") return legacyForm(first, second, third, fourth)
|
|
353
|
+
if (typeof first === "string") return currentForm(first, second, third, fourth)
|
|
354
|
+
return undefined
|
|
355
|
+
}
|
|
356
|
+
|
|
320
357
|
/**
|
|
321
358
|
* Undo the human's review-round code edits. A path changed since the human
|
|
322
359
|
* left it is not overwritten; requireRevert names it instead.
|
|
@@ -179,7 +179,7 @@ describe("seamBuild", () => {
|
|
|
179
179
|
|
|
180
180
|
it("refactor-first runs the reviewed refactor in slice refactor and resolves HEAD after it", async () => {
|
|
181
181
|
const h = await driveSeam("refactor-first")
|
|
182
|
-
expect(h.outcome).toEqual({ reference: "abc", base: "after-refactor" })
|
|
182
|
+
expect(h.outcome).toEqual({ reference: "abc", base: "after-refactor", refactored: true })
|
|
183
183
|
expect(h.slices).toEqual(["refactor"])
|
|
184
184
|
expect(h.inside.refactor![0]).toBe("refactor.deriving")
|
|
185
185
|
expect(h.inside.refactor).toContain("refactor.build.review.closing")
|
|
@@ -221,6 +221,7 @@ describe("seamBuild", () => {
|
|
|
221
221
|
})
|
|
222
222
|
|
|
223
223
|
describe("implement", () => {
|
|
224
|
+
let lastOptions: Extract<StepRequest, { kind: "agent" }>["options"] | undefined
|
|
224
225
|
const building = async (
|
|
225
226
|
reference: string | undefined,
|
|
226
227
|
tier?: "low" | "high",
|
|
@@ -228,6 +229,7 @@ describe("implement", () => {
|
|
|
228
229
|
call: () => Promise<void> = () => implement({ reference, tier, amend }),
|
|
229
230
|
) => {
|
|
230
231
|
const requests: StepRequest[] = []
|
|
232
|
+
lastOptions = undefined
|
|
231
233
|
installContext(
|
|
232
234
|
fixtureContext(
|
|
233
235
|
{},
|
|
@@ -245,9 +247,24 @@ describe("implement", () => {
|
|
|
245
247
|
await call()
|
|
246
248
|
const request = requests.find((r) => r.kind === "agent" && r.name === "building")
|
|
247
249
|
if (request?.kind !== "agent") throw new Error("no building turn")
|
|
250
|
+
lastOptions = request.options
|
|
248
251
|
return request.prompt
|
|
249
252
|
}
|
|
250
253
|
|
|
254
|
+
it("after a refactor, lets an empty turn complete the step and says so", async () => {
|
|
255
|
+
const prompt = await building("abc", undefined, undefined, () =>
|
|
256
|
+
implement({ reference: "abc", afterRefactor: true }),
|
|
257
|
+
)
|
|
258
|
+
expect(lastOptions?.allowEmpty).toBe(true)
|
|
259
|
+
expect(prompt).toMatch(/change\s+nothing and finish your turn/)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it("without a refactor, an empty turn stays an attempt", async () => {
|
|
263
|
+
const prompt = await building("abc")
|
|
264
|
+
expect(lastOptions?.allowEmpty).toBeUndefined()
|
|
265
|
+
expect(prompt).not.toMatch(/change\s+nothing/)
|
|
266
|
+
})
|
|
267
|
+
|
|
251
268
|
it("the deprecated positional form renders the same prompt as the options form", async () => {
|
|
252
269
|
const modern = await building("abc", "low")
|
|
253
270
|
const legacy = await building("abc", "low", undefined, () => implement("abc", "low"))
|
|
@@ -45,6 +45,8 @@ export interface ImplementOptions {
|
|
|
45
45
|
readonly reference?: string | undefined
|
|
46
46
|
readonly tier?: RiskTier | undefined
|
|
47
47
|
readonly amend?: boolean | undefined
|
|
48
|
+
/** After a refactor slice, which may already carry the feature: an empty turn completes the step. */
|
|
49
|
+
readonly afterRefactor?: boolean | undefined
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export function implement(options?: ImplementOptions): Promise<void>
|
|
@@ -64,11 +66,17 @@ export function implement(
|
|
|
64
66
|
return scope("implement", async () => {
|
|
65
67
|
await t.agentWithSkills(
|
|
66
68
|
"building",
|
|
67
|
-
t.implementBuildingPrompt(
|
|
69
|
+
t.implementBuildingPrompt(
|
|
70
|
+
options.reference,
|
|
71
|
+
options.tier,
|
|
72
|
+
options.amend,
|
|
73
|
+
options.afterRefactor,
|
|
74
|
+
),
|
|
68
75
|
{
|
|
69
76
|
label: "Implementing",
|
|
70
77
|
model: coder(),
|
|
71
78
|
system: t.builderSystem(),
|
|
79
|
+
...(options.afterRefactor === true ? { allowEmpty: true } : {}),
|
|
72
80
|
},
|
|
73
81
|
)
|
|
74
82
|
await healthy(() =>
|
|
@@ -124,6 +132,7 @@ export interface SeamOutcome {
|
|
|
124
132
|
readonly reference: string
|
|
125
133
|
/** The feature review's base: HEAD after the refactor on refactor-first, else the caller's base. */
|
|
126
134
|
readonly base: string
|
|
135
|
+
readonly refactored?: boolean
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
export const seamBuild = async (base: string): Promise<SeamOutcome> => {
|
|
@@ -140,6 +149,6 @@ export const seamBuild = async (base: string): Promise<SeamOutcome> => {
|
|
|
140
149
|
return { reference, base }
|
|
141
150
|
}
|
|
142
151
|
if (refactorStrays(shape).length === 0) return { reference, base }
|
|
143
|
-
await slice("refactor", () => reviewedRefactor(shape, { reference }))
|
|
144
|
-
return { reference, base: head() }
|
|
152
|
+
const refactored = await slice("refactor", () => reviewedRefactor(shape, { reference }))
|
|
153
|
+
return { reference, base: head(), refactored }
|
|
145
154
|
}
|
|
@@ -1290,17 +1290,25 @@ export const implementBuildingPrompt = (
|
|
|
1290
1290
|
reference: string | undefined,
|
|
1291
1291
|
tier?: "low" | "high",
|
|
1292
1292
|
amend?: boolean,
|
|
1293
|
+
afterRefactor?: boolean,
|
|
1293
1294
|
): string =>
|
|
1294
1295
|
`${stateFileRules}
|
|
1295
1296
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1296
1297
|
${
|
|
1297
|
-
|
|
1298
|
-
? `-
|
|
1298
|
+
afterRefactor === true
|
|
1299
|
+
? `- A reviewed refactor has just landed and may already carry part of the change,
|
|
1300
|
+
though never a release flag. Build only what is still missing, held to every
|
|
1301
|
+
rule below; if nothing is, change nothing and finish your turn
|
|
1302
|
+
`
|
|
1303
|
+
: ""
|
|
1304
|
+
}${
|
|
1305
|
+
amend === true
|
|
1306
|
+
? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
1299
1307
|
the re-derived requirements instead of rewriting it
|
|
1300
1308
|
`
|
|
1301
|
-
|
|
1309
|
+
: `- Implement them properly, fitting the codebase's seams and conventions
|
|
1302
1310
|
`
|
|
1303
|
-
}${
|
|
1311
|
+
}${
|
|
1304
1312
|
reference === undefined || amend === true
|
|
1305
1313
|
? ""
|
|
1306
1314
|
: `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|