@pmelab/gtd 29.1.1 → 29.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 +3 -0
- package/dist/chunks/{reserved-fqf-c9dc.mjs → reserved-BzeofpoC.mjs} +1 -10
- package/dist/chunks/{tail-Do11DVcw.mjs → tail-eS_ZSImR.mjs} +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +7 -6
- 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 +6 -14
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +13 -5
- package/src/workflows/feature/feature.test.ts +1 -1
- package/src/workflows/feature/index.ts +1 -1
- package/src/workflows/feature/review.test.ts +103 -3
- package/src/workflows/feature/review.ts +18 -10
- package/src/workflows/feature/seam.test.ts +15 -1
- package/src/workflows/index.ts +1 -1
- package/src/workflows/shared/options.test.ts +9 -3
- package/src/workflows/shared/text.test.ts +3 -3
- package/src/workflows/shared/text.ts +8 -33
package/README.md
CHANGED
|
@@ -601,6 +601,9 @@ means `high`. Each lap lands a `Gtd-Mark: risk-tier=<low|high>` trailer and a
|
|
|
601
601
|
lap's only. Its "needs a human reviewer" line for `high` is advisory, not a
|
|
602
602
|
gate.
|
|
603
603
|
|
|
604
|
+
A `gtd.config.ts` call to `buildTail` that fits neither call form is rejected;
|
|
605
|
+
see [docs/configuration.md](docs/configuration.md).
|
|
606
|
+
|
|
604
607
|
### Rolling out and cleaning up release flags
|
|
605
608
|
|
|
606
609
|
Two more bundled workflows retire a release flag. They are opt-in: the
|
|
@@ -776,16 +776,7 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
|
776
776
|
- Introduce no new flag read and add no second read of an existing one:
|
|
777
777
|
every match of \`${pattern}\` may move, never increase in number`;
|
|
778
778
|
const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
|
|
779
|
-
|
|
780
|
-
return refactorBuildingPromptOf(typeof first === "string" ? {
|
|
781
|
-
pkg: first,
|
|
782
|
-
strays: strays ?? [],
|
|
783
|
-
pattern: pattern ?? "",
|
|
784
|
-
reference,
|
|
785
|
-
feedback
|
|
786
|
-
} : first);
|
|
787
|
-
}
|
|
788
|
-
const refactorBuildingPromptOf = ({ pkg, strays, pattern, reference, feedback = false }) => `${stateFileRules}
|
|
779
|
+
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false }) => `${stateFileRules}
|
|
789
780
|
- The package to implement is \`${pkg}\`; never delete it
|
|
790
781
|
${feedback ? `- The package carries a \`## Review feedback\` section: a human's review of the
|
|
791
782
|
last refactor. Act on it, within the rules below
|
|
@@ -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-BzeofpoC.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-BzeofpoC.mjs";
|
|
3
|
+
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-eS_ZSImR.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-BzeofpoC.mjs";
|
|
4
|
+
import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-eS_ZSImR.mjs";
|
|
5
5
|
//#region src/workflows/shared/fences.ts
|
|
6
6
|
const markFenced = (text) => {
|
|
7
7
|
let fence;
|
|
@@ -476,13 +476,14 @@ const review = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
476
476
|
});
|
|
477
477
|
};
|
|
478
478
|
function buildTail(first, second, third = false, fourth = {}) {
|
|
479
|
-
|
|
480
|
-
const options = typeof first === "string" ? second ?? {} : {
|
|
479
|
+
if (typeof first !== "string" && typeof first !== "boolean" && typeof second !== "string") return Promise.reject(/* @__PURE__ */ new Error("buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither"));
|
|
480
|
+
const [base, options] = typeof first === "string" ? [first, second ?? {}] : [second, {
|
|
481
481
|
...fourth,
|
|
482
482
|
fixFirst: first,
|
|
483
483
|
green: third
|
|
484
|
-
};
|
|
485
|
-
|
|
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);
|
|
486
487
|
}
|
|
487
488
|
/**
|
|
488
489
|
* Undo the human's review-round code edits. A path changed since the human
|
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-BzeofpoC.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-eS_ZSImR.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-BzeofpoC.mjs";
|
|
3
|
+
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-eS_ZSImR.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-BzeofpoC.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
|
@@ -17560,16 +17560,7 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
|
17560
17560
|
- Introduce no new flag read and add no second read of an existing one:
|
|
17561
17561
|
every match of \`${pattern}\` may move, never increase in number`;
|
|
17562
17562
|
const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
|
|
17563
|
-
|
|
17564
|
-
return refactorBuildingPromptOf(typeof first === "string" ? {
|
|
17565
|
-
pkg: first,
|
|
17566
|
-
strays: strays ?? [],
|
|
17567
|
-
pattern: pattern ?? "",
|
|
17568
|
-
reference,
|
|
17569
|
-
feedback
|
|
17570
|
-
} : first);
|
|
17571
|
-
}
|
|
17572
|
-
const refactorBuildingPromptOf = ({ pkg, strays, pattern, reference, feedback = false }) => `${stateFileRules}
|
|
17563
|
+
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false }) => `${stateFileRules}
|
|
17573
17564
|
- The package to implement is \`${pkg}\`; never delete it
|
|
17574
17565
|
${feedback ? `- The package carries a \`## Review feedback\` section: a human's review of the
|
|
17575
17566
|
last refactor. Act on it, within the rules below
|
|
@@ -38308,13 +38299,14 @@ const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
|
38308
38299
|
});
|
|
38309
38300
|
};
|
|
38310
38301
|
function buildTail(first, second, third = false, fourth = {}) {
|
|
38311
|
-
|
|
38312
|
-
const options = typeof first === "string" ? second ?? {} : {
|
|
38302
|
+
if (typeof first !== "string" && typeof first !== "boolean" && typeof second !== "string") return Promise.reject(/* @__PURE__ */ new Error("buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither"));
|
|
38303
|
+
const [base, options] = typeof first === "string" ? [first, second ?? {}] : [second, {
|
|
38313
38304
|
...fourth,
|
|
38314
38305
|
fixFirst: first,
|
|
38315
38306
|
green: third
|
|
38316
|
-
};
|
|
38317
|
-
|
|
38307
|
+
}];
|
|
38308
|
+
const { fixFirst, green, ...reviewOptions } = options;
|
|
38309
|
+
return tail(fixFirst ?? false, (escalations) => review$1(base, escalations, reviewOptions), green ?? false, options.guard, options.lenses, options.guardRests, options.lensBase, options.rules);
|
|
38318
38310
|
}
|
|
38319
38311
|
/**
|
|
38320
38312
|
* Undo the human's review-round code edits. A path changed since the human
|
package/package.json
CHANGED
|
@@ -56,7 +56,7 @@ export {
|
|
|
56
56
|
export default feature
|
|
57
57
|
|
|
58
58
|
export async function hotfix() {
|
|
59
|
-
return afterTail(await buildTail(
|
|
59
|
+
return afterTail(await buildTail(start(), { fixFirst: true }))
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export const doors = { hotfix: { workflow: "hotfix" } }
|
|
@@ -227,9 +227,11 @@ pass (which writes `.gtd/packages/`), then builds the packages. Add a human
|
|
|
227
227
|
sign-off between the two:
|
|
228
228
|
|
|
229
229
|
```ts
|
|
230
|
-
const planAndBuild = async (): Promise<void> => {
|
|
230
|
+
const planAndBuild = async (firstBase: string): Promise<void> => {
|
|
231
|
+
let base = firstBase
|
|
232
|
+
let lap: "first" | "feedback" = "first"
|
|
231
233
|
for (;;) {
|
|
232
|
-
await design()
|
|
234
|
+
await design(base)
|
|
233
235
|
await architecture()
|
|
234
236
|
await human("approve-plan", {
|
|
235
237
|
message:
|
|
@@ -238,8 +240,14 @@ const planAndBuild = async (): Promise<void> => {
|
|
|
238
240
|
acceptClean: true,
|
|
239
241
|
})
|
|
240
242
|
await packages()
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
const outcome = await buildTail(base, {
|
|
244
|
+
green: true,
|
|
245
|
+
...(lap === "first" ? {} : { lap }),
|
|
246
|
+
})
|
|
247
|
+
if (outcome.verdict === "signoff") return
|
|
248
|
+
await reUnwind(outcome)
|
|
249
|
+
base = outcome.base
|
|
250
|
+
lap = "feedback"
|
|
243
251
|
}
|
|
244
252
|
}
|
|
245
253
|
```
|
|
@@ -131,7 +131,7 @@ describe("afterTail feedback lap", () => {
|
|
|
131
131
|
`export { feature as default, defaults, envDefaults, summary, base, steering, skills, access } from "@pmelab/gtd/workflows"`,
|
|
132
132
|
`export async function tail() {`,
|
|
133
133
|
` await gate("review-gate", "The baseline is red.")`,
|
|
134
|
-
` return afterTail(await buildTail(
|
|
134
|
+
` return afterTail(await buildTail(start()))`,
|
|
135
135
|
`}`,
|
|
136
136
|
].join("\n")
|
|
137
137
|
|
|
@@ -5,7 +5,7 @@ export * from "./planning.js"
|
|
|
5
5
|
export * from "./packages.js"
|
|
6
6
|
export * from "./refactor.js"
|
|
7
7
|
export * from "./seam.js"
|
|
8
|
-
export { buildTail, type ReviewOutcome } from "./review.js"
|
|
8
|
+
export { buildTail, type BuildTailOptions, type ReviewOutcome } from "./review.js"
|
|
9
9
|
export * from "./feature.js"
|
|
10
10
|
export * from "../shared/reserved.js"
|
|
11
11
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./review.js"
|
|
17
17
|
import type { ReviewNote } from "../../steering/index.js"
|
|
18
18
|
import * as bundled from "../index.js"
|
|
19
|
+
import type { BuildTailOptions as PublicBuildTailOptions } from "./index.js"
|
|
19
20
|
import { fixtureContext } from "../shared/text.fixture.js"
|
|
20
21
|
|
|
21
22
|
afterEach(() => installContext(undefined))
|
|
@@ -771,7 +772,10 @@ describe("severity routing", () => {
|
|
|
771
772
|
})
|
|
772
773
|
|
|
773
774
|
describe("BuildTailOptions.lensBase", () => {
|
|
774
|
-
const lensPrompts = async (
|
|
775
|
+
const lensPrompts = async (
|
|
776
|
+
options: BuildTailOptions,
|
|
777
|
+
call: () => Promise<unknown> = () => buildTail("base", { ...options, green: true }),
|
|
778
|
+
): Promise<string[]> => {
|
|
775
779
|
const prompts: string[] = []
|
|
776
780
|
installContext(
|
|
777
781
|
fixtureContext(
|
|
@@ -793,7 +797,7 @@ describe("BuildTailOptions.lensBase", () => {
|
|
|
793
797
|
},
|
|
794
798
|
),
|
|
795
799
|
)
|
|
796
|
-
await
|
|
800
|
+
await call().catch((error: unknown) => {
|
|
797
801
|
if (!(error instanceof Stop)) throw error
|
|
798
802
|
})
|
|
799
803
|
return prompts
|
|
@@ -810,8 +814,104 @@ describe("BuildTailOptions.lensBase", () => {
|
|
|
810
814
|
expect(prompts).toHaveLength(2)
|
|
811
815
|
for (const prompt of prompts) expect(prompt).toContain("from `START-SHA`")
|
|
812
816
|
})
|
|
817
|
+
|
|
818
|
+
it("the deprecated positional form runs the same tail as the options form", async () => {
|
|
819
|
+
const options = { lensBase: "SLICE-BASE" }
|
|
820
|
+
const modern = await lensPrompts(options)
|
|
821
|
+
const legacy = await lensPrompts(options, () => buildTail(false, "base", true, options))
|
|
822
|
+
expect(legacy).toEqual(modern)
|
|
823
|
+
expect(legacy).toHaveLength(2)
|
|
824
|
+
})
|
|
825
|
+
|
|
826
|
+
describe("buildTail flags", () => {
|
|
827
|
+
const requestNames = async (
|
|
828
|
+
call: () => Promise<unknown>,
|
|
829
|
+
names: string[] = [],
|
|
830
|
+
): Promise<string[]> => {
|
|
831
|
+
installContext(
|
|
832
|
+
fixtureContext(
|
|
833
|
+
{},
|
|
834
|
+
{
|
|
835
|
+
step: (request: StepRequest) => {
|
|
836
|
+
if (request.kind === "restart") return Promise.resolve()
|
|
837
|
+
names.push(`${request.kind}:${request.name}`)
|
|
838
|
+
if (names.length >= 4) throw new Stop()
|
|
839
|
+
return Promise.resolve()
|
|
840
|
+
},
|
|
841
|
+
pushScope: () => undefined,
|
|
842
|
+
popScope: () => undefined,
|
|
843
|
+
changesSince: () => {
|
|
844
|
+
throw new Stop()
|
|
845
|
+
},
|
|
846
|
+
start: () => "START-SHA",
|
|
847
|
+
},
|
|
848
|
+
),
|
|
849
|
+
)
|
|
850
|
+
await call().catch((error: unknown) => {
|
|
851
|
+
if (!(error instanceof Stop)) throw error
|
|
852
|
+
})
|
|
853
|
+
return names
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
for (const [fixFirst, green] of [
|
|
857
|
+
[true, false],
|
|
858
|
+
[true, true],
|
|
859
|
+
[false, true],
|
|
860
|
+
[false, false],
|
|
861
|
+
] as const) {
|
|
862
|
+
it(`fixFirst ${fixFirst}, green ${green} match the options form`, async () => {
|
|
863
|
+
const modern = await requestNames(() => buildTail("base", { fixFirst, green }))
|
|
864
|
+
const legacy = await requestNames(() => buildTail(fixFirst, "base", green))
|
|
865
|
+
expect(legacy).toEqual(modern)
|
|
866
|
+
})
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
it("fixFirst and green each change the request sequence", async () => {
|
|
870
|
+
const seq = (fixFirst: boolean, green: boolean) =>
|
|
871
|
+
requestNames(() => buildTail(fixFirst, "base", green))
|
|
872
|
+
const base = await seq(false, false)
|
|
873
|
+
expect(await seq(true, false)).not.toEqual(base)
|
|
874
|
+
expect(await seq(false, true)).not.toEqual(base)
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
for (const bad of [undefined, null, 1, {}]) {
|
|
878
|
+
it(`rejects naming both forms for sole first argument ${String(bad)}`, async () => {
|
|
879
|
+
const names: string[] = []
|
|
880
|
+
const rejected = expect(requestNames(() => buildTail(bad as never), names)).rejects
|
|
881
|
+
await rejected.toThrow("buildTail(base, options?)")
|
|
882
|
+
await rejected.toThrow("buildTail(fixFirst, base, green?, options?)")
|
|
883
|
+
expect(names).toEqual([])
|
|
884
|
+
})
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
it("rejects an undefined first argument followed by an options object", async () => {
|
|
888
|
+
const names: string[] = []
|
|
889
|
+
const rejected = expect(
|
|
890
|
+
requestNames(() => buildTail(undefined as never, { green: true } as never), names),
|
|
891
|
+
).rejects
|
|
892
|
+
await rejected.toThrow("buildTail(base, options?)")
|
|
893
|
+
await rejected.toThrow("buildTail(fixFirst, base, green?, options?)")
|
|
894
|
+
expect(names).toEqual([])
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
it("still runs buildTail(undefined, base) as a legacy call", async () => {
|
|
898
|
+
const names: string[] = []
|
|
899
|
+
await requestNames(() => buildTail(undefined as never, "base"), names)
|
|
900
|
+
expect(names).not.toEqual([])
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
// pins `| undefined` on BuildTailOptions under exactOptionalPropertyTypes (typecheck)
|
|
904
|
+
it("explicitly undefined fixFirst and green default like omitted ones", async () => {
|
|
905
|
+
expect(
|
|
906
|
+
await requestNames(() => buildTail("base", { fixFirst: undefined, green: undefined })),
|
|
907
|
+
).toEqual(await requestNames(() => buildTail("base")))
|
|
908
|
+
})
|
|
909
|
+
})
|
|
813
910
|
})
|
|
814
911
|
|
|
912
|
+
// pins the public re-exports from the feature entry point and the published workflows entry
|
|
913
|
+
export type _PublicBuildTailOptions = [PublicBuildTailOptions, bundled.BuildTailOptions]
|
|
914
|
+
|
|
815
915
|
describe("buildTail without rules", () => {
|
|
816
916
|
const RULES = "Change only these paths"
|
|
817
917
|
|
|
@@ -850,7 +950,7 @@ describe("buildTail without rules", () => {
|
|
|
850
950
|
},
|
|
851
951
|
),
|
|
852
952
|
)
|
|
853
|
-
await buildTail(
|
|
953
|
+
await buildTail("base", { fixFirst: true }).catch((error: unknown) => {
|
|
854
954
|
if (!(error instanceof Stop)) throw error
|
|
855
955
|
})
|
|
856
956
|
return prompts
|
|
@@ -257,9 +257,9 @@ export interface FeedbackTarget {
|
|
|
257
257
|
|
|
258
258
|
export interface BuildTailOptions {
|
|
259
259
|
/** Fix first when entered red; default `false`. */
|
|
260
|
-
readonly fixFirst?: boolean
|
|
261
|
-
/**
|
|
262
|
-
readonly green?: boolean
|
|
260
|
+
readonly fixFirst?: boolean | undefined
|
|
261
|
+
/** Asserts the tree is already green, so the tail skips keep-green; default `false`. */
|
|
262
|
+
readonly green?: boolean | undefined
|
|
263
263
|
/** Runs after every code-changing turn: fix-check, quality fix, risk fix, nit fix, escalation describe. */
|
|
264
264
|
readonly guard?: Guard
|
|
265
265
|
/** `false`: `guard` skips the escalation's human rests. */
|
|
@@ -292,15 +292,23 @@ export function buildTail(
|
|
|
292
292
|
third = false,
|
|
293
293
|
fourth: Omit<BuildTailOptions, "fixFirst" | "green"> = {},
|
|
294
294
|
): Promise<kit.ReviewOutcome> {
|
|
295
|
-
|
|
296
|
-
|
|
295
|
+
// Before any step runs, so a bad call never leaves a half-walked flow.
|
|
296
|
+
if (typeof first !== "string" && typeof first !== "boolean" && typeof second !== "string") {
|
|
297
|
+
return Promise.reject(
|
|
298
|
+
new Error(
|
|
299
|
+
"buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither",
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
const [base, options]: [string, BuildTailOptions] =
|
|
297
304
|
typeof first === "string"
|
|
298
|
-
? (
|
|
299
|
-
: { ...fourth, fixFirst: first, green: third }
|
|
305
|
+
? [first, (second as BuildTailOptions | undefined) ?? {}]
|
|
306
|
+
: [second as string, { ...fourth, fixFirst: first, green: third }]
|
|
307
|
+
const { fixFirst, green, ...reviewOptions } = options
|
|
300
308
|
return tail(
|
|
301
|
-
|
|
302
|
-
(escalations) => review(base, escalations,
|
|
303
|
-
|
|
309
|
+
fixFirst ?? false,
|
|
310
|
+
(escalations) => review(base, escalations, reviewOptions),
|
|
311
|
+
green ?? false,
|
|
304
312
|
options.guard,
|
|
305
313
|
options.lenses,
|
|
306
314
|
options.guardRests,
|
|
@@ -225,6 +225,7 @@ describe("implement", () => {
|
|
|
225
225
|
reference: string | undefined,
|
|
226
226
|
tier?: "low" | "high",
|
|
227
227
|
amend?: boolean,
|
|
228
|
+
call: () => Promise<void> = () => implement({ reference, tier, amend }),
|
|
228
229
|
) => {
|
|
229
230
|
const requests: StepRequest[] = []
|
|
230
231
|
installContext(
|
|
@@ -241,12 +242,25 @@ describe("implement", () => {
|
|
|
241
242
|
},
|
|
242
243
|
),
|
|
243
244
|
)
|
|
244
|
-
await
|
|
245
|
+
await call()
|
|
245
246
|
const request = requests.find((r) => r.kind === "agent" && r.name === "building")
|
|
246
247
|
if (request?.kind !== "agent") throw new Error("no building turn")
|
|
247
248
|
return request.prompt
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
it("the deprecated positional form renders the same prompt as the options form", async () => {
|
|
252
|
+
const modern = await building("abc", "low")
|
|
253
|
+
const legacy = await building("abc", "low", undefined, () => implement("abc", "low"))
|
|
254
|
+
expect(legacy).toBe(modern)
|
|
255
|
+
expect(legacy).toContain("git show abc")
|
|
256
|
+
const amended = await building(undefined, undefined, true)
|
|
257
|
+
expect(
|
|
258
|
+
await building(undefined, undefined, undefined, () => implement(undefined, undefined, true)),
|
|
259
|
+
).toBe(amended)
|
|
260
|
+
const bare = await building(undefined, undefined, undefined, () => implement())
|
|
261
|
+
expect(bare).toBe(await building(undefined))
|
|
262
|
+
})
|
|
263
|
+
|
|
250
264
|
it("names the reference with a git show line", async () => {
|
|
251
265
|
expect(await building("abc")).toContain("git show abc")
|
|
252
266
|
})
|
package/src/workflows/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ export * from "./feature/planning.js"
|
|
|
24
24
|
export * from "./feature/packages.js"
|
|
25
25
|
export * from "./feature/refactor.js"
|
|
26
26
|
export * from "./feature/seam.js"
|
|
27
|
-
export { buildTail, type ReviewOutcome } from "./feature/review.js"
|
|
27
|
+
export { buildTail, type BuildTailOptions, type ReviewOutcome } from "./feature/review.js"
|
|
28
28
|
export * from "./feature/feature.js"
|
|
29
29
|
export { base, review, reviewFlow } from "./review/review.js"
|
|
30
30
|
export { fix, fixFlow, fixReviewNotes } from "./fix/fix.js"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { FLAGS, type FlagDeclaration } from "../../flags/index.js"
|
|
2
3
|
import { configure, type WorkflowDoors } from "../../flows/index.js"
|
|
3
4
|
import * as workflows from "../index.js"
|
|
4
5
|
import * as options from "./options.js"
|
|
@@ -10,13 +11,19 @@ describe("the shipped workflow factories", () => {
|
|
|
10
11
|
it("take exactly the settings the shipped workflows declare as options", () => {
|
|
11
12
|
expect(Object.keys(ShippedOptions.fields).sort()).toEqual(
|
|
12
13
|
[...Object.keys(defaults), ...Object.keys(envDefaults)]
|
|
13
|
-
.filter((name) => name
|
|
14
|
+
.filter((name) => !name.startsWith("flag_"))
|
|
14
15
|
.sort(),
|
|
15
16
|
)
|
|
16
17
|
})
|
|
17
18
|
|
|
19
|
+
it("declare a flag_<name> default for exactly the registered flags", () => {
|
|
20
|
+
const declared = Object.keys(defaults).filter((name) => name.startsWith("flag_"))
|
|
21
|
+
expect(declared.sort()).toEqual(FLAGS.map(({ name }) => `flag_${name}`).sort())
|
|
22
|
+
for (const { name, defaultOn } of FLAGS as readonly FlagDeclaration[])
|
|
23
|
+
expect(defaults).toHaveProperty(`flag_${name}`, defaultOn ? "1" : "0")
|
|
24
|
+
})
|
|
25
|
+
|
|
18
26
|
it("declare seamAllow and flag_naive_pass defaults; flag_naive_pass is no option", () => {
|
|
19
|
-
expect(defaults.flag_naive_pass).toBe("0")
|
|
20
27
|
expect(Object.keys(ShippedOptions.fields)).not.toContain("flag_naive_pass")
|
|
21
28
|
expect(configure(feature({ seamAllow: "docs/**" }))).toMatchObject({
|
|
22
29
|
ok: true,
|
|
@@ -25,7 +32,6 @@ describe("the shipped workflow factories", () => {
|
|
|
25
32
|
})
|
|
26
33
|
|
|
27
34
|
it("declare flag_risk_tier off by default; it is no factory option", () => {
|
|
28
|
-
expect(defaults.flag_risk_tier).toBe("0")
|
|
29
35
|
expect(Object.keys(ShippedOptions.fields)).not.toContain("flag_risk_tier")
|
|
30
36
|
})
|
|
31
37
|
|
|
@@ -1063,7 +1063,7 @@ describe("the refactor prompts", () => {
|
|
|
1063
1063
|
|
|
1064
1064
|
it("building and fix rules forbid every test file, with no stray exception", () => {
|
|
1065
1065
|
for (const prompt of renderText(() => [
|
|
1066
|
-
refactorBuildingPrompt(".gtd/REFACTOR.md", strays, pattern),
|
|
1066
|
+
refactorBuildingPrompt({ pkg: ".gtd/REFACTOR.md", strays, pattern }),
|
|
1067
1067
|
refactorFixPrompt(strays, pattern),
|
|
1068
1068
|
])) {
|
|
1069
1069
|
expect(prompt).toContain("never a test file")
|
|
@@ -1073,8 +1073,8 @@ describe("the refactor prompts", () => {
|
|
|
1073
1073
|
|
|
1074
1074
|
it("building adds the act-on-review-feedback line only when asked", () => {
|
|
1075
1075
|
const [plain, feedback] = renderText(() => [
|
|
1076
|
-
refactorBuildingPrompt(".gtd/REFACTOR.md", strays, pattern),
|
|
1077
|
-
refactorBuildingPrompt(".gtd/REFACTOR.md", strays, pattern,
|
|
1076
|
+
refactorBuildingPrompt({ pkg: ".gtd/REFACTOR.md", strays, pattern }),
|
|
1077
|
+
refactorBuildingPrompt({ pkg: ".gtd/REFACTOR.md", strays, pattern, feedback: true }),
|
|
1078
1078
|
])
|
|
1079
1079
|
expect(plain).not.toContain("## Review feedback")
|
|
1080
1080
|
expect(feedback).toContain("## Review feedback")
|
|
@@ -473,44 +473,19 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
|
473
473
|
|
|
474
474
|
const withRules = (rules?: string): string => (rules === undefined ? "" : `${rules}\n`)
|
|
475
475
|
|
|
476
|
-
|
|
477
|
-
pkg: string
|
|
478
|
-
strays: readonly string[]
|
|
479
|
-
pattern: string
|
|
480
|
-
reference?: string | undefined
|
|
481
|
-
feedback?: boolean | undefined
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
export function refactorBuildingPrompt(args: RefactorBuildingArgs): string
|
|
485
|
-
/** @deprecated Pass `refactorBuildingPrompt({ pkg, strays, pattern, reference, feedback })`. */
|
|
486
|
-
export function refactorBuildingPrompt(
|
|
487
|
-
pkg: string,
|
|
488
|
-
strays: readonly string[],
|
|
489
|
-
pattern: string,
|
|
490
|
-
reference?: string,
|
|
491
|
-
feedback?: boolean,
|
|
492
|
-
): string
|
|
493
|
-
export function refactorBuildingPrompt(
|
|
494
|
-
first: string | RefactorBuildingArgs,
|
|
495
|
-
strays?: readonly string[],
|
|
496
|
-
pattern?: string,
|
|
497
|
-
reference?: string,
|
|
498
|
-
feedback?: boolean,
|
|
499
|
-
): string {
|
|
500
|
-
return refactorBuildingPromptOf(
|
|
501
|
-
typeof first === "string"
|
|
502
|
-
? { pkg: first, strays: strays ?? [], pattern: pattern ?? "", reference, feedback }
|
|
503
|
-
: first,
|
|
504
|
-
)
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const refactorBuildingPromptOf = ({
|
|
476
|
+
export const refactorBuildingPrompt = ({
|
|
508
477
|
pkg,
|
|
509
478
|
strays,
|
|
510
479
|
pattern,
|
|
511
480
|
reference,
|
|
512
481
|
feedback = false,
|
|
513
|
-
}:
|
|
482
|
+
}: {
|
|
483
|
+
pkg: string
|
|
484
|
+
strays: readonly string[]
|
|
485
|
+
pattern: string
|
|
486
|
+
reference?: string | undefined
|
|
487
|
+
feedback?: boolean | undefined
|
|
488
|
+
}): string =>
|
|
514
489
|
`${stateFileRules}
|
|
515
490
|
- The package to implement is \`${pkg}\`; never delete it
|
|
516
491
|
${
|