@pmelab/gtd 27.2.0 → 27.4.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 (67) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +14 -1
  3. package/dist/chunks/{reserved-B2QaImnN.mjs → reserved-BPdVZkgv.mjs} +29 -4
  4. package/dist/chunks/{tail-BnhIxCYv.mjs → tail-iLAMoigl.mjs} +1 -1
  5. package/dist/chunks/{wire-BY1S-obW.mjs → wire-C12dahrH.mjs} +6 -4
  6. package/dist/flows.mjs +24 -1
  7. package/dist/gtd.bundle.mjs +174 -23
  8. package/dist/testkit.mjs +327 -27
  9. package/dist/wire.mjs +1 -1
  10. package/dist/workflows/cleanup.mjs +2 -2
  11. package/dist/workflows/feature.mjs +119 -6
  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 +146 -8
  16. package/package.json +1 -1
  17. package/src/cli/Cli.ts +5 -5
  18. package/src/cli/History.ts +5 -0
  19. package/src/cli/Program.ts +1 -0
  20. package/src/edge/Edge.test.ts +160 -0
  21. package/src/edge/Edge.ts +90 -8
  22. package/src/emit/GitScript.test.ts +39 -3
  23. package/src/emit/GitScript.ts +25 -1
  24. package/src/emit/index.ts +2 -0
  25. package/src/flows/helpers.ts +28 -0
  26. package/src/flows/runtime.ts +7 -0
  27. package/src/install/Install.ts +2 -1
  28. package/src/platform/Git.test.ts +25 -1
  29. package/src/platform/Git.ts +30 -0
  30. package/src/platform/index.ts +1 -0
  31. package/src/replay/Format.test.ts +16 -1
  32. package/src/replay/Replay.test.ts +91 -0
  33. package/src/replay/Replay.ts +22 -2
  34. package/src/replay/Trailers.test.ts +110 -1
  35. package/src/replay/Trailers.ts +37 -1
  36. package/src/replay/index.ts +2 -0
  37. package/src/replay/seamGate.test.ts +62 -0
  38. package/src/step/LandStep.ts +1 -0
  39. package/src/step/planStep.test.ts +32 -0
  40. package/src/step/planStep.ts +3 -1
  41. package/src/testkit/EmittedScriptRecognizer.test.ts +27 -0
  42. package/src/testkit/EmittedScriptRecognizer.ts +33 -0
  43. package/src/testkit/EmittedScripts.test.ts +9 -0
  44. package/src/testkit/FakeGitOperations.ts +5 -0
  45. package/src/testkit/GitTiers.ts +158 -0
  46. package/src/testkit/InMemRepo.ts +38 -3
  47. package/src/wire/BeatDocument.test.ts +25 -0
  48. package/src/wire/BeatDocument.ts +4 -2
  49. package/src/wire/BeatPlain.test.ts +1 -0
  50. package/src/wire/BeatStatus.test.ts +2 -0
  51. package/src/wire/BeatStatus.ts +13 -2
  52. package/src/wire/WireSchema.test.ts +10 -0
  53. package/src/wire/types.ts +8 -0
  54. package/src/workflow/Workflow.ts +1 -0
  55. package/src/workflows/feature/index.ts +1 -0
  56. package/src/workflows/feature/refactor.test.ts +466 -0
  57. package/src/workflows/feature/refactor.ts +178 -0
  58. package/src/workflows/index.ts +1 -0
  59. package/src/workflows/shared/access.ts +2 -1
  60. package/src/workflows/shared/health.test.ts +67 -3
  61. package/src/workflows/shared/health.ts +5 -3
  62. package/src/workflows/shared/options.test.ts +8 -0
  63. package/src/workflows/shared/options.ts +1 -0
  64. package/src/workflows/shared/skills.test.ts +6 -0
  65. package/src/workflows/shared/skills.ts +5 -1
  66. package/src/workflows/shared/text.ts +35 -0
  67. package/src/workflows/shared/vars.ts +1 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "27.2.0",
3
+ "version": "27.4.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
package/README.md CHANGED
@@ -581,7 +581,7 @@ kept before it squashes.
581
581
 
582
582
  ```sh
583
583
  gtd history list # one line per kept process
584
- gtd history show <ref> # replay its steps and judge verdicts
584
+ gtd history show <ref> # replay its steps, judge verdicts and approvals
585
585
  gtd history fetch | sh # pull every kept ref from origin
586
586
  ```
587
587
 
@@ -601,6 +601,19 @@ of on the branch (see [Kept tips](docs/configuration.md#kept-tips)). Those refs
601
601
  are not process history: `gtd history` does not list them, and the
602
602
  `refs/gtd/history/*` refspecs above neither push nor fetch them.
603
603
 
604
+ A workflow can rest at an approval gate (`human(name, { approve: true })`; the
605
+ `seamGate` helper does this after a `refactor-first` diff-shape judgment). Only
606
+ a clean `gtd land` approves it, and the commit records who in a
607
+ `Gtd-Approved: {"step":"<step>#<n>","name":"<name>","email":"<email>"}` trailer;
608
+ a landing with any edit is refused (as is a script run after the tree changed —
609
+ revert and land again), and `gtd abandon` is the only other way out. `approve`
610
+ cannot be combined with `mode` or `file` (refused when replay reaches the step).
611
+ Git hooks do not run on the approval commit; `commit.gpgSign` still applies, so
612
+ a failing signer refuses the approval. `gtd next --json` lists the process's
613
+ approvals as `approvals` (`[]` when none), and `gtd history show` prints each as
614
+ `approved <step>#<n> by <name> <<email>>`. See
615
+ [Step options](docs/configuration.md#step-options).
616
+
604
617
  ## Entrypoints
605
618
 
606
619
  gtd is one package, `@pmelab/gtd`, and every entrypoint shares its version and
@@ -591,6 +591,26 @@ ${fixFeedbackPrompt}
591
591
  then legitimately report itself already satisfied later
592
592
  - Leave everything uncommitted and finish your turn — do not commit
593
593
  `;
594
+ const refactorRules = (strays, pattern) => `- Behaviour-neutral: the suite's outcome must not change
595
+ - Change only these paths, nothing else — a test file only when it is
596
+ one of them:
597
+ ${strays.map((path) => ` - \`${path}\``).join("\n")}
598
+ - Introduce no new flag read and add no second read of an existing one:
599
+ every match of \`${pattern}\` may move, never increase in number`;
600
+ const refactorBuildingPrompt = (pkg, strays, pattern, reference) => `${stateFileRules}
601
+ - The package to implement is \`${pkg}\`; never delete it
602
+ ${refactorRules(strays, pattern)}
603
+ ${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
604
+ earlier attempt at this change. Read it for ideas; never apply,
605
+ cherry-pick or check it out — write a fresh refactor
606
+ `}- Leave everything uncommitted and finish your turn — do not commit
607
+ `;
608
+ const refactorFixPrompt = (strays, pattern) => `${stateFileRules}
609
+ ${fixFeedbackPrompt}
610
+ ${refactorRules(strays, pattern)}
611
+ - Never do another package's work, even when it would green the suite
612
+ - Leave everything uncommitted and finish your turn — do not commit
613
+ `;
594
614
  const healthJudgeMessage = () => `The check is still red, and this isn't the first round —
595
615
  \`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
596
616
  is in history. Run \`gtd judge answer\` and pipe a
@@ -1161,9 +1181,9 @@ const guarded = (turn, guard) => async (...a) => {
1161
1181
  await turn(...a);
1162
1182
  guard?.();
1163
1183
  };
1164
- const guardedSteps = (guard) => ({
1184
+ const guardedSteps = (guard, describeGuard) => ({
1165
1185
  limit: escalationSteps.limit,
1166
- describe: guarded(escalationSteps.describe, guard),
1186
+ describe: guarded(guarded(escalationSteps.describe, guard), describeGuard),
1167
1187
  stop: guarded(escalationSteps.stop, guard),
1168
1188
  exhausted: guarded(escalationSteps.exhausted, guard)
1169
1189
  });
@@ -1215,7 +1235,7 @@ const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
1215
1235
  fixesSoFar: options.fixesSoFar ?? 0,
1216
1236
  sameFailure,
1217
1237
  escalations: options.escalations ?? { rounds: 0 },
1218
- escalation: guardedSteps(options.guard)
1238
+ escalation: guardedSteps(options.guard, options.describeGuard)
1219
1239
  });
1220
1240
  /**
1221
1241
  * Hold the process at `<name>.blocked` until the suite is green. `guard` runs
@@ -1236,6 +1256,7 @@ const defaults = {
1236
1256
  judgeIdenticalMinP: "0.7",
1237
1257
  reviewNoteActionable: "0.7",
1238
1258
  reviewSeverityMinP: "0.7",
1259
+ refactorFlagPattern: "\\bGTD_FLAG_\\w+",
1239
1260
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge"
1240
1261
  };
1241
1262
  /** Environment settings. */
@@ -1253,6 +1274,7 @@ const ShippedOptions = Struct({
1253
1274
  judgeIdenticalMinP: setting,
1254
1275
  reviewNoteActionable: setting,
1255
1276
  reviewSeverityMinP: setting,
1277
+ refactorFlagPattern: setting,
1256
1278
  qualityReviews: setting,
1257
1279
  testCommand: setting,
1258
1280
  plannerModel: setting,
@@ -1314,6 +1336,9 @@ const skills = (vars) => ({
1314
1336
  "build.review.fix.nits": ["incremental-implementation", "code-simplification"],
1315
1337
  "build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
1316
1338
  notes: ["incremental-implementation", "code-simplification"],
1339
+ refactor: ["ponytail"],
1340
+ "refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
1341
+ "refactor.health": ["ponytail", "debugging-and-error-recovery"],
1317
1342
  ...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens])
1318
1343
  });
1319
1344
  //#endregion
@@ -1337,4 +1362,4 @@ const steering = {
1337
1362
  [REVIEW]: "review"
1338
1363
  };
1339
1364
  //#endregion
1340
- export { reviewerSystem as $, architectureAuthorPrompt as A, designGateAnswerMessage as B, fixRisks as C, reviewing as D, reviewQuality as E, buildReviewCollectingPrompt as F, handOffAwaitReviewMessage as G, designTriagePrompt as H, buildReviewFixNitsPrompt as I, packagesItemFixSuitePrompt as J, idleMessage as K, buildReviewTriageMessage as L, architectureGateAnswerMessage as M, buildReviewAnswerQuestionsPrompt as N, agentWithSkills as O, buildReviewAwaitAfterDeferMessage as P, reviewNotesCapture as Q, builderSystem as R, fixQuality as S, reviewMissing as T, finisherSystem as U, designSystem as V, fixReviewNotesPrompt as W, reviewEditsCapture as X, reviewEditNotesCapture as Y, reviewGateBlockedMessage as Z, REQUIREMENTS as _, flagShipped as a, coder as b, envDefaults as c, gate as d, startGateBlockedMessage as et, guarded as f, QUALITY as g, FEEDBACK as h, skills as i, architectureDecomposePrompt as j, architectSystem as k, baseline as l, ARCHITECTURE as m, summary as n, unwindFailure as nt, shipped as o, healthy as p, packagesItemBuildingPrompt as q, access as r, defaults as s, steering as t, unwindFailedMessage as tt, escalation as u, REVIEW as v, planner as w, fixCheck as x, awaitReview as y, deferNotesScript as z };
1365
+ export { reviewGateBlockedMessage as $, architectureAuthorPrompt as A, designGateAnswerMessage as B, fixRisks as C, reviewing as D, reviewQuality as E, buildReviewCollectingPrompt as F, handOffAwaitReviewMessage as G, designTriagePrompt as H, buildReviewFixNitsPrompt as I, packagesItemFixSuitePrompt as J, idleMessage as K, buildReviewTriageMessage as L, architectureGateAnswerMessage as M, buildReviewAnswerQuestionsPrompt as N, agentWithSkills as O, buildReviewAwaitAfterDeferMessage as P, reviewEditsCapture as Q, builderSystem as R, fixQuality as S, reviewMissing as T, finisherSystem as U, designSystem as V, fixReviewNotesPrompt as W, refactorFixPrompt as X, refactorBuildingPrompt as Y, reviewEditNotesCapture as Z, REQUIREMENTS as _, flagShipped as a, coder as b, envDefaults as c, gate as d, reviewNotesCapture as et, guarded as f, QUALITY as g, FEEDBACK as h, skills as i, unwindFailure as it, architectureDecomposePrompt as j, architectSystem as k, baseline as l, ARCHITECTURE as m, summary as n, startGateBlockedMessage as nt, shipped as o, healthy as p, packagesItemBuildingPrompt as q, access as r, unwindFailedMessage as rt, defaults as s, steering as t, reviewerSystem as tt, escalation as u, REVIEW as v, planner as w, fixCheck as x, awaitReview as y, deferNotesScript as z };
@@ -1,6 +1,6 @@
1
1
  import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
2
2
  import { fixFindings, lensesOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
3
- import { C as fixRisks, D as reviewing, E as reviewQuality, G as handOffAwaitReviewMessage, S as fixQuality, T as reviewMissing, f as guarded, g as QUALITY, p as healthy, u as escalation, v as REVIEW, x as fixCheck, y as awaitReview } from "./reserved-B2QaImnN.mjs";
3
+ import { C as fixRisks, D as reviewing, E as reviewQuality, G as handOffAwaitReviewMessage, S as fixQuality, T as reviewMissing, f as guarded, g as QUALITY, p as healthy, u as escalation, v as REVIEW, x as fixCheck, y as awaitReview } from "./reserved-BPdVZkgv.mjs";
4
4
  //#region src/workflows/shared/tail.ts
5
5
  /** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
6
6
  const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
@@ -110,7 +110,7 @@ const demandOf = (input) => {
110
110
  * instead of failing loudly.
111
111
  */
112
112
  const statusOf = (input) => {
113
- const { rendered, idle, initial, workflow, log, changes, next, cost, costByModel } = input;
113
+ const { rendered, idle, initial, workflow, log, changes, next, cost, costByModel, approvals } = input;
114
114
  return {
115
115
  idle,
116
116
  initial,
@@ -128,7 +128,8 @@ const statusOf = (input) => {
128
128
  next,
129
129
  cost,
130
130
  costByModel,
131
- judge: rendered.judge
131
+ judge: rendered.judge,
132
+ approvals
132
133
  };
133
134
  };
134
135
  //#endregion
@@ -168,7 +169,7 @@ const dispatchFieldsOf = (demand) => {
168
169
  }
169
170
  };
170
171
  /**
171
- * Flatten a `Demand` plus its `BeatStatus` into the single 24-key document
172
+ * Flatten a `Demand` plus its `BeatStatus` into the single 25-key document
172
173
  * `gtd next --json` emits — the ONLY place the two are joined. `cost`/
173
174
  * `costByModel` are omitted together, exactly when no cost was recorded
174
175
  * (`cost <= 0`).
@@ -200,7 +201,8 @@ const beatDocument = (demand, status) => {
200
201
  costByModel: hasCost ? status.costByModel : void 0,
201
202
  judge: status.judge,
202
203
  skills,
203
- access
204
+ access,
205
+ approvals: status.approvals
204
206
  };
205
207
  };
206
208
  const renderBeatJson = (document) => JSON.stringify(document) + "\n";
package/dist/flows.mjs CHANGED
@@ -400,6 +400,28 @@ const judgeDiffShape = async (name, options) => {
400
400
  judgment
401
401
  };
402
402
  };
403
+ const displayPath = (path) => /[\x00-\x1f\x7f-\x9f]/.test(path) ? JSON.stringify(path) : path;
404
+ const seamReport = (shape) => {
405
+ const unreadable = new Set(shape.unreadable);
406
+ return [
407
+ "Seam report: the judge found the diff's shape needs a refactor first. These paths stray outside the allowlist:",
408
+ "",
409
+ ...shape.strays.map((path) => `- ${displayPath(path)}${unreadable.has(path) ? " (unreadable — not judged)" : ""}`),
410
+ "",
411
+ "Landing a clean tree with `gtd land` approves this diff exactly as judged; edits are refused. To stop instead, run `gtd abandon`."
412
+ ].join("\n");
413
+ };
414
+ /**
415
+ * Rest at an approval gate `name` showing the seam report — only when
416
+ * `shape.choice` is `refactor-first`. `land` and `reject` reach no step.
417
+ */
418
+ const seamGate = async (name, shape) => {
419
+ if (shape.choice !== "refactor-first") return;
420
+ await human(name, {
421
+ message: seamReport(shape),
422
+ approve: true
423
+ });
424
+ };
403
425
  //#endregion
404
426
  //#region src/flows/factory.ts
405
427
  const FACTORY = Symbol.for("@pmelab/gtd/workflow-factory");
@@ -607,6 +629,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
607
629
  revertScript: () => revertScript,
608
630
  run: () => run,
609
631
  scope: () => scope,
632
+ seamGate: () => seamGate,
610
633
  sectionBodies: () => sectionBodies,
611
634
  sections: () => sections,
612
635
  showScript: () => showScript,
@@ -617,4 +640,4 @@ var flows_exports = /* @__PURE__ */ __exportAll({
617
640
  wrote: () => wrote
618
641
  });
619
642
  //#endregion
620
- export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, judgeDiffShape, kept, matches, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, sectionBodies, sections, showScript, skillsFor, start, flows_exports as t, threads, vars, wrote };
643
+ export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, judgeDiffShape, kept, matches, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, seamGate, sectionBodies, sections, showScript, skillsFor, start, flows_exports as t, threads, vars, wrote };
@@ -20269,6 +20269,13 @@ const makeGitImpl = (executor, root) => {
20269
20269
  hasCommits: () => exec("git", "rev-parse", "--verify", "HEAD").pipe(map$4(() => true), catchAll(() => succeed$3(false))),
20270
20270
  readFileAtRef: (ref, path) => exec("git", "show", `${ref}:${path}`),
20271
20271
  diffPaths: (base, tip) => exec("git", "diff", "--name-only", "-z", "--no-renames", "--no-relative", "--ignore-submodules=none", "--end-of-options", base, tip, "--").pipe(map$4(splitNul)),
20272
+ authorIdent: () => exec("git", "var", "GIT_AUTHOR_IDENT").pipe(map$4((out) => {
20273
+ const match = /^(.*?) <([^<>]*)> \d+ [+-]\d{4}$/.exec(out.trim());
20274
+ return match === null || match[1] === "" || match[2] === "" ? none$4() : some({
20275
+ name: match[1],
20276
+ email: match[2]
20277
+ });
20278
+ }), catchAll(() => succeed$3(none$4()))),
20272
20279
  changedPaths: (base) => gen(function* () {
20273
20280
  const ref = base ?? "HEAD";
20274
20281
  const nameStatusOut = yield* exec("git", "diff", "--name-status", "-z", ref).pipe(catchAll(() => succeed$3("")));
@@ -20286,6 +20293,7 @@ const makeGitImpl = (executor, root) => {
20286
20293
  }
20287
20294
  return all;
20288
20295
  }),
20296
+ stagedPaths: () => exec("git", "diff", "--cached", "--name-status", "--no-renames", "-z", "HEAD").pipe(catchAll(() => succeed$3("")), map$4((out) => parseNameStatus(splitNul(out)))),
20289
20297
  resolveRef: (ref) => exec("git", "rev-parse", "--verify", "--end-of-options", ref).pipe(map$4((s) => s.trim()), flatMap$4((hash) => resolvedRefOrCorrupted(ref, hash))),
20290
20298
  readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
20291
20299
  isAncestor: (a, b) => run$4(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$1(succeed$2(CommandExecutor, executor)), map$4(() => true), catchAll(() => succeed$3(false))),
@@ -21231,7 +21239,8 @@ skills, \`skills\` (an array of the skill names, already trimmed and split —
21231
21239
  (\`{read, write}\`, each an array of globs or \`null\` for unrestricted). When set: \`model\`, \`memory\`,
21232
21240
  \`label\`, \`file\`, \`mode\`, \`edges\`, \`judge\` (the pending judgment's rendered
21233
21241
  document). When a cost has been recorded (a prior \`gtd land --cost=<n>\`):
21234
- \`cost\`, \`costByModel\`.
21242
+ \`cost\`, \`costByModel\`. Always \`approvals\` (an array of
21243
+ \`{step, name, email, commit}\`, \`[]\` when none).
21235
21244
 
21236
21245
  \`gtd land\` fields, in fixed order: \`schema\`, \`script\` (the POSIX sh to run), \`settled\`
21237
21246
  (true for a no-op at a \`script\` rest — stop immediately, nothing more to
@@ -22013,6 +22022,28 @@ const judgeDiffShape = async (name, options) => {
22013
22022
  judgment
22014
22023
  };
22015
22024
  };
22025
+ const displayPath = (path) => /[\x00-\x1f\x7f-\x9f]/.test(path) ? JSON.stringify(path) : path;
22026
+ const seamReport = (shape) => {
22027
+ const unreadable = new Set(shape.unreadable);
22028
+ return [
22029
+ "Seam report: the judge found the diff's shape needs a refactor first. These paths stray outside the allowlist:",
22030
+ "",
22031
+ ...shape.strays.map((path) => `- ${displayPath(path)}${unreadable.has(path) ? " (unreadable — not judged)" : ""}`),
22032
+ "",
22033
+ "Landing a clean tree with `gtd land` approves this diff exactly as judged; edits are refused. To stop instead, run `gtd abandon`."
22034
+ ].join("\n");
22035
+ };
22036
+ /**
22037
+ * Rest at an approval gate `name` showing the seam report — only when
22038
+ * `shape.choice` is `refactor-first`. `land` and `reject` reach no step.
22039
+ */
22040
+ const seamGate = async (name, shape) => {
22041
+ if (shape.choice !== "refactor-first") return;
22042
+ await human(name, {
22043
+ message: seamReport(shape),
22044
+ approve: true
22045
+ });
22046
+ };
22016
22047
  //#endregion
22017
22048
  //#region src/flows/factory.ts
22018
22049
  const FACTORY = Symbol.for("@pmelab/gtd/workflow-factory");
@@ -22220,6 +22251,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
22220
22251
  revertScript: () => revertScript,
22221
22252
  run: () => run$3,
22222
22253
  scope: () => scope,
22254
+ seamGate: () => seamGate,
22223
22255
  sectionBodies: () => sectionBodies,
22224
22256
  sections: () => sections,
22225
22257
  showScript: () => showScript,
@@ -37282,7 +37314,7 @@ var init_Demand = __esmMin((() => {
37282
37314
  var statusOf;
37283
37315
  var init_BeatStatus = __esmMin((() => {
37284
37316
  statusOf = (input) => {
37285
- const { rendered, idle, initial, workflow, log, changes, next, cost, costByModel } = input;
37317
+ const { rendered, idle, initial, workflow, log, changes, next, cost, costByModel, approvals } = input;
37286
37318
  return {
37287
37319
  idle,
37288
37320
  initial,
@@ -37300,7 +37332,8 @@ var init_BeatStatus = __esmMin((() => {
37300
37332
  next,
37301
37333
  cost,
37302
37334
  costByModel,
37303
- judge: rendered.judge
37335
+ judge: rendered.judge,
37336
+ approvals
37304
37337
  };
37305
37338
  };
37306
37339
  }));
@@ -37360,7 +37393,8 @@ var init_BeatDocument = __esmMin((() => {
37360
37393
  costByModel: hasCost ? status.costByModel : void 0,
37361
37394
  judge: status.judge,
37362
37395
  skills,
37363
- access
37396
+ access,
37397
+ approvals: status.approvals
37364
37398
  };
37365
37399
  };
37366
37400
  renderBeatJson = (document) => JSON.stringify(document) + "\n";
@@ -37689,12 +37723,38 @@ const TRAILER_RE = /^(Gtd-[A-Za-z-]+):[ \t]*(.*?)[ \t]*$/gm;
37689
37723
  const COST_RE = /^([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+))?$/;
37690
37724
  const VAR_RE = /^([^=\s]+)=(.*)$/;
37691
37725
  const CONFIG_RE = /^([0-9a-f]{16}) (.+)$/;
37692
- const CONTROL_CHARACTER$1 = /[\x00-\x1f\x7f-\x9f]/;
37726
+ const CONTROL_CHARACTER$1 = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/;
37693
37727
  const encodePath = (path) => path.trim() !== path || path.startsWith("\"") || CONTROL_CHARACTER$1.test(path) ? JSON.stringify(path) : path;
37694
37728
  const decodePath = (raw) => {
37695
37729
  const path = raw.startsWith("\"") ? parseJson$1(raw) : raw;
37696
37730
  return typeof path === "string" && path !== "" && !CONTROL_CHARACTER$1.test(path) ? path : void 0;
37697
37731
  };
37732
+ const decodeApproval = (raw) => {
37733
+ const parsed = parseJson$1(raw);
37734
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
37735
+ const { step, name, email } = parsed;
37736
+ if (typeof step !== "string" || typeof name !== "string" || typeof email !== "string") return void 0;
37737
+ if ([
37738
+ step,
37739
+ name,
37740
+ email
37741
+ ].some((field) => CONTROL_CHARACTER$1.test(field))) return void 0;
37742
+ const id = parseStepId(step);
37743
+ return id === void 0 ? void 0 : {
37744
+ step: id,
37745
+ name,
37746
+ email
37747
+ };
37748
+ };
37749
+ const encodeApproval = ({ step, name, email }) => JSON.stringify({
37750
+ step: formatStepId(step),
37751
+ name,
37752
+ email
37753
+ });
37754
+ const isRecordableApproval = (approval) => {
37755
+ const back = decodeApproval(encodeApproval(approval));
37756
+ return back !== void 0 && encodeApproval(back) === encodeApproval(approval);
37757
+ };
37698
37758
  const FORMAT_RE = /^[1-9][0-9]*$/;
37699
37759
  const HASH_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
37700
37760
  const TRAILER_READERS = {
@@ -37729,6 +37789,9 @@ const TRAILER_READERS = {
37729
37789
  "Gtd-Kept": (value, into) => {
37730
37790
  if (HASH_RE.test(value)) into.kept ??= value;
37731
37791
  },
37792
+ "Gtd-Approved": (value, into) => {
37793
+ into.approved ??= decodeApproval(value);
37794
+ },
37732
37795
  "Gtd-Format": (value, into) => {
37733
37796
  if (FORMAT_RE.test(value)) into.format ??= Number(value);
37734
37797
  },
@@ -37750,6 +37813,7 @@ const parseCommitMessage = (message) => {
37750
37813
  branch: void 0,
37751
37814
  version: void 0,
37752
37815
  kept: void 0,
37816
+ approved: void 0,
37753
37817
  judge: [],
37754
37818
  vars: {},
37755
37819
  config: {},
@@ -37775,6 +37839,7 @@ const formatCommitMessage = (spec) => {
37775
37839
  for (const [path, hash] of sortedEntries(spec.config)) lines.push(`Gtd-Config: ${hash} ${encodePath(path)}`);
37776
37840
  if (spec.cost !== void 0) lines.push(`Gtd-Cost: ${spec.cost.cost}${spec.cost.model !== void 0 ? ` ${spec.cost.model}` : ""}`);
37777
37841
  for (const verdict of spec.judge ?? []) lines.push(`Gtd-Judge: ${JSON.stringify(verdict)}`);
37842
+ if (spec.approved !== void 0) lines.push(`Gtd-Approved: ${encodeApproval(spec.approved)}`);
37778
37843
  if (spec.branch !== void 0) lines.push(`Gtd-Branch: ${spec.branch}`);
37779
37844
  if (spec.version !== void 0) lines.push(`Gtd-Version: ${spec.version}`);
37780
37845
  lines.push(`Gtd-Format: 1`);
@@ -37870,7 +37935,8 @@ const KNOWN_OPTIONS = {
37870
37935
  human: /* @__PURE__ */ new Set([
37871
37936
  ...STEERING_OPTIONS,
37872
37937
  "message",
37873
- "acceptClean"
37938
+ "acceptClean",
37939
+ "approve"
37874
37940
  ]),
37875
37941
  run: new Set(STEERING_OPTIONS),
37876
37942
  judge: /* @__PURE__ */ new Set([...STEERING_OPTIONS, "message"])
@@ -37880,6 +37946,13 @@ const unknownOptions = (step) => {
37880
37946
  if (unknown.length === 0) return void 0;
37881
37947
  return `gtd: step "${step.name}": unknown key(s) ${unknown.join(", ")} in ${step.kind}() options`;
37882
37948
  };
37949
+ const approveConflict = (step) => {
37950
+ const request = step.request;
37951
+ if (request.kind !== "human" || request.options.approve !== true) return void 0;
37952
+ const keys = ["mode", "file"].filter((key) => request.options[key] !== void 0);
37953
+ if (keys.length === 0) return void 0;
37954
+ return `gtd: step "${step.name}": approve cannot be combined with ${keys.join(", ")} in human() options — an approval records exactly the judged tree, and a steering file is rewritten on landing`;
37955
+ };
37883
37956
  const replay = async (input) => {
37884
37957
  const commits = input.episode.commits.map((c) => ({
37885
37958
  ...c,
@@ -38039,12 +38112,21 @@ const replay = async (input) => {
38039
38112
  kind: "divergence",
38040
38113
  message: `gtd: commit ${short(expected.hash)} names "${expected.name}" as the next step, but replay reached "${step.name}" — ${DIVERGED}`
38041
38114
  };
38042
- const error = unknownOptions(step) ?? checkIdentity(step);
38115
+ const error = unknownOptions(step) ?? approveConflict(step) ?? checkIdentity(step);
38043
38116
  return error === void 0 ? void 0 : {
38044
38117
  kind: "failed",
38045
38118
  message: error
38046
38119
  };
38047
38120
  };
38121
+ const missingApproval = (step, commit, expected) => {
38122
+ if (step.request.kind !== "human" || step.request.options.approve !== true) return void 0;
38123
+ const approved = commit.parsed.approved;
38124
+ if (approved !== void 0 && formatStepId(approved.step) === expected) return void 0;
38125
+ return {
38126
+ kind: "divergence",
38127
+ message: `gtd: commit ${short(commit.hash)} completes approval gate "${expected}" but records no Gtd-Approved for it — ${DIVERGED}`
38128
+ };
38129
+ };
38048
38130
  const consumeCommit = (step, commit) => {
38049
38131
  const recorded = commit.parsed.step;
38050
38132
  const expected = formatStepId(step.id);
@@ -38055,6 +38137,8 @@ const replay = async (input) => {
38055
38137
  message: `gtd: commit ${short(commit.hash)} ${what}, but replay expected "${expected}" — ${DIVERGED}`
38056
38138
  });
38057
38139
  }
38140
+ const unapproved = missingApproval(step, commit, expected);
38141
+ if (unapproved !== void 0) return finish(unapproved);
38058
38142
  cursor++;
38059
38143
  advance(commit);
38060
38144
  if (commit.parsed.kept !== void 0) if (commit.kept === void 0) keptTips.delete(step.name);
@@ -39169,6 +39253,9 @@ const planStep = (snapshot, opts = {}) => {
39169
39253
  kind: "keepTurn",
39170
39254
  ref: landing.keepTurn,
39171
39255
  message
39256
+ } : landing.spec.approved !== void 0 ? {
39257
+ kind: "commitClean",
39258
+ message
39172
39259
  } : {
39173
39260
  kind: "commitAll",
39174
39261
  message
@@ -39376,6 +39463,12 @@ const pinnedConfigOf = (history) => {
39376
39463
  };
39377
39464
  }
39378
39465
  };
39466
+ const approvalEntryOf = (hash, message) => message.approved === void 0 ? [] : [{
39467
+ step: formatStepId(message.approved.step),
39468
+ name: message.approved.name,
39469
+ email: message.approved.email,
39470
+ commit: hash
39471
+ }];
39379
39472
  const buildRun = (history, location, closingHash) => {
39380
39473
  const processCommits = history.slice(location.processStart);
39381
39474
  const parsed = processCommits.map((c) => parseCommitMessage(c.message));
@@ -39391,6 +39484,7 @@ const buildRun = (history, location, closingHash) => {
39391
39484
  trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
39392
39485
  costEntries: parsed.flatMap(costEntriesOf),
39393
39486
  judgeVerdicts: parsed.flatMap((m) => m.judge),
39487
+ approvals: processCommits.flatMap((c, i) => approvalEntryOf(c.hash, parsed[i])),
39394
39488
  pinnedVars: { ...first?.vars },
39395
39489
  pinnedConfig: pinnedConfigOf(history.slice(Math.max(location.baseIndex, 0))),
39396
39490
  headTurn: headTurnOf(head),
@@ -39676,7 +39770,8 @@ const messageDef = (common, request, step) => ({
39676
39770
  ...common,
39677
39771
  kind: "message",
39678
39772
  content: request.options.message ?? request.options.label ?? step.name,
39679
- ...optional$1("acceptClean", request.options.acceptClean)
39773
+ ...optional$1("acceptClean", request.options.approve === true || request.options.acceptClean),
39774
+ ...optional$1("approve", request.options.approve)
39680
39775
  });
39681
39776
  const stepDefOf = (step) => {
39682
39777
  const request = step.request;
@@ -39805,6 +39900,12 @@ const setupFor = (config, run, workspace, host, startOverrides = {}) => gen(func
39805
39900
  }
39806
39901
  };
39807
39902
  });
39903
+ const pendingChanges = (git, withIndex) => gen(function* () {
39904
+ const worktree = yield* git.changedPaths();
39905
+ if (!withIndex) return worktree;
39906
+ const seen = new Set(worktree.map((e) => e.path));
39907
+ return [...worktree, ...(yield* git.stagedPaths()).filter((e) => !seen.has(e.path))];
39908
+ });
39808
39909
  /** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
39809
39910
  const restAt = (ref, startOverrides = {}) => gen(function* () {
39810
39911
  const git = yield* GitService;
@@ -39820,7 +39921,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
39820
39921
  yield* (yield* Narrator).narrate(`rest resolved: ${step.name} (awaits ${step.actor})`);
39821
39922
  const stepDef = stepDefOf(step);
39822
39923
  if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$7(/* @__PURE__ */ new Error(`gtd config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
39823
- const changes = ref === void 0 ? (yield* git.changedPaths()).map((e) => ({
39924
+ const changes = ref === void 0 ? (yield* pendingChanges(git, stepDef.approve === true)).map((e) => ({
39824
39925
  status: normalizeStatus(e.status),
39825
39926
  path: e.path
39826
39927
  })) : [];
@@ -39906,6 +40007,25 @@ const noProcessUnderway = (rest) => rest.run.workflow === void 0 && rest.state =
39906
40007
  /** `idle` means exactly one thing: no process underway, clean tree. */
39907
40008
  const restIsIdle = (rest) => noProcessUnderway(rest) && rest.changes.length === 0;
39908
40009
  const isHumanReviewGate = (def) => def.actor === "human" && def.mode === "review";
40010
+ const approvalRefusal = (rest, approver) => {
40011
+ if (rest.stepDef.approve !== true) return void 0;
40012
+ const id = rest.step.id;
40013
+ if (rest.changes.length > 0) return {
40014
+ kind: "refusal",
40015
+ message: `gtd land: approval-gate: ${formatStepId(id)} approves exactly the judged diff — revert your edits and run \`gtd land\` on a clean tree to approve, or run \`gtd abandon\``
40016
+ };
40017
+ if (isNone(approver)) return {
40018
+ kind: "refusal",
40019
+ message: `gtd land: approval-gate: ${formatStepId(id)} records who approved it, but git has no author identity — set user.name and user.email`
40020
+ };
40021
+ if (!isRecordableApproval({
40022
+ step: id,
40023
+ ...approver.value
40024
+ })) return {
40025
+ kind: "refusal",
40026
+ message: `gtd land: approval-gate: ${formatStepId(id)} cannot record ${approver.value.name} <${approver.value.email}> as approver — it would not read back unchanged from its Gtd-Approved trailer (a control character, for example)`
40027
+ };
40028
+ };
39909
40029
  /** The landing a clean tree decides before any replay, if it decides one. */
39910
40030
  const cleanLanding = (rest) => {
39911
40031
  if (rest.changes.length > 0) return void 0;
@@ -39960,7 +40080,11 @@ const closingTrailers = (branch) => ({
39960
40080
  });
39961
40081
  const isSettledNoop = (rest, to) => rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state;
39962
40082
  const keptTurn = (rest) => rest.stepDef.keep === true && rest.changes.length > 0 ? { keepTurn: keptRef(rest.run.startHash, rest.state) } : {};
39963
- const landingTo = (rest, to, branch) => {
40083
+ const approvedTrailer = (rest, approver) => isSome(approver) ? { approved: {
40084
+ step: rest.step.id,
40085
+ ...approver.value
40086
+ } } : {};
40087
+ const landingTo = (rest, to, branch, approver) => {
39964
40088
  if (isSettledNoop(rest, to)) return {
39965
40089
  kind: "noop",
39966
40090
  settled: true
@@ -39978,6 +40102,7 @@ const landingTo = (rest, to, branch) => {
39978
40102
  from: rest.state,
39979
40103
  to,
39980
40104
  step: rest.step.id,
40105
+ ...approvedTrailer(rest, approver),
39981
40106
  ...starts ? {
39982
40107
  vars: rest.vars,
39983
40108
  config: rest.config
@@ -39993,7 +40118,9 @@ const landingTo = (rest, to, branch) => {
39993
40118
  * replay with the working tree as the pending turn. The target step is
39994
40119
  * computed by replaying, never matched.
39995
40120
  */
39996
- const decideLanding = (rest, verdicts, branch) => gen(function* () {
40121
+ const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
40122
+ const unapproved = approvalRefusal(rest, approver);
40123
+ if (unapproved !== void 0) return unapproved;
39997
40124
  const early = cleanLanding(rest);
39998
40125
  if (early !== void 0) return early;
39999
40126
  if (rest.stepDef.kind === "prompt" && rest.stepDef.access?.write != null) {
@@ -40009,14 +40136,16 @@ const decideLanding = (rest, verdicts, branch) => gen(function* () {
40009
40136
  }));
40010
40137
  const to = yield* landingTarget(outcome, rest);
40011
40138
  if (typeof to !== "string") return to;
40012
- return landingTo(rest, to, branch);
40139
+ return landingTo(rest, to, branch, approver);
40013
40140
  });
40014
40141
  /** Where landing the pending turn would leave the process, or `undefined` when it would land nothing. */
40015
- const previewLanding = (rest) => decideLanding(rest, void 0, none$4()).pipe(map$4((landing) => landing.kind === "commit" ? landing.to : void 0), catchAll(() => succeed$3(void 0)));
40142
+ const previewLanding = (rest) => decideLanding(rest, void 0, none$4(), none$4()).pipe(map$4((landing) => landing.kind === "commit" ? landing.to : void 0), catchAll(() => succeed$3(void 0)));
40016
40143
  /** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
40017
40144
  const snapshotFromRest = (rest, verdicts) => gen(function* () {
40018
- const branch = yield* (yield* GitService).currentBranch();
40019
- const landing = yield* decideLanding(rest, verdicts, branch);
40145
+ const git = yield* GitService;
40146
+ const branch = yield* git.currentBranch();
40147
+ const approver = rest.stepDef.approve === true ? yield* git.authorIdent() : none$4();
40148
+ const landing = yield* decideLanding(rest, verdicts, branch, approver);
40020
40149
  return {
40021
40150
  stepDef: rest.stepDef,
40022
40151
  state: rest.state,
@@ -40070,7 +40199,8 @@ const historySteps = (run) => {
40070
40199
  actor: entry.actor,
40071
40200
  from: message.parsed?.from,
40072
40201
  to: entry.state,
40073
- verdicts: message.judge
40202
+ verdicts: message.judge,
40203
+ ...optional$1("approval", approvalEntryOf(entry.hash, message)[0])
40074
40204
  };
40075
40205
  });
40076
40206
  };
@@ -40128,8 +40258,26 @@ const commitAllowEmpty = (message) => {
40128
40258
  `fi`
40129
40259
  ].join("\n");
40130
40260
  };
40261
+ const CHANGED_SINCE_LAND = "gtd: the working tree changed since `gtd land` — revert the edits and run `gtd land` again to approve";
40131
40262
  /** `git add -A || exit 1` (an `&&` left side would not abort under `set -e`) then `commitAllowEmpty`. */
40132
40263
  const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
40264
+ /**
40265
+ * An approval records exactly the judged tree: HEAD is pinned before the
40266
+ * clean check and the commit is built over its tree with `commit-tree`, so no
40267
+ * hook runs and none can add to it. A HEAD that moved fails the guarded
40268
+ * `update-ref`. `commit-tree` ignores `commit.gpgSign`, so it is read here.
40269
+ */
40270
+ const commitClean = (message) => [
40271
+ `gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
40272
+ `if [ -n "$(git status --porcelain --untracked-files=all --ignore-submodules=none)" ]; then`,
40273
+ ` printf '%s\\n' ${shellQuote$1(CHANGED_SINCE_LAND)} >&2`,
40274
+ ` exit 1`,
40275
+ `fi`,
40276
+ `gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
40277
+ `if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
40278
+ `gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
40279
+ `git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head" || exit 1`
40280
+ ].join("\n");
40133
40281
  const mixedResetTo = (ref) => `git reset --mixed ${shellQuote$1(ref)}`;
40134
40282
  const hardResetTo = (ref) => `git reset --hard ${shellQuote$1(ref)}`;
40135
40283
  /**
@@ -40155,7 +40303,7 @@ const keepTurn = (refPrefix, message) => {
40155
40303
  `gtd_kept=$(git commit-tree "$gtd_tree" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
40156
40304
  `git update-ref ${keptRef} "$gtd_kept" || exit 1`,
40157
40305
  `gtd_marker=$(printf '%s\\nGtd-Kept: %s' ${shellQuote$1(message)} "$gtd_kept")`,
40158
- `if ! gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m "$gtd_marker") || ! git update-ref HEAD "$gtd_tip" "$gtd_head"; then`,
40306
+ `if ! gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m "$gtd_marker") || ! git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head"; then`,
40159
40307
  ` git reset -q && git update-ref -d ${keptRef}`,
40160
40308
  ` exit 1`,
40161
40309
  `fi`,
@@ -40193,6 +40341,7 @@ const renderOutcome = (outcome) => {
40193
40341
  const renderGitWrite = (write) => {
40194
40342
  switch (write.kind) {
40195
40343
  case "commitAll": return commitAll(write.message);
40344
+ case "commitClean": return commitClean(write.message);
40196
40345
  case "updateRef": return updateRef(write.ref, write.target);
40197
40346
  case "keepTurn": return keepTurn(write.ref, write.message);
40198
40347
  }
@@ -40676,6 +40825,7 @@ const runHistoryShowCommand = (ref, json, out) => gen(function* () {
40676
40825
  for (const step of trail.steps) {
40677
40826
  lines.push(`${seven(step.hash)} ${step.subject}`);
40678
40827
  for (const v of step.verdicts) lines.push(` judge ${v.id} = ${v.answer} (p=${v.p})`);
40828
+ if (step.approval !== void 0) lines.push(` approved ${step.approval.step} by ${step.approval.name} <${step.approval.email}>`);
40679
40829
  }
40680
40830
  lines.push(`ended (${trail.ended})`);
40681
40831
  out.write(`${lines.join("\n")}\n`);
@@ -41215,7 +41365,8 @@ const gatherBeatDocument = (rest, rendered) => gen(function* () {
41215
41365
  changes: computeStatusChanges(rest.changes),
41216
41366
  next: yield* computeNextMatch(rest),
41217
41367
  cost: rest.cost.total,
41218
- costByModel: rest.cost.byModel
41368
+ costByModel: rest.cost.byModel,
41369
+ approvals: rest.run.approvals
41219
41370
  }));
41220
41371
  });
41221
41372
  /**
@@ -41906,11 +42057,11 @@ const COMMAND_ROWS = [
41906
42057
  arity: { name: "ref" },
41907
42058
  details: [
41908
42059
  "Replay one kept process under today's config and print its",
41909
- "steps and judge verdicts. <ref> is a full ref, an entry hash",
41910
- "(or unique prefix) or a branch name. Refuses (exit 1) when a",
41911
- "config file differs from its content at the ref's tip. A",
41912
- "version mismatch only warns on stderr. --json prints the",
41913
- "trail as one document; writes nothing"
42060
+ "steps, judge verdicts and approvals. <ref> is a full ref, an",
42061
+ "entry hash (or unique prefix) or a branch name. Refuses",
42062
+ "(exit 1) when a config file differs from its content at the",
42063
+ "ref's tip. A version mismatch only warns on stderr. --json",
42064
+ "prints the trail as one document; writes nothing"
41914
42065
  ]
41915
42066
  },
41916
42067
  {