@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "29.1.1",
3
+ "version": "29.2.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
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
- function refactorBuildingPrompt(first, strays, pattern, reference, feedback) {
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-fqf-c9dc.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-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-fqf-c9dc.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-Do11DVcw.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-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-fqf-c9dc.mjs";
4
- import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-Do11DVcw.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-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
- const base = typeof first === "string" ? first : second;
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
- return tail(options.fixFirst ?? false, (escalations) => review(base, escalations, options), options.green ?? false, options.guard, options.lenses, options.guardRests, options.lensBase, options.rules);
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
@@ -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-fqf-c9dc.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-Do11DVcw.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-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-fqf-c9dc.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-Do11DVcw.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-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-fqf-c9dc.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-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.";
@@ -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
- function refactorBuildingPrompt(first, strays, pattern, reference, feedback) {
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
- const base = typeof first === "string" ? first : second;
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
- return tail(options.fixFirst ?? false, (escalations) => review$1(base, escalations, options), options.green ?? false, options.guard, options.lenses, options.guardRests, options.lensBase, options.rules);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "29.1.1",
3
+ "version": "29.2.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
@@ -56,7 +56,7 @@ export {
56
56
  export default feature
57
57
 
58
58
  export async function hotfix() {
59
- return afterTail(await buildTail(true, start()))
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
- if ((await buildTail(false)) === "signoff") return
242
- await reUnwind()
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(false, start()))`,
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 (options: BuildTailOptions): Promise<string[]> => {
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 buildTail(false, "base", true, options).catch((error: unknown) => {
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(true, "base", false, {}).catch((error: unknown) => {
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
- /** Skip keeping green; default `false`. */
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
- const base = typeof first === "string" ? first : (second as string)
296
- const options: BuildTailOptions =
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
- ? ((second as BuildTailOptions | undefined) ?? {})
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
- options.fixFirst ?? false,
302
- (escalations) => review(base, escalations, options),
303
- options.green ?? false,
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 implement(reference, tier, amend)
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
  })
@@ -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 !== "flag_naive_pass" && name !== "flag_risk_tier")
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, undefined, true),
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
- interface RefactorBuildingArgs {
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
- }: RefactorBuildingArgs): string =>
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
  ${