@pmelab/gtd 38.0.0 → 38.2.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 (39) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +36 -3
  3. package/dist/chunks/{Schema-DO5ttJaI.mjs → Schema-C6fLlQzx.mjs} +1 -1
  4. package/dist/chunks/{reserved-sfMdy7v7.mjs → reserved-C3PgP7XB.mjs} +56 -12
  5. package/dist/chunks/{tail-Cgca5Gp-.mjs → tail-Bkxki2nK.mjs} +1 -1
  6. package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +292 -53
  8. package/dist/testkit.mjs +257 -21
  9. package/dist/wire.mjs +1 -1
  10. package/dist/workflows/cleanup.mjs +2 -2
  11. package/dist/workflows/feature.mjs +158 -42
  12. package/dist/workflows/fix.mjs +2 -2
  13. package/dist/workflows/review.mjs +2 -2
  14. package/dist/workflows/rollout.mjs +1 -1
  15. package/dist/workflows.mjs +230 -70
  16. package/package.json +1 -1
  17. package/src/cli/Cli.test.ts +15 -3
  18. package/src/cli/Cli.ts +61 -28
  19. package/src/cli/History.ts +1 -19
  20. package/src/cli/Metrics.test.ts +224 -0
  21. package/src/cli/Metrics.ts +310 -0
  22. package/src/cli/Output.ts +13 -0
  23. package/src/cli/Program.test.ts +3 -1
  24. package/src/cli/Program.ts +4 -0
  25. package/src/testkit/steps.ts +4 -0
  26. package/src/wire/ContextLimit.ts +1 -1
  27. package/src/workflows/feature/continuation-cap.feature +65 -0
  28. package/src/workflows/feature/continuation-rebuild.feature +88 -0
  29. package/src/workflows/feature/continuation-slices.feature +64 -0
  30. package/src/workflows/feature/continuation.feature +205 -0
  31. package/src/workflows/feature/criteria.ts +11 -0
  32. package/src/workflows/feature/refactor.ts +64 -30
  33. package/src/workflows/feature/seam.ts +102 -30
  34. package/src/workflows/shared/continuation.ts +92 -0
  35. package/src/workflows/shared/options.ts +2 -0
  36. package/src/workflows/shared/steps.ts +1 -0
  37. package/src/workflows/shared/text.test.ts +6 -2
  38. package/src/workflows/shared/text.ts +104 -8
  39. package/src/workflows/shared/vars.ts +5 -0
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
- 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-CSsa3Wxd.mjs";
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,200 @@ 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
+ });
10332
+ const sum = (record) => Object.values(record).reduce((a, b) => a + b, 0);
10333
+ const finish = (t) => ({
10334
+ slices: t.slices,
10335
+ refactorFirstRate: t.slices.decided === 0 ? null : round(t.slices.rebuilt / t.slices.decided, 3),
10336
+ continuations: t.continuations,
10337
+ cost: {
10338
+ total: round(sum(t.byModel)),
10339
+ byModel: roundAll(t.byModel),
10340
+ byPhase: roundAll(t.byPhase),
10341
+ doubleWork: {
10342
+ naive: round(t.doubleWork.naive),
10343
+ rebuild: round(t.doubleWork.rebuild),
10344
+ total: round(t.doubleWork.naive + t.doubleWork.rebuild)
10345
+ }
10346
+ },
10347
+ findings: {
10348
+ total: sum(t.byLens),
10349
+ byLens: t.byLens
10350
+ }
10351
+ });
10352
+ const tallyCost = (t, open, costs, phase) => {
10353
+ for (const { cost, model } of costs) {
10354
+ add(t.byModel, model ?? "unspecified", cost);
10355
+ t.byPhase[phase] += cost;
10356
+ if (phase === "naive") open.naive += cost;
10357
+ if (phase === "implement" && open.rebuilding) t.doubleWork.rebuild += cost;
10358
+ }
10359
+ };
10360
+ /** 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. */
10361
+ const tallyDecision = (t, open, at, to) => {
10362
+ if (at === "implement.closing") open.rebuilding = false;
10363
+ if (open.applying !== void 0 && at === "implement.applying") {
10364
+ const naive = open.applying;
10365
+ open.applying = void 0;
10366
+ if (to !== "implement.building") return;
10367
+ t.slices.landed--;
10368
+ rebuild(t, open, naive);
10369
+ return;
10370
+ }
10371
+ if (!inSeam(at) || inSeam(to)) return;
10372
+ t.slices.decided++;
10373
+ const naive = open.naive;
10374
+ open.naive = 0;
10375
+ if (to === "implement.applying") {
10376
+ open.applying = naive;
10377
+ open.rebuilding = false;
10378
+ t.slices.landed++;
10379
+ return;
10380
+ }
10381
+ rebuild(t, open, naive);
10382
+ if (at === "seam.note") t.slices.refactored++;
10383
+ };
10384
+ const rebuild = (t, open, naive) => {
10385
+ open.rebuilding = true;
10386
+ t.slices.rebuilt++;
10387
+ t.doubleWork.naive += naive;
10388
+ };
10389
+ /** Oldest first; a commit gtd did not write is skipped. */
10390
+ const processMetrics = (commits) => {
10391
+ const t = emptyTally();
10392
+ const open = {
10393
+ naive: 0,
10394
+ rebuilding: false,
10395
+ applying: void 0
10396
+ };
10397
+ for (const commit of commits) {
10398
+ const message = parseCommitMessage(commit.message);
10399
+ const subject = message.parsed;
10400
+ if (subject === void 0) continue;
10401
+ const at = subject.from ?? subject.to;
10402
+ tallyCost(t, open, message.cost, phaseOf(at));
10403
+ if (subject.actor === "agent" && CONTINUE.test(at)) add(t.continuations, at.replace(CONTINUE, "$1"), 1);
10404
+ const lens = LENS.exec(at)?.[1];
10405
+ if (lens !== void 0 && commit.findings > 0) add(t.byLens, lens, commit.findings);
10406
+ tallyDecision(t, open, at, subject.to);
10407
+ }
10408
+ return finish(t);
10409
+ };
10410
+ const totalMetrics = (all) => {
10411
+ const t = emptyTally();
10412
+ for (const m of all) {
10413
+ for (const key of Object.keys(t.slices)) t.slices[key] += m.slices[key];
10414
+ for (const [k, v] of Object.entries(m.continuations)) add(t.continuations, k, v);
10415
+ for (const [k, v] of Object.entries(m.cost.byModel)) add(t.byModel, k, v);
10416
+ for (const key of Object.keys(t.byPhase)) t.byPhase[key] += m.cost.byPhase[key];
10417
+ t.doubleWork.naive += m.cost.doubleWork.naive;
10418
+ t.doubleWork.rebuild += m.cost.doubleWork.rebuild;
10419
+ for (const [k, v] of Object.entries(m.findings.byLens)) add(t.byLens, k, v);
10420
+ }
10421
+ return {
10422
+ processes: all.length,
10423
+ ...finish(t)
10424
+ };
10425
+ };
10426
+ const chunks = (content) => content === void 0 ? 0 : (content.match(/^## /gm) ?? []).length;
10427
+ const readProcessMetrics = (entry) => gen(function* () {
10428
+ const git = yield* GitService;
10429
+ const base = getOrUndefined(yield* git.readRefOption(`${entry.entry}^`));
10430
+ const commits = yield* git.commitHistory(base, entry.tip);
10431
+ const quality = (hash) => git.readFileAtRef(hash, QUALITY).pipe(map$4((content) => content), orElseSucceed(() => void 0));
10432
+ let count = chunks(base === void 0 ? void 0 : yield* quality(base));
10433
+ const read = [];
10434
+ for (const { hash, message, touched } of commits) {
10435
+ const before = count;
10436
+ if (touched.includes(QUALITY)) count = chunks(yield* quality(hash));
10437
+ read.push({
10438
+ message,
10439
+ findings: Math.max(0, count - before)
10440
+ });
10441
+ }
10442
+ return {
10443
+ workflow: (commits[0] === void 0 ? void 0 : parseCommitMessage(commits[0].message).workflow) ?? null,
10444
+ ...processMetrics(read)
10445
+ };
10446
+ });
10447
+ const printable = (s) => s.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, "?");
10448
+ const listed = (record) => Object.entries(record).map(([k, v]) => `${printable(k)} ${v}`).join(", ");
10449
+ const line = (label, m) => {
10450
+ const { slices, cost } = m;
10451
+ const percent = m.refactorFirstRate === null ? "-" : `${Math.round(m.refactorFirstRate * 100)}%`;
10452
+ const continued = sum(m.continuations);
10453
+ return [
10454
+ `${label}: ${slices.decided} slices, ${slices.rebuilt} rebuilt (refactor-first ${percent}), ${slices.refactored} refactored`,
10455
+ `${continued} continuations${continued > 0 ? ` (${listed(m.continuations)})` : ""}`,
10456
+ `cost ${cost.total} (${listed(cost.byPhase)}; by model ${listed(cost.byModel) || "-"}), double work ${cost.doubleWork.total}`,
10457
+ `${m.findings.total} findings${m.findings.total > 0 ? ` (${listed(m.findings.byLens)})` : ""}`
10458
+ ].join("; ");
10459
+ };
10460
+ const runHistoryMetricsCommand = (refs, json, out) => gen(function* () {
10461
+ const entries = yield* historyEntries;
10462
+ 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();
10463
+ const processes = yield* forEach$2(chosen.filter((e, i) => chosen.findIndex((o) => o.ref === e.ref) === i), (e) => readProcessMetrics(e).pipe(map$4((metrics) => ({
10464
+ ref: e.ref,
10465
+ entry: e.entry,
10466
+ branch: e.branch,
10467
+ tip: e.tip,
10468
+ ...metrics
10469
+ }))), { concurrency: 4 });
10470
+ const document = {
10471
+ processes,
10472
+ total: totalMetrics(processes)
10473
+ };
10474
+ if (yield* writeJson(json, out, document)) return;
10475
+ if (processes.length === 0) {
10476
+ out.write("no history refs; run gtd history fetch | sh to pull them\n");
10477
+ return;
10478
+ }
10479
+ const lines = processes.map((p) => line(`${p.entry.slice(0, 7)} ${printable(p.branch ?? "-")}${p.workflow === null ? "" : ` (${printable(p.workflow)})`}`, p));
10480
+ lines.push(line(`total (${processes.length} processes)`, document.total));
10481
+ out.write(`${lines.join("\n")}\n`);
10482
+ });
10483
+ //#endregion
10291
10484
  //#region src/cli/Program.ts
10292
10485
  const selfGtd = () => {
10293
10486
  const script = process.argv[1];
@@ -10939,6 +11132,7 @@ const dispatchVoidCommand = (command, json, out) => {
10939
11132
  case "judgeRun": return runJudgeRunCommand(command, out);
10940
11133
  case "historyList": return runHistoryListCommand(json, out);
10941
11134
  case "historyShow": return runHistoryShowCommand(command.ref, json, out);
11135
+ case "historyMetrics": return runHistoryMetricsCommand(command.refs, json, out);
10942
11136
  case "historyFetch": return runHistoryFetchCommand(command.remote, out);
10943
11137
  }
10944
11138
  };
@@ -10977,18 +11171,30 @@ const share = (raw, flag) => {
10977
11171
  const n = Number(raw);
10978
11172
  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
11173
  };
11174
+ const JSON_KINDS = /* @__PURE__ */ new Set([
11175
+ "next",
11176
+ "land",
11177
+ "judge",
11178
+ "judgeAnswer",
11179
+ "doors",
11180
+ "slices",
11181
+ "historyList",
11182
+ "historyShow",
11183
+ "historyMetrics",
11184
+ "strays"
11185
+ ]);
10980
11186
  const FLAGS = [
10981
11187
  {
10982
11188
  name: "--json",
10983
11189
  arity: "optional",
10984
11190
  repeatable: false,
10985
- scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "slices" || kind === "historyList" || kind === "historyShow" || kind === "strays",
11191
+ scope: (kind) => JSON_KINDS.has(kind),
10986
11192
  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",
11193
+ 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
11194
  valueHint: "<path>",
10989
11195
  help: [
10990
11196
  "(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;",
11197
+ "list/gtd history show/gtd history metrics/gtd strays only) output structured JSON. Bare --json prints the whole document;",
10992
11198
  "--json=<path> (a dotted key path into that document, e.g.",
10993
11199
  "kind, content, session.id) prints just that value: a",
10994
11200
  "scalar raw and unquoted, a boolean as true/false, a list",
@@ -11638,6 +11844,25 @@ const COMMAND_ROWS = [
11638
11844
  "prints the trail as one document; writes nothing"
11639
11845
  ]
11640
11846
  },
11847
+ {
11848
+ token: "history metrics",
11849
+ kind: "historyMetrics",
11850
+ arity: {
11851
+ name: "ref",
11852
+ many: true
11853
+ },
11854
+ details: [
11855
+ "Report how the kept processes went: slices whose naive pass",
11856
+ "landed or was rebuilt (the refactor-first rate), continuations",
11857
+ "per phase, Gtd-Cost by model and phase with the double work of",
11858
+ "a rebuild, and quality-lap findings per lens — per process and",
11859
+ "in total. Each <ref> is read as by history show, but a branch",
11860
+ "name means every process kept from it; none means every ref.",
11861
+ "Reads commit subjects and trailers, never replays, so a",
11862
+ "changed config is no refusal. --json prints { processes, total",
11863
+ "} as one document; writes nothing"
11864
+ ]
11865
+ },
11641
11866
  {
11642
11867
  token: "history fetch",
11643
11868
  kind: "historyFetch",
@@ -11669,7 +11894,7 @@ const flagHeader = (row) => {
11669
11894
  };
11670
11895
  const renderHelp = () => {
11671
11896
  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}>`;
11897
+ 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
11898
  return renderBlock(header, row.details);
11674
11899
  });
11675
11900
  return `Usage: gtd [command] [options]\n\nCommands:\n${[
@@ -11777,12 +12002,14 @@ const conflictViolation = (present) => {
11777
12002
  for (const other of row.conflicts ?? []) if (present.has(other)) return `gtd: ${row.name} and ${other} are mutually exclusive`;
11778
12003
  }
11779
12004
  };
12005
+ const tooMany = (cmd, rest, expected, max) => rest.length > max ? `gtd ${cmd}: too many arguments — expected ${expected}, got: ${rest.join(", ")}` : void 0;
11780
12006
  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;
12007
+ if (arity === "none") return tooMany(cmd, rest, "none", 0);
11782
12008
  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;
12009
+ if ("many" in arity) return void 0;
12010
+ if ("optional" in arity) return tooMany(cmd, rest, `at most one ${arity.name}`, 1);
11784
12011
  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(", ")}`;
12012
+ return "variadic" in arity ? void 0 : tooMany(cmd, rest, `one ${arity.name}`, 1);
11786
12013
  };
11787
12014
  const pairArityError = (cmd, rest, [first, second]) => {
11788
12015
  if (rest.length === 0) return `gtd ${cmd}: missing ${first} and ${second} arguments`;
@@ -11833,7 +12060,7 @@ const parseArgv = (argv) => {
11833
12060
  };
11834
12061
  const isHistory = first === "history";
11835
12062
  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);
12063
+ if (isHistory && row === void 0) return usagePlan("gtd history: expected a subcommand — list, show <ref>, metrics [ref...] or fetch [remote]", jsonSeen);
11837
12064
  const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer";
11838
12065
  const selectsStart = workflowPresent && first === void 0;
11839
12066
  const judgeRun = row?.kind === "judge" && positionals[1] === "run";
@@ -11966,6 +12193,15 @@ const parseArgv = (argv) => {
11966
12193
  json,
11967
12194
  verbose
11968
12195
  };
12196
+ if (kind === "historyMetrics") return {
12197
+ kind: "command",
12198
+ command: {
12199
+ kind: "historyMetrics",
12200
+ refs: restPositionals
12201
+ },
12202
+ json,
12203
+ verbose
12204
+ };
11969
12205
  if (kind === "historyFetch") return {
11970
12206
  kind: "command",
11971
12207
  command: {
package/dist/wire.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { A as beatKindOf, B as ACCESS_REFUSAL, C as renderLandPlain, D as renderBeatJson, E as beatDocument, F as EXIT_RUNTIME_ERROR, H as WIRE_SCHEMA, I as EXIT_SIGINT, L as EXIT_SIGTERM, M as stallDiagnosis, N as EXIT_CODES, O as statusOf, P as EXIT_OK, R as EXIT_STRAYS_FOUND, S as renderLandJson, T as renderBeatPlain, U as renderFormat, V as UNATTRIBUTED_MODEL, _ as selectPath, a as contextLimitFault, b as landProseText, c as hookOutput, d as transcriptTokens, f as usageTokens, g as resolveSession, h as globMatches, i as contextHook, j as demandOf, k as BEAT_KINDS, l as parseContextWindow, m as slicesDocument, n as CONTEXT_STOP_FILE_ENV, o as contextShapeFault, p as renderSlicesJson, r as DEFAULT_CONTEXT_LIMIT, s as contextWindowFor, t as CONTEXT_LIMIT_MARKER, u as subagentTranscriptPath, v as judgeJson, w as DEMAND_BRIEFING, x as noopText, y as landFields, z as EXIT_USAGE_ERROR } from "./chunks/wire-CSsa3Wxd.mjs";
1
+ import { A as beatKindOf, B as ACCESS_REFUSAL, C as renderLandPlain, D as renderBeatJson, E as beatDocument, F as EXIT_RUNTIME_ERROR, H as WIRE_SCHEMA, I as EXIT_SIGINT, L as EXIT_SIGTERM, M as stallDiagnosis, N as EXIT_CODES, O as statusOf, P as EXIT_OK, R as EXIT_STRAYS_FOUND, S as renderLandJson, T as renderBeatPlain, U as renderFormat, V as UNATTRIBUTED_MODEL, _ as selectPath, a as contextLimitFault, b as landProseText, c as hookOutput, d as transcriptTokens, f as usageTokens, g as resolveSession, h as globMatches, i as contextHook, j as demandOf, k as BEAT_KINDS, l as parseContextWindow, m as slicesDocument, n as CONTEXT_STOP_FILE_ENV, o as contextShapeFault, p as renderSlicesJson, r as DEFAULT_CONTEXT_LIMIT, s as contextWindowFor, t as CONTEXT_LIMIT_MARKER, u as subagentTranscriptPath, v as judgeJson, w as DEMAND_BRIEFING, x as noopText, y as landFields, z as EXIT_USAGE_ERROR } from "./chunks/wire-BJuZWR3C.mjs";
2
2
  export { ACCESS_REFUSAL, BEAT_KINDS, CONTEXT_LIMIT_MARKER, CONTEXT_STOP_FILE_ENV, DEFAULT_CONTEXT_LIMIT, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, EXIT_STRAYS_FOUND, EXIT_USAGE_ERROR, UNATTRIBUTED_MODEL, WIRE_SCHEMA, beatDocument, beatKindOf, contextHook, contextLimitFault, contextShapeFault, contextWindowFor, demandOf, globMatches, hookOutput, judgeJson, landFields, landProseText, noopText, parseContextWindow, renderBeatJson, renderBeatPlain, renderFormat, renderLandJson, renderLandPlain, renderSlicesJson, resolveSession, selectPath, slicesDocument, stallDiagnosis, statusOf, subagentTranscriptPath, transcriptTokens, usageTokens };
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { Ct as flagReferences, Et as requireFlag, Tt as registryEntry, bt as envDefaults, i as skills, n as summary, o as flagShipped, r as access, t as steering, x as coder, yt as defaults } from "../chunks/reserved-sfMdy7v7.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-Cgca5Gp-.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-C3PgP7XB.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-Bkxki2nK.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 () => {