@pmelab/gtd 42.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 (62) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +96 -48
  3. package/claude/hooks/sketch.ts +1 -1
  4. package/dist/chunks/{reserved-D7MNY9QJ.mjs → reserved-DJk_6-ju.mjs} +75 -46
  5. package/dist/chunks/review-X7Jfz0dY.mjs +249 -0
  6. package/dist/chunks/{steering-oCDdQlpP.mjs → steering-D4LUQ-av.mjs} +95 -12
  7. package/dist/chunks/{tail-D2o2h5cc.mjs → tail-5w5mxGTq.mjs} +3 -3
  8. package/dist/gtd.bundle.mjs +488 -30
  9. package/dist/steering.mjs +2 -2
  10. package/dist/testkit.mjs +447 -27
  11. package/dist/web/main.js +42 -13
  12. package/dist/workflow-kit.mjs +61 -25
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +7 -290
  15. package/dist/workflows/fix.mjs +3 -3
  16. package/dist/workflows/review.mjs +27 -7
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +287 -161
  19. package/package.json +1 -1
  20. package/src/cli/Cli.test.ts +33 -1
  21. package/src/cli/Cli.ts +70 -6
  22. package/src/cli/History.ts +15 -0
  23. package/src/cli/Judges.test.ts +373 -0
  24. package/src/cli/Judges.ts +624 -0
  25. package/src/cli/Metrics.ts +3 -16
  26. package/src/cli/Program.test.ts +3 -1
  27. package/src/cli/Program.ts +9 -0
  28. package/src/steering/FORMATS.md +4 -4
  29. package/src/steering/index.ts +12 -1
  30. package/src/steering/review.test.ts +107 -0
  31. package/src/steering/review.ts +159 -9
  32. package/src/testkit/EmittedScriptRecognizer.ts +0 -14
  33. package/src/testkit/steps.ts +4 -0
  34. package/src/workflow-kit/review.test.ts +170 -26
  35. package/src/workflow-kit/review.ts +95 -19
  36. package/src/workflows/feature/coverage.ts +1 -1
  37. package/src/workflows/feature/feature.test.ts +8 -0
  38. package/src/workflows/feature/feature.ts +2 -2
  39. package/src/workflows/feature/index.ts +1 -1
  40. package/src/workflows/feature/lens-review.feature +16 -4
  41. package/src/workflows/feature/refactor.ts +1 -1
  42. package/src/workflows/feature/review-answers.feature +179 -0
  43. package/src/workflows/feature/seam.ts +1 -1
  44. package/src/workflows/feature/steps.test.ts +2 -76
  45. package/src/workflows/feature/steps.ts +5 -48
  46. package/src/workflows/fix/fix.feature +20 -21
  47. package/src/workflows/index.ts +1 -1
  48. package/src/workflows/review/review-door.feature +149 -0
  49. package/src/workflows/review/review.test.ts +1 -73
  50. package/src/workflows/review/review.ts +34 -6
  51. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +6 -5
  52. package/src/workflows/shared/access.test.ts +2 -11
  53. package/src/workflows/shared/access.ts +0 -2
  54. package/src/workflows/{feature → shared}/review.test.ts +9 -61
  55. package/src/workflows/{feature → shared}/review.ts +40 -48
  56. package/src/workflows/shared/skills.test.ts +2 -0
  57. package/src/workflows/shared/skills.ts +2 -0
  58. package/src/workflows/shared/steps.test.ts +64 -2
  59. package/src/workflows/shared/steps.ts +40 -1
  60. package/src/workflows/shared/tail.ts +2 -2
  61. package/src/workflows/shared/text.test.ts +40 -89
  62. package/src/workflows/shared/text.ts +63 -54
package/dist/steering.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { C as headingSections, S as headingSectionBodies, _ as openThreadFindings, a as unansweredQuestions, b as parseThreadsWithFindings, c as stripCodeThreads, d as reviewNotes, f as reviewRisks, g as THREAD_REPLY_ACTION_TITLE, h as FOOTNOTE_ACTION_TITLE, i as steeringFormatOrFreeForm, l as freeFormFormat, m as isAnswered, n as openThreads, o as viewOf, p as FREE_TEXT_PLACEHOLDER, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES, u as applySteeringEdits, v as parseFootnotes, w as formatFinding, x as getParseCount, y as parseThreads } from "./chunks/steering-oCDdQlpP.mjs";
2
- export { BUILT_IN_MODE_NAMES, FOOTNOTE_ACTION_TITLE, FREE_TEXT_PLACEHOLDER, THREAD_REPLY_ACTION_TITLE, applySteeringEdits, formatFinding, freeFormFormat, getParseCount, headingSectionBodies, headingSections, isAnswered, openThreadFindings, openThreads, parseCodeThreads, parseFootnotes, parseThreads, parseThreadsWithFindings, reviewNotes, reviewRisks, steeringFormatFor, steeringFormatOrFreeForm, stripCodeThreads, unansweredQuestions, viewOf };
1
+ import { C as parseFootnotes, D as headingSectionBodies, E as getParseCount, O as headingSections, S as openThreadFindings, T as parseThreadsWithFindings, _ as unansweredNotes, a as unansweredQuestions, b as FOOTNOTE_ACTION_TITLE, c as stripCodeThreads, d as ANSWER_FIX, f as ANSWER_LEAD, g as reviewRisks, h as reviewNotes, i as steeringFormatOrFreeForm, k as formatFinding, l as freeFormFormat, m as reviewDecisions, n as openThreads, o as viewOf, p as ANSWER_WONT_FIX, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES, u as applySteeringEdits, v as FREE_TEXT_PLACEHOLDER, w as parseThreads, x as THREAD_REPLY_ACTION_TITLE, y as isAnswered } from "./chunks/steering-D4LUQ-av.mjs";
2
+ export { ANSWER_FIX, ANSWER_LEAD, ANSWER_WONT_FIX, BUILT_IN_MODE_NAMES, FOOTNOTE_ACTION_TITLE, FREE_TEXT_PLACEHOLDER, THREAD_REPLY_ACTION_TITLE, applySteeringEdits, formatFinding, freeFormFormat, getParseCount, headingSectionBodies, headingSections, isAnswered, openThreadFindings, openThreads, parseCodeThreads, parseFootnotes, parseThreads, parseThreadsWithFindings, reviewDecisions, reviewNotes, reviewRisks, steeringFormatFor, steeringFormatOrFreeForm, stripCodeThreads, unansweredNotes, unansweredQuestions, viewOf };
package/dist/testkit.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { applyScript, checkScript, configProblems, configure, installContext, isFactoryWorkflow, moveScript, protocolMismatch, removeScript, restoreScript, revertScript, showScript, t as flows_exports, writeScript } from "./flows.mjs";
1
+ import { answered, applyScript, checkScript, configProblems, configure, installContext, isFactoryWorkflow, moveScript, numeric, protocolMismatch, removeScript, restoreScript, revertScript, showScript, t as flows_exports, writeScript } from "./flows.mjs";
2
2
  import { A as beatKindOf, B as ACCESS_REFUSAL, C as renderLandPlain, D as renderBeatJson, E as beatDocument, O as statusOf, S as renderLandJson, T as renderBeatPlain, U as renderFormat, _ as selectPath, a as contextLimitFault, g as resolveSession, h as globMatches, j as demandOf, l as parseContextWindow, m as slicesDocument, o as contextShapeFault, p as renderSlicesJson, v as judgeJson, x as noopText, y as landFields } from "./chunks/wire-BJuZWR3C.mjs";
3
- import { C as headingSections, S as headingSectionBodies, a as unansweredQuestions, b as parseThreadsWithFindings, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES, w as formatFinding } from "./chunks/steering-oCDdQlpP.mjs";
3
+ import { D as headingSectionBodies, O as headingSections, T as parseThreadsWithFindings, a as unansweredQuestions, k as formatFinding, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES } from "./chunks/steering-D4LUQ-av.mjs";
4
4
  import { $ as logWarning, $i as BaseProto, $n as map$5, $r as make$19, $t as provide$1, A as catchIf, Ai as add$1, An as scopeMake, Ar as flatMap$3, At as suspend$5, B as flatMap$4, Bi as getOrElse, Bn as get$3, Br as sync$1, Bt as unsafeMakeLatch, C as all$1, Ci as toArray, Cn as isInterrupted, Cr as exitFail, Ct as sandbox, D as async, Di as GenericTag, Dn as squash, Dr as exitZipRight, Dt as scopedWith$1, E as asVoid$1, Ei as unsafeHead, En as reduceWithContext, Er as exitVoid, Et as scoped$4, F as ensuring$2, Fi as fromIterable, Fn as join$3, Fr as map$3, Ft as tryMap, G as forkIn, Gi as match$4, Gn as TypeId$10, Gr as whenEffect, Gt as zipLeft, H as fnUntraced, Hi as isNone, Hn as modify$1, Hr as uninterruptible, Ht as when, I as exit, Ii as splitAt, In as sequential, Ir as onInterrupt, It as tryPromise, J as ignore, Ji as isLeft, Jn as all, Jr as DeferredTypeId$1, Jt as spaced$1, K as forkScoped, Ki as none, Kn as Class, Kr as withFiberRuntime, Kt as zipRight$3, L as fail$5, Li as filter$1, Ln as bind, Lr as scopeClose, Lt as try_, M as die$2, Mi as getOption, Mn as _await$2, Mr as interruptAsFiber, Mt as tap, N as dieMessage, Ni as omit, Nn as inheritAll$1, Nr as interruptFiber, Nt as tapErrorCause, O as catchAll$1, Oi as Reference, On as ensuring$3, Or as fiberId, Ot as serviceOption, P as either, Pi as empty$5, Pn as interruptAllAs$1, Pr as interruptible$1, Pt as timeout, Q as isEffect, Qi as pipeArguments, Qn as isFailure, Qr as ids, Qt as mergeAll$2, R as failCause$4, Ri as flatMap$5, Rn as bindTo, Rr as succeed$4, Rt as uninterruptible$1, S as addFinalizer$1, Si as takeRight, Sn as isDieType, Sr as exitAs, St as runtime, T as as, Ti as unsafeGet, Tn as map$6, Tr as exitSucceed, Tt as scopeWith, U as forEach$2, Ui as isSome, Un as set$1, Ur as uninterruptibleMask$1, Ut as withFiberRuntime$1, V as flatten$4, Vi as getOrUndefined, Vn as make$16, Vr as tap$1, Vt as unsafeMakeSemaphore$1, W as forkDaemon, Wi as map$8, Wn as update$1, Wr as void_$2, Wt as zip$1, X as interruptible, Xi as match$2, Xn as fail$4, Xr as combine, Xt as tagged, Y as interrupt$4, Yi as left, Yn as die, Yr as empty$6, Yt as TaggedError$1, Z as intoDeferred, Zi as right, Zn as failCause$3, Zr as combineAll, Zt as fromEffect$5, _ as decodeSync, _i as of, _n as fail$3, _r as deferredIsDone, _t as raceWith, a as Literal, aa as constFalse, ai as size$2, an as updateContext$1, ar as zipRight$2, at as matchCause, b as acquireRelease, bi as reverse, bn as interrupt$3, br as deferredUnsafeDone, bt as runPromise, c as String$, ca as constVoid, ci as append$1, cn as Scope, cr as asyncInterrupt, ct as negate, d as Union, da as pipe, di as empty$4, dn as close, dr as deferredAwait, dt as orElse, ea as NodeInspectSymbol, ei as none$1, en as provideMerge$1, er as mapBoth, et as makeSemaphore, f as between, fi as filter, fn as extend, fr as deferredComplete, ft as orElseSucceed, gi as map$7, gn as die$1, gr as deferredInterruptWith, gt as race, hi as isEmpty$2, hn as NoSuchElementException, hr as deferredInterrupt, ht as provideService, i as Defect, ia as hasProperty, ii as has, in as unsafeRunPromiseExit, ir as zip$2, it as match$5, j as catchTag, ji as get$5, jn as scopeTag, jr as interrupt$2, jt as sync$2, k as catchAllCause, ki as Tag, kn as forEachConcurrentDiscard, kr as fiberRefLocally, kt as succeed$6, l as Struct, la as dual, li as appendAll, ln as addFinalizer, lr as catchAll, lt as onError, m as optional$1, mi as isChunk, mn as make$18, mr as deferredFailCause, mt as provide$2, n as Boolean$, na as toJSON, ni as make$17, nn as succeed$3, nr as succeed$5, nt as mapBoth$1, o as Never, oa as constTrue, oi as decode, on as forkIn$1, or as as$1, ot as matchCauseEffect, p as decodeUnknown, pi as fromIterable$1, pn as fork, pr as deferredFail, pt as promise, q as gen, qi as some, qn as spanTag, qr as zipRight$1, qt as zipWith, r as Cause, ra as equals, ri as set$2, rn as unsafeFork, rr as void_$3, rt as mapError, s as Number$, sa as constUndefined, si as isFinite, sn as unsafeMakeSemaphore, sr as asVoid, st as matchEffect, t as Array$, ta as format, ti as get$4, tn as scoped$3, tr as match$3, tt as map$4, u as TaggedError, ua as identity, ui as drop, un as addFinalizerExit, ur as currentContext$1, ut as orDie, v as encodeSync, vi as prepend, vn as failureOrCause, vr as deferredMake, vt as repeat, w as annotateLogs, wi as unsafeFromArray, wn as isInterruptedOnly, wr as exitFailCause, wt as scope, x as acquireUseRelease$2, xi as take$2, xn as interruptors, xr as deferredUnsafeMake, xt as runPromiseExit$1, y as _void, yi as reduce, yn as flipCauseOption, yr as deferredSucceed, yt as retry, z as fiberIdWith, zi as fromNullable, zn as sleep, zr as suspend$4, zt as uninterruptibleMask } from "./chunks/Schema-C6fLlQzx.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import * as Crypto from "node:crypto";
@@ -10253,6 +10253,13 @@ const resolveHistoryRef = (arg, entries) => {
10253
10253
  if (byBranch !== void 0) return succeed$6(byBranch);
10254
10254
  return fail$5(/* @__PURE__ */ new Error(`gtd history: no history ref matches "${arg}"`));
10255
10255
  };
10256
+ /** Every kept process, or those `refs` name — a branch name meaning every process kept from it, not just its newest. */
10257
+ const selectHistoryEntries = (refs) => gen(function* () {
10258
+ const entries = yield* historyEntries;
10259
+ if (refs.length === 0) return entries;
10260
+ const chosen = (yield* forEach$2(refs, (ref) => resolveHistoryRef(ref, entries).pipe(map$4((hit) => hit.branch === ref ? entries.filter((e) => e.branch === ref) : [hit])))).flat();
10261
+ return chosen.filter((e, i) => chosen.findIndex((o) => o.ref === e.ref) === i);
10262
+ });
10256
10263
  const seven = (hash) => hash.slice(0, 7);
10257
10264
  const runHistoryListCommand = (json, out) => gen(function* () {
10258
10265
  const entries = yield* historyEntries;
@@ -10288,7 +10295,7 @@ const runHistoryFetchCommand = (remote, out) => sync$2(() => {
10288
10295
  });
10289
10296
  //#endregion
10290
10297
  //#region src/cli/Metrics.ts
10291
- const QUALITY = ".gtd/QUALITY.md";
10298
+ const QUALITY$1 = ".gtd/QUALITY.md";
10292
10299
  const LENS = /^((?:.*\.)?quality\.[^.]+)\.reviewing$/;
10293
10300
  const CONTINUE = /(^|\.)continue\./;
10294
10301
  const inSeam = (step) => step.startsWith("seam.");
@@ -10435,12 +10442,12 @@ const readProcessMetrics = (entry) => gen(function* () {
10435
10442
  const git = yield* GitService;
10436
10443
  const base = getOrUndefined(yield* git.readRefOption(`${entry.entry}^`));
10437
10444
  const commits = yield* git.commitHistory(base, entry.tip);
10438
- const quality = (hash) => git.readFileAtRef(hash, QUALITY).pipe(map$4((content) => content), orElseSucceed(() => void 0));
10445
+ const quality = (hash) => git.readFileAtRef(hash, QUALITY$1).pipe(map$4((content) => content), orElseSucceed(() => void 0));
10439
10446
  let count = chunks(base === void 0 ? void 0 : yield* quality(base));
10440
10447
  const read = [];
10441
10448
  for (const { hash, message, touched } of commits) {
10442
10449
  const before = count;
10443
- if (touched.includes(QUALITY)) count = chunks(yield* quality(hash));
10450
+ if (touched.includes(QUALITY$1)) count = chunks(yield* quality(hash));
10444
10451
  read.push({
10445
10452
  message,
10446
10453
  findings: Math.max(0, count - before)
@@ -10466,9 +10473,7 @@ const line = (label, m) => {
10466
10473
  ].join("; ");
10467
10474
  };
10468
10475
  const runHistoryMetricsCommand = (refs, json, out) => gen(function* () {
10469
- const entries = yield* historyEntries;
10470
- const chosen = refs.length === 0 ? entries : (yield* forEach$2(refs, (ref) => resolveHistoryRef(ref, entries).pipe(map$4((hit) => hit.branch === ref ? entries.filter((e) => e.branch === ref) : [hit])))).flat();
10471
- const processes = yield* forEach$2(chosen.filter((e, i) => chosen.findIndex((o) => o.ref === e.ref) === i), (e) => readProcessMetrics(e).pipe(map$4((metrics) => ({
10476
+ const processes = yield* forEach$2(yield* selectHistoryEntries(refs), (e) => readProcessMetrics(e).pipe(map$4((metrics) => ({
10472
10477
  ref: e.ref,
10473
10478
  entry: e.entry,
10474
10479
  branch: e.branch,
@@ -10489,6 +10494,381 @@ const runHistoryMetricsCommand = (refs, json, out) => gen(function* () {
10489
10494
  out.write(`${lines.join("\n")}\n`);
10490
10495
  });
10491
10496
  //#endregion
10497
+ //#region src/cli/Judges.ts
10498
+ const FEEDBACK = ".gtd/FEEDBACK.md";
10499
+ const QUALITY = ".gtd/QUALITY.md";
10500
+ const REVIEW = ".gtd/REVIEW.md";
10501
+ const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
10502
+ const JUDGE_STEPS = [
10503
+ ["retry", /(^|\.)health\.judge$/],
10504
+ ["triage", /(^|\.)quality\.triage$/],
10505
+ ["clear", /(^|\.)design\.clear$/]
10506
+ ];
10507
+ const answerOf = (answer) => typeof answer === "boolean" ? answer ? "yes" : "no" : String(answer);
10508
+ const verdictOf = (answer, p) => ({
10509
+ answer: answerOf(answer),
10510
+ p
10511
+ });
10512
+ const THRESHOLDS = {
10513
+ judgeIdenticalMinP: {
10514
+ judge: "retry",
10515
+ decide: (answer, p, minP) => answered(verdictOf(answer, p), "identical", minP) ? "identical" : "retry",
10516
+ labels: {
10517
+ identical: [
10518
+ "escalation",
10519
+ "escalations",
10520
+ "an escalation"
10521
+ ],
10522
+ retry: [
10523
+ "plain retry",
10524
+ "plain retries",
10525
+ "a plain retry"
10526
+ ]
10527
+ }
10528
+ },
10529
+ qualityTriageMinP: {
10530
+ judge: "triage",
10531
+ decide: (answer, p, minP) => ["mechanical", "noise"].find((k) => answered(verdictOf(answer, p), k, minP)) ?? "design",
10532
+ labels: {
10533
+ mechanical: [
10534
+ "auto-fix",
10535
+ "auto-fixes",
10536
+ "an auto-fix"
10537
+ ],
10538
+ noise: [
10539
+ "noise drop",
10540
+ "noise drops",
10541
+ "a noise drop"
10542
+ ],
10543
+ design: [
10544
+ "note",
10545
+ "notes",
10546
+ "a note"
10547
+ ]
10548
+ }
10549
+ }
10550
+ };
10551
+ const UNTITLED = "Quality findings";
10552
+ /** The quality lap's own numbering: prose outside every `## ` section is finding Q1. */
10553
+ const findingsOf = (text) => {
10554
+ const sections = headingSectionBodies(text);
10555
+ const stray = sections.reduce((rest, s) => rest.replace(s.body, "\n"), text).split("\n").filter((line) => !/^#\s/.test(line)).join("\n").trim();
10556
+ return [...stray === "" ? [] : [{
10557
+ title: UNTITLED,
10558
+ body: stray
10559
+ }], ...sections.map((s) => ({
10560
+ title: s.title,
10561
+ body: s.body.split("\n").slice(1).join("\n").trim()
10562
+ }))].map((f, i) => ({
10563
+ id: `Q${i + 1}`,
10564
+ ...f
10565
+ }));
10566
+ };
10567
+ /** The criteria list, one item per line, wrapping undone: prose around it is no criterion. */
10568
+ const criteriaOf = (text) => {
10569
+ if (text === void 0) return void 0;
10570
+ const section = headingSectionBodies(text).find((s) => /^acceptance criteria$/i.test(s.title.trim()));
10571
+ if (section === void 0) return void 0;
10572
+ const items = [];
10573
+ let open = false;
10574
+ for (const line of section.body.split("\n").slice(1)) {
10575
+ const item = /^\s*[-*+]\s+(.*)$/.exec(line);
10576
+ if (item !== null) items.push(item[1].trim());
10577
+ else if (open && line.trim() !== "") items.push(`${items.pop()} ${line.trim()}`);
10578
+ open = item !== null || open && line.trim() !== "";
10579
+ }
10580
+ return items.map((i) => i.replace(/\s+/g, " ")).join("\n");
10581
+ };
10582
+ const criteriaChanged = (before, after) => criteriaOf(before) !== criteriaOf(after);
10583
+ const chunkLines = (doc, title) => headingSectionBodies(doc).find((s) => s.title === title)?.body.split("\n").slice(1).map((l) => l.trim()).filter((l) => l !== "") ?? [];
10584
+ const retryOutcome = (reads, i, scope) => {
10585
+ const isCheck = (r) => r.at === `${scope}health.check` || r.at === `${scope}health.recheck`;
10586
+ const after = reads.slice(i + 1);
10587
+ const fix = after.findIndex((r) => isCheck(r) && r.commit.feedback === void 0 || r.actor === "agent" && !r.at.startsWith(`${scope}health.`));
10588
+ if (fix === -1 || after[fix].actor !== "agent") return null;
10589
+ const check = after.findIndex((r, k) => k > fix && r.at === `${scope}health.check`);
10590
+ if (check === -1) return null;
10591
+ if (after.slice(0, check).some((r) => r.actor === "human" && r.commit.code === true)) return null;
10592
+ return after[check].commit.feedback === void 0 ? "overturned" : "held";
10593
+ };
10594
+ const RESHOWN = {
10595
+ mechanical: "Fixed automatically",
10596
+ noise: "Dropped as noise"
10597
+ };
10598
+ const isItem = (line) => line.startsWith("- ");
10599
+ /** The line at `at` and the note lines under it, up to the next list item. */
10600
+ const block = (lines, at) => {
10601
+ const next = lines.findIndex((l, k) => k > at && isItem(l));
10602
+ return lines.slice(at, next === -1 ? void 0 : next).join("\n");
10603
+ };
10604
+ /** Whether the review edited `title`'s line in `chunk`, or wrote under it; null when the review did not list it there. */
10605
+ const reviewedLine = (review, chunk, title) => {
10606
+ const before = chunkLines(review.before, chunk);
10607
+ const at = before.findIndex((l) => l.startsWith(`- ${title} (`));
10608
+ if (at === -1) return null;
10609
+ const after = chunkLines(review.after, chunk);
10610
+ const kept = after.indexOf(before[at]);
10611
+ return (kept === -1 ? after.some((l) => !before.includes(l) && l.startsWith(`- ${title}`) && !before.some((o, k) => k !== at && o.startsWith(`- ${title}`) && l.startsWith(o.split(" (")[0]))) : block(after, kept) !== block(before, at)) ? "overturned" : "held";
10612
+ };
10613
+ const triageOutcome = (reads, i, answer, finding) => {
10614
+ const chunk = RESHOWN[answer];
10615
+ if (chunk === void 0 || finding === void 0) return null;
10616
+ const next = reads.slice(i + 1).find((r) => judgeOf(r.at) === "triage" || r.actor === "human" && r.commit.review !== void 0);
10617
+ const review = next?.actor === "human" ? next.commit.review : void 0;
10618
+ return review === void 0 ? null : reviewedLine(review, chunk, finding.title);
10619
+ };
10620
+ const CLEAR_MIN_P = .8;
10621
+ const clearOutcome = (reads, i, v) => {
10622
+ if (!answered(verdictOf(v.answer, v.p), "yes", CLEAR_MIN_P)) return null;
10623
+ if (!reads[i].to.endsWith("design.normalise")) return null;
10624
+ return reads.slice(i + 1).some((r) => r.actor === "human" && /(^|\.)design\./.test(r.at) && r.commit.criteriaChanged === true) ? "overturned" : "held";
10625
+ };
10626
+ const judgeOf = (step) => JUDGE_STEPS.find(([, pattern]) => pattern.test(step))?.[0];
10627
+ const reportsBefore = (reads, i, scope) => reads.slice(0, i).filter((r) => (r.at === `${scope}health.check` || r.at === `${scope}health.recheck`) && r.commit.feedback !== void 0).map((r) => r.commit.feedback);
10628
+ /** Oldest first: every verdict of a calibrated judge, with its outcome. */
10629
+ const processJudgments = (commits) => {
10630
+ const reads = commits.map((commit) => {
10631
+ const message = parseCommitMessage(commit.message);
10632
+ const subject = message.parsed;
10633
+ return {
10634
+ commit,
10635
+ actor: subject?.actor,
10636
+ at: subject === void 0 ? "" : subject.from ?? subject.to,
10637
+ to: subject?.to ?? "",
10638
+ judge: message.judge
10639
+ };
10640
+ });
10641
+ const sketch = commits[0]?.sketch;
10642
+ return reads.flatMap((r, i) => {
10643
+ const judge = judgeOf(r.at);
10644
+ if (judge === void 0) return [];
10645
+ const scope = r.at.slice(0, r.at.length - r.at.split(".").slice(-2).join(".").length);
10646
+ const at = {
10647
+ reads,
10648
+ i,
10649
+ scope,
10650
+ sketch
10651
+ };
10652
+ return r.judge.map((v) => ({
10653
+ judge,
10654
+ commit: r.commit.hash,
10655
+ step: r.at,
10656
+ question: v.id,
10657
+ ...JUDGES[judge](at, v),
10658
+ answer: v.answer,
10659
+ p: v.p
10660
+ }));
10661
+ });
10662
+ };
10663
+ const NOT_COMPARABLE = { tailsNotComparable: "true" };
10664
+ const field = (key, value) => value === void 0 ? {} : { [key]: value };
10665
+ const JUDGES = {
10666
+ retry: ({ reads, i, scope }, v) => {
10667
+ const reports = reportsBefore(reads, i, scope);
10668
+ const previous = reports.at(-2);
10669
+ const current = reports.at(-1);
10670
+ const comparable = previous?.trim() !== "" && current?.trim() !== "";
10671
+ const known = previous !== void 0 && current !== void 0;
10672
+ return {
10673
+ state: comparable ? {
10674
+ ...field("previous", previous),
10675
+ ...field("current", current)
10676
+ } : NOT_COMPARABLE,
10677
+ outcome: known && comparable && answerOf(v.answer) === "identical" ? retryOutcome(reads, i, scope) : null
10678
+ };
10679
+ },
10680
+ triage: ({ reads, i }, v) => {
10681
+ const finding = reads[i].commit.findings?.find((f) => f.id === v.id);
10682
+ return {
10683
+ state: field(v.id, finding && `## ${finding.title}\n\n${finding.body}`),
10684
+ outcome: triageOutcome(reads, i, answerOf(v.answer), finding)
10685
+ };
10686
+ },
10687
+ clear: ({ reads, i, sketch }, v) => ({
10688
+ state: field("sketch", sketch),
10689
+ outcome: clearOutcome(reads, i, v)
10690
+ })
10691
+ };
10692
+ const rate = (overturned, scored) => Math.round(overturned / scored * 1e3) / 1e3;
10693
+ const bucketOf = (p) => Math.min(9, Math.max(0, Math.floor(p * 10))) / 10;
10694
+ const calibrate = (judgments) => {
10695
+ const of = (judge) => {
10696
+ const all = judgments.filter((j) => j.judge === judge);
10697
+ const scored = all.filter((j) => j.outcome !== null);
10698
+ const buckets = /* @__PURE__ */ new Map();
10699
+ for (const j of scored) {
10700
+ const b = buckets.get(bucketOf(j.p)) ?? {
10701
+ scored: 0,
10702
+ overturned: 0
10703
+ };
10704
+ b.scored++;
10705
+ if (j.outcome === "overturned") b.overturned++;
10706
+ buckets.set(bucketOf(j.p), b);
10707
+ }
10708
+ const overturned = scored.filter((j) => j.outcome === "overturned").length;
10709
+ return {
10710
+ verdicts: all.length,
10711
+ scored: scored.length,
10712
+ overturned,
10713
+ buckets: [...buckets.entries()].sort(([a], [b]) => a - b).map(([p, b]) => ({
10714
+ p,
10715
+ ...b,
10716
+ rate: rate(b.overturned, b.scored)
10717
+ }))
10718
+ };
10719
+ };
10720
+ return {
10721
+ retry: of("retry"),
10722
+ triage: of("triage"),
10723
+ clear: of("clear")
10724
+ };
10725
+ };
10726
+ /** Every verdict of `threshold`'s judge as `[decided then, decided under value]`. */
10727
+ const redecided = (processes, name, value) => {
10728
+ const threshold = THRESHOLDS[name];
10729
+ const now = numeric(value, Infinity);
10730
+ return processes.filter(({ vars }) => Object.hasOwn(vars, name)).flatMap(({ vars, judgments }) => judgments.filter((j) => j.judge === threshold.judge && j.state !== NOT_COMPARABLE).map((j) => {
10731
+ const was = numeric(vars[name], Infinity);
10732
+ return [
10733
+ threshold.decide(j.answer, j.p, was),
10734
+ threshold.decide(j.answer, j.p, now),
10735
+ j
10736
+ ];
10737
+ }));
10738
+ };
10739
+ const whatIf = (processes, name, value) => {
10740
+ const changes = /* @__PURE__ */ new Map();
10741
+ for (const [from, to, j] of redecided(processes, name, value)) {
10742
+ if (from === to) continue;
10743
+ const c = changes.get(`${from} ${to}`) ?? {
10744
+ from,
10745
+ to,
10746
+ count: 0,
10747
+ overturned: 0
10748
+ };
10749
+ changes.set(`${from} ${to}`, {
10750
+ ...c,
10751
+ count: c.count + 1,
10752
+ overturned: c.overturned + (j.outcome === "overturned" ? 1 : 0)
10753
+ });
10754
+ }
10755
+ return {
10756
+ var: name,
10757
+ value,
10758
+ judge: THRESHOLDS[name].judge,
10759
+ changes: [...changes.values()]
10760
+ };
10761
+ };
10762
+ const changeLine = (labels, c) => {
10763
+ const [one, many] = labels[c.from];
10764
+ const [, toMany, toOne] = labels[c.to];
10765
+ const what = c.count === 1 ? `1 ${one} becomes ${toOne}` : `${c.count} ${many} become ${toMany}`;
10766
+ return c.overturned > 0 ? `${what} (${c.overturned} overturned)` : what;
10767
+ };
10768
+ const reportFact = (at, c, from) => /(^|\.)health\.(re)?check$/.test(from) && c.touched.includes(FEEDBACK) ? at(c.hash, FEEDBACK).pipe(map$4((r) => r === void 0 ? {} : { feedback: stripStamp(r) })) : succeed$6({});
10769
+ const findingsFact = (at, parent, from) => judgeOf(from) === "triage" ? at(parent, QUALITY).pipe(map$4((q) => ({ findings: findingsOf(q ?? "") }))) : succeed$6({});
10770
+ const reviewFact = (at, c, parent, human) => human && c.touched.includes(REVIEW) ? all$1([at(parent, REVIEW), at(c.hash, REVIEW)]).pipe(map$4(([before, after]) => ({ review: {
10771
+ before: before ?? "",
10772
+ after: after ?? ""
10773
+ } }))) : succeed$6({});
10774
+ const criteriaFact = (at, c, parent, human) => human && c.touched.includes(REQUIREMENTS) ? all$1([at(parent, REQUIREMENTS), at(c.hash, REQUIREMENTS)]).pipe(map$4(([before, after]) => ({ criteriaChanged: criteriaChanged(before, after) }))) : succeed$6({});
10775
+ const sketchFact = (at, c, opening) => {
10776
+ const todo = c.touched.filter((p) => /(^|\/)TODO\.md$/.test(p));
10777
+ return opening && todo.length === 1 ? at(c.hash, todo[0]).pipe(map$4((s) => s === void 0 ? {} : { sketch: s })) : succeed$6({});
10778
+ };
10779
+ /** Only the files a verdict's outcome or state reads, and only at the commits that can matter. */
10780
+ const commitFacts = (at, c, parent, opening) => {
10781
+ const step = parseCommitMessage(c.message).parsed;
10782
+ const from = step === void 0 ? "" : step.from ?? step.to;
10783
+ return all$1([
10784
+ reportFact(at, c, from),
10785
+ findingsFact(at, parent, from),
10786
+ reviewFact(at, c, parent, step?.actor === "human"),
10787
+ criteriaFact(at, c, parent, step?.actor === "human"),
10788
+ sketchFact(at, c, opening)
10789
+ ]).pipe(map$4((facts) => Object.assign({
10790
+ hash: c.hash,
10791
+ message: c.message
10792
+ }, c.touched.some((p) => p !== ".gtd" && !p.startsWith(".gtd/")) ? { code: true } : {}, ...facts)));
10793
+ };
10794
+ const readProcessJudgments = (entry) => gen(function* () {
10795
+ const git = yield* GitService;
10796
+ const base = getOrUndefined(yield* git.readRefOption(`${entry.entry}^`));
10797
+ const commits = yield* git.commitHistory(base, entry.tip);
10798
+ const vars = commits[0] === void 0 ? {} : parseCommitMessage(commits[0].message).vars;
10799
+ if (!commits.some((c) => {
10800
+ const m = parseCommitMessage(c.message);
10801
+ return m.judge.length > 0 && judgeOf(m.parsed?.from ?? m.parsed?.to ?? "") !== void 0;
10802
+ })) return {
10803
+ vars,
10804
+ judgments: []
10805
+ };
10806
+ const at = (ref, path) => ref === void 0 ? succeed$6(void 0) : git.readFileAtRef(ref, path).pipe(map$4((content) => content), orElseSucceed(() => void 0));
10807
+ const read = yield* forEach$2(commits, (commit, i) => commitFacts(at, commit, i === 0 ? base : commits[i - 1].hash, i === 0));
10808
+ return {
10809
+ vars,
10810
+ judgments: processJudgments(read)
10811
+ };
10812
+ });
10813
+ const stripStamp = (text) => text.replace(/\n<!-- gtd check [0-9a-f]+ -->\n?$/, "\n");
10814
+ const NAMES = [
10815
+ "retry",
10816
+ "triage",
10817
+ "clear"
10818
+ ];
10819
+ const calibrationLine = (name, c) => {
10820
+ const buckets = c.buckets.map((b) => `${b.p.toFixed(1)}-${(b.p + .1).toFixed(1)}: ${b.overturned}/${b.scored}`).join(", ");
10821
+ return `${name}: ${c.verdicts} verdicts, ${c.scored} scored, ${c.overturned} overturned${buckets === "" ? "" : ` (${buckets})`}`;
10822
+ };
10823
+ const runHistoryJudgesCommand = (refs, vars, jsonl, json, out) => gen(function* () {
10824
+ const processes = yield* forEach$2(yield* selectHistoryEntries(refs), (e) => readProcessJudgments(e).pipe(map$4((p) => ({
10825
+ ...p,
10826
+ entry: e.entry,
10827
+ ref: e.ref,
10828
+ branch: e.branch
10829
+ }))), { concurrency: 4 });
10830
+ if (jsonl) {
10831
+ for (const p of processes) for (const { judge, commit, step, question, state, answer, p: pr, outcome } of p.judgments) out.write(`${JSON.stringify({
10832
+ process: p.entry,
10833
+ judge,
10834
+ commit,
10835
+ step,
10836
+ question,
10837
+ state,
10838
+ answer,
10839
+ p: pr,
10840
+ outcome
10841
+ })}\n`);
10842
+ return;
10843
+ }
10844
+ const judges = calibrate(processes.flatMap((p) => p.judgments));
10845
+ const whatIfs = Object.entries(vars).map(([name, value]) => whatIf(processes, name, value));
10846
+ if (yield* writeJson(json, out, {
10847
+ processes: processes.map((p) => ({
10848
+ ref: p.ref,
10849
+ entry: p.entry,
10850
+ branch: p.branch,
10851
+ judges: calibrate(p.judgments)
10852
+ })),
10853
+ total: {
10854
+ processes: processes.length,
10855
+ judges
10856
+ },
10857
+ whatIf: whatIfs
10858
+ })) return;
10859
+ if (processes.length === 0) {
10860
+ out.write("no history refs; run gtd history fetch | sh to pull them\n");
10861
+ return;
10862
+ }
10863
+ const lines = NAMES.map((name) => calibrationLine(name, judges[name]));
10864
+ for (const w of whatIfs) {
10865
+ const labels = THRESHOLDS[w.var].labels;
10866
+ const changes = w.changes.map((c) => changeLine(labels, c)).join(", ");
10867
+ lines.push(`what-if ${w.var}=${printable(w.value)}: ${changes || "no decision changes"}`);
10868
+ }
10869
+ out.write(`${lines.join("\n")}\n`);
10870
+ });
10871
+ //#endregion
10492
10872
  //#region src/cli/Program.ts
10493
10873
  const selfGtd = () => {
10494
10874
  const script = process.argv[1];
@@ -11141,6 +11521,7 @@ const dispatchVoidCommand = (command, json, out) => {
11141
11521
  case "historyList": return runHistoryListCommand(json, out);
11142
11522
  case "historyShow": return runHistoryShowCommand(command.ref, json, out);
11143
11523
  case "historyMetrics": return runHistoryMetricsCommand(command.refs, json, out);
11524
+ case "historyJudges": return runHistoryJudgesCommand(command.refs, command.vars, command.jsonl, json, out);
11144
11525
  case "historyFetch": return runHistoryFetchCommand(command.remote, out);
11145
11526
  }
11146
11527
  };
@@ -11189,6 +11570,7 @@ const JSON_KINDS = /* @__PURE__ */ new Set([
11189
11570
  "historyList",
11190
11571
  "historyShow",
11191
11572
  "historyMetrics",
11573
+ "historyJudges",
11192
11574
  "strays"
11193
11575
  ]);
11194
11576
  const FLAGS = [
@@ -11198,11 +11580,11 @@ const FLAGS = [
11198
11580
  repeatable: false,
11199
11581
  scope: (kind) => JSON_KINDS.has(kind),
11200
11582
  decode: ([raw]) => right(raw ?? ""),
11201
- scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd slices`/`gtd history list`/`gtd history show`/`gtd history metrics`/`gtd strays` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
11583
+ scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd slices`/`gtd history list`/`gtd history show`/`gtd history metrics`/`gtd history judges`/`gtd strays` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
11202
11584
  valueHint: "<path>",
11203
11585
  help: [
11204
11586
  "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd slices/gtd history",
11205
- "list/gtd history show/gtd history metrics/gtd strays only) output structured JSON. Bare --json prints the whole document;",
11587
+ "list/gtd history show/gtd history metrics/gtd history judges/gtd strays only) output structured JSON. Bare --json prints the whole document;",
11206
11588
  "--json=<path> (a dotted key path into that document, e.g.",
11207
11589
  "kind, content, session.id) prints just that value: a",
11208
11590
  "scalar raw and unquoted, a boolean as true/false, a list",
@@ -11380,7 +11762,7 @@ const FLAGS = [
11380
11762
  name: "--var",
11381
11763
  arity: 1,
11382
11764
  repeatable: true,
11383
- scope: (kind) => kind === "start",
11765
+ scope: (kind) => kind === "start" || kind === "historyJudges",
11384
11766
  decode: (raws) => {
11385
11767
  const vars = {};
11386
11768
  const seen = /* @__PURE__ */ new Set();
@@ -11403,15 +11785,28 @@ const FLAGS = [
11403
11785
  }
11404
11786
  return right(vars);
11405
11787
  },
11406
- scopeError: "gtd: --var requires --workflow",
11788
+ scopeError: "gtd: --var requires --workflow (or `gtd history judges`)",
11407
11789
  valueHint: "<name>=<value>",
11408
11790
  help: [
11409
11791
  "(with --workflow; repeatable) pin a process setting for the",
11410
11792
  "new process; the name must already be declared by the",
11411
11793
  "workflow's defaults (environment settings are not",
11412
- "pinnable)"
11794
+ "pinnable). (gtd history judges) re-decide past verdicts",
11795
+ "under this threshold: judgeIdenticalMinP or",
11796
+ "qualityTriageMinP"
11413
11797
  ]
11414
11798
  },
11799
+ {
11800
+ name: "--jsonl",
11801
+ arity: 0,
11802
+ repeatable: false,
11803
+ scope: (kind) => kind === "historyJudges",
11804
+ decode: () => right(true),
11805
+ scopeError: "gtd: --jsonl is only valid for `gtd history judges`",
11806
+ valueHint: "",
11807
+ help: ["(gtd history judges only) print one JSON line per verdict", "instead of the report"],
11808
+ conflicts: ["--json", "--var"]
11809
+ },
11415
11810
  {
11416
11811
  name: "--open-questions",
11417
11812
  arity: 0,
@@ -11872,6 +12267,26 @@ const COMMAND_ROWS = [
11872
12267
  "} as one document; writes nothing"
11873
12268
  ]
11874
12269
  },
12270
+ {
12271
+ token: "history judges",
12272
+ kind: "historyJudges",
12273
+ arity: {
12274
+ name: "ref",
12275
+ many: true
12276
+ },
12277
+ details: [
12278
+ "Calibrate the judges of the kept processes from their",
12279
+ "recorded Gtd-Judge verdicts: per judge (retry, triage, clear)",
12280
+ "and p bucket, how many verdicts later events overturned.",
12281
+ "--var <name>=<value> re-decides every past verdict under",
12282
+ "another threshold and prints what would have changed;",
12283
+ "--jsonl prints one { process, judge, commit, step, question,",
12284
+ "state, answer, p, outcome } line per verdict instead. <ref>",
12285
+ "is read as by history metrics. Never replays and asks no",
12286
+ "model. --json prints { processes, total, whatIf }; writes",
12287
+ "nothing"
12288
+ ]
12289
+ },
11875
12290
  {
11876
12291
  token: "history fetch",
11877
12292
  kind: "historyFetch",
@@ -12069,7 +12484,7 @@ const parseArgv = (argv) => {
12069
12484
  };
12070
12485
  const isHistory = first === "history";
12071
12486
  const row = first === void 0 ? void 0 : commandByToken(isHistory ? `history ${positionals[1] ?? ""}` : first);
12072
- if (isHistory && row === void 0) return usagePlan("gtd history: expected a subcommand — list, show <ref>, metrics [ref...] or fetch [remote]", jsonSeen);
12487
+ if (isHistory && row === void 0) return usagePlan("gtd history: expected a subcommand — list, show <ref>, metrics [ref...], judges [ref...] or fetch [remote]", jsonSeen);
12073
12488
  const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer";
12074
12489
  const selectsStart = workflowPresent && first === void 0;
12075
12490
  const judgeRun = row?.kind === "judge" && positionals[1] === "run";
@@ -12211,6 +12626,24 @@ const parseArgv = (argv) => {
12211
12626
  json,
12212
12627
  verbose
12213
12628
  };
12629
+ if (kind === "historyJudges") {
12630
+ const vars = bag["--var"] ?? {};
12631
+ const unknown = Object.keys(vars).find((name) => !Object.hasOwn(THRESHOLDS, name));
12632
+ if (unknown !== void 0) return usagePlan(`gtd history judges: --var ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`, jsonSeen);
12633
+ const nan = Object.entries(vars).find(([, value]) => value.trim() === "" || !Number.isFinite(Number(value)));
12634
+ if (nan !== void 0) return usagePlan(`gtd history judges: --var ${nan[0]} must be a number — got "${nan[1]}"`, jsonSeen);
12635
+ return {
12636
+ kind: "command",
12637
+ command: {
12638
+ kind: "historyJudges",
12639
+ refs: restPositionals,
12640
+ vars,
12641
+ jsonl: bag["--jsonl"] ?? false
12642
+ },
12643
+ json,
12644
+ verbose
12645
+ };
12646
+ }
12214
12647
  if (kind === "historyFetch") return {
12215
12648
  kind: "command",
12216
12649
  command: {
@@ -13836,18 +14269,6 @@ const splitBlocks = (script) => {
13836
14269
  if (current.trim().length > 0) blocks.push(current.trim());
13837
14270
  return blocks;
13838
14271
  };
13839
- /** `deferNotesScript`: append its one section to `.gtd/TODO.md`, reading the section's lines off the final `printf`. */
13840
- const DEFER_HEAD = "mkdir -p .gtd\nif [ -s .gtd/TODO.md ]; then";
13841
- const recognizeDeferNotes = (repo, block) => {
13842
- if (!block.startsWith(DEFER_HEAD)) return void 0;
13843
- const at = block.lastIndexOf("\nprintf '%s\\n' ");
13844
- if (at === -1) return void 0;
13845
- const lines = extractQuotedTokens(block.slice(at + 15));
13846
- const old = repo.readFile(".gtd/TODO.md") ?? "";
13847
- const sep = old === "" ? "" : `${old.endsWith("\n") ? "" : "\n"}\n`;
13848
- repo.writeFile(".gtd/TODO.md", `${old}${sep}${lines.join("\n")}\n`);
13849
- return { kind: "applied" };
13850
- };
13851
14272
  /** `mkdir -p <dir>` then `printf '%s\n' <quoted lines> > <path>`: a flow's one-file write. Re-rendered from what was extracted, so any other shape stays unrecognized. */
13852
14273
  const recognizeWriteLines = (repo, block) => {
13853
14274
  const [mkdir, printf, ...rest] = block.split("\n");
@@ -13867,7 +14288,6 @@ const recognizersFor = (repo, commands, git) => [
13867
14288
  (block) => recognizeFileExistsGuard(repo, block),
13868
14289
  (block) => recognizeBinaryGuard(commands, block),
13869
14290
  (block) => git ? recognizeGitBuilders(repo, block) : void 0,
13870
- (block) => recognizeDeferNotes(repo, block),
13871
14291
  (block) => recognizeWriteLines(repo, block),
13872
14292
  (block) => recognizeFailurePromptWrapper(repo, commands, block),
13873
14293
  (block) => recognizePresentationSubshell(repo, commands, block, git),