@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -13
- package/dist/chunks/{reserved-COEMFEpy.mjs → reserved-DJk_6-ju.mjs} +41 -1
- package/dist/chunks/review-X7Jfz0dY.mjs +249 -0
- package/dist/chunks/{tail-C_xn8U7U.mjs → tail-5w5mxGTq.mjs} +3 -3
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +6 -278
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +27 -7
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +74 -44
- package/package.json +1 -1
- package/src/steering/FORMATS.md +4 -4
- package/src/workflows/feature/coverage.ts +1 -1
- package/src/workflows/feature/feature.test.ts +8 -0
- package/src/workflows/feature/feature.ts +2 -2
- package/src/workflows/feature/index.ts +1 -1
- package/src/workflows/feature/refactor.ts +1 -1
- package/src/workflows/feature/seam.ts +1 -1
- package/src/workflows/feature/steps.test.ts +2 -64
- package/src/workflows/feature/steps.ts +5 -41
- package/src/workflows/fix/fix.feature +20 -21
- package/src/workflows/index.ts +1 -1
- package/src/workflows/review/review-door.feature +149 -0
- package/src/workflows/review/review.test.ts +1 -73
- package/src/workflows/review/review.ts +34 -6
- package/src/workflows/{feature → shared}/review.test.ts +4 -8
- package/src/workflows/{feature → shared}/review.ts +13 -10
- package/src/workflows/shared/skills.test.ts +2 -0
- package/src/workflows/shared/skills.ts +2 -0
- package/src/workflows/shared/steps.test.ts +64 -2
- package/src/workflows/shared/steps.ts +40 -1
- package/src/workflows/shared/tail.ts +2 -2
- 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,
|
|
2
|
-
import { lensEntriesOf
|
|
3
|
-
import { $ as
|
|
4
|
-
import { a as refusePendingHandOff,
|
|
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) =>
|
|
1176
|
+
const reviewedTail = (base, lensBase) => tail((escalations, report) => humanReview(base, escalations, { report }), {
|
|
1449
1177
|
green: true,
|
|
1450
1178
|
lensBase,
|
|
1451
1179
|
triage: true
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -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 {
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
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 {
|
|
3
|
-
import { a as refusePendingHandOff
|
|
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
|
-
*
|
|
7
|
-
* lap, then the human review
|
|
8
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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.";
|
package/dist/workflows.mjs
CHANGED
|
@@ -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$
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
38735
|
-
* are answered in place, nits fixed in one batch,
|
|
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
|
|
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$
|
|
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) =>
|
|
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) =>
|
|
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
|
-
*
|
|
39867
|
-
* lap, then the human review
|
|
39868
|
-
*
|
|
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
|
-
|
|
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
package/src/steering/FORMATS.md
CHANGED
|
@@ -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
|
|
28
|
-
|
|
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
|
|
31
|
-
|
|
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 "
|
|
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
|
+
]
|