@pmelab/gtd 43.0.0 → 44.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 (34) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +17 -13
  3. package/dist/chunks/{reserved-COEMFEpy.mjs → reserved-DJk_6-ju.mjs} +41 -1
  4. package/dist/chunks/review-X7Jfz0dY.mjs +249 -0
  5. package/dist/chunks/{tail-C_xn8U7U.mjs → tail-5w5mxGTq.mjs} +3 -3
  6. package/dist/workflows/cleanup.mjs +2 -2
  7. package/dist/workflows/feature.mjs +6 -278
  8. package/dist/workflows/fix.mjs +2 -2
  9. package/dist/workflows/review.mjs +27 -7
  10. package/dist/workflows/rollout.mjs +1 -1
  11. package/dist/workflows.mjs +74 -44
  12. package/package.json +1 -1
  13. package/src/steering/FORMATS.md +4 -4
  14. package/src/workflows/feature/coverage.ts +1 -1
  15. package/src/workflows/feature/feature.test.ts +8 -0
  16. package/src/workflows/feature/feature.ts +2 -2
  17. package/src/workflows/feature/index.ts +1 -1
  18. package/src/workflows/feature/refactor.ts +1 -1
  19. package/src/workflows/feature/seam.ts +1 -1
  20. package/src/workflows/feature/steps.test.ts +2 -64
  21. package/src/workflows/feature/steps.ts +5 -41
  22. package/src/workflows/fix/fix.feature +20 -21
  23. package/src/workflows/index.ts +1 -1
  24. package/src/workflows/review/review-door.feature +149 -0
  25. package/src/workflows/review/review.test.ts +1 -73
  26. package/src/workflows/review/review.ts +34 -6
  27. package/src/workflows/{feature → shared}/review.test.ts +4 -8
  28. package/src/workflows/{feature → shared}/review.ts +13 -10
  29. package/src/workflows/shared/skills.test.ts +2 -0
  30. package/src/workflows/shared/skills.ts +2 -0
  31. package/src/workflows/shared/steps.test.ts +64 -2
  32. package/src/workflows/shared/steps.ts +40 -1
  33. package/src/workflows/shared/tail.ts +2 -2
  34. package/src/workflows/shared/text.ts +11 -0
@@ -1,7 +1,8 @@
1
- import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, showScript, slice, start, vars, writeScript, wrote } from "../flows.mjs";
2
- import { lensEntriesOf, removedAssertions, review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as naivePassPrompt, A as agentWithSkills, B as coverageQuestion, C as fixCheck, Ct as envDefaults, D as reviewMissing, E as planner, F as builderSystem, G as designTriagePrompt, H as criterionLine, I as clearCriteriaMessage, L as clearCriteriaQuestion, M as buildReviewCollectingPrompt, N as buildReviewFixNitsPrompt, P as buildReviewTriageMessage, Q as naiveApplyFailure, R as composedReview, S as coder, St as defaults, T as fixRisks, U as designGateAnswerMessage, V as coveringPrompt, W as designSystem, X as implementBuildingPrompt, Y as idleMessage, Z as implementFixPrompt, _ as REFACTOR_NOTE, _t as slicingPrompt, a as flagModeOn, at as refactorBuildingPrompt, b as SLICES, bt as unwindFailedMessage, ct as refactorNotePrompt, d as guarded, dt as reviewEditsCapture, et as normaliseCriteriaScript, f as healthy, g as REFACTOR, gt as seamSteerMessage, h as QUALITY, ht as seamByHandMessage, i as skills, it as qualityUnresolvedMessage, j as buildReviewAnswerQuestionsPrompt, lt as refactorRules, m as HANDOFF, mt as reviewerSystem, n as summary, ot as refactorDroppedRefusal, p as FEEDBACK, pt as reviewNotesCapture, r as access, s as shipped, st as refactorFixPrompt, t as steering, tt as oversizedMessage, u as gate, ut as reviewEditNotesCapture, v as REQUIREMENTS, vt as startGateBlockedMessage, wt as testFileGlobs, x as awaitReview, xt as unwindFailure, y as REVIEW, yt as uncoveredMessage, z as coverageMessage } from "../chunks/reserved-COEMFEpy.mjs";
4
- import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "../chunks/tail-C_xn8U7U.mjs";
1
+ import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
2
+ import { lensEntriesOf } from "../workflow-kit.mjs";
3
+ import { $ as naiveApplyFailure, B as coverageMessage, C as coder, Ct as unwindFailure, E as fixNits, Et as testFileGlobs, G as designSystem, H as coveringPrompt, I as builderSystem, K as designTriagePrompt, L as clearCriteriaMessage, N as agentWithSkills, P as buildReviewCollectingPrompt, Q as implementFixPrompt, R as clearCriteriaQuestion, St as unwindFailedMessage, T as fixCheck, Tt as envDefaults, U as criterionLine, V as coverageQuestion, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REFACTOR_NOTE, _t as seamByHandMessage, a as flagModeOn, at as qualityUnresolvedMessage, b as SLICES, bt as startGateBlockedMessage, ct as refactorFixPrompt, et as naivePassPrompt, f as healthy, g as REFACTOR, gt as reviewerSystem, h as QUALITY, i as skills, k as planner, lt as refactorNotePrompt, m as HANDOFF, n as summary, nt as oversizedMessage, ot as refactorBuildingPrompt, p as FEEDBACK, r as access, s as shipped, st as refactorDroppedRefusal, t as steering, tt as normaliseCriteriaScript, u as gate, ut as refactorRules, v as REQUIREMENTS, vt as seamSteerMessage, w as collecting, wt as defaults, x as answerReviewQuestions, xt as uncoveredMessage, yt as slicingPrompt } from "../chunks/reserved-DJk_6-ju.mjs";
4
+ import { a as refusePendingHandOff, o as tail } from "../chunks/tail-5w5mxGTq.mjs";
5
+ import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-X7Jfz0dY.mjs";
5
6
  import { posix } from "node:path";
6
7
  //#region src/workflows/shared/fences.ts
7
8
  const markFenced = (text) => {
@@ -219,36 +220,6 @@ const answerProductQuestions = () => human("gate.answer", {
219
220
  mode: "qa",
220
221
  acceptClean: true
221
222
  });
222
- /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
223
- const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", buildReviewAnswerQuestionsPrompt(notes), {
224
- label: "Answering your questions",
225
- file: REVIEW,
226
- mode: "review",
227
- model: planner(),
228
- system: reviewerSystem()
229
- });
230
- /** Fix every `nit` note in one turn. */
231
- const fixNits = (notes, access, rules) => {
232
- const fixing = (name) => agentWithSkills(name, buildReviewFixNitsPrompt(notes, rules), {
233
- label: "Fixing your nits",
234
- file: REVIEW,
235
- model: planner(),
236
- system: reviewerSystem()
237
- });
238
- return access === void 0 ? fixing("review.fix.nits.fixing") : scope({
239
- name: "review.fix.nits",
240
- access
241
- }, () => fixing("fixing"));
242
- };
243
- /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
244
- const collecting = (capture, round) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, void 0, round), {
245
- label: "Collecting your feedback",
246
- file: REQUIREMENTS,
247
- mode: "qa",
248
- model: planner(),
249
- system: reviewerSystem(),
250
- allowEmpty: true
251
- });
252
223
  /** Turn the refactor review round `capture` describes into `.gtd/REFACTOR.md`, or change nothing. */
253
224
  const refactorCollecting = (capture, round, strays) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, {
254
225
  strays,
@@ -286,249 +257,6 @@ const design = (base, guard) => scope("design", async () => {
286
257
  } while (await noteGate(REQUIREMENTS, answerProductQuestions));
287
258
  });
288
259
  //#endregion
289
- //#region src/workflows/feature/review.ts
290
- const VERDICTS = [
291
- "edit",
292
- "question",
293
- "nit",
294
- "praise"
295
- ];
296
- /** One four-way choice per note the human added to `.gtd/REVIEW.md`. */
297
- const verdictQuestion = (note) => ({
298
- id: note.id,
299
- primitive: "choice",
300
- instructions: `Classify the human's note on review item "${note.anchor}" (in .gtd/REVIEW.md). Its evidence holds the anchor, the reviewer's text and the human's text.`,
301
- criteria: "edit: a request to change behaviour, design or scope — anything needing a plan, or any note you are unsure about. question: asks something and wants an answer, with no change requested. nit: a small, local, unambiguous fix (naming, typo, formatting, comment wording) needing no re-plan. praise: an approving remark with nothing to do."
302
- });
303
- const TIER_ORDER = {
304
- critical: 0,
305
- should: 1,
306
- could: 2
307
- };
308
- /** Capture order: critical, should, could; within a tier one-way doors, then new concepts. */
309
- const captureKey = (s) => s === void 0 ? 2 : TIER_ORDER[s.tier] * 3 + s.rank;
310
- const SEVERITY_ANSWERS = {
311
- security: ["yes", "no"],
312
- reversibility: ["one-way", "two-way"],
313
- blastRadius: [
314
- "local",
315
- "module",
316
- "contract"
317
- ],
318
- newConcept: ["yes", "no"]
319
- };
320
- /** Anything unanswered, unrecognised, cut or below `minP` is `critical`, never `could`. */
321
- const severityOf = (note, judgment, minP) => {
322
- const ans = (q) => judgment.answers[`${note.id}.${q}`];
323
- const a = (q) => ans(q)?.answer;
324
- const rank = () => a("reversibility") === "one-way" ? 0 : a("newConcept") === "yes" ? 1 : 2;
325
- if (note.forced) return {
326
- tier: "critical",
327
- reason: "critical: forced by `!`",
328
- rank: 2
329
- };
330
- if (judgment.truncated.includes(note.id) || Object.entries(SEVERITY_ANSWERS).some(([q, allowed]) => !allowed.includes(a(q) ?? "") || !((ans(q)?.p ?? NaN) >= minP))) return {
331
- tier: "critical",
332
- reason: "critical: judge unsure",
333
- rank: 2
334
- };
335
- const critical = [...a("security") === "yes" ? ["security"] : [], ...a("reversibility") === "one-way" ? ["one-way door"] : []];
336
- if (critical.length > 0) return {
337
- tier: "critical",
338
- reason: `critical: ${critical.join(", ")}`,
339
- rank: rank()
340
- };
341
- const should = [...a("blastRadius") === "contract" ? ["contract"] : [], ...a("newConcept") === "yes" ? ["new concept"] : []];
342
- if (should.length > 0) return {
343
- tier: "should",
344
- reason: `should: ${should.join(", ")}`,
345
- rank: rank()
346
- };
347
- return {
348
- tier: "could",
349
- reason: "could",
350
- rank: 2
351
- };
352
- };
353
- const severityQuestions = (note) => [
354
- {
355
- id: `${note.id}.security`,
356
- primitive: "noul",
357
- instructions: `Does the change the human asks for in note "${note.id}" (on "${note.anchor}") touch security — authentication, authorisation, secrets, untrusted input or data exposure?`,
358
- criteria: "yes: it changes or could weaken a security property. no: it does not."
359
- },
360
- {
361
- id: `${note.id}.reversibility`,
362
- primitive: "choice",
363
- instructions: `Once the change in note "${note.id}" ships, can it be undone without breaking anyone who already depends on it?`,
364
- criteria: "one-way: a published API, file format, persisted data, protocol or other contract that cannot be taken back. two-way: can be reverted freely."
365
- },
366
- {
367
- id: `${note.id}.blastRadius`,
368
- primitive: "choice",
369
- instructions: `How far does the change in note "${note.id}" reach?`,
370
- criteria: "local: one function or file. module: a whole module or several files. contract: a public interface other code or users depend on."
371
- },
372
- {
373
- id: `${note.id}.newConcept`,
374
- primitive: "noul",
375
- instructions: `Does the change in note "${note.id}" introduce a concept new to this codebase — a new abstraction, entity or term? Judge from the evidence, never a glossary; reusing or extending an existing concept is not new.`,
376
- criteria: "yes: a genuinely new concept. no: reuses or extends an existing one."
377
- }
378
- ];
379
- /**
380
- * One judge rest: a verdict per unforced note (a forced note is asked
381
- * nothing) plus four severity answers. Dismissing a note is the risky
382
- * direction, so only a confident non-`edit` verdict on uncut evidence counts;
383
- * a cut, unanswered or below-floor note is an `edit`. Severity answers on a
384
- * non-`edit` note are ignored; on an `edit` they order the collecting turn.
385
- */
386
- const triage$1 = (severities) => async (notes) => {
387
- const asked = notes.filter((n) => !n.forced);
388
- const judgment = asked.length === 0 ? {
389
- answers: {},
390
- truncated: []
391
- } : await judge("review.triage", {
392
- questions: asked.flatMap((n) => [verdictQuestion(n), ...severityQuestions(n)]),
393
- evidence: Object.fromEntries(asked.map((n) => [n.id, `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`])),
394
- message: buildReviewTriageMessage(),
395
- label: "Judging each note"
396
- });
397
- const { answers, truncated } = judgment;
398
- const minP = numeric(vars.reviewNoteActionable, Infinity);
399
- const severityMinP = numeric(vars.reviewSeverityMinP, Infinity);
400
- severities.clear();
401
- return new Map(notes.map((n) => {
402
- const verdict = n.forced ? void 0 : VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
403
- if (verdict !== void 0 && !truncated.includes(n.id)) return [n.id, verdict];
404
- severities.set(n.id, severityOf(n, judgment, severityMinP));
405
- return [n.id, "edit"];
406
- }));
407
- };
408
- const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
409
- /**
410
- * gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
411
- * turn — and removes what the fixer left in `.gtd/QUALITY.md`. A re-review
412
- * after fixed nits restores the doc as committed at `carry`, answers kept;
413
- * a fresh one leaves out the notes the human already answered.
414
- */
415
- const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new Set()) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
416
- ...report,
417
- weakened: removedAssertions(changesSince(base)),
418
- nitFix,
419
- answered
420
- }), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
421
- label: "Composing the review",
422
- file: REVIEW,
423
- base
424
- });
425
- /**
426
- * The quality lap's notes reach a human over everything since `base`, who
427
- * answers each one — fix as suggested, won't fix, or their own answer — and
428
- * may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
429
- * answer or comment is judged: edits go to another planning lap, questions
430
- * are answered in place, nits fixed in one batch, praise dropped.
431
- */
432
- const review = (base, escalations = { rounds: 0 }, options = {}) => {
433
- const { guard, guardRests, fixAccess, rules } = options;
434
- const feedback = options.feedback ?? {
435
- file: ".gtd/REQUIREMENTS.md",
436
- collecting,
437
- unmatched: "gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing"
438
- };
439
- const severities = /* @__PURE__ */ new Map();
440
- let nitFix;
441
- const answered = /* @__PURE__ */ new Set();
442
- return review$1(base, {
443
- file: REVIEW,
444
- feedback: feedback.file,
445
- isCode: isCode$1,
446
- reviewing: (b, carry) => {
447
- const fixedAt = nitFix;
448
- nitFix = void 0;
449
- return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt, answered);
450
- },
451
- fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
452
- keepGreen: () => healthy(() => fixCheck(rules), {
453
- escalations,
454
- guard,
455
- guardRests,
456
- rules
457
- }),
458
- awaitReview: (b) => awaitReview(b),
459
- reviewMissing,
460
- collecting: feedback.collecting,
461
- triage: triage$1(severities),
462
- decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
463
- answerQuestions: answerReviewQuestions,
464
- fixNits: async (notes) => {
465
- await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
466
- nitFix = head();
467
- },
468
- close: closeReview,
469
- captures: {
470
- edits: reviewEditsCapture,
471
- notes: reviewNotesCapture,
472
- editNotes: (round, edits, answeredAt) => reviewEditNotesCapture(round, edits.map((n, i) => ({
473
- n,
474
- i,
475
- s: severities.get(n.id),
476
- key: captureKey(severities.get(n.id))
477
- })).sort((x, y) => x.key - y.key || x.i - y.i).map(({ n, s }) => ({
478
- id: n.id,
479
- anchor: n.anchor,
480
- text: n.forced ? n.text.replace(/^!\s*/, "") : n.text,
481
- ...s === void 0 ? {} : { label: s.reason }
482
- })), answeredAt)
483
- },
484
- refusals: {
485
- deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
486
- unmatched: feedback.unmatched,
487
- ...feedback.dropped === void 0 ? {} : { dropped: feedback.dropped }
488
- }
489
- });
490
- };
491
- function buildTail(first, second, ...rest) {
492
- const options = callForm(first, second, rest);
493
- if (options === void 0) return Promise.reject(/* @__PURE__ */ new Error("buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither"));
494
- const [base, form] = options;
495
- return tail((escalations, report) => review(base, escalations, {
496
- ...form,
497
- report
498
- }), {
499
- ...form,
500
- triage: true
501
- });
502
- }
503
- const isOptionalBoolean = (value) => value == null || typeof value === "boolean";
504
- const legacyForm = (fixFirst, base, green, options) => isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options) ? [base, {
505
- ...options,
506
- fixFirst: fixFirst ?? void 0,
507
- green: green ?? void 0
508
- }] : void 0;
509
- const currentForm = (base, options, third, fourth) => isOptionalObject(options) && third == null && fourth == null ? [base, options ?? {}] : void 0;
510
- const callForm = (first, second, [third, fourth, ...extra]) => {
511
- if (extra.some((value) => value != null)) return void 0;
512
- if (typeof second === "string") return legacyForm(first, second, third, fourth);
513
- if (typeof first === "string") return currentForm(first, second, third, fourth);
514
- };
515
- /**
516
- * Undo the human's review-round code edits. A path changed since the human
517
- * left it is not overwritten; requireRevert names it instead.
518
- */
519
- const reUnwind = async (feedback) => {
520
- const { base, restoreFrom, edited } = feedback;
521
- await run("re-unwind", restoreScript(base, {
522
- restore: edited.filter((c) => c.status !== "added").map((c) => c.path),
523
- remove: edited.filter((c) => c.status === "added").map((c) => c.path)
524
- }, restoreFrom), {
525
- label: "Re-unwinding your review edit",
526
- file: REVIEW,
527
- base
528
- });
529
- requireRevert(edited, base);
530
- };
531
- //#endregion
532
260
  //#region src/workflows/shared/continuation.ts
533
261
  const phase = (name, steered) => ({
534
262
  name,
@@ -1445,7 +1173,7 @@ const settleQuality = async () => {
1445
1173
  if (rested) await healthy(() => fixCheck());
1446
1174
  };
1447
1175
  /** The triaged quality lap, then the human review since `base`. */
1448
- const reviewedTail = (base, lensBase) => tail((escalations, report) => review(base, escalations, { report }), {
1176
+ const reviewedTail = (base, lensBase) => tail((escalations, report) => humanReview(base, escalations, { report }), {
1449
1177
  green: true,
1450
1178
  lensBase,
1451
1179
  triage: true
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
2
  import { g as reviewRisks, h as reviewNotes } from "../chunks/steering-D4LUQ-av.mjs";
3
- import { A as agentWithSkills, Ct as envDefaults, K as finisherSystem, S as coder, St as defaults, c as baseline, i as skills, n as summary, q as fixReviewNotesPrompt, r as access, s as shipped, t as steering, y as REVIEW } from "../chunks/reserved-COEMFEpy.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-C_xn8U7U.mjs";
3
+ import { C as coder, J as fixReviewNotesPrompt, N as agentWithSkills, Tt as envDefaults, c as baseline, i as skills, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, wt as defaults, y as REVIEW } from "../chunks/reserved-DJk_6-ju.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-5w5mxGTq.mjs";
5
5
  //#region src/workflows/fix/fix.ts
6
6
  /**
7
7
  * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
@@ -1,16 +1,36 @@
1
- import { start } from "../flows.mjs";
2
- import { Ct as envDefaults, St as defaults, ft as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, u as gate } from "../chunks/reserved-COEMFEpy.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C_xn8U7U.mjs";
1
+ import { read, refuse, removeScript, run, start } from "../flows.mjs";
2
+ import { C as coder, N as agentWithSkills, Tt as envDefaults, i as skills, mt as reviewGateBlockedMessage, n as summary, pt as reviewFeedbackFixPrompt, q as finisherSystem, r as access, s as shipped, t as steering, u as gate, v as REQUIREMENTS, wt as defaults } from "../chunks/reserved-DJk_6-ju.mjs";
3
+ import { a as refusePendingHandOff } from "../chunks/tail-5w5mxGTq.mjs";
4
+ import { i as reUnwind, t as buildTail } from "../chunks/review-X7Jfz0dY.mjs";
4
5
  //#region src/workflows/review/review.ts
6
+ /** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
7
+ const fixFeedback = async () => {
8
+ await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
9
+ label: "Fixing your review feedback",
10
+ file: REQUIREMENTS,
11
+ model: coder(),
12
+ system: finisherSystem(),
13
+ allowEmpty: true
14
+ });
15
+ await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
16
+ };
5
17
  /**
6
- * Pure review of everything since `reviewBase`: a green gate, the quality
7
- * lap, then the human review. A note ends the process with `.gtd/REVIEW.md`
8
- * left for `fix`; it never re-plans.
18
+ * The feature's review of everything since `reviewBase` — a green gate, the
19
+ * triaged quality lap, then the human review — with no design phase: a
20
+ * review that asks for changes is fixed in place and reviewed again, until
21
+ * sign-off.
9
22
  */
10
23
  const reviewFlow = async () => {
11
24
  refusePendingHandOff();
25
+ if (read(".gtd/REQUIREMENTS.md") !== void 0) refuse(`gtd: ${REQUIREMENTS} is left over — finish or remove it before a review.`);
12
26
  await gate("review-gate", reviewGateBlockedMessage());
13
- await handOffTail(false, start());
27
+ const base = start();
28
+ let outcome = await buildTail(base, { green: true });
29
+ while (outcome.verdict === "feedback") {
30
+ await reUnwind(outcome);
31
+ await fixFeedback();
32
+ outcome = await buildTail(base, { lensBase: outcome.base });
33
+ }
14
34
  };
15
35
  const review = shipped(reviewFlow, { review: {
16
36
  args: [{
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { Ct as envDefaults, Dt as flipProblems, S as coder, St as defaults, Tt as flagMentions, i as skills, kt as requireFlag, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate } from "../chunks/reserved-COEMFEpy.mjs";
2
+ import { C as coder, Dt as flagMentions, Tt as envDefaults, i as skills, jt as requireFlag, kt as flipProblems, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as defaults } from "../chunks/reserved-DJk_6-ju.mjs";
3
3
  //#region src/workflows/rollout/rollout.ts
4
4
  const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
5
5
  const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
@@ -16909,7 +16909,7 @@ const reviewOnce = async (p, base, carry) => {
16909
16909
  * another lap, questions are answered in place, nits fixed in one batch,
16910
16910
  * praise dropped.
16911
16911
  */
16912
- const review$2 = async (base, p) => {
16912
+ const review$1 = async (base, p) => {
16913
16913
  let carry;
16914
16914
  for (;;) {
16915
16915
  await reviewOnce(p, base, carry);
@@ -17937,6 +17937,15 @@ ${notes.length === 0 ? "(none)\n" : notesCapture(notes)}
17937
17937
  The reviewer's risks are:
17938
17938
 
17939
17939
  ${risks.length === 0 ? "(none)\n" : notesCapture(risks)}`;
17940
+ const reviewFeedbackFixPrompt = () => `${stateFileRules}
17941
+ - Read \`.gtd/REQUIREMENTS.md\` — the concerns the human's review asked for,
17942
+ one \`## \` heading each, in build order
17943
+ - Build every concern in this one turn; an answered question is context,
17944
+ not a concern
17945
+ - Where a change is behavioural, add a test that fails without it
17946
+ - Leave \`.gtd/REQUIREMENTS.md\` untouched — the next step removes it
17947
+ - Leave everything uncommitted and finish your turn
17948
+ `;
17940
17949
  const buildReviewReviewMissingMessage = (commit) => `The review round committed no \`.gtd/REVIEW.md\` (at ${commit}), so there is
17941
17950
  nothing to sign off on.
17942
17951
 
@@ -18565,6 +18574,36 @@ const awaitReview = (base, message = buildReviewAwaitReviewMessage(base)) => hum
18565
18574
  base,
18566
18575
  acceptClean: true
18567
18576
  });
18577
+ /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
18578
+ const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", buildReviewAnswerQuestionsPrompt(notes), {
18579
+ label: "Answering your questions",
18580
+ file: REVIEW,
18581
+ mode: "review",
18582
+ model: planner(),
18583
+ system: reviewerSystem()
18584
+ });
18585
+ /** Fix every `nit` note in one turn. */
18586
+ const fixNits = (notes, access, rules) => {
18587
+ const fixing = (name) => agentWithSkills(name, buildReviewFixNitsPrompt(notes, rules), {
18588
+ label: "Fixing your nits",
18589
+ file: REVIEW,
18590
+ model: planner(),
18591
+ system: reviewerSystem()
18592
+ });
18593
+ return access === void 0 ? fixing("review.fix.nits.fixing") : scope({
18594
+ name: "review.fix.nits",
18595
+ access
18596
+ }, () => fixing("fixing"));
18597
+ };
18598
+ /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
18599
+ const collecting = (capture, round) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, void 0, round), {
18600
+ label: "Collecting your feedback",
18601
+ file: REQUIREMENTS,
18602
+ mode: "qa",
18603
+ model: planner(),
18604
+ system: reviewerSystem(),
18605
+ allowEmpty: true
18606
+ });
18568
18607
  /** The round at commit `round` left no `.gtd/REVIEW.md`. */
18569
18608
  const reviewMissing = (round) => human("review.review-missing", {
18570
18609
  message: buildReviewReviewMissingMessage(round),
@@ -38282,7 +38321,7 @@ const runTail = (finish, options) => scope("build", async () => {
38282
38321
  return finish(escalations, NO_FINDINGS);
38283
38322
  });
38284
38323
  /**
38285
- * The review and fix workflows' human review since `base`: a sign-off closes
38324
+ * The fix workflow's human review since `base`: a sign-off closes
38286
38325
  * `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
38287
38326
  * `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
38288
38327
  */
@@ -38302,7 +38341,7 @@ const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
38302
38341
  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.",
38303
38342
  deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
38304
38343
  });
38305
- /** The tail the review and fix workflows run: ends at the hand-off review since `base`. */
38344
+ /** The tail the fix workflow runs: ends at the hand-off review since `base`. */
38306
38345
  const handOffTail = (fixFirst, base, guard) => tail((escalations) => handOffReview(base, escalations, guard), {
38307
38346
  fixFirst,
38308
38347
  guard
@@ -38524,36 +38563,6 @@ const answerProductQuestions = () => human("gate.answer", {
38524
38563
  mode: "qa",
38525
38564
  acceptClean: true
38526
38565
  });
38527
- /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
38528
- const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", buildReviewAnswerQuestionsPrompt(notes), {
38529
- label: "Answering your questions",
38530
- file: REVIEW,
38531
- mode: "review",
38532
- model: planner(),
38533
- system: reviewerSystem()
38534
- });
38535
- /** Fix every `nit` note in one turn. */
38536
- const fixNits = (notes, access, rules) => {
38537
- const fixing = (name) => agentWithSkills(name, buildReviewFixNitsPrompt(notes, rules), {
38538
- label: "Fixing your nits",
38539
- file: REVIEW,
38540
- model: planner(),
38541
- system: reviewerSystem()
38542
- });
38543
- return access === void 0 ? fixing("review.fix.nits.fixing") : scope({
38544
- name: "review.fix.nits",
38545
- access
38546
- }, () => fixing("fixing"));
38547
- };
38548
- /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
38549
- const collecting = (capture, round) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, void 0, round), {
38550
- label: "Collecting your feedback",
38551
- file: REQUIREMENTS,
38552
- mode: "qa",
38553
- model: planner(),
38554
- system: reviewerSystem(),
38555
- allowEmpty: true
38556
- });
38557
38566
  /** Turn the refactor review round `capture` describes into `.gtd/REFACTOR.md`, or change nothing. */
38558
38567
  const refactorCollecting = (capture, round, strays) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, {
38559
38568
  strays,
@@ -38591,7 +38600,7 @@ const design = (base, guard) => scope("design", async () => {
38591
38600
  } while (await noteGate(REQUIREMENTS, answerProductQuestions));
38592
38601
  });
38593
38602
  //#endregion
38594
- //#region src/workflows/feature/review.ts
38603
+ //#region src/workflows/shared/review.ts
38595
38604
  const VERDICTS = [
38596
38605
  "edit",
38597
38606
  "question",
@@ -38731,10 +38740,11 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
38731
38740
  * The quality lap's notes reach a human over everything since `base`, who
38732
38741
  * answers each one — fix as suggested, won't fix, or their own answer — and
38733
38742
  * may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
38734
- * answer or comment is judged: edits go to another planning lap, questions
38735
- * are answered in place, nits fixed in one batch, praise dropped.
38743
+ * answer or comment is judged: edits end the review as `feedback` for the
38744
+ * caller to build, questions are answered in place, nits fixed in one batch,
38745
+ * praise dropped.
38736
38746
  */
38737
- const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
38747
+ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
38738
38748
  const { guard, guardRests, fixAccess, rules } = options;
38739
38749
  const feedback = options.feedback ?? {
38740
38750
  file: ".gtd/REQUIREMENTS.md",
@@ -38744,7 +38754,7 @@ const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
38744
38754
  const severities = /* @__PURE__ */ new Map();
38745
38755
  let nitFix;
38746
38756
  const answered = /* @__PURE__ */ new Set();
38747
- return review$2(base, {
38757
+ return review$1(base, {
38748
38758
  file: REVIEW,
38749
38759
  feedback: feedback.file,
38750
38760
  isCode: isCode$1,
@@ -38797,7 +38807,7 @@ function buildTail(first, second, ...rest) {
38797
38807
  const options = callForm(first, second, rest);
38798
38808
  if (options === void 0) return Promise.reject(/* @__PURE__ */ new Error("buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither"));
38799
38809
  const [base, form] = options;
38800
- return tail((escalations, report) => review$1(base, escalations, {
38810
+ return tail((escalations, report) => humanReview(base, escalations, {
38801
38811
  ...form,
38802
38812
  report
38803
38813
  }), {
@@ -39750,7 +39760,7 @@ const settleQuality = async () => {
39750
39760
  if (rested) await healthy(() => fixCheck());
39751
39761
  };
39752
39762
  /** The triaged quality lap, then the human review since `base`. */
39753
- const reviewedTail = (base, lensBase) => tail((escalations, report) => review$1(base, escalations, { report }), {
39763
+ const reviewedTail = (base, lensBase) => tail((escalations, report) => humanReview(base, escalations, { report }), {
39754
39764
  green: true,
39755
39765
  lensBase,
39756
39766
  triage: true
@@ -39829,6 +39839,7 @@ const skills = (vars) => {
39829
39839
  return {
39830
39840
  design: ["spec-driven-development", "planning-and-task-breakdown"],
39831
39841
  notes: ["incremental-implementation", "code-simplification"],
39842
+ feedback: ["test-driven-development", "incremental-implementation"],
39832
39843
  ...build,
39833
39844
  ...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
39834
39845
  refactor: ["ponytail"],
@@ -39862,15 +39873,34 @@ const steering = {
39862
39873
  };
39863
39874
  //#endregion
39864
39875
  //#region src/workflows/review/review.ts
39876
+ /** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
39877
+ const fixFeedback = async () => {
39878
+ await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
39879
+ label: "Fixing your review feedback",
39880
+ file: REQUIREMENTS,
39881
+ model: coder(),
39882
+ system: finisherSystem(),
39883
+ allowEmpty: true
39884
+ });
39885
+ await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
39886
+ };
39865
39887
  /**
39866
- * Pure review of everything since `reviewBase`: a green gate, the quality
39867
- * lap, then the human review. A note ends the process with `.gtd/REVIEW.md`
39868
- * left for `fix`; it never re-plans.
39888
+ * The feature's review of everything since `reviewBase` — a green gate, the
39889
+ * triaged quality lap, then the human review — with no design phase: a
39890
+ * review that asks for changes is fixed in place and reviewed again, until
39891
+ * sign-off.
39869
39892
  */
39870
39893
  const reviewFlow = async () => {
39871
39894
  refusePendingHandOff();
39895
+ if (read(".gtd/REQUIREMENTS.md") !== void 0) refuse(`gtd: ${REQUIREMENTS} is left over — finish or remove it before a review.`);
39872
39896
  await gate("review-gate", reviewGateBlockedMessage());
39873
- await handOffTail(false, start());
39897
+ const base = start();
39898
+ let outcome = await buildTail(base, { green: true });
39899
+ while (outcome.verdict === "feedback") {
39900
+ await reUnwind(outcome);
39901
+ await fixFeedback();
39902
+ outcome = await buildTail(base, { lensBase: outcome.base });
39903
+ }
39874
39904
  };
39875
39905
  const review = shipped(reviewFlow, { review: {
39876
39906
  args: [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "43.0.0",
3
+ "version": "44.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
@@ -24,11 +24,11 @@ read them.
24
24
  its first line starts `H:`.
25
25
  - **Review notes**: what a human added to a `review` document since the reviewer
26
26
  wrote it — changed chunk prose, pointer notes, and new one-shot footnotes,
27
- never threads. **This is the hand-off from the `review` workflow to the `fix`
28
- workflow**: `review` leaves the annotated file beside the reviewer's own copy
27
+ never threads. **This is the hand-off from a `fix` review to the next `fix`**:
28
+ the review leaves the annotated file beside the reviewer's own copy
29
29
  (`.gtd/REVIEW.orig.md`), and `fix` reads its work from the notes between the
30
- two and from the risks, so a file `review` wrote must read the same in any
31
- `fix` that consumes it.
30
+ two and from the risks, so a hand-off file must read the same in any `fix`
31
+ that consumes it.
32
32
 
33
33
  ## What may change
34
34
 
@@ -19,7 +19,7 @@ import {
19
19
  type CriteriaSlice,
20
20
  } from "./criteria.js"
21
21
  import { isTestFile } from "./refactor.js"
22
- import { isCode } from "./review.js"
22
+ import { isCode } from "../shared/review.js"
23
23
 
24
24
  /** Below this the judge's `yes` is not trusted: a wrongly covered ID ends the feature without its test. */
25
25
  const COVERED_MIN_P = 0.7
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"
2
2
  import { runsItsScript, testRepo } from "../../testkit/index.js"
3
3
  import { CONFIG_SCAFFOLD } from "../../install/index.js"
4
4
  import { reviewDoc } from "../shared/config.fixture.js"
5
+ import type * as featureEntry from "./index.js"
5
6
 
6
7
  describe("an ordinary feature start", () => {
7
8
  it("unwinds a captured TODO, checks the baseline and starts design from it", async () => {
@@ -363,3 +364,10 @@ describe("flagMode", () => {
363
364
  expect(repo.subjects()).toEqual(["chore: initial commit"])
364
365
  })
365
366
  })
367
+
368
+ // pins the public re-exports from the feature entry point
369
+ export type _PublicBuildTailOptions = [
370
+ featureEntry.BuildTailOptions,
371
+ featureEntry.FeedbackTarget,
372
+ featureEntry.ReviewRange,
373
+ ]