@pmelab/gtd 8.4.0 → 8.6.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 (4) hide show
  1. package/README.md +212 -58
  2. package/bin/gtd +184 -21
  3. package/dist/gtd.bundle.mjs +4436 -2966
  4. package/package.json +11 -9
package/README.md CHANGED
@@ -6,10 +6,10 @@
6
6
  **Chat is a terrible source of truth. Git isn't.**
7
7
 
8
8
  **gtd** is a git-aware CLI that derives the entire agentic workflow — capture,
9
- plan, build, test, review — from your repository state, and prints the next
10
- prompt for whatever agent you point at it. Every step is a commit. Tests are run
11
- by the tool and branched on by exit code, so the agent never grades its own
12
- homework.
9
+ plan, build, check, review — from your repository state, and prints the next
10
+ prompt for whatever agent you point at it. Every turn is a commit. The test
11
+ command is printed for your driver to run, and the branch is decided by what the
12
+ run leaves in the tree — so the agent never grades its own homework.
13
13
 
14
14
  No chat scrollback. No lost sessions. No infinite fix loops. Just git.
15
15
 
@@ -120,31 +120,31 @@ for). The two steering-file entries are chosen by which file you create:
120
120
  Both flows converge on the same tail: an agent hands you a `.gtd/REVIEW.md`
121
121
  checkbox review of the diff — the prompt never inlines the diff itself; it names
122
122
  the commit the changes are based at and the agent runs `git diff` to read the
123
- range before writing the review. While the cycle rests at that gate, gtd opens a
124
- **review checkout window**: HEAD is rewound to the review base with the working
125
- tree untouched, so the whole reviewable change shows up as ordinary uncommitted
126
- changes in your editor's normal git integration (and files added during the
127
- cycle show up as ordinary untracked files, so discarding one deletes it). The
128
- window closes on the next gtd invocation. Tick a box as you review each hunk
129
- (ticking just records "I read this"), and leave a **comment** to request
130
- changes: a note on a line, an inline `// TODO`-style comment in the code, or a
131
- direct code edit. Any comment sends a build + re-review round — an agent first
132
- turns your comments into an explicit instruction list, then a build turn
133
- implements it (a re-review then covers only the follow-through, and a hand-edit
134
- is treated as your own fix the agent completes without reverting your lines; a
135
- comment can't be silently dropped — a build turn that addresses nothing is
136
- refused). For the simple flow, the build turn that follows through on feedback
137
- and the turn that drafts the final squash message both resume the same session
138
- that built the feature in the first place, since the review tail is nested
139
- inside that build identity rather than sitting beside it. Ticking every box with
140
- no comment is the sign-off, which collapses the whole cycle into one commit (a
141
- **squash finale** whose message an agent drafts). Stepping with a box still
142
- unticked and no comment is refused (finish reviewing first), as is deleting
143
- `.gtd/REVIEW.md`.
123
+ range before writing the review. While the process rests at that gate, gtd opens
124
+ a **review checkout window**: HEAD is rewound to the review base with the
125
+ working tree untouched, so the whole reviewable change shows up as ordinary
126
+ uncommitted changes in your editor's normal git integration (and files added
127
+ during the process show up as ordinary untracked files, so discarding one
128
+ deletes it). The window closes on the next gtd invocation. Tick a box as you
129
+ review each hunk (ticking just records "I read this"), and leave a **comment**
130
+ to request changes: a note on a line, an inline `// TODO`-style comment in the
131
+ code, or a direct code edit. Any comment sends a build + re-review round — an
132
+ agent first turns your comments into an explicit instruction list, then a build
133
+ turn implements it (a re-review then covers only the follow-through, and a
134
+ hand-edit is treated as your own fix the agent completes without reverting your
135
+ lines; a comment can't be silently dropped — a build turn that addresses nothing
136
+ is refused). For the simple flow, the build turn that follows through on
137
+ feedback and the turn that drafts the final squash message both resume the same
138
+ session that built the feature in the first place, since the review tail is
139
+ nested inside that build identity rather than sitting beside it. Ticking every
140
+ box with no comment is the sign-off, which collapses the whole process into one
141
+ commit (a **squash finale** whose message an agent drafts). Stepping with a box
142
+ still unticked and no comment is refused (finish reviewing first), as is
143
+ deleting `.gtd/REVIEW.md`.
144
144
 
145
145
  The same review tail also has a direct entry point —
146
146
  `gtd --entry review-gate.check --var reviewBase=<commitish>` starts a brand new
147
- process reviewing `<commitish>..HEAD` with no cycle of its own, e.g. a
147
+ process reviewing `<commitish>..HEAD` with no build of its own, e.g. a
148
148
  colleague's PR branch (`review-gate.check`'s `reviewBase:` is a template bound
149
149
  to the `reviewBase` var, so supplying it via `--var` fixes the whole process's
150
150
  diff base to that commitish). Its squash keeps and describes only the fixes made
@@ -207,14 +207,12 @@ Commands:
207
207
  just-finished invocation's token cost and model on the
208
208
  turn commit (summed into it.processCost/processCostByModel).
209
209
  Pass --entry <state> to start a brand NEW process at
210
- <state> instead — any declared, non-commit state (e.g.
211
- review-gate.check or fix-precheck on the bundled unified
212
- template) — with repeatable --var <name>=<value> supplying
213
- that new process's fixed it.vars overrides
210
+ <state> instead — any declared, non-commit state — with
211
+ repeatable --var <name>=<value> supplying that new
212
+ process's fixed it.vars overrides
214
213
  (no command) --entry <state>
215
214
  Short form of 'step human --entry <state>' — starts a new
216
- process authenticated as human, e.g.
217
- 'gtd --entry review-gate.check'
215
+ process authenticated as human, e.g. 'gtd --entry <state>'
218
216
  abandon End the process currently underway without completing it:
219
217
  close any open review checkout window, then rewind HEAD to
220
218
  the commit the process started from, keeping everything it
@@ -226,17 +224,29 @@ Commands:
226
224
  Refuses on a dirty working tree, when there is no retained
227
225
  history, or when HEAD has advanced past the squash with
228
226
  commits that would be lost
229
- next Print the resolved rest's rendered script/prompt/message
230
- (no mutation)
227
+ next Print the resolved rest's rendered script/prompt/message (no mutation)
231
228
  status Print the resolved rest's state/actor and which declared
232
229
  pattern (if any) each pending change matches (no mutation)
233
- validate Format and validate the steering file the resolved rest
234
- declares, with its mode's commands (its file:/mode:);
235
- exits non-zero with the findings when it is invalid
230
+ validate Print the script that formats (when declared) then
231
+ validates the resolved rest's steering file, using its
232
+ mode's commands (its file:/mode:), instead of running it —
233
+ a driver runs the script and reads the findings from its
234
+ own exit code/output. Always exits 0; --json emits
235
+ {state, file?, mode?, script} (script is "" when there is
236
+ nothing to validate; plain text prints "nothing to
237
+ validate" in that case)
236
238
  lsp Start the LSP server for .gtd/ steering files (stdio)
237
239
  visualize Serve an interactive diagram of the active workflow on a
238
240
  local web server (--port <n>, --no-open; --json prints the
239
241
  model and exits)
242
+ check <mode> <file>
243
+ Read <file> and run the built-in steering format named
244
+ <mode> (see `gtd validate`'s modes: qa, review) over its
245
+ contents, printing each finding one per line and exiting
246
+ non-zero when there are any. Resolves no workflow state and
247
+ reads no config — standalone, runnable from any directory
248
+ with <mode>/<file> given explicitly. This is what a
249
+ workflow's emitted validation script invokes as a leaf step
240
250
  version Print version and exit
241
251
  help Print this help and exit
242
252
 
@@ -255,9 +265,6 @@ Options:
255
265
  (with --entry; repeatable) supply a fixed it.vars
256
266
  override for the new process; the name must already be
257
267
  declared by the workflow's own vars: or the .gtdrc vars:
258
- --once (bare gtd or gtd loop only) run exactly one loop beat (one
259
- human-gate capture, one script check+step, or one agent
260
- prompt+step), then exit
261
268
  --version, -v Print version and exit
262
269
  --help, -h Print this help and exit
263
270
  ```
@@ -312,9 +319,16 @@ envelope on **stdout**, and still exits 1:
312
319
  { "state": "error", "prompt": "<message>" }
313
320
  ```
314
321
 
322
+ This covers every failure mode, not just a command's own refusal: a **usage
323
+ error** (an unknown flag, a missing argument, `gtd --entry version`'s "not an
324
+ enterable state") and a **defect** (a layer throwing outside the ordinary error
325
+ channel) both get the same envelope — there is no failure path that reaches
326
+ `--json` without one.
327
+
315
328
  A human-readable `gtd: <message>` line is still written to **stderr** regardless
316
329
  of `--json` — the envelope adds a structured stdout channel, it does not replace
317
- the plain-text one.
330
+ the plain-text one. Stderr always carries exactly one `gtd: ` prefix: a message
331
+ already authored with its own `gtd:`/`gtd <cmd>:` prefix is never doubled.
318
332
 
319
333
  ## Driving the loop
320
334
 
@@ -453,7 +467,7 @@ A few things to know before relying on it:
453
467
  owns a `herdr:claude` session until the process exits; while it does, the
454
468
  wrapper's reports are silently ignored and the sidebar stops tracking `gtd`.
455
469
  - **`blocked` covers the resting `idle` state too** — gtd's own `idle` is a
456
- human gate (it waits for you to write a steering file), so a finished cycle
470
+ human gate (it waits for you to write a steering file), so a finished process
457
471
  reads as "your turn", which is what it is.
458
472
  - The status **persists after the wrapper exits** — that's the point: the
459
473
  sidebar keeps showing which worktree is waiting on you. Hand the pane back to
@@ -468,6 +482,123 @@ Optionally,
468
482
  sets a value renderable as `$summary` in an Agent sidebar row, if you want more
469
483
  than the state itself.
470
484
 
485
+ ## Writing your own driver
486
+
487
+ `bin/gtd` is gtd's own reference driver, not a privileged one — the engine
488
+ itself is a supported public surface, and anything below holds for any driver
489
+ you write against it. gtd decides and prints; it never touches git itself. The
490
+ four commands that change anything — `gtd step [<actor>]` (including
491
+ `--entry <state>`), `gtd abandon`, and `gtd restore` — perform no git write when
492
+ run. Instead, under `--json`, each one's output object carries two extra string
493
+ fields, `required` and `optional`: bash scripts for YOU to execute. (Plain-text
494
+ output, without `--json`, never shows these — it prints only the human-facing
495
+ result line, e.g. `committed: <subject>`, so a plain-text caller never sees a
496
+ script at all.) A driver that only prints gtd's plain-text output is not driving
497
+ anything; to actually land a turn, run `gtd ... --json`, pull `required`/
498
+ `optional` out with `jq`, and execute them.
499
+
500
+ - **`required`** is everything that decides what lands in git — closing an open
501
+ review checkout window, the resting state's own steering-mode
502
+ `format:`/`validate:` commands, then the commit or squash itself (`gtd step`
503
+ and `gtd --entry <state>`), or the ref update and reset that undo a process
504
+ (`gtd abandon`, `gtd restore`). Run this one. Skipping it means the turn never
505
+ lands.
506
+ - **`optional`** is presentation only: re-opening the review checkout window
507
+ (the `<<<<<<< HEAD` diff view) after a `gtd step` lands at a
508
+ `reviewWindow: true` state, so an editor's diff view has something to show.
509
+ Skip it and you lose nothing but that view — the workflow is still driven
510
+ correctly. `gtd abandon`/`gtd restore` always emit `optional: ""` (there is no
511
+ window to reopen after either).
512
+
513
+ Either field may be the empty string `""`, meaning "nothing to do here" — a
514
+ no-op `gtd step` (a clean tree matching no `on` pattern) emits
515
+ `required: ""`/`optional: ""` and exits zero; run nothing and move on. Both
516
+ scripts are self-contained (each carries its own precondition assert and retry
517
+ helper) and safe to run standalone, in sequence, or not at all — paste either
518
+ into a terminal and it does exactly what it says.
519
+
520
+ ### Failure taxonomy and recovery
521
+
522
+ Two different things can go wrong, and they mean different things:
523
+
524
+ - **`gtd` itself exits non-zero.** Nothing was attempted — this is a refusal (a
525
+ guard rejected the turn) or a usage error. No script was ever produced.
526
+ - **An emitted script exits non-zero when YOU run it.** Something may have
527
+ partially happened — e.g. a `gtd_retry`-wrapped git write landed but a later
528
+ step in the same script failed.
529
+
530
+ Recovery is the same in both cases: **re-invoke `gtd`.** It re-reads the real
531
+ repository state fresh every time — never a cached plan — and emits whatever
532
+ still needs to happen from there. This works because every emitted script opens
533
+ by asserting its own precondition
534
+ (`[ "$(git rev-parse --verify --quiet HEAD 2>/dev/null)" = <expected> ] || { ...; exit 1; }`,
535
+ and the same shape for a review window's saved ref — see `src/Emit.ts`'s
536
+ `headAssertion`/`reviewWindowAssertion`), so a script generated against a
537
+ repository state that has since moved refuses loudly instead of corrupting
538
+ anything. In a repository with no commits yet, `<expected>` is the empty string
539
+ — the deciding read's own convention for "no commits yet" — so the first
540
+ workflow commit is allowed to land instead of being blocked by an unborn `HEAD`.
541
+ **Emitted scripts are re-runnable**: this is the single most important property
542
+ for a driver's recovery logic. Re-running a script that already fully applied is
543
+ a no-op (its git writes are `--allow-empty` commits, idempotent ref updates, and
544
+ tolerant staged-restore calls), and re-running one that only partially applied
545
+ resumes correctly, because the precondition either still holds (nothing landed
546
+ yet — safe to retry verbatim) or gtd's next invocation reads the new real state
547
+ and emits a fresh script for what remains. A driver never needs its own
548
+ retry/resume logic beyond "if the script failed, ask gtd again."
549
+
550
+ ### Prerequisites
551
+
552
+ - **`jq`** — to pull the `required`/`optional` strings back out of gtd's
553
+ `--json` output.
554
+ - **`gtd` on `PATH`** — a mode's seeded `validate:` command (the one the
555
+ compiler fills in for the built-in `qa`/`review` formats) is literally the
556
+ string `gtd check <mode> '<file>'`, invoked by NAME from inside an emitted
557
+ script, not by absolute path (see `src/SteeringFormats.ts`'s
558
+ `seededValidateCommand`). This is a deliberate trade: a readable, overridable,
559
+ copy-pasteable command in exchange for depending on shell name resolution at
560
+ the moment the script runs. The sharp edge: if the `gtd` binary you invoked to
561
+ GENERATE the script differs from the `gtd` that resolves on `PATH` when the
562
+ script later RUNS (a locally-built dev binary vs. a globally-installed
563
+ release, say), you can get version skew between the two — the command that
564
+ validates may not be the command that decided. Keep the two in sync (one `gtd`
565
+ on `PATH`, consistently) if you care about that gap.
566
+
567
+ ### The normalization-only contract on `format:`
568
+
569
+ A mode's `format:` command may reformat a steering file — whitespace, wrapping,
570
+ reordering — but must NEVER change what a step-capture guard would decide. gtd's
571
+ guards (the review sign-off check, the feedback-progress check, the
572
+ answer-completeness check — `src/StepGuards.ts`) decide ONCE, against whichever
573
+ bytes are on disk at the moment `gtd step` runs, which may be before OR after an
574
+ emitted script's own `format:` line has run (the script runs `format:` then
575
+ `validate:` then the commit — see `src/SteeringMode.ts`'s
576
+ `renderSteeringCommands` — but gtd's decision and the driver's script execution
577
+ are different processes at different times, so there's no guaranteed ordering
578
+ between "gtd decided" and "the script formatted"). That's only safe because
579
+ every built-in guard judges content, not incidental formatting (e.g. it
580
+ normalizes `[ ]`/`[x]` checkboxes before comparing). If you plug in your own
581
+ `format:` command, the same rule binds it: a formatter that also changes meaning
582
+ — ticking a box, stripping a paragraph — makes the guard's decision and the
583
+ file's actual content disagree, and gtd will not catch that for you.
584
+
585
+ ### Built-in steering formats are ordinary modes
586
+
587
+ `qa` and `review` are gtd's two built-in steering-file formats (parsed and
588
+ validated in-process because `gtd lsp` needs the same parsers for live
589
+ diagnostics), but their `validate:` is not hardcoded or hidden: the compiler
590
+ SEEDS every workflow's `modes:` map with `qa`/`review` entries whose `validate:`
591
+ is the same `gtd check <mode> '<file>'` string described above
592
+ (`src/PatternConfig.ts`, `src/SteeringFormats.ts`'s `seededValidateCommand`).
593
+ That seeded command is visible in `gtd visualize`'s compiled model and in the
594
+ editor JSON schema like any other mode, and it's overridable the same way any
595
+ mode is — declare `modes: { qa: { validate: "your-own-command" } }` (in the
596
+ workflow or in `.gtdrc`) and your command displaces the seed; declaring only a
597
+ `format:` for `qa`/`review` composes with the seeded `validate:` rather than
598
+ replacing it. There is no special-cased built-in behavior a driver needs to know
599
+ about beyond the ordinary mode-resolution rules already documented under
600
+ [The `workflow:` key](#the-workflow-key).
601
+
471
602
  ## Configuration
472
603
 
473
604
  gtd reads an optional `.gtdrc` config file via
@@ -527,7 +658,7 @@ workflow:
527
658
  anyKey: anyScalarValue
528
659
  modes: # optional — steering-file modes a state's `mode:` may name
529
660
  <name>:
530
- format: <shell command> # at least one of format/validate
661
+ format: <shell command> # both optional — {} is the format-only tier
531
662
  validate: <shell command>
532
663
  entry:
533
664
  default: <machine name> # which machine is the ROOT instance
@@ -553,11 +684,14 @@ workflow:
553
684
  retry:
554
685
  max: <number>
555
686
  otherwise: <targetState>
687
+ label: <string> # optional, opaque display name passed through `gtd next --json`/`gtd status --json`
556
688
  file: <string> # optional, an Eta template naming the state's steering file
557
- mode: <modeName> # optional, requires "file" — a built-in (qa/review/prose) or a `modes:` entry
689
+ mode: <modeName> # optional, requires "file" — must be declared in `modes:` (qa/review are seeded for you; everything else, including prose, you declare)
558
690
  reviewWindow: true # optional — open the review checkout window at rest here
559
691
  reviewBase: true # optional — anchor the review window's diff base to this state's most-recent commit
560
692
  # reviewBase: <Eta template> # OR a template — rendered (only meaningful entering via --entry) to a commitish that fixes the WHOLE PROCESS's diff base
693
+ requireProgress: true # optional, requires "file" — refuse a turn whose only change deletes this state's own `file:`
694
+ answerGate: true # optional, requires "file" — refuse a turn until every open question in the (qa-mode) `file:` is answered
561
695
  entry: true # optional — an EXTRA reachability root (`entries.manual`), enterable via `gtd --entry <this state's qualified name>` — NOT a precondition for `--entry` (any declared, non-commit state is a valid target)
562
696
  <local>: { machine: <name>, with: { <param>: <value> } } # a REFERENCE — instantiates <name> as a child, qualified as `<local>.<childLocal>`
563
697
  ```
@@ -587,7 +721,7 @@ conversation across both call sites.
587
721
 
588
722
  Besides `it.vars` (below), a `script`/`prompt`/`message`/`commit` template sees:
589
723
 
590
- - **`it.startCommit`** — the process's diff base (the commit the current cycle
724
+ - **`it.startCommit`** — the process's diff base (the commit the current process
591
725
  started from, or the base a `--var reviewBase=<commitish>` entry resolved to).
592
726
  - **`it.reviewBase`** — the previous review round's boundary, falling back to
593
727
  `it.startCommit` on a first review.
@@ -607,9 +741,9 @@ model, pattern grammar, load-time rules, and how to verify a change compiles.
607
741
 
608
742
  > **Upgrading from a pre-8.2 `workflow:`?** The old flat `states:` shape (with a
609
743
  > per-state `initial: true`/`reviewEntry: true`/`fixEntry: true` flag) is no
610
- > longer accepted — finish or `gtd abandon` any in-flight cycle before
611
- > upgrading, since the old and new shapes aren't compatible mid-cycle. Wrap your
612
- > states under a single
744
+ > longer accepted — finish or `gtd abandon` any in-flight process before
745
+ > upgrading, since the old and new shapes aren't compatible mid-process. Wrap
746
+ > your states under a single
613
747
  > `machines: { <name>: { entry: <initial state>, states: {...} } }` and declare
614
748
  > `entry: { default: <name> }` at the top level (moving any
615
749
  > `reviewEntry`/`fixEntry` state to a plain per-state `entry: true` flag,
@@ -785,13 +919,18 @@ own `- [ ]` line — diagnostics for both (live as you edit), and a
785
919
 
786
920
  It is config-driven via each state's `file:`/`mode:`, and falls back to basename
787
921
  dispatch (`REVIEW.md` → `review`) with no config in sight. `qa` and `review` are
788
- gtd's built-in steering-file MODES with a VALIDATOR gtd itself implements; a
789
- mode's `format:` and `validate:` are shell commands a workflow (or a project's
790
- `.gtdrc`) declares for itself, so you bring your own formatter and your own
791
- checkers. A third built-in, `prose` — format-only, no validator, used by the
792
- simple flow's plan file — has no live editor support: `gtd lsp` publishes no
793
- symbols or diagnostics for it, though `gtd validate` and the `gtd step` gate
794
- still format and validate it like any other mode.
922
+ gtd's built-in steering-file FORMATS, each with its own outline/actions/
923
+ go-to-definition and a VALIDATOR gtd itself implements; a mode's `format:` and
924
+ `validate:` are shell commands a workflow (or a project's `.gtdrc`) declares for
925
+ itself, so you bring your own formatter and your own checkers. Overriding one of
926
+ `qa`/`review`'s `validate:` with a shell command does NOT lose its outline/
927
+ actions — those come from the FORMAT (the name), independent of who validates —
928
+ but `gtd lsp` never runs a shell command per keystroke, so live diagnostics
929
+ become one `Information` notice pointing at `gtd validate` instead of the
930
+ built-in findings. Any OTHER mode name — `prose` (used by the simple flow's plan
931
+ file), or a project's own declared name — has no built-in format, so it gets no
932
+ live editor support at all: `gtd validate`'s emitted script and the `gtd step`
933
+ gate still format and validate it like any other mode.
795
934
 
796
935
  ## Development
797
936
 
@@ -800,8 +939,10 @@ npm install
800
939
  npm run dev # run from source, no build (node dev/run.mjs)
801
940
  npm run build # tsdown → dist/gtd.bundle.mjs
802
941
  npm test # format:check, typecheck, lint, unit + e2e tests, fallow
803
- npm run test:unit # vitest unit tests (the pure resolver) — --project unit
804
- npm run test:e2e # gherkin e2e via vitest + quickpickle — --project e2e
942
+ npm run test:unit # vitest unit tests (the pure resolver) — --project unit
943
+ npm run test:e2e # gherkin e2e, both tiers — test:e2e:inmem + test:e2e:live
944
+ npm run test:e2e:inmem # @inmem scenarios, in-process, no build — --project e2e-inmem
945
+ npm run test:e2e:live # @live scenarios against the built bundle — --project e2e-live
805
946
  npm run test:mutation # StrykerJS mutation testing (manual only, ~10 min)
806
947
  npm run typecheck
807
948
  npm run lint
@@ -817,7 +958,20 @@ The decision core is pure and IO-free: the pattern machine's shape (states,
817
958
  patterns, retry) is a plain-data `WorkflowDefinition` (`src/PatternMachine.ts`),
818
959
  and the same module's `resolveState`/`step` are the pure resolver/interpreter
819
960
  over it — so the whole engine is trivially unit-testable in isolation. All
820
- git/filesystem/template IO is confined to the edge (`src/Edge.ts`).
961
+ git/filesystem/template IO is confined to the edge (`src/Edge.ts`), whose
962
+ `currentRest` resolves everything derivable from where the process rests right
963
+ now into a single snapshot, and whose `planStep`/`planEntry` pair separates
964
+ deciding a transition from performing it — so a command's own capture guards run
965
+ between the two. The edge reaches the outside world through two content ports
966
+ (`src/RepoFiles.ts` for working-tree and committed reads, `src/CommandRunner.ts`
967
+ for the one place gtd spawns a subprocess itself), with the `gtd step` capture
968
+ guards (`src/StepGuards.ts`) built on top of them.
969
+
970
+ `src/testing/` holds the in-memory git/config/filesystem test double every
971
+ `@inmem` scenario and unit test runs against instead of a real repo — it never
972
+ ships (a lint rule and a build-time check both enforce that) and is trustworthy
973
+ only because the same `GitOperations` contract suite runs against a real git
974
+ repo too.
821
975
 
822
976
  Releases are automatic: push releasable Conventional Commits (`fix:`, `feat:`,
823
977
  or breaking changes) to `main` and semantic-release computes the next version,