@pmelab/gtd 55.0.0 → 56.0.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.
Files changed (61) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +60 -61
  3. package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-B8EC-WE8.mjs} +77 -203
  4. package/dist/chunks/{review-BJGKwlYv.mjs → review-HdrX0FJ7.mjs} +30 -125
  5. package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
  6. package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-BSLah2mA.mjs} +144 -56
  7. package/dist/gtd.bundle.mjs +259 -22
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +6 -6
  10. package/dist/web/main.css +1 -1
  11. package/dist/web/main.js +551 -66
  12. package/dist/workflow-kit.mjs +1 -1
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +14 -16
  15. package/dist/workflows/fix.mjs +3 -3
  16. package/dist/workflows/review.mjs +3 -3
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +522 -453
  19. package/package.json +1 -1
  20. package/skills/authoring/SKILL.md +3 -3
  21. package/src/cli/Cli.ts +5 -5
  22. package/src/edge/Edge.test.ts +1 -1
  23. package/src/steering/FORMATS.md +5 -1
  24. package/src/steering/SteeringFormats.test.ts +2 -2
  25. package/src/steering/contract.test.ts +1 -0
  26. package/src/steering/findings.test.ts +47 -0
  27. package/src/steering/findings.ts +350 -0
  28. package/src/steering/fixtures/findings/gate.md +34 -0
  29. package/src/steering/fixtures/findings/gate.read.json +87 -0
  30. package/src/steering/index.test.ts +2 -2
  31. package/src/steering/index.ts +4 -3
  32. package/src/steering/review.test.ts +1 -75
  33. package/src/steering/review.ts +1 -74
  34. package/src/workflow/compile.test.ts +1 -0
  35. package/src/workflows/feature/criteria-coverage.feature +1 -6
  36. package/src/workflows/feature/feature.test.ts +1 -1
  37. package/src/workflows/feature/feature.ts +12 -20
  38. package/src/workflows/feature/lens-review.feature +323 -171
  39. package/src/workflows/feature/refactor-absorbs.feature +1 -7
  40. package/src/workflows/feature/refactor-first.feature +2 -10
  41. package/src/workflows/feature/refactor-rebuild.feature +4 -24
  42. package/src/workflows/feature/refactor-review.feature +33 -117
  43. package/src/workflows/feature/refactor.test.ts +0 -2
  44. package/src/workflows/feature/refactor.ts +0 -1
  45. package/src/workflows/feature/review-gate.feature +6 -22
  46. package/src/workflows/feature/reviewer-walkthrough.feature +16 -103
  47. package/src/workflows/feature/standards.feature +75 -34
  48. package/src/workflows/fix/fix-lens-origin.feature +20 -7
  49. package/src/workflows/fix/fix-quality.feature +102 -23
  50. package/src/workflows/review/review-door.feature +62 -59
  51. package/src/workflows/shared/learn.test.ts +0 -14
  52. package/src/workflows/shared/learn.ts +1 -6
  53. package/src/workflows/shared/lenses.ts +149 -43
  54. package/src/workflows/shared/review.test.ts +27 -26
  55. package/src/workflows/shared/review.ts +29 -196
  56. package/src/workflows/shared/tail.ts +18 -9
  57. package/src/workflows/shared/text.test.ts +103 -288
  58. package/src/workflows/shared/text.ts +131 -329
  59. package/src/workflows/feature/review-answers.feature +0 -324
  60. package/src/workflows/feature/severity-budget.feature +0 -116
  61. package/src/workflows/feature/severity-sorted-review.feature +0 -198
@@ -2118,7 +2118,7 @@ describe("answer boxes", async () => {
2118
2118
  })
2119
2119
 
2120
2120
  describe("review — sectioned layout", async () => {
2121
- const { reviewChunks, reviewDecisions, reviewSections, sectionedReview, unansweredNotes } =
2121
+ const { reviewChunks, reviewDecisions, reviewSections, unansweredNotes } =
2122
2122
  await import("./review.js")
2123
2123
  const boxes = (ticked?: 0 | 1 | 2): string[] => [
2124
2124
  "Tick exactly one:",
@@ -2230,78 +2230,4 @@ describe("review — sectioned layout", async () => {
2230
2230
  .map((a) => a.title)
2231
2231
  expect(titles.some((t) => t.includes("A finding"))).toBe(false)
2232
2232
  })
2233
-
2234
- describe("sectionedReview", () => {
2235
- const flat = [
2236
- ...head,
2237
- "## Weakened scenarios",
2238
- "",
2239
- "- a.feature — s — Then x",
2240
- "",
2241
- "## A finding",
2242
- "",
2243
- "_should: contract_",
2244
- "",
2245
- "Leaks a token.",
2246
- "",
2247
- ...boxes(),
2248
- "",
2249
- "Wiring",
2250
- "------",
2251
- "",
2252
- "### Detail",
2253
- "",
2254
- "- [ ] ./src/b.ts#3-4 — wires it",
2255
- "",
2256
- "## Fixed automatically",
2257
- "",
2258
- "- f (fedcba9)",
2259
- "",
2260
- ].join("\n")
2261
-
2262
- it("moves boxed chunks under Findings and the named ones under Walkthrough, a level deeper", () => {
2263
- expect(sectionedReview(flat, new Set(["Wiring"]))).toBe(
2264
- [
2265
- ...head,
2266
- "## Weakened scenarios",
2267
- "",
2268
- "- a.feature — s — Then x",
2269
- "",
2270
- "## Findings",
2271
- "",
2272
- "### A finding",
2273
- "",
2274
- "_should: contract_",
2275
- "",
2276
- "Leaks a token.",
2277
- "",
2278
- ...boxes(),
2279
- "",
2280
- "## Walkthrough",
2281
- "",
2282
- "### Wiring",
2283
- "",
2284
- "#### Detail",
2285
- "",
2286
- "- [ ] ./src/b.ts#3-4 — wires it",
2287
- "",
2288
- "## Fixed automatically",
2289
- "",
2290
- "- f (fedcba9)",
2291
- "",
2292
- ].join("\n"),
2293
- )
2294
- })
2295
-
2296
- it("is valid and stable once sectioned", () => {
2297
- const once = sectionedReview(flat, new Set(["Wiring"]))
2298
- expect(review.validate(once)).toEqual([])
2299
- expect(sectionedReview(once, new Set(["Wiring"]))).toBe(once)
2300
- })
2301
-
2302
- it("leaves a review with nothing to group byte for byte", () => {
2303
- const plain = [...head, "## Nothing to note", "", "Review the diff yourself.", ""].join("\n")
2304
- expect(sectionedReview(plain, new Set())).toBe(plain)
2305
- })
2306
- })
2307
2233
  })
@@ -96,7 +96,7 @@ export const ANSWER_WONT_FIX = "won't fix"
96
96
 
97
97
  /** A chunk's leading `_critical: security_` paragraph: the tier gtd ranked it with, and why. */
98
98
  const SEVERITY_LINE_RE = /^_((?:critical|should|could)(?::[^_\n]*)?)_$/
99
- export const severityLine = (label: string): string => `_${label}_`
99
+ const severityLine = (label: string): string => `_${label}_`
100
100
 
101
101
  interface ReviewDoc {
102
102
  readonly shortHash?: string
@@ -1591,76 +1591,3 @@ export const reviewSections = (
1591
1591
  section: s.section,
1592
1592
  line: s.headingLine,
1593
1593
  }))
1594
-
1595
- const isBlankLine = (line: string | undefined): boolean => line !== undefined && line.trim() === ""
1596
-
1597
- /** How many lines a chunk's heading takes: two for a setext heading and its underline. */
1598
- const headingSize = ({ heading, headingLine }: ChunkBlock): number =>
1599
- toLspPosition(heading.position!.end).line - headingLine + 1
1600
-
1601
- /** \`block\` with its heading an ATX \`### \` on one line, and every heading in its body one level deeper. */
1602
- const demoted = (chunk: ChunkBlock, block: readonly string[]): string[] => {
1603
- const out = [...block]
1604
- for (const node of chunk.body) {
1605
- if (node.type !== "heading" || node.depth < 3 || node.depth > 5) continue
1606
- const at = toLspPosition(node.position!.start).line - chunk.headingLine
1607
- out[at] = out[at]!.replace(/^( {0,3})#/, "$1##")
1608
- }
1609
- const size = headingSize(chunk)
1610
- const title = size > 1 ? `### ${out[0]!.trim()}` : out[0]!.replace(/^ {0,3}##/, "###")
1611
- return [title, ...out.slice(size)]
1612
- }
1613
-
1614
- /** A chunk's \`size\` heading lines, then its body with the blank lines around it trimmed. */
1615
- const chunkText = (block: readonly string[], size: number): string => {
1616
- const rest = block.slice(size)
1617
- while (rest.length > 0 && isBlankLine(rest[0])) rest.shift()
1618
- while (rest.length > 0 && isBlankLine(rest.at(-1))) rest.pop()
1619
- const heading = block.slice(0, size).join("\n")
1620
- return rest.length === 0 ? heading : `${heading}\n\n${rest.join("\n")}`
1621
- }
1622
-
1623
- /**
1624
- * A flat review regrouped: every chunk with answer boxes under `## Findings`
1625
- * and every chunk titled in `walkthrough` under `## Walkthrough`, each in its
1626
- * current order and one heading level deeper. Any other chunk stays a `## ` of
1627
- * its own, before the sections when it came before them, after otherwise. An
1628
- * already sectioned review comes back unchanged.
1629
- */
1630
- export const sectionedReview = (content: string, walkthrough: ReadonlySet<string>): string => {
1631
- const tree = parseMarkdown(content)
1632
- const { chunks, sections } = splitChunks(tree, content)
1633
- if (sections.length > 0 || chunks.length === 0) return content
1634
- const { changesets } = parseReviewDoc(content)
1635
- const lines = content.split("\n")
1636
- const sectionOfChunk = (i: number): ReviewSection | undefined =>
1637
- changesets[i]!.answers !== undefined
1638
- ? "findings"
1639
- : walkthrough.has(changesets[i]!.title)
1640
- ? "walkthrough"
1641
- : undefined
1642
- const text = (i: number, demote: boolean): string => {
1643
- const chunk = chunks[i]!
1644
- const block = lines.slice(chunk.headingLine, chunks[i + 1]?.headingLine ?? lines.length)
1645
- return chunkText(demote ? demoted(chunk, block) : block, demote ? 1 : headingSize(chunk))
1646
- }
1647
- const grouped = chunks.map((_, i) => sectionOfChunk(i))
1648
- const first = grouped.findIndex((g) => g !== undefined)
1649
- if (first === -1) return content
1650
- const others = (keep: (i: number) => boolean): string[] =>
1651
- chunks.flatMap((_, i) => (grouped[i] === undefined && keep(i) ? [text(i, false)] : []))
1652
- const section = (s: ReviewSection): string[] => {
1653
- const members = chunks.flatMap((_, i) => (grouped[i] === s ? [text(i, true)] : []))
1654
- return members.length === 0 ? [] : [`## ${REVIEW_SECTIONS[s]}`, ...members]
1655
- }
1656
- const head = lines.slice(0, chunks[0]!.headingLine).join("\n").trim()
1657
- return `${[
1658
- head,
1659
- ...others((i) => i < first),
1660
- ...section("findings"),
1661
- ...section("walkthrough"),
1662
- ...others((i) => i > first),
1663
- ]
1664
- .filter((p) => p !== "")
1665
- .join("\n\n")}\n`
1666
- }
@@ -10,6 +10,7 @@ describe("compileConfig", () => {
10
10
  modes: {
11
11
  qa: { validate: seededValidateCommand("qa") },
12
12
  review: { validate: seededValidateCommand("review") },
13
+ findings: { validate: seededValidateCommand("findings") },
13
14
  },
14
15
  skills: {},
15
16
  access: {},
@@ -88,10 +88,6 @@ Feature: the criteria-coverage lens checks the change's tests against its criter
88
88
 
89
89
  - [ ] ./src/calc.ts#2-2 — subtract(a, b)
90
90
  """
91
- And judge step "build.review.ranking" answers:
92
- """
93
- []
94
- """
95
91
  And step "build.review.carrying" runs its script
96
92
  When the process is driven
97
93
  Then the process rests at "build.review.await-review"
@@ -103,7 +99,6 @@ Feature: the criteria-coverage lens checks the change's tests against its criter
103
99
  gtd(judge): build.quality.triage → build.fix.quality.fixing
104
100
  gtd(agent): build.fix.quality.fixing → build.health.check
105
101
  gtd(check): build.health.check → build.review.reviewing
106
- gtd(agent): build.review.reviewing → build.review.ranking
107
- gtd(judge): build.review.ranking → build.review.carrying
102
+ gtd(agent): build.review.reviewing → build.review.carrying
108
103
  gtd(check): build.review.carrying → build.review.await-review
109
104
  """
@@ -91,7 +91,7 @@ describe("afterTail feedback lap", () => {
91
91
  await repo.drive({
92
92
  "review-gate.check": () => undefined,
93
93
  "build.health.check": () => undefined,
94
- "build.review.composing": (_, r) => r.write(".gtd/REVIEW.md", review()),
94
+ "build.review.reviewing": (_, r) => r.write(".gtd/REVIEW.md", review()),
95
95
  })
96
96
  repo.write(".gtd/REVIEW.md", review("also export multiply"))
97
97
  await repo.land()
@@ -11,7 +11,7 @@ import {
11
11
  import { gate } from "../shared/health.js"
12
12
  import { flagModeOn, shipped } from "../shared/options.js"
13
13
  import { FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
14
- import { refusePendingHandOff, tail } from "../shared/tail.js"
14
+ import { refusePendingHandOff } from "../shared/tail.js"
15
15
  import * as t from "../shared/text.js"
16
16
  import {
17
17
  criterionIds,
@@ -22,7 +22,7 @@ import {
22
22
  type StatedCriteria,
23
23
  } from "./criteria.js"
24
24
  import { design } from "./planning.js"
25
- import { buildTail, humanReview, isCode, reUnwind, type ReviewOutcome } from "../shared/review.js"
25
+ import { buildTail, isCode, reUnwind, type ReviewOutcome } from "../shared/review.js"
26
26
  import { prototypeRounds } from "./prototype.js"
27
27
  import { implement, implementing, seamBuild } from "./seam.js"
28
28
  import { recordRisk, riskTierOn } from "./tier.js"
@@ -32,10 +32,9 @@ import { recordRisk, riskTierOn } from "./tier.js"
32
32
  // green-baseline gate → design → a naive pass, judged with `flagMode` on: it
33
33
  // lands as the implementation, or (refactoring first when the judge asks) is
34
34
  // built fresh from → the health loop → a hands-on test of every criterion,
35
- // fixed and re-tested → the triaged quality lap, whose leftovers are carried
36
- // on → a reviewer's walkthrough of the whole change, its findings among its
37
- // chunks, for the human, which signs off (the episode ends back at `idle`) or
38
- // sends a design → implement lap.
35
+ // fixed and re-tested → the triaged quality lap and its finding gate → a
36
+ // reviewer's walkthrough of the whole change for the human, which signs off
37
+ // (the episode ends back at `idle`) or sends a design → implement lap.
39
38
 
40
39
  /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
41
40
  export const unwind = (): Promise<void> => {
@@ -74,7 +73,7 @@ export const buildFeature = async (firstBase: string): Promise<void> => {
74
73
  !changesSince(seam.base).some((c) => isCode(c.path) && !carried.includes(c.path))
75
74
  ? firstBase
76
75
  : seam.base
77
- const outcome = await reviewedTail(base, base, requirements)
76
+ const outcome = await buildTail(base, { green: true, lensBase: base, requirements })
78
77
  await featureFeedback(outcome, base, { closed, used })
79
78
  }
80
79
 
@@ -85,18 +84,6 @@ const gather = (handedOut: Set<string>): readonly string[] => {
85
84
  return [...handedOut]
86
85
  }
87
86
 
88
- /** The quality lap, then a reviewer's walkthrough since `base` for the human. */
89
- const reviewedTail = (
90
- base: string,
91
- lensBase: string,
92
- requirements: string | undefined,
93
- lap: "first" | "feedback" = "first",
94
- ): Promise<ReviewOutcome> =>
95
- tail(
96
- (escalations, report) => humanReview(base, escalations, { report, walkthrough: true, lap }),
97
- { green: true, lensBase, requirements },
98
- )
99
-
100
87
  // seam.naive / seam.judge never run on a feedback lap: its design → implement is built unjudged
101
88
  // and unrefactored.
102
89
  const featureFeedback = async (
@@ -118,7 +105,12 @@ const featureFeedback = async (
118
105
  const tier = riskTierOn() ? recordRisk() : undefined
119
106
  const requirements = read(REQUIREMENTS)
120
107
  closed = await implementing({ tier, amend: true })
121
- outcome = await reviewedTail(outcome.base, lensBase, requirements, "feedback")
108
+ outcome = await buildTail(outcome.base, {
109
+ green: true,
110
+ lensBase,
111
+ requirements,
112
+ lap: "feedback",
113
+ })
122
114
  }
123
115
  }
124
116