@pmelab/gtd 28.1.0 → 29.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 (54) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +28 -17
  3. package/claude/hooks/assist.ts +1 -1
  4. package/claude/hooks/register.tsx +1 -1
  5. package/claude/hooks/ship.ts +963 -143
  6. package/dist/chunks/{reserved-DeBS4Ymh.mjs → reserved-DoHUII2D.mjs} +206 -121
  7. package/dist/chunks/{tail-B2SER2Ih.mjs → tail-C6YBG5zh.mjs} +19 -12
  8. package/dist/testkit.mjs +2 -1
  9. package/dist/workflow-kit.mjs +51 -21
  10. package/dist/workflows/cleanup.mjs +2 -2
  11. package/dist/workflows/feature.mjs +247 -96
  12. package/dist/workflows/fix.mjs +2 -2
  13. package/dist/workflows/review.mjs +2 -2
  14. package/dist/workflows/rollout.mjs +1 -1
  15. package/dist/workflows.mjs +528 -262
  16. package/package.json +1 -1
  17. package/src/testkit/TestRepo.ts +3 -0
  18. package/src/testkit/index.ts +1 -1
  19. package/src/testkit/steps.ts +44 -0
  20. package/src/workflow-kit/review.test.ts +366 -8
  21. package/src/workflow-kit/review.ts +109 -26
  22. package/src/workflows/feature/feature.test.ts +313 -1
  23. package/src/workflows/feature/feature.ts +48 -43
  24. package/src/workflows/feature/one-step-land.feature +4 -0
  25. package/src/workflows/feature/refactor-first.feature +54 -24
  26. package/src/workflows/feature/refactor-review.feature +1706 -0
  27. package/src/workflows/feature/refactor.test.ts +592 -2
  28. package/src/workflows/feature/refactor.ts +160 -42
  29. package/src/workflows/feature/review.test.ts +322 -5
  30. package/src/workflows/feature/review.ts +77 -10
  31. package/src/workflows/feature/seam-rejection.feature +4 -0
  32. package/src/workflows/feature/seam.feature +3 -4
  33. package/src/workflows/feature/seam.test.ts +196 -1
  34. package/src/workflows/feature/seam.ts +23 -9
  35. package/src/workflows/feature/steps.test.ts +54 -2
  36. package/src/workflows/feature/steps.ts +45 -9
  37. package/src/workflows/feature/tier.test.ts +4 -0
  38. package/src/workflows/feature/tier.ts +2 -20
  39. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +97 -0
  40. package/src/workflows/shared/access.test.ts +31 -1
  41. package/src/workflows/shared/access.ts +7 -1
  42. package/src/workflows/shared/fences.ts +19 -0
  43. package/src/workflows/shared/health.ts +32 -12
  44. package/src/workflows/shared/options.test.ts +18 -0
  45. package/src/workflows/shared/options.ts +1 -0
  46. package/src/workflows/shared/prose.ts +4 -2
  47. package/src/workflows/shared/skills.test.ts +20 -1
  48. package/src/workflows/shared/skills.ts +43 -32
  49. package/src/workflows/shared/steps.ts +33 -19
  50. package/src/workflows/shared/tail.test.ts +84 -1
  51. package/src/workflows/shared/tail.ts +30 -12
  52. package/src/workflows/shared/text.test.ts +266 -4
  53. package/src/workflows/shared/text.ts +183 -70
  54. package/src/workflows/shared/vars.ts +12 -2
@@ -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 { C as reviewMissing, T as reviewing, U as handOffAwaitReviewMessage, _ as awaitReview, b as fixQuality, c as escalation, d as healthy, g as REVIEW, m as QUALITY, u as guarded, w as reviewQuality, x as fixRisks, y as fixCheck } from "./reserved-DeBS4Ymh.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-DoHUII2D.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";
@@ -9,13 +9,13 @@ const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
9
9
  * finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
10
10
  * has any.
11
11
  */
12
- const qualityLap = (guard) => qualityLap$1(lensesOf(vars.qualityReviews ?? ""), guarded(reviewQuality, guard), QUALITY);
12
+ const qualityLap = (guard, lenses, base) => qualityLap$1(lenses ?? lensesOf(vars.qualityReviews ?? ""), guarded((lens) => reviewQuality(lens, base), guard), QUALITY);
13
13
  /** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
14
- const fixQualityFindings = (escalations, guard) => fixFindings({
15
- fix: guarded(fixQuality, guard),
14
+ const fixQualityFindings = (escalations, guard, guardRests = true, rules) => fixFindings({
15
+ fix: guarded(() => fixQuality(rules), guard),
16
16
  findings: QUALITY,
17
17
  cap: 3,
18
- escalate: () => escalation(escalations, guard)
18
+ escalate: () => escalation(escalations, guard, guardRests, rules)
19
19
  });
20
20
  /** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
21
21
  const closeReview = (round) => run("review.closing", removeScript([REVIEW, REVIEW_ORIG]), {
@@ -30,26 +30,33 @@ const refusePendingHandOff = () => {
30
30
  * The build tail: fix (when entered red), keep green, the quality lap, then
31
31
  * `finish` — the human review, as the calling workflow runs it. `green`: the
32
32
  * caller just left the full suite green (the package loop), so no run opens it.
33
+ * `guardRests: false` leaves the escalation's human rests unguarded.
33
34
  */
34
- const tail = (fixFirst, finish, green = false, guard) => scope("build", async () => {
35
+ const tail = (fixFirst, finish, green = false, guard, lenses, guardRests = true, lensBase, rules) => scope("build", async () => {
35
36
  const escalations = { rounds: 0 };
36
- const fix = fixCheck;
37
+ const fix = () => fixCheck(rules);
37
38
  const repair = async () => {
38
39
  await guarded(fix, guard)();
39
40
  await healthy(fix, {
40
41
  fixesSoFar: 1,
41
42
  escalations,
42
- guard
43
+ guard,
44
+ guardRests,
45
+ rules
43
46
  });
44
47
  };
45
48
  if (fixFirst) await repair();
46
49
  else if (!green) await healthy(fix, {
47
50
  escalations,
48
- guard
51
+ guard,
52
+ guardRests,
53
+ rules
49
54
  });
50
- if (await qualityLap(guard) === "findings") if (await fixQualityFindings(escalations, guard)) await healthy(fix, {
55
+ if (await qualityLap(guard, lenses, lensBase) === "findings") if (await fixQualityFindings(escalations, guard, guardRests, rules)) await healthy(fix, {
51
56
  escalations,
52
- guard
57
+ guard,
58
+ guardRests,
59
+ rules
53
60
  });
54
61
  else await repair();
55
62
  return finish(escalations);
@@ -61,7 +68,7 @@ const tail = (fixFirst, finish, green = false, guard) => scope("build", async ()
61
68
  */
62
69
  const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
63
70
  file: REVIEW,
64
- reviewing: guarded(reviewing, guard),
71
+ reviewing: guarded((b, carry) => reviewing(b, carry), guard),
65
72
  fixRisks: guarded(fixRisks, guard),
66
73
  keepGreen: () => healthy(fixCheck, {
67
74
  escalations,
package/dist/testkit.mjs CHANGED
@@ -13726,7 +13726,8 @@ const testRepo = (options) => {
13726
13726
  throw new Error(`the process did not rest within ${maxBeats} beats`);
13727
13727
  },
13728
13728
  subjects: () => repo.commitHistory().map(({ message }) => message.split("\n")[0] ?? ""),
13729
- messages: () => repo.commitHistory().map(({ message }) => message)
13729
+ messages: () => repo.commitHistory().map(({ message }) => message),
13730
+ hashes: () => repo.commitHistory().map(({ hash }) => hash)
13730
13731
  };
13731
13732
  return self;
13732
13733
  };
@@ -1,5 +1,5 @@
1
1
  import { changes, changesSince, hasThreadFor, head, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
2
- import { c as stripCodeThreads, d as reviewNotes, f as reviewRisks } from "./chunks/steering-DVILanVf.mjs";
2
+ import { c as stripCodeThreads, d as reviewNotes, f as reviewRisks, y as parseThreads } from "./chunks/steering-DVILanVf.mjs";
3
3
  //#region src/workflow-kit/health.ts
4
4
  /** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
5
5
  const escalation = async (count, steps) => {
@@ -122,14 +122,28 @@ const removedAssertions = (changes) => changes.filter((c) => c.path.endsWith(".f
122
122
  }).filter((r) => r.lines.length > 0);
123
123
  //#endregion
124
124
  //#region src/workflow-kit/review.ts
125
- const collect = async (p, capture) => {
126
- await p.collecting(capture);
125
+ const collect = async (p, capture, round, requested) => {
126
+ await p.collecting(capture, round);
127
127
  if (read(p.feedback) !== void 0) return "feedback";
128
- if (changes().length === 0) return "signoff";
128
+ if (changes().length === 0) {
129
+ if (p.refusals.dropped !== void 0 && requested !== void 0 && requested.notes.length + requested.paths.length > 0) return refuse(p.refusals.dropped(requested));
130
+ return "signoff";
131
+ }
129
132
  return refuse(p.refusals.unmatched);
130
133
  };
131
- /** A review with ticks, footnote markers and thread definitions removed: what is left is line notes. */
132
- const notesOf = (text) => text.replace(/^\[\^[^\]]+\]:[^\n]*(\n(?: {2,}|\t)[^\n]*|\n(?=\s*\n(?: {2,}|\t)))*/gm, "").replace(/\[\^[^\]]+\]/g, "").replace(/\[x\]/gi, "[ ]").replace(/\s+/g, " ").trim();
134
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
135
+ /**
136
+ * A review with ticks and THREAD footnotes (definition plus markers) removed:
137
+ * what is left is line notes and one-shot footnotes, which the triage judges.
138
+ */
139
+ const notesOf = (text) => {
140
+ let out = text;
141
+ for (const { name } of parseThreads(text)) {
142
+ const n = escapeRe(name);
143
+ out = out.replace(new RegExp(`^\\[\\^${n}\\]:[^\\n]*(\\n(?: {2,}|\\t)[^\\n]*|\\n(?=\\s*\\n(?: {2,}|\\t)))*`, "gim"), "").replace(new RegExp(`\\[\\^${n}\\]`, "gi"), "");
144
+ }
145
+ return out.replace(/\[x\]/gi, "[ ]").replace(/\s+/g, " ").trim();
146
+ };
133
147
  /**
134
148
  * The human gate and its reply rounds: a question gets an answer inside
135
149
  * the review file and rests at the gate again, without a lap. Resolves the
@@ -147,7 +161,7 @@ const converse = async (p, base, collectedBefore) => {
147
161
  }
148
162
  requireThreadsClosed(p.file);
149
163
  if (!hasThreadFor("agent", p.file)) return collectedAt;
150
- await p.collecting(p.captures.edits(head()));
164
+ await p.collecting(p.captures.edits(head()), "reply");
151
165
  collectedAt = head();
152
166
  requireReplies(p.file);
153
167
  if (!hasThreadFor("human", p.file)) return collectedAt;
@@ -176,7 +190,10 @@ const routeNotes = async (p, notes, r) => {
176
190
  if (deferred.length > 0) await p.defer(deferred);
177
191
  if (edits.length > 0) {
178
192
  await r.close();
179
- return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt)));
193
+ return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt), "triaged", {
194
+ notes: edits,
195
+ paths: []
196
+ }));
180
197
  }
181
198
  if (nits.length > 0) {
182
199
  await r.close();
@@ -207,34 +224,47 @@ const settledAfter = (p, collectedAt) => ({
207
224
  folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
208
225
  settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
209
226
  });
227
+ const finishNotes = async (p, before, reopened, ctx) => {
228
+ const notes = reviewNotes(before, read(p.file) ?? "");
229
+ if (notes.length > 0) return routeNotes(p, notes, ctx);
230
+ if (reopened) return ctx.unfolded();
231
+ await ctx.close();
232
+ return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"));
233
+ };
234
+ const toOutcome = (verdict, rest) => verdict === "signoff" ? { verdict } : {
235
+ verdict,
236
+ ...rest
237
+ };
238
+ const isReopened = (p, r) => r.settled && !r.folded && p.refusals.dropped !== void 0;
210
239
  const finish = async (p, reviewed, collectedAt) => {
211
240
  const round = head();
212
241
  const since = changesSince(reviewed);
213
242
  const edited = since.filter((c) => isCodeEdit(p, c));
214
- const baselineText = since.get(p.file)?.before ?? "";
215
- const record = read(p.file) ?? "";
216
243
  const { folded, settled } = afterCollect(p, collectedAt);
217
244
  const noted = since.some((c) => c.path === p.file);
218
245
  const close = () => p.close(round);
219
- const outcome = (verdict) => verdict === "signoff" ? { verdict } : {
220
- verdict,
246
+ const outcome = (verdict) => toOutcome(verdict, {
221
247
  base: round,
222
248
  restoreFrom: reviewed,
223
249
  edited
224
- };
250
+ });
225
251
  const unfolded = async () => {
226
252
  await close();
227
253
  return outcome(folded ? "feedback" : "signoff");
228
254
  };
229
- const collectWith = async (capture) => {
255
+ const reopened = isReopened(p, {
256
+ settled,
257
+ folded
258
+ });
259
+ if (settled && !reopened || edited.length === 0 && !noted) return unfolded();
260
+ if (edited.length > 0) {
230
261
  await close();
231
- return outcome(await collect(p, capture));
232
- };
233
- if (settled || edited.length === 0 && !noted) return unfolded();
234
- if (edited.length > 0) return collectWith(p.captures.edits(round));
235
- const notes = reviewNotes(baselineText, record);
236
- if (notes.length === 0) return collectWith(p.captures.notes(round));
237
- return routeNotes(p, notes, {
262
+ return outcome(await collect(p, p.captures.edits(round, reopened ? reviewed : void 0), "edits", {
263
+ notes: [],
264
+ paths: edited.map((c) => c.path)
265
+ }));
266
+ }
267
+ return finishNotes(p, since.get(p.file)?.before ?? "", reopened, {
238
268
  round,
239
269
  close,
240
270
  outcome,
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { a as flagShipped, ct as defaults, dt as flagReferences, i as skills, lt as envDefaults, mt as requireFlag, n as summary, pt as registryEntry, r as access, t as steering, v as coder } from "../chunks/reserved-DeBS4Ymh.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-B2SER2Ih.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-DoHUII2D.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C6YBG5zh.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 () => {