@pmelab/gtd 38.1.0 → 39.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/dist/testkit.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { applyScript, checkScript, configProblems, configure, installContext, isFactoryWorkflow, moveScript, protocolMismatch, removeScript, restoreScript, revertScript, showScript, t as flows_exports } 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
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-DVILanVf.mjs";
4
- import { $ as logWarning, $i as NodeInspectSymbol, $n as map$5, $r as make$19, $t as provide$1, A as catchIf, Ai as add, 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 none, Gn as TypeId$10, Gr as whenEffect, Gt as zipLeft, H as fnUntraced, Hi as isSome, 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 left, Jn as all, Jr as DeferredTypeId$1, Jt as spaced$1, K as forkScoped, Ki as some, 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 BaseProto, 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 map$8, Un as set$1, Ur as uninterruptibleMask$1, Ut as withFiberRuntime$1, V as flatten$4, Vi as isNone, Vn as make$16, Vr as tap$1, Vt as unsafeMakeSemaphore$1, W as forkDaemon, Wi as match$4, Wn as update$1, Wr as void_$2, Wt as zip$1, X as interruptible, Xi as right, Xn as fail$4, Xr as combine, Xt as tagged, Y as interrupt$4, Yi as match$2, Yn as die, Yr as empty$6, Yt as TaggedError$1, Z as intoDeferred, Zi as pipeArguments, 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 constTrue, 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 dual, ci as append$1, cn as Scope, cr as asyncInterrupt, ct as negate, d as Union, di as empty$4, dn as close, dr as deferredAwait, dt as orElse, ea as format, 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 constFalse, 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 identity, 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 equals, ni as make$17, nn as succeed$3, nr as succeed$5, nt as mapBoth$1, o as Never, oa as constUndefined, 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 isLeft, qn as spanTag, qr as zipRight$1, qt as zipWith, r as Cause, ra as hasProperty, ri as set$2, rn as unsafeFork, rr as void_$3, rt as mapError, s as Number$, sa as constVoid, si as isFinite, sn as unsafeMakeSemaphore, sr as asVoid, st as matchEffect, t as Array$, ta as toJSON, ti as get$4, tn as scoped$3, tr as match$3, tt as map$4, u as TaggedError, ua as pipe, 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-DO5ttJaI.mjs";
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";
7
7
  import { createHash } from "node:crypto";
@@ -2686,7 +2686,7 @@ const get$1 = (self_) => {
2686
2686
  case "Acquired":
2687
2687
  self.state.refCount++;
2688
2688
  return self.state.fiber ? as$1(interruptFiber(self.state.fiber), self.state) : succeed$4(self.state);
2689
- case "Empty": return scopeMake().pipe(bindTo("scope"), bind("value", ({ scope }) => restore(fiberRefLocally(self.acquire, currentContext$1, add(self.context, scopeTag, scope)))), map$3(({ scope, value }) => {
2689
+ case "Empty": return scopeMake().pipe(bindTo("scope"), bind("value", ({ scope }) => restore(fiberRefLocally(self.acquire, currentContext$1, add$1(self.context, scopeTag, scope)))), map$3(({ scope, value }) => {
2690
2690
  const state = {
2691
2691
  _tag: "Acquired",
2692
2692
  value,
@@ -10190,6 +10190,13 @@ const writeSelection = (out, fields, path) => gen(function* () {
10190
10190
  if (selection.kind === "value") out.write(`${selection.text}\n`);
10191
10191
  else if (selection.kind === "unknown") return yield* fail$5(new SelectorUsageError(`gtd: unknown --json selector "${selection.path}" — see \`gtd --help\``));
10192
10192
  });
10193
+ /** Writes `document` per `json` (bare → whole, `=path` → one value); returns false for `off`. */
10194
+ const writeJson = (json, out, document) => gen(function* () {
10195
+ if (json.kind === "off") return false;
10196
+ if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
10197
+ else yield* writeSelection(out, document, json.path);
10198
+ return true;
10199
+ });
10193
10200
  //#endregion
10194
10201
  //#region src/cli/Strays.ts
10195
10202
  const resolveSide = (git, side) => git.resolveRef(`${side}^{commit}`).pipe(mapError(() => new GtdError(`gtd strays: unknown revision '${side}'`)));
@@ -10247,13 +10254,6 @@ const resolveHistoryRef = (arg, entries) => {
10247
10254
  return fail$5(/* @__PURE__ */ new Error(`gtd history: no history ref matches "${arg}"`));
10248
10255
  };
10249
10256
  const seven = (hash) => hash.slice(0, 7);
10250
- /** Writes `document` per `json` (bare → whole, `=path` → one value); returns false for `off`. */
10251
- const writeJson = (json, out, document) => gen(function* () {
10252
- if (json.kind === "off") return false;
10253
- if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
10254
- else yield* writeSelection(out, document, json.path);
10255
- return true;
10256
- });
10257
10257
  const runHistoryListCommand = (json, out) => gen(function* () {
10258
10258
  const entries = yield* historyEntries;
10259
10259
  if (yield* writeJson(json, out, entries)) return;
@@ -10266,13 +10266,12 @@ const runHistoryListCommand = (json, out) => gen(function* () {
10266
10266
  const runHistoryShowCommand = (ref, json, out) => gen(function* () {
10267
10267
  const entry = yield* resolveHistoryRef(ref, yield* historyEntries);
10268
10268
  const trail = yield* historyTrail(entry.tip);
10269
- const document = {
10269
+ if (yield* writeJson(json, out, {
10270
10270
  ref: entry.ref,
10271
10271
  entry: entry.entry,
10272
10272
  branch: entry.branch,
10273
10273
  ...trail
10274
- };
10275
- if (yield* writeJson(json, out, document)) return;
10274
+ })) return;
10276
10275
  if (trail.versionWarning !== void 0) yield* (yield* Narrator).warn(trail.versionWarning);
10277
10276
  const lines = [`${entry.ref} (branch ${entry.branch ?? "-"}, tip ${seven(entry.tip)})`];
10278
10277
  for (const step of trail.steps) {
@@ -10288,6 +10287,208 @@ const runHistoryFetchCommand = (remote, out) => sync$2(() => {
10288
10287
  out.write(`${combinedScript(fetchHistoryRefs(remote), "")}\n`);
10289
10288
  });
10290
10289
  //#endregion
10290
+ //#region src/cli/Metrics.ts
10291
+ const QUALITY = ".gtd/QUALITY.md";
10292
+ const LENS = /^((?:.*\.)?quality\.[^.]+)\.reviewing$/;
10293
+ const CONTINUE = /(^|\.)continue\./;
10294
+ const inSeam = (step) => step.startsWith("seam.");
10295
+ const phaseOf = (step) => {
10296
+ const scope = step.split(".")[0];
10297
+ if (inSeam(step) && step.endsWith(".naive")) return "naive";
10298
+ if (step === "seam.note" || scope === "refactor") return "refactor";
10299
+ if (scope === "implement") return "implement";
10300
+ return "other";
10301
+ };
10302
+ const round = (n, places = 6) => {
10303
+ const f = 10 ** places;
10304
+ return Math.round(n * f) / f;
10305
+ };
10306
+ const record = () => Object.create(null);
10307
+ const add = (into, key, n) => {
10308
+ into[key] = (into[key] ?? 0) + n;
10309
+ };
10310
+ const roundAll = (record) => Object.fromEntries(Object.entries(record).map(([k, v]) => [k, round(v)]));
10311
+ const emptyTally = () => ({
10312
+ slices: {
10313
+ decided: 0,
10314
+ landed: 0,
10315
+ rebuilt: 0,
10316
+ refactored: 0
10317
+ },
10318
+ continuations: record(),
10319
+ byModel: record(),
10320
+ byPhase: {
10321
+ naive: 0,
10322
+ refactor: 0,
10323
+ implement: 0,
10324
+ other: 0
10325
+ },
10326
+ doubleWork: {
10327
+ naive: 0,
10328
+ rebuild: 0
10329
+ },
10330
+ byLens: record(),
10331
+ flaky: record()
10332
+ });
10333
+ const sum = (record) => Object.values(record).reduce((a, b) => a + b, 0);
10334
+ const finish = (t) => ({
10335
+ slices: t.slices,
10336
+ refactorFirstRate: t.slices.decided === 0 ? null : round(t.slices.rebuilt / t.slices.decided, 3),
10337
+ continuations: t.continuations,
10338
+ cost: {
10339
+ total: round(sum(t.byModel)),
10340
+ byModel: roundAll(t.byModel),
10341
+ byPhase: roundAll(t.byPhase),
10342
+ doubleWork: {
10343
+ naive: round(t.doubleWork.naive),
10344
+ rebuild: round(t.doubleWork.rebuild),
10345
+ total: round(t.doubleWork.naive + t.doubleWork.rebuild)
10346
+ }
10347
+ },
10348
+ findings: {
10349
+ total: sum(t.byLens),
10350
+ byLens: t.byLens
10351
+ },
10352
+ flaky: {
10353
+ total: sum(t.flaky),
10354
+ byStep: t.flaky
10355
+ }
10356
+ });
10357
+ const tallyCost = (t, open, costs, phase) => {
10358
+ for (const { cost, model } of costs) {
10359
+ add(t.byModel, model ?? "unspecified", cost);
10360
+ t.byPhase[phase] += cost;
10361
+ if (phase === "naive") open.naive += cost;
10362
+ if (phase === "implement" && open.rebuilding) t.doubleWork.rebuild += cost;
10363
+ }
10364
+ };
10365
+ /** A slice is decided where its turns leave the seam: for `implement.applying` the naive pass landed — unless it fails to apply and is built afresh — anywhere else it is rebuilt. */
10366
+ const tallyDecision = (t, open, at, to) => {
10367
+ if (at === "implement.closing") open.rebuilding = false;
10368
+ if (open.applying !== void 0 && at === "implement.applying") {
10369
+ const naive = open.applying;
10370
+ open.applying = void 0;
10371
+ if (to !== "implement.building") return;
10372
+ t.slices.landed--;
10373
+ rebuild(t, open, naive);
10374
+ return;
10375
+ }
10376
+ if (!inSeam(at) || inSeam(to)) return;
10377
+ t.slices.decided++;
10378
+ const naive = open.naive;
10379
+ open.naive = 0;
10380
+ if (to === "implement.applying") {
10381
+ open.applying = naive;
10382
+ open.rebuilding = false;
10383
+ t.slices.landed++;
10384
+ return;
10385
+ }
10386
+ rebuild(t, open, naive);
10387
+ if (at === "seam.note") t.slices.refactored++;
10388
+ };
10389
+ const rebuild = (t, open, naive) => {
10390
+ open.rebuilding = true;
10391
+ t.slices.rebuilt++;
10392
+ t.doubleWork.naive += naive;
10393
+ };
10394
+ /** Oldest first; a commit gtd did not write is skipped. */
10395
+ const processMetrics = (commits) => {
10396
+ const t = emptyTally();
10397
+ const open = {
10398
+ naive: 0,
10399
+ rebuilding: false,
10400
+ applying: void 0
10401
+ };
10402
+ for (const commit of commits) {
10403
+ const message = parseCommitMessage(commit.message);
10404
+ const subject = message.parsed;
10405
+ if (subject === void 0) continue;
10406
+ const at = subject.from ?? subject.to;
10407
+ tallyCost(t, open, message.cost, phaseOf(at));
10408
+ if (subject.actor === "agent" && CONTINUE.test(at)) add(t.continuations, at.replace(CONTINUE, "$1"), 1);
10409
+ const lens = LENS.exec(at)?.[1];
10410
+ if (lens !== void 0 && commit.findings > 0) add(t.byLens, lens, commit.findings);
10411
+ if (message.marks.some((m) => m.name === "flaky")) add(t.flaky, at, 1);
10412
+ tallyDecision(t, open, at, subject.to);
10413
+ }
10414
+ return finish(t);
10415
+ };
10416
+ const totalMetrics = (all) => {
10417
+ const t = emptyTally();
10418
+ for (const m of all) {
10419
+ for (const key of Object.keys(t.slices)) t.slices[key] += m.slices[key];
10420
+ for (const [k, v] of Object.entries(m.continuations)) add(t.continuations, k, v);
10421
+ for (const [k, v] of Object.entries(m.cost.byModel)) add(t.byModel, k, v);
10422
+ for (const key of Object.keys(t.byPhase)) t.byPhase[key] += m.cost.byPhase[key];
10423
+ t.doubleWork.naive += m.cost.doubleWork.naive;
10424
+ t.doubleWork.rebuild += m.cost.doubleWork.rebuild;
10425
+ for (const [k, v] of Object.entries(m.findings.byLens)) add(t.byLens, k, v);
10426
+ for (const [k, v] of Object.entries(m.flaky.byStep)) add(t.flaky, k, v);
10427
+ }
10428
+ return {
10429
+ processes: all.length,
10430
+ ...finish(t)
10431
+ };
10432
+ };
10433
+ const chunks = (content) => content === void 0 ? 0 : (content.match(/^## /gm) ?? []).length;
10434
+ const readProcessMetrics = (entry) => gen(function* () {
10435
+ const git = yield* GitService;
10436
+ const base = getOrUndefined(yield* git.readRefOption(`${entry.entry}^`));
10437
+ const commits = yield* git.commitHistory(base, entry.tip);
10438
+ const quality = (hash) => git.readFileAtRef(hash, QUALITY).pipe(map$4((content) => content), orElseSucceed(() => void 0));
10439
+ let count = chunks(base === void 0 ? void 0 : yield* quality(base));
10440
+ const read = [];
10441
+ for (const { hash, message, touched } of commits) {
10442
+ const before = count;
10443
+ if (touched.includes(QUALITY)) count = chunks(yield* quality(hash));
10444
+ read.push({
10445
+ message,
10446
+ findings: Math.max(0, count - before)
10447
+ });
10448
+ }
10449
+ return {
10450
+ workflow: (commits[0] === void 0 ? void 0 : parseCommitMessage(commits[0].message).workflow) ?? null,
10451
+ ...processMetrics(read)
10452
+ };
10453
+ });
10454
+ const printable = (s) => s.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, "?");
10455
+ const listed = (record) => Object.entries(record).map(([k, v]) => `${printable(k)} ${v}`).join(", ");
10456
+ const line = (label, m) => {
10457
+ const { slices, cost } = m;
10458
+ const percent = m.refactorFirstRate === null ? "-" : `${Math.round(m.refactorFirstRate * 100)}%`;
10459
+ const continued = sum(m.continuations);
10460
+ return [
10461
+ `${label}: ${slices.decided} slices, ${slices.rebuilt} rebuilt (refactor-first ${percent}), ${slices.refactored} refactored`,
10462
+ `${continued} continuations${continued > 0 ? ` (${listed(m.continuations)})` : ""}`,
10463
+ `cost ${cost.total} (${listed(cost.byPhase)}; by model ${listed(cost.byModel) || "-"}), double work ${cost.doubleWork.total}`,
10464
+ `${m.findings.total} findings${m.findings.total > 0 ? ` (${listed(m.findings.byLens)})` : ""}`,
10465
+ `${m.flaky.total} flaky${m.flaky.total > 0 ? ` (${listed(m.flaky.byStep)})` : ""}`
10466
+ ].join("; ");
10467
+ };
10468
+ 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) => ({
10472
+ ref: e.ref,
10473
+ entry: e.entry,
10474
+ branch: e.branch,
10475
+ tip: e.tip,
10476
+ ...metrics
10477
+ }))), { concurrency: 4 });
10478
+ const document = {
10479
+ processes,
10480
+ total: totalMetrics(processes)
10481
+ };
10482
+ if (yield* writeJson(json, out, document)) return;
10483
+ if (processes.length === 0) {
10484
+ out.write("no history refs; run gtd history fetch | sh to pull them\n");
10485
+ return;
10486
+ }
10487
+ const lines = processes.map((p) => line(`${p.entry.slice(0, 7)} ${printable(p.branch ?? "-")}${p.workflow === null ? "" : ` (${printable(p.workflow)})`}`, p));
10488
+ lines.push(line(`total (${processes.length} processes)`, document.total));
10489
+ out.write(`${lines.join("\n")}\n`);
10490
+ });
10491
+ //#endregion
10291
10492
  //#region src/cli/Program.ts
10292
10493
  const selfGtd = () => {
10293
10494
  const script = process.argv[1];
@@ -10939,6 +11140,7 @@ const dispatchVoidCommand = (command, json, out) => {
10939
11140
  case "judgeRun": return runJudgeRunCommand(command, out);
10940
11141
  case "historyList": return runHistoryListCommand(json, out);
10941
11142
  case "historyShow": return runHistoryShowCommand(command.ref, json, out);
11143
+ case "historyMetrics": return runHistoryMetricsCommand(command.refs, json, out);
10942
11144
  case "historyFetch": return runHistoryFetchCommand(command.remote, out);
10943
11145
  }
10944
11146
  };
@@ -10977,18 +11179,30 @@ const share = (raw, flag) => {
10977
11179
  const n = Number(raw);
10978
11180
  return raw.trim() !== "" && n > 0 && n <= 1 ? right(n) : left(`gtd: ${flag} must be a share of the context window in (0, 1] — got "${raw}"`);
10979
11181
  };
11182
+ const JSON_KINDS = /* @__PURE__ */ new Set([
11183
+ "next",
11184
+ "land",
11185
+ "judge",
11186
+ "judgeAnswer",
11187
+ "doors",
11188
+ "slices",
11189
+ "historyList",
11190
+ "historyShow",
11191
+ "historyMetrics",
11192
+ "strays"
11193
+ ]);
10980
11194
  const FLAGS = [
10981
11195
  {
10982
11196
  name: "--json",
10983
11197
  arity: "optional",
10984
11198
  repeatable: false,
10985
- scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "slices" || kind === "historyList" || kind === "historyShow" || kind === "strays",
11199
+ scope: (kind) => JSON_KINDS.has(kind),
10986
11200
  decode: ([raw]) => right(raw ?? ""),
10987
- 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 strays` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
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",
10988
11202
  valueHint: "<path>",
10989
11203
  help: [
10990
11204
  "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd slices/gtd history",
10991
- "list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
11205
+ "list/gtd history show/gtd history metrics/gtd strays only) output structured JSON. Bare --json prints the whole document;",
10992
11206
  "--json=<path> (a dotted key path into that document, e.g.",
10993
11207
  "kind, content, session.id) prints just that value: a",
10994
11208
  "scalar raw and unquoted, a boolean as true/false, a list",
@@ -11638,6 +11852,26 @@ const COMMAND_ROWS = [
11638
11852
  "prints the trail as one document; writes nothing"
11639
11853
  ]
11640
11854
  },
11855
+ {
11856
+ token: "history metrics",
11857
+ kind: "historyMetrics",
11858
+ arity: {
11859
+ name: "ref",
11860
+ many: true
11861
+ },
11862
+ details: [
11863
+ "Report how the kept processes went: slices whose naive pass",
11864
+ "landed or was rebuilt (the refactor-first rate), continuations",
11865
+ "per phase, Gtd-Cost by model and phase with the double work of",
11866
+ "a rebuild, quality-lap findings per lens, and red checks that",
11867
+ "passed on their re-check — per process and in total. Each <ref>",
11868
+ "is read as by history show, but a branch name means every",
11869
+ "process kept from it; none means every ref.",
11870
+ "Reads commit subjects and trailers, never replays, so a",
11871
+ "changed config is no refusal. --json prints { processes, total",
11872
+ "} as one document; writes nothing"
11873
+ ]
11874
+ },
11641
11875
  {
11642
11876
  token: "history fetch",
11643
11877
  kind: "historyFetch",
@@ -11669,7 +11903,7 @@ const flagHeader = (row) => {
11669
11903
  };
11670
11904
  const renderHelp = () => {
11671
11905
  const commandBlocks = COMMAND_ROWS.map((row) => {
11672
- const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : "variadic" in row.arity ? `${row.token} <${row.arity.name}> [${row.arity.variadic}...]` : "optional" in row.arity ? `${row.token} [${row.arity.name}]` : `${row.token} <${row.arity.name}>`;
11906
+ const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : "variadic" in row.arity ? `${row.token} <${row.arity.name}> [${row.arity.variadic}...]` : "optional" in row.arity ? `${row.token} [${row.arity.name}]` : "many" in row.arity ? `${row.token} [${row.arity.name}...]` : `${row.token} <${row.arity.name}>`;
11673
11907
  return renderBlock(header, row.details);
11674
11908
  });
11675
11909
  return `Usage: gtd [command] [options]\n\nCommands:\n${[
@@ -11777,12 +12011,14 @@ const conflictViolation = (present) => {
11777
12011
  for (const other of row.conflicts ?? []) if (present.has(other)) return `gtd: ${row.name} and ${other} are mutually exclusive`;
11778
12012
  }
11779
12013
  };
12014
+ const tooMany = (cmd, rest, expected, max) => rest.length > max ? `gtd ${cmd}: too many arguments — expected ${expected}, got: ${rest.join(", ")}` : void 0;
11780
12015
  const arityError = (cmd, rest, arity) => {
11781
- if (arity === "none") return rest.length > 0 ? `gtd ${cmd}: too many arguments — expected none, got: ${rest.join(", ")}` : void 0;
12016
+ if (arity === "none") return tooMany(cmd, rest, "none", 0);
11782
12017
  if ("names" in arity) return pairArityError(cmd, rest, arity.names);
11783
- if ("optional" in arity) return rest.length > 1 ? `gtd ${cmd}: too many arguments — expected at most one ${arity.name}, got: ${rest.join(", ")}` : void 0;
12018
+ if ("many" in arity) return void 0;
12019
+ if ("optional" in arity) return tooMany(cmd, rest, `at most one ${arity.name}`, 1);
11784
12020
  if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`;
11785
- if (!("variadic" in arity) && rest.length > 1) return `gtd ${cmd}: too many arguments — expected one ${arity.name}, got: ${rest.join(", ")}`;
12021
+ return "variadic" in arity ? void 0 : tooMany(cmd, rest, `one ${arity.name}`, 1);
11786
12022
  };
11787
12023
  const pairArityError = (cmd, rest, [first, second]) => {
11788
12024
  if (rest.length === 0) return `gtd ${cmd}: missing ${first} and ${second} arguments`;
@@ -11833,7 +12069,7 @@ const parseArgv = (argv) => {
11833
12069
  };
11834
12070
  const isHistory = first === "history";
11835
12071
  const row = first === void 0 ? void 0 : commandByToken(isHistory ? `history ${positionals[1] ?? ""}` : first);
11836
- if (isHistory && row === void 0) return usagePlan("gtd history: expected a subcommand — list, show <ref> or fetch [remote]", jsonSeen);
12072
+ if (isHistory && row === void 0) return usagePlan("gtd history: expected a subcommand — list, show <ref>, metrics [ref...] or fetch [remote]", jsonSeen);
11837
12073
  const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer";
11838
12074
  const selectsStart = workflowPresent && first === void 0;
11839
12075
  const judgeRun = row?.kind === "judge" && positionals[1] === "run";
@@ -11966,6 +12202,15 @@ const parseArgv = (argv) => {
11966
12202
  json,
11967
12203
  verbose
11968
12204
  };
12205
+ if (kind === "historyMetrics") return {
12206
+ kind: "command",
12207
+ command: {
12208
+ kind: "historyMetrics",
12209
+ refs: restPositionals
12210
+ },
12211
+ json,
12212
+ verbose
12213
+ };
11969
12214
  if (kind === "historyFetch") return {
11970
12215
  kind: "command",
11971
12216
  command: {
@@ -17,10 +17,15 @@ const untilSettled = async (check, pending) => {
17
17
  };
18
18
  const STAMP = /\n<!-- gtd check [0-9a-f]+ -->\n?$/;
19
19
  const stripStamp = (text) => text.replace(STAMP, "\n");
20
+ /** `true` when the re-check passed, else the red re-check's report — the one the escalation describes. */
21
+ const recheckReport = async (health) => {
22
+ if (health.recheck === void 0) return void 0;
23
+ return await health.recheck() || stripStamp(read(health.report) ?? "");
24
+ };
20
25
  /**
21
26
  * Loop until the suite is green: check, and on red run `fix` — escalating
22
27
  * first once `fixCap` fixes are spent, or `sameFailure` calls the failure
23
- * identical to the previous round's.
28
+ * identical to the previous round's, unless `recheck` passes.
24
29
  */
25
30
  const healthy = async (fix, health) => {
26
31
  let fixes = health.fixesSoFar ?? 0;
@@ -34,6 +39,12 @@ const healthy = async (fix, health) => {
34
39
  const stuck = previous !== void 0 && await health.sameFailure(previous, current);
35
40
  previous = current;
36
41
  if (stuck || fixes >= health.fixCap) {
42
+ const rechecked = await recheckReport(health);
43
+ if (rechecked === true) {
44
+ health.escalations.rounds = 0;
45
+ return;
46
+ }
47
+ previous = rechecked ?? previous;
37
48
  await escalation(health.escalations, health.escalation);
38
49
  fixes = 0;
39
50
  }
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { Dt as registryEntry, Ot as requireFlag, S as coder, St as envDefaults, Tt as flagReferences, i as skills, n as summary, o as flagShipped, r as access, t as steering, xt as defaults } from "../chunks/reserved-CfQ8eurM.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C7yaWvaS.mjs";
2
+ import { Dt as registryEntry, Ot as requireFlag, S as coder, St as envDefaults, Tt as flagReferences, i as skills, n as summary, o as flagShipped, r as access, t as steering, xt as defaults } from "../chunks/reserved-B6tTYjeb.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-_CZTP7Hu.mjs";
4
4
  //#region src/workflows/cleanup/cleanup.ts
5
5
  const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
6
6
  const cleanupFlow = async () => {
@@ -1,7 +1,7 @@
1
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, slice, start, vars, wrote } from "../flows.mjs";
2
2
  import { lensEntriesOf, review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as naiveApplyFailure, A as agentWithSkills, B as coverageQuestion, C as fixCheck, Ct as testFileGlobs, D as reviewMissing, E as planner, F as buildReviewTriageMessage, G as designSystem, H as criterionLine, I as builderSystem, K as designTriagePrompt, L as clearCriteriaMessage, M as buildReviewAwaitAfterDeferMessage, N as buildReviewCollectingPrompt, P as buildReviewFixNitsPrompt, Q as implementFixPrompt, R as clearCriteriaQuestion, S as coder, St as envDefaults, T as fixRisks, U as deferNotesScript, V as coveringPrompt, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REFACTOR_NOTE, _t as startGateBlockedMessage, a as flagModeOn, at as refactorDroppedRefusal, b as SLICES, bt as unwindFailure, ct as refactorRules, d as guarded, et as naivePassPrompt, f as healthy, ft as reviewNotesCapture, g as REFACTOR, gt as slicingPrompt, h as QUALITY, ht as seamSteerMessage, i as skills, it as refactorBuildingPrompt, j as buildReviewAnswerQuestionsPrompt, k as reviewing, lt as reviewEditNotesCapture, m as HANDOFF, mt as seamByHandMessage, n as summary, nt as oversizedMessage, ot as refactorFixPrompt, p as FEEDBACK, pt as reviewerSystem, r as access, rt as qualityUnresolvedMessage, s as shipped, st as refactorNotePrompt, t as steering, tt as normaliseCriteriaScript, u as gate, ut as reviewEditsCapture, v as REQUIREMENTS, vt as uncoveredMessage, x as awaitReview, xt as defaults, y as REVIEW, yt as unwindFailedMessage, z as coverageMessage } from "../chunks/reserved-CfQ8eurM.mjs";
4
- import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-C7yaWvaS.mjs";
3
+ import { $ as naiveApplyFailure, A as agentWithSkills, B as coverageQuestion, C as fixCheck, Ct as testFileGlobs, D as reviewMissing, E as planner, F as buildReviewTriageMessage, G as designSystem, H as criterionLine, I as builderSystem, K as designTriagePrompt, L as clearCriteriaMessage, M as buildReviewAwaitAfterDeferMessage, N as buildReviewCollectingPrompt, P as buildReviewFixNitsPrompt, Q as implementFixPrompt, R as clearCriteriaQuestion, S as coder, St as envDefaults, T as fixRisks, U as deferNotesScript, V as coveringPrompt, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REFACTOR_NOTE, _t as startGateBlockedMessage, a as flagModeOn, at as refactorDroppedRefusal, b as SLICES, bt as unwindFailure, ct as refactorRules, d as guarded, et as naivePassPrompt, f as healthy, ft as reviewNotesCapture, g as REFACTOR, gt as slicingPrompt, h as QUALITY, ht as seamSteerMessage, i as skills, it as refactorBuildingPrompt, j as buildReviewAnswerQuestionsPrompt, k as reviewing, lt as reviewEditNotesCapture, m as HANDOFF, mt as seamByHandMessage, n as summary, nt as oversizedMessage, ot as refactorFixPrompt, p as FEEDBACK, pt as reviewerSystem, r as access, rt as qualityUnresolvedMessage, s as shipped, st as refactorNotePrompt, t as steering, tt as normaliseCriteriaScript, u as gate, ut as reviewEditsCapture, v as REQUIREMENTS, vt as uncoveredMessage, x as awaitReview, xt as defaults, y as REVIEW, yt as unwindFailedMessage, z as coverageMessage } from "../chunks/reserved-B6tTYjeb.mjs";
4
+ import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-_CZTP7Hu.mjs";
5
5
  import { posix } from "node:path";
6
6
  //#region src/workflows/shared/fences.ts
7
7
  const markFenced = (text) => {
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
2
  import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
3
- import { A as agentWithSkills, J as fixReviewNotesPrompt, S as coder, St as envDefaults, c as baseline, i as skills, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, xt as defaults, y as REVIEW } from "../chunks/reserved-CfQ8eurM.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-C7yaWvaS.mjs";
3
+ import { A as agentWithSkills, J as fixReviewNotesPrompt, S as coder, St as envDefaults, c as baseline, i as skills, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, xt as defaults, y as REVIEW } from "../chunks/reserved-B6tTYjeb.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-_CZTP7Hu.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,6 +1,6 @@
1
1
  import { start } from "../flows.mjs";
2
- import { St as envDefaults, dt as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, u as gate, xt as defaults } from "../chunks/reserved-CfQ8eurM.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C7yaWvaS.mjs";
2
+ import { St as envDefaults, dt as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, u as gate, xt as defaults } from "../chunks/reserved-B6tTYjeb.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-_CZTP7Hu.mjs";
4
4
  //#region src/workflows/review/review.ts
5
5
  /**
6
6
  * Pure review of everything since `reviewBase`: a green gate, the quality
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { Et as flipProblems, Ot as requireFlag, S as coder, St as envDefaults, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as flagMentions, xt as defaults } from "../chunks/reserved-CfQ8eurM.mjs";
2
+ import { Et as flipProblems, Ot as requireFlag, S as coder, St as envDefaults, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as flagMentions, xt as defaults } from "../chunks/reserved-B6tTYjeb.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.";
@@ -451,10 +451,15 @@ const escalation$1 = async (count, steps) => {
451
451
  };
452
452
  const STAMP = /\n<!-- gtd check [0-9a-f]+ -->\n?$/;
453
453
  const stripStamp = (text) => text.replace(STAMP, "\n");
454
+ /** `true` when the re-check passed, else the red re-check's report — the one the escalation describes. */
455
+ const recheckReport = async (health) => {
456
+ if (health.recheck === void 0) return void 0;
457
+ return await health.recheck() || stripStamp(read(health.report) ?? "");
458
+ };
454
459
  /**
455
460
  * Loop until the suite is green: check, and on red run `fix` — escalating
456
461
  * first once `fixCap` fixes are spent, or `sameFailure` calls the failure
457
- * identical to the previous round's.
462
+ * identical to the previous round's, unless `recheck` passes.
458
463
  */
459
464
  const healthy$1 = async (fix, health) => {
460
465
  let fixes = health.fixesSoFar ?? 0;
@@ -468,6 +473,12 @@ const healthy$1 = async (fix, health) => {
468
473
  const stuck = previous !== void 0 && await health.sameFailure(previous, current);
469
474
  previous = current;
470
475
  if (stuck || fixes >= health.fixCap) {
476
+ const rechecked = await recheckReport(health);
477
+ if (rechecked === true) {
478
+ health.escalations.rounds = 0;
479
+ return;
480
+ }
481
+ previous = rechecked ?? previous;
471
482
  await escalation$1(health.escalations, health.escalation);
472
483
  fixes = 0;
473
484
  }
@@ -18385,6 +18396,19 @@ const baseline = (name, label = "Checking the baseline") => check(name, env.test
18385
18396
  report: FEEDBACK,
18386
18397
  label
18387
18398
  });
18399
+ /**
18400
+ * Re-run a red check as step `name` before a person sees it. Green records
18401
+ * `Gtd-Mark: flaky=<red>` on the re-check's landing and resolves `true`.
18402
+ */
18403
+ const recheck = async (name, red, options = {}) => {
18404
+ const green = await check(name, env.testCommand ?? "", {
18405
+ ...options,
18406
+ report: FEEDBACK,
18407
+ label: "Re-running checks"
18408
+ });
18409
+ if (green) mark("flaky", red);
18410
+ return green;
18411
+ };
18388
18412
  const escalationSteps = (rules) => ({
18389
18413
  limit: 2,
18390
18414
  describe: () => describeEscalation(rules),
@@ -18435,15 +18459,17 @@ const sameFailure = async (previous, current) => {
18435
18459
  });
18436
18460
  return comparable && truncated.length === 0 && answered(answers.verdict, "identical", numeric(vars.judgeIdenticalMinP, Infinity));
18437
18461
  };
18462
+ const healthSweep = (options) => [ESCALATION, ...options.sweepOnGreen ?? []];
18438
18463
  const runSuite = (options) => check("health.check", env.testCommand ?? "", {
18439
18464
  report: FEEDBACK,
18440
18465
  label: "Running checks",
18441
- sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []]
18466
+ sweepOnGreen: healthSweep(options)
18442
18467
  });
18443
18468
  /**
18444
18469
  * Loop until the suite is green: check, and on red run `fix` — escalating
18445
18470
  * first once `FIX_CAP` fixes are spent, or a judge calls the failure
18446
- * identical to the previous round's.
18471
+ * identical to the previous round's. An escalation re-runs the suite once
18472
+ * first; a green re-run ends the loop.
18447
18473
  */
18448
18474
  const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
18449
18475
  check: () => runSuite(options),
@@ -18452,14 +18478,16 @@ const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
18452
18478
  fixesSoFar: options.fixesSoFar ?? 0,
18453
18479
  sameFailure,
18454
18480
  escalations: options.escalations ?? { rounds: 0 },
18481
+ recheck: () => recheck("health.recheck", "health.check", { sweepOnGreen: healthSweep(options) }),
18455
18482
  escalation: guardedSteps(options.guard, options.describeGuard, options.guardRests, options.rules)
18456
18483
  });
18457
18484
  /**
18458
- * Hold the process at `<name>.blocked` until the suite is green. `guard` runs
18485
+ * Hold the process at `<name>.blocked` until the suite is green, re-running a
18486
+ * red check once as `<name>.recheck` before resting. `guard` runs
18459
18487
  * after each `blocked` landing only — never after a check, whose refusal would
18460
18488
  * re-run the same script forever.
18461
18489
  */
18462
- const gate = (name, message, guard) => gate$1(name, () => baseline("check"), guarded(() => human("blocked", {
18490
+ const gate = (name, message, guard) => gate$1(name, async () => await baseline("check") || recheck("recheck", `${name}.check`), guarded(() => human("blocked", {
18463
18491
  message,
18464
18492
  label: "Baseline is red",
18465
18493
  file: FEEDBACK
@@ -39578,4 +39606,4 @@ var workflows_default = feature;
39578
39606
  */
39579
39607
  const doors = {};
39580
39608
  //#endregion
39581
- export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unwind };
39609
+ export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unwind };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "38.1.0",
3
+ "version": "39.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": {