@pmelab/gtd 28.2.0 → 29.1.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +28 -14
- package/claude/hooks/handoff.ts +2 -3
- package/claude/hooks/ship.ts +762 -62
- package/dist/chunks/{reserved-DeBS4Ymh.mjs → reserved-DoHUII2D.mjs} +206 -121
- package/dist/chunks/{tail-B2SER2Ih.mjs → tail-C6YBG5zh.mjs} +19 -12
- package/dist/testkit.mjs +2 -1
- package/dist/workflow-kit.mjs +51 -21
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +247 -96
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +528 -262
- package/package.json +1 -1
- package/src/testkit/TestRepo.ts +3 -0
- package/src/testkit/index.ts +1 -1
- package/src/testkit/steps.ts +44 -0
- package/src/workflow-kit/review.test.ts +366 -8
- package/src/workflow-kit/review.ts +109 -26
- package/src/workflows/feature/feature.test.ts +313 -1
- package/src/workflows/feature/feature.ts +48 -43
- package/src/workflows/feature/one-step-land.feature +4 -0
- package/src/workflows/feature/refactor-first.feature +54 -24
- package/src/workflows/feature/refactor-review.feature +1706 -0
- package/src/workflows/feature/refactor.test.ts +592 -2
- package/src/workflows/feature/refactor.ts +160 -42
- package/src/workflows/feature/review.test.ts +322 -5
- package/src/workflows/feature/review.ts +77 -10
- package/src/workflows/feature/seam-rejection.feature +4 -0
- package/src/workflows/feature/seam.feature +3 -4
- package/src/workflows/feature/seam.test.ts +196 -1
- package/src/workflows/feature/seam.ts +23 -9
- package/src/workflows/feature/steps.test.ts +54 -2
- package/src/workflows/feature/steps.ts +45 -9
- package/src/workflows/feature/tier.test.ts +4 -0
- package/src/workflows/feature/tier.ts +2 -20
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +97 -0
- package/src/workflows/shared/access.test.ts +31 -1
- package/src/workflows/shared/access.ts +7 -1
- package/src/workflows/shared/fences.ts +19 -0
- package/src/workflows/shared/health.ts +32 -12
- package/src/workflows/shared/options.test.ts +18 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/prose.ts +4 -2
- package/src/workflows/shared/skills.test.ts +20 -1
- package/src/workflows/shared/skills.ts +43 -32
- package/src/workflows/shared/steps.ts +33 -19
- package/src/workflows/shared/tail.test.ts +84 -1
- package/src/workflows/shared/tail.ts +30 -12
- package/src/workflows/shared/text.test.ts +266 -3
- package/src/workflows/shared/text.ts +183 -69
- 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 {
|
|
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
|
};
|
package/dist/workflow-kit.mjs
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
132
|
-
|
|
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
|
|
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
|
|
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,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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 {
|
|
3
|
-
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
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 () => {
|