@pmelab/gtd 54.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 (72) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +109 -104
  3. package/dist/chunks/{reserved-CQ7amcKG.mjs → reserved-B8EC-WE8.mjs} +124 -230
  4. package/dist/chunks/{review-CXzUXJHL.mjs → review-HdrX0FJ7.mjs} +30 -125
  5. package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
  6. package/dist/chunks/{tail-JZ9PAOwp.mjs → tail-BSLah2mA.mjs} +155 -63
  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 +47 -102
  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 +614 -574
  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 +104 -0
  36. package/src/workflows/feature/feature.test.ts +1 -1
  37. package/src/workflows/feature/feature.ts +17 -20
  38. package/src/workflows/feature/green.ts +2 -5
  39. package/src/workflows/feature/lens-review.feature +323 -171
  40. package/src/workflows/feature/naive-acceptance.feature +31 -32
  41. package/src/workflows/feature/prototype.ts +5 -8
  42. package/src/workflows/feature/refactor-absorbs.feature +1 -7
  43. package/src/workflows/feature/refactor-first.feature +2 -10
  44. package/src/workflows/feature/refactor-rebuild.feature +20 -36
  45. package/src/workflows/feature/refactor-review.feature +33 -117
  46. package/src/workflows/feature/refactor.test.ts +0 -2
  47. package/src/workflows/feature/refactor.ts +0 -1
  48. package/src/workflows/feature/review-gate.feature +6 -22
  49. package/src/workflows/feature/reviewer-walkthrough.feature +16 -103
  50. package/src/workflows/feature/seam.test.ts +19 -101
  51. package/src/workflows/feature/seam.ts +13 -89
  52. package/src/workflows/feature/standards.feature +75 -34
  53. package/src/workflows/fix/fix-lens-origin.feature +20 -7
  54. package/src/workflows/fix/fix-quality.feature +104 -23
  55. package/src/workflows/review/review-door.feature +62 -59
  56. package/src/workflows/shared/access.test.ts +1 -0
  57. package/src/workflows/shared/learn.test.ts +0 -14
  58. package/src/workflows/shared/learn.ts +1 -6
  59. package/src/workflows/shared/lenses.ts +149 -43
  60. package/src/workflows/shared/review.test.ts +30 -27
  61. package/src/workflows/shared/review.ts +29 -196
  62. package/src/workflows/shared/skills.test.ts +3 -1
  63. package/src/workflows/shared/steps.test.ts +9 -6
  64. package/src/workflows/shared/steps.ts +12 -3
  65. package/src/workflows/shared/tail.test.ts +20 -2
  66. package/src/workflows/shared/tail.ts +38 -16
  67. package/src/workflows/shared/text.test.ts +122 -294
  68. package/src/workflows/shared/text.ts +181 -358
  69. package/src/workflows/shared/vars.ts +1 -1
  70. package/src/workflows/feature/review-answers.feature +0 -324
  71. package/src/workflows/feature/severity-budget.feature +0 -116
  72. package/src/workflows/feature/severity-sorted-review.feature +0 -198
@@ -1,11 +1,14 @@
1
- import { answered, head, judge, mark, numeric, read, refuse, removeScript, run, scope, sectionBodies, showScript, vars, writeScript } from "../flows.mjs";
1
+ import { answered, head, human, judge, mark, numeric, read, refuse, removeScript, run, scope, sectionBodies, showScript, vars, writeScript } from "../flows.mjs";
2
+ import { F as formatFinding, p as findingEntries, r as steeringFormatFor, x as reviewRisks } from "./steering-CCNvngSH.mjs";
2
3
  import { lensEntriesOf, lensOf, qualityLap, reviewToHandOff } from "../workflow-kit.mjs";
3
- import { A as reviewing, C as fixCheck, E as fixRisks, K as handOffAwaitReviewMessage, O as reviewMissing, T as fixQuality, at as qualityFindingsFile, b as awaitReview, d as healthy, k as reviewQuality, kt as withLapFindings, m as QUALITY, ot as qualityTriageMessage, u as guarded, v as REVIEW } from "./reserved-CQ7amcKG.mjs";
4
+ import { A as reviewing, C as fixCheck, E as fixRisks, Et as withSummary, H as findingGateFile, K as handOffAwaitReviewMessage, O as reviewMissing, T as fixQuality, Tt as withGuidance, U as findingGateMessage, b as awaitReview, d as healthy, k as reviewQuality, m as QUALITY, ot as qualityFindingsFile, st as qualityTriageMessage, u as guarded, v as REVIEW } from "./reserved-B8EC-WE8.mjs";
4
5
  //#region src/workflows/shared/lenses.ts
5
6
  const NO_FINDINGS = {
6
- notes: [],
7
7
  fixed: [],
8
- noise: []
8
+ gateFixed: [],
9
+ dropped: [],
10
+ noise: [],
11
+ unresolved: []
9
12
  };
10
13
  const UNTITLED = "Quality findings";
11
14
  const below = (section) => section.split("\n").slice(1).join("\n").trim();
@@ -78,31 +81,18 @@ const classify = async (findings) => {
78
81
  design: of("design")
79
82
  };
80
83
  };
81
- /**
82
- * Judge each finding in `.gtd/QUALITY.md`, set the noise and design ones
83
- * aside, and fix the mechanical ones for up to `FIX_CAP` turns. Leaves
84
- * `.gtd/QUALITY.md` holding only what the fixer left: the caller must remove
85
- * it, or the next lap appends to stale findings.
86
- */
87
- const triageFindings = async (options) => {
88
- const { escalations, guard, guardRests, rules } = options;
89
- const withLens = (f) => {
90
- const lens = options.lensOf?.(f);
91
- return lens === void 0 ? f : {
92
- ...f,
93
- lens
94
- };
95
- };
96
- const findings = findingsOf(read(".gtd/QUALITY.md") ?? "").map(withLens);
97
- if (findings.length === 0) return NO_FINDINGS;
98
- const { mechanical, noise, design } = await classify(findings);
99
- const sorted = qualityFindingsFile(mechanical);
100
- if (read(".gtd/QUALITY.md") !== sorted) await run("quality.sorting", mechanical.length === 0 ? removeScript([QUALITY]) : writeScript(QUALITY, sorted), { label: "Setting findings aside for the review" });
84
+ const NOTHING_FIXED = {
85
+ fixed: [],
86
+ open: [],
87
+ unclear: []
88
+ };
89
+ /** Up to `FIX_CAP` fix turns over `.gtd/QUALITY.md`, which holds one chunk per finding in `findings`. */
90
+ const fixTurns = async (findings, options) => {
101
91
  const fixed = [];
102
92
  const unclear = [];
103
- let open = mechanical;
93
+ let open = findings;
104
94
  for (let turns = 0; turns < 3 && open.length > 0; turns++) {
105
- await guarded(() => fixQuality(rules), guard)();
95
+ await guarded(() => fixQuality(options.rules), options.guard)();
106
96
  const turn = afterFix(open, findingsOf(read(".gtd/QUALITY.md") ?? ""));
107
97
  const commit = head();
108
98
  fixed.push(...turn.fixed.map((f) => ({
@@ -111,41 +101,135 @@ const triageFindings = async (options) => {
111
101
  })));
112
102
  unclear.push(...turn.unclear);
113
103
  open = turn.open.map((f) => ({
114
- ...mechanical.find((m) => m.title === f.title),
104
+ ...findings.find((m) => m.title === f.title),
115
105
  ...f
116
106
  }));
117
107
  }
118
- if (mechanical.length > 0) await healthy(() => fixCheck(rules), {
108
+ const { escalations, guard, guardRests, rules } = options;
109
+ await healthy(() => fixCheck(rules), {
119
110
  escalations,
120
111
  guard,
121
112
  guardRests,
122
113
  rules
123
114
  });
124
115
  return {
125
- notes: [
116
+ fixed,
117
+ open,
118
+ unclear
119
+ };
120
+ };
121
+ /** Matches each entry the human left to the finding it was written from, by title, first unused first; `left` is what no entry claimed. */
122
+ const matcher = (findings) => {
123
+ const left = [...findings];
124
+ const of = (e) => {
125
+ const at = left.findIndex((f) => f.title === e.title);
126
+ return at < 0 ? void 0 : left.splice(at, 1)[0];
127
+ };
128
+ return {
129
+ of,
130
+ left
131
+ };
132
+ };
133
+ const gateProblems = (text) => steeringFormatFor("findings").validate(text).map((f) => ` - ${formatFinding(QUALITY, f)}`);
134
+ /**
135
+ * The human gate over the open notes and the noise: a ticked finding is
136
+ * fixed — never re-planned — and the rest dropped. No notes and no noise, no gate.
137
+ */
138
+ const findingGate = async (notes, noise, options) => {
139
+ if (notes.length + noise.length === 0) return NO_FINDINGS;
140
+ const written = findingGateFile(notes, noise);
141
+ await run("quality.gating", writeScript(QUALITY, written), { label: "Listing the findings for you" });
142
+ await human("quality.gate", {
143
+ message: findingGateMessage(),
144
+ label: "Deciding on the quality findings",
145
+ file: QUALITY,
146
+ mode: "findings",
147
+ acceptClean: true
148
+ });
149
+ const answered = read(".gtd/QUALITY.md") ?? written;
150
+ const problems = gateProblems(answered);
151
+ if (problems.length > 0) refuse(`gtd land: finding-gate: ${problems.length} problem(s) in ${QUALITY} — keep one \`- [ ] fix\` box per finding, ticked to fix it or unticked to drop it, and write guidance only under a ticked one:\n${problems.join("\n")}`);
152
+ const noteOf = matcher(notes);
153
+ const noiseOf = matcher(noise);
154
+ const decided = findingEntries(answered).map((e) => ({
155
+ e,
156
+ finding: (e.noise ? noiseOf.of(e) : noteOf.of(e)) ?? {
157
+ id: "",
158
+ title: e.title,
159
+ body: e.body
160
+ }
161
+ }));
162
+ const ticked = decided.filter(({ e }) => e.fix).map(({ e, finding }) => ({
163
+ ...finding,
164
+ body: withGuidance(finding.body, e.guidance)
165
+ }));
166
+ let fixing = NOTHING_FIXED;
167
+ if (ticked.length > 0) {
168
+ await run("quality.picking", writeScript(QUALITY, qualityFindingsFile(ticked)), { label: "Keeping the findings you ticked" });
169
+ fixing = await fixTurns(ticked, options);
170
+ }
171
+ const unticked = (noisy) => decided.filter(({ e }) => !e.fix && e.noise === noisy).map(({ finding }) => finding);
172
+ return {
173
+ gateFixed: fixing.fixed,
174
+ dropped: [...unticked(false), ...noteOf.left],
175
+ noise: [...unticked(true), ...noiseOf.left],
176
+ unresolved: [...fixing.open, ...fixing.unclear.map((f) => ({
177
+ ...f,
178
+ unclear: true
179
+ }))]
180
+ };
181
+ };
182
+ /**
183
+ * Judge each finding in `.gtd/QUALITY.md` and fix the mechanical ones for up
184
+ * to `FIX_CAP` turns; the rest — design, unresolved and noise — go to the
185
+ * finding gate. Leaves `.gtd/QUALITY.md` holding only what the last fixer
186
+ * left: the caller must remove it, or the next lap appends to stale findings.
187
+ */
188
+ const triageFindings = async (options) => {
189
+ const withLens = (f) => {
190
+ const lens = options.lensOf?.(f);
191
+ return lens === void 0 ? f : {
192
+ ...f,
193
+ lens
194
+ };
195
+ };
196
+ const findings = findingsOf(read(".gtd/QUALITY.md") ?? "").map(withLens);
197
+ if (findings.length === 0) return NO_FINDINGS;
198
+ const { mechanical, noise, design } = await classify(findings);
199
+ let auto = NOTHING_FIXED;
200
+ if (mechanical.length > 0) {
201
+ const sorted = qualityFindingsFile(mechanical);
202
+ if (read(".gtd/QUALITY.md") !== sorted) await run("quality.sorting", writeScript(QUALITY, sorted), { label: "Setting findings aside for the finding gate" });
203
+ auto = await fixTurns(mechanical, options);
204
+ }
205
+ return {
206
+ ...await findingGate([
126
207
  ...design,
127
- ...open.map((f) => ({
208
+ ...auto.open.map((f) => ({
128
209
  ...f,
129
210
  unresolved: true
130
211
  })),
131
- ...unclear.map((f) => ({
212
+ ...auto.unclear.map((f) => ({
132
213
  ...f,
133
214
  unclear: true
134
215
  }))
135
- ],
136
- fixed,
137
- noise
216
+ ], noise, options),
217
+ fixed: auto.fixed
138
218
  };
139
219
  };
140
220
  //#endregion
141
221
  //#region src/workflows/shared/tail.ts
142
222
  /** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
143
223
  const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
144
- const lensLap = (guard, lenses, base, seen) => qualityLap(lenses ?? lensEntriesOf(vars.qualityReviews ?? ""), guarded(async (entry) => {
145
- const { lens, model } = lensOf(entry);
146
- await reviewQuality(lens, base, model);
147
- seen(lens, findingsOf(read(".gtd/QUALITY.md") ?? ""));
148
- }, guard), QUALITY);
224
+ const COVERAGE = "criteria-coverage";
225
+ const lensLap = (guard, lenses, base, given, seen) => {
226
+ const requirements = given ?? read(".gtd/REQUIREMENTS.md");
227
+ return qualityLap((lenses ?? lensEntriesOf(vars.qualityReviews ?? "")).filter((entry) => requirements !== void 0 || lensOf(entry).lens !== COVERAGE), guarded(async (entry) => {
228
+ const { lens, model } = lensOf(entry);
229
+ await reviewQuality(lens, base, model, lens === COVERAGE ? requirements : void 0);
230
+ seen(lens, findingsOf(read(".gtd/QUALITY.md") ?? ""));
231
+ }, guard), QUALITY);
232
+ };
149
233
  const closeQuality = async () => {
150
234
  if (read(".gtd/QUALITY.md") === void 0) return;
151
235
  await run("quality.closing", removeScript([QUALITY]), { label: "Clearing quality findings" });
@@ -182,7 +266,7 @@ const tailForm = (first, second, rest) => {
182
266
  if (typeof first === "function" && isOptionalObject(second) && rest.every((v) => v == null)) return [first, second ?? {}];
183
267
  };
184
268
  const runTail = (finish, options) => scope("build", async () => {
185
- const { guard, lenses, lensBase, rules } = options;
269
+ const { guard, lenses, lensBase, rules, requirements } = options;
186
270
  const guardRests = options.guardRests === void 0 ? true : options.guardRests;
187
271
  const escalations = { rounds: 0 };
188
272
  const fix = () => fixCheck(rules);
@@ -202,7 +286,7 @@ const runTail = (finish, options) => scope("build", async () => {
202
286
  rules
203
287
  });
204
288
  const byTitle = [];
205
- const lap = await lensLap(guard, lenses, lensBase, (lens, findings) => {
289
+ const lap = await lensLap(guard, lenses, lensBase, requirements, (lens, findings) => {
206
290
  const before = byTitle.map((f) => f.title);
207
291
  for (const { title } of findings) {
208
292
  const at = before.indexOf(title);
@@ -232,28 +316,36 @@ const runTail = (finish, options) => scope("build", async () => {
232
316
  * `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
233
317
  * `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
234
318
  */
235
- const handOffReview = (base, escalations, guard, report = NO_FINDINGS) => reviewToHandOff(base, {
236
- file: REVIEW,
237
- reviewing: guarded(async (b, carry) => {
238
- await reviewing(b, carry);
239
- const review = read(REVIEW);
240
- if (review === void 0) return;
241
- const full = withLapFindings(review, report);
242
- if (full !== review) await run("review.carrying", writeScript(REVIEW, full), { label: "Adding the quality lap's findings" });
243
- }, guard),
244
- fixRisks: guarded(fixRisks, guard),
245
- keepGreen: () => healthy(fixCheck, {
246
- escalations,
247
- guard
248
- }),
249
- awaitReview: (b) => awaitReview(b, handOffAwaitReviewMessage(b)),
250
- reviewMissing,
251
- close: closeReview,
252
- handOff: (reviewed) => run("review.handing-off", showScript(reviewed, REVIEW, REVIEW_ORIG), { label: "Handing your notes to fix" }),
253
- threadsPending: "gtd land: open-threads: a thread in .gtd/REVIEW.md or an `H:` code comment waits on the agent — nothing answers it here; delete it, or turn it into a note for fix.",
254
- unparsed: "gtd land: review-doc: your edit to .gtd/REVIEW.md contains no note fix can read — restore the deleted chunk, or write the note as a line under its entry.",
255
- deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
256
- });
319
+ const handOffReview = (base, escalations, guard, report = NO_FINDINGS) => {
320
+ let riskFixed = false;
321
+ return reviewToHandOff(base, {
322
+ file: REVIEW,
323
+ reviewing: guarded(async (b, carry) => {
324
+ const afterRiskFix = riskFixed;
325
+ riskFixed = false;
326
+ await reviewing(b, carry);
327
+ const review = read(REVIEW);
328
+ if (review === void 0 || !afterRiskFix && reviewRisks(review).length > 0) return;
329
+ const full = withSummary(review, { report });
330
+ if (full !== review) await run("review.carrying", writeScript(REVIEW, full), { label: "Adding the quality lap's summary" });
331
+ }, guard),
332
+ fixRisks: guarded((risks) => {
333
+ riskFixed = true;
334
+ return fixRisks(risks);
335
+ }, guard),
336
+ keepGreen: () => healthy(fixCheck, {
337
+ escalations,
338
+ guard
339
+ }),
340
+ awaitReview: (b) => awaitReview(b, handOffAwaitReviewMessage(b)),
341
+ reviewMissing,
342
+ close: closeReview,
343
+ handOff: (reviewed) => run("review.handing-off", showScript(reviewed, REVIEW, REVIEW_ORIG), { label: "Handing your notes to fix" }),
344
+ threadsPending: "gtd land: open-threads: a thread in .gtd/REVIEW.md or an `H:` code comment waits on the agent — nothing answers it here; delete it, or turn it into a note for fix.",
345
+ unparsed: "gtd land: review-doc: your edit to .gtd/REVIEW.md contains no note fix can read — restore the deleted chunk, or write the note as a line under its entry.",
346
+ deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
347
+ });
348
+ };
257
349
  /** The tail the fix workflow runs: ends at the hand-off review since `base`, which gets what the lap left. */
258
350
  const handOffTail = (fixFirst, base, guard) => tail((escalations, report) => handOffReview(base, escalations, guard, report), {
259
351
  fixFirst,