@pmelab/gtd 29.1.0 → 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.
@@ -256,6 +256,10 @@ export interface FeedbackTarget {
256
256
  }
257
257
 
258
258
  export interface BuildTailOptions {
259
+ /** Fix first when entered red; default `false`. */
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
259
263
  /** Runs after every code-changing turn: fix-check, quality fix, risk fix, nit fix, escalation describe. */
260
264
  readonly guard?: Guard
261
265
  /** `false`: `guard` skips the escalation's human rests. */
@@ -274,22 +278,44 @@ export interface BuildTailOptions {
274
278
  }
275
279
 
276
280
  /** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
277
- export const buildTail = (
281
+ export function buildTail(base: string, options?: BuildTailOptions): Promise<kit.ReviewOutcome>
282
+ /** @deprecated Pass `buildTail(base, { fixFirst, green, ...options })`. */
283
+ export function buildTail(
278
284
  fixFirst: boolean,
279
285
  base: string,
280
- green = false,
281
- options: BuildTailOptions = {},
282
- ): Promise<kit.ReviewOutcome> =>
283
- tail(
284
- fixFirst,
285
- (escalations) => review(base, escalations, options),
286
- green,
286
+ green?: boolean,
287
+ options?: Omit<BuildTailOptions, "fixFirst" | "green">,
288
+ ): Promise<kit.ReviewOutcome>
289
+ export function buildTail(
290
+ first: boolean | string,
291
+ second?: string | BuildTailOptions,
292
+ third = false,
293
+ fourth: Omit<BuildTailOptions, "fixFirst" | "green"> = {},
294
+ ): Promise<kit.ReviewOutcome> {
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] =
304
+ typeof first === "string"
305
+ ? [first, (second as BuildTailOptions | undefined) ?? {}]
306
+ : [second as string, { ...fourth, fixFirst: first, green: third }]
307
+ const { fixFirst, green, ...reviewOptions } = options
308
+ return tail(
309
+ fixFirst ?? false,
310
+ (escalations) => review(base, escalations, reviewOptions),
311
+ green ?? false,
287
312
  options.guard,
288
313
  options.lenses,
289
314
  options.guardRests,
290
315
  options.lensBase,
291
316
  options.rules,
292
317
  )
318
+ }
293
319
 
294
320
  /**
295
321
  * Undo the human's review-round code edits. A path changed since the human
@@ -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
  })
@@ -41,17 +41,36 @@ export const naivePass = (rejection?: t.SeamRejection): Promise<void> =>
41
41
  system: t.builderSystem(),
42
42
  })
43
43
 
44
- export const implement = (
44
+ export interface ImplementOptions {
45
+ readonly reference?: string | undefined
46
+ readonly tier?: RiskTier | undefined
47
+ readonly amend?: boolean | undefined
48
+ }
49
+
50
+ export function implement(options?: ImplementOptions): Promise<void>
51
+ /** @deprecated Pass `implement({ reference, tier, amend })`. */
52
+ export function implement(
45
53
  reference: string | undefined,
46
54
  tier?: RiskTier,
47
55
  amend?: boolean,
48
- ): Promise<void> =>
49
- scope("implement", async () => {
50
- await t.agentWithSkills("building", t.implementBuildingPrompt(reference, tier, amend), {
51
- label: "Implementing",
52
- model: coder(),
53
- system: t.builderSystem(),
54
- })
56
+ ): Promise<void>
57
+ export function implement(
58
+ first?: string | ImplementOptions,
59
+ tier?: RiskTier,
60
+ amend?: boolean,
61
+ ): Promise<void> {
62
+ const options: ImplementOptions =
63
+ typeof first === "object" && first !== null ? first : { reference: first, tier, amend }
64
+ return scope("implement", async () => {
65
+ await t.agentWithSkills(
66
+ "building",
67
+ t.implementBuildingPrompt(options.reference, options.tier, options.amend),
68
+ {
69
+ label: "Implementing",
70
+ model: coder(),
71
+ system: t.builderSystem(),
72
+ },
73
+ )
55
74
  await healthy(() =>
56
75
  t.agentWithSkills("fix.suite.fixing", t.implementFixPrompt(), {
57
76
  label: "Fixing the check",
@@ -62,6 +81,7 @@ export const implement = (
62
81
  )
63
82
  await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" })
64
83
  })
84
+ }
65
85
 
66
86
  interface Decision {
67
87
  readonly reference: string
@@ -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
 
@@ -9,6 +9,8 @@ import {
9
9
  buildReviewReviewingPrompt,
10
10
  agentWithSkills,
11
11
  architectureAuthorPrompt,
12
+ architectureDecomposePrompt,
13
+ packagesItemBuildingPrompt,
12
14
  architectureGateAnswerMessage,
13
15
  buildFixPrompt,
14
16
  buildFixQualityPrompt,
@@ -1061,7 +1063,7 @@ describe("the refactor prompts", () => {
1061
1063
 
1062
1064
  it("building and fix rules forbid every test file, with no stray exception", () => {
1063
1065
  for (const prompt of renderText(() => [
1064
- refactorBuildingPrompt(".gtd/REFACTOR.md", strays, pattern),
1066
+ refactorBuildingPrompt({ pkg: ".gtd/REFACTOR.md", strays, pattern }),
1065
1067
  refactorFixPrompt(strays, pattern),
1066
1068
  ])) {
1067
1069
  expect(prompt).toContain("never a test file")
@@ -1071,8 +1073,8 @@ describe("the refactor prompts", () => {
1071
1073
 
1072
1074
  it("building adds the act-on-review-feedback line only when asked", () => {
1073
1075
  const [plain, feedback] = renderText(() => [
1074
- refactorBuildingPrompt(".gtd/REFACTOR.md", strays, pattern),
1075
- 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 }),
1076
1078
  ])
1077
1079
  expect(plain).not.toContain("## Review feedback")
1078
1080
  expect(feedback).toContain("## Review feedback")
@@ -1266,3 +1268,45 @@ describe("buildReviewReviewingPrompt lap wording", () => {
1266
1268
  },
1267
1269
  )
1268
1270
  })
1271
+
1272
+ describe("architectureDecomposePrompt tests rule", () => {
1273
+ it("states one rule: write-or-change, relied-on marked (unchanged), no other untouched", () => {
1274
+ const prompt = renderText(() => architectureDecomposePrompt()).replace(/\s+/g, " ")
1275
+ expect(prompt).toContain("List every test the package writes or changes")
1276
+ expect(prompt).toContain("also list an existing test a criterion relies on")
1277
+ expect(prompt).toContain("list no other untouched test")
1278
+ expect(prompt).toContain("- unit: \\`<path>\\` (unchanged)")
1279
+ expect(prompt).toContain("- e2e: \\`<path>\\` (unchanged)")
1280
+ expect(prompt).toContain("only decompose places the marker")
1281
+ })
1282
+ })
1283
+
1284
+ describe("packagesItemBuildingPrompt", () => {
1285
+ const prompt = renderText(() => packagesItemBuildingPrompt(".gtd/packages/01-a.md")).replace(
1286
+ /\s+/g,
1287
+ " ",
1288
+ )
1289
+ it("carries the evidence kinds and the already-done check", () => {
1290
+ for (const w of [
1291
+ "evidence per criterion",
1292
+ "commit, file, symbol, issue URL, or command output — and change nothing else",
1293
+ "it was not already done",
1294
+ "via `gh`",
1295
+ ])
1296
+ expect(prompt).toContain(w)
1297
+ })
1298
+ it("states the already-met, no-tests out-of-tree and implement-normally rules flat", () => {
1299
+ expect(prompt).toContain("Every criterion already met in the tree")
1300
+ expect(prompt).toContain("No `## Tests` and work outside the tree")
1301
+ expect(prompt).toContain(
1302
+ "Any other package**: implement normally, write no `.gtd/SATISFIED.md`",
1303
+ )
1304
+ })
1305
+ it("says an (unchanged) line must exist and stay untouched", () => {
1306
+ expect(prompt).toContain("(unchanged)")
1307
+ expect(prompt).toContain("stay untouched")
1308
+ expect(prompt).toContain("creating, editing or deleting it refuses the turn")
1309
+ expect(prompt).toContain("must exist before the turn")
1310
+ expect(prompt).toContain("stop without creating it")
1311
+ })
1312
+ })
@@ -376,7 +376,12 @@ ${stateFileRules}
376
376
  - Every package with tests has a \`## Tests\` section, one line per
377
377
  declared test, copied from the architecture entries:
378
378
  \`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
379
- none
379
+ none. List every test the package writes or changes; also list an
380
+ existing test a criterion relies on, marked \`(unchanged)\` —
381
+ \`- unit: \\\`<path>\\\` (unchanged)\` or
382
+ \`- e2e: \\\`<path>\\\` (unchanged)\`; list no other untouched test;
383
+ never mark a file the package also writes or changes — list it once,
384
+ unmarked; only decompose places the marker
380
385
  - Leave everything uncommitted and finish
381
386
  `
382
387
 
@@ -413,15 +418,26 @@ export const packagesItemBuildingPrompt = (pkg: string): string =>
413
418
  never delete the package file (the declared-tests guard reads it
414
419
  after you)
415
420
  - The package to implement is \`${pkg}\`
416
- - First check its acceptance criteria against the current tree —
417
- an earlier fix turn may already satisfy them. If **every**
418
- criterion is met, implement nothing: write \`.gtd/SATISFIED.md\`
419
- with each criterion's concrete evidence (commit, file, or
420
- symbol), change nothing else, and finish. Otherwise implement
421
- normally and skip that file
421
+ - First check the acceptance criteria against the current tree —
422
+ an earlier fix turn may already meet them
423
+ - **Every criterion already met in the tree**: write
424
+ \`.gtd/SATISFIED.md\` with evidence per criterion — commit, file,
425
+ symbol, issue URL, or command output — and change nothing else
426
+ - **No \`## Tests\` and work outside the tree**: first check it was
427
+ not already done by an earlier attempt (e.g. an existing issue
428
+ with the same title via \`gh\`) and reuse that as evidence, or do
429
+ the work; then write \`.gtd/SATISFIED.md\` with that evidence and
430
+ change nothing else in the tree
431
+ - **Any other package**: implement normally, write no
432
+ \`.gtd/SATISFIED.md\`
422
433
  - Write the tests the package declares under \`## Tests\` first,
423
434
  then the code that makes them pass; the turn is refused unless
424
- every declared test is in your diff
435
+ every declared test is in your diff — except a line marked
436
+ \`(unchanged)\`, which must exist before the turn and stay
437
+ untouched — creating, editing or deleting it refuses the turn. If
438
+ it does not exist yet, or the marker is wrong because the package
439
+ must change that test, stop without creating it and leave it to
440
+ a person
425
441
  - A declared e2e test is a scenario in the repository's own e2e
426
442
  framework for the behaviour its \`## E2E Scenarios\` entry names.
427
443
  Word it to fit the existing steps — reuse and generalise them
@@ -457,13 +473,19 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
457
473
 
458
474
  const withRules = (rules?: string): string => (rules === undefined ? "" : `${rules}\n`)
459
475
 
460
- export const refactorBuildingPrompt = (
461
- pkg: string,
462
- strays: readonly string[],
463
- pattern: string,
464
- reference?: string,
476
+ export const refactorBuildingPrompt = ({
477
+ pkg,
478
+ strays,
479
+ pattern,
480
+ reference,
465
481
  feedback = false,
466
- ): string =>
482
+ }: {
483
+ pkg: string
484
+ strays: readonly string[]
485
+ pattern: string
486
+ reference?: string | undefined
487
+ feedback?: boolean | undefined
488
+ }): string =>
467
489
  `${stateFileRules}
468
490
  - The package to implement is \`${pkg}\`; never delete it
469
491
  ${