Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b9667b9
wip: bank native daily verb implementation
laynepenney Aug 16, 2026
1592c3e
test(gr2): bind daily verb CLI forwarding
laynepenney Aug 17, 2026
60de7e2
Merge pull request #887 from synapt-dev/wip/gr2-native-daily-verbs-bank
laynepenney Aug 17, 2026
4f01709
fix: make PR merge refusals observable
laynepenney Aug 19, 2026
888641c
Merge pull request #888 from synapt-dev/fix/pr-merge-honest-outcomes
laynepenney Aug 19, 2026
447557d
fix(link): refuse stale gripspace sources
laynepenney Aug 19, 2026
070c6cf
Merge pull request #890 from synapt-dev/fix/link-apply-stale-source-r…
laynepenney Aug 19, 2026
e84c4ca
feat(gr2): propagation state machine prototype on synthetic repos
laynepenney Aug 19, 2026
854eeea
fix(gr2): make the coordinate key injective over opaque fields
laynepenney Aug 19, 2026
1b818c0
fix(gr2): keep already-reached targets in an aggregate replay; constr…
laynepenney Aug 19, 2026
554ceaf
fix(gr2): witness the corrupted-cursor refusal on both driver paths
laynepenney Aug 19, 2026
15242d0
Merge pull request #889 from synapt-dev/feat/gr2-propagation-prototype-0
laynepenney Aug 19, 2026
078dfab
feat(gr2): Prototype 1 propagation daemon on one declared managed rep…
laynepenney Aug 19, 2026
47352e2
fix(link): verify gripspace pins before applying
laynepenney Aug 19, 2026
d6baa30
fix(gr2): Prototype 1 daemon never prompts, tolerates a failed tick, …
laynepenney Aug 19, 2026
ff47998
fix(gr2): the daemon loop survives a destination it cannot read; name…
laynepenney Aug 19, 2026
92fff14
fix(link): accept abbreviated commit pins during freshness checks
laynepenney Aug 19, 2026
f792d66
fix(gr2): the daemon's not-new shortcut no longer hides a corrupted c…
laynepenney Aug 19, 2026
2ca8750
fix(link): distinguish commit pins from stale refs
laynepenney Aug 19, 2026
da6516e
Merge pull request #892 from synapt-dev/fix/gripspace-pin-freshness
laynepenney Aug 20, 2026
270209c
Merge pull request #894 from synapt-dev/feat/gr2-propagation-prototype-1
laynepenney Aug 20, 2026
eb33308
feat(gr2): Prototype 2 — contributions land on a canonical remote by …
laynepenney Aug 19, 2026
e2b1f18
feat(gr2): Prototype 2 protocol — ownership as a recorded fact, contr…
laynepenney Aug 19, 2026
4793796
measure(gr2): per-state latency read from the receipts' own timestamp…
laynepenney Aug 19, 2026
37eced3
fix(gr2): a torn journal line stops being fatal, and stops eating its…
laynepenney Aug 20, 2026
a5635a0
Merge pull request #896 from synapt-dev/fix/gr2-journal-torn-line
laynepenney Aug 20, 2026
b74e086
Merge pull request #897 from synapt-dev/feat/gr2-propagation-prototype-2
laynepenney Aug 20, 2026
f051ab8
fix: AppendSurface survives a torn write, and counts what it skips
laynepenney Aug 20, 2026
dd35296
fix: validate a line's shape and guard its decode, not just its parse
laynepenney Aug 20, 2026
879927c
Merge pull request #898 from synapt-dev/fix/append-surface-torn-line
laynepenney Aug 20, 2026
6e5320c
fix: one torn-line-safe JSONL home for both lane prototypes
laynepenney Aug 20, 2026
9367567
Merge pull request #899 from synapt-dev/fix/jsonl-store-torn-line
laynepenney Aug 20, 2026
69e9267
fix(events): repair torn outbox lines and report unreadable ones
laynepenney Aug 20, 2026
1dd7474
Merge pull request #900 from synapt-dev/fix/events-torn-line-and-count
laynepenney Aug 20, 2026
a9263bf
fix(checkout): count repo failures and disjoin them into the exit code
laynepenney Aug 21, 2026
f6f178c
Merge pull request #903 from synapt-dev/fix/checkout-exit-code-on-rep…
laynepenney Aug 21, 2026
af17ae9
feat(workspace): initialize spec from topology
laynepenney Aug 21, 2026
01ef911
test(workspace): cover encoding refusal before directory creation
laynepenney Aug 21, 2026
3bce8c2
fix(workspace): honor declared topology name
laynepenney Aug 21, 2026
50f2c48
Merge pull request #901 from synapt-dev/sentinel/workspace-spec-from-…
laynepenney Aug 21, 2026
0e320e5
fix(pr): count failed merges and disjoin them into the exit code
laynepenney Aug 21, 2026
6192e8c
Merge pull request #907 from synapt-dev/feat/pr-merge-exit-code-on-fa…
laynepenney Aug 21, 2026
2eca904
chore: release gr 1.2.0
laynepenney Aug 22, 2026
cce27a4
Merge pull request #909 from synapt-dev/chore/release-1.2.0
laynepenney Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,69 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.2.0] - 2026-08-21

**Scope, stated first because the range and the artifact are not the same thing.**

The work promoted in this release is the range **`v1.1.0..6192e8c5`** — from the 1.1.0 tag to the
`dev` tip at freeze — which is **42 commits / 14 first-parent units**. The release-prep commit
that carries this entry sits one beyond that and is deliberately excluded; counting it makes the
range 43 / 15. Both numbers are true of different ranges, so the range is named rather than left
to inference.

Units are classified by diffing each against its **first parent**. A merge commit has no
canonical diff, and `git show <merge> --name-only` returns nothing for these, so the method is
part of the claim.

**Five of those 14 units are in the published `gitgrip` crate; nine are not.** gr2 is a separate
Python surface at version `0.1.0` that is not distributed, so its nine units — the propagation
prototypes, the append/torn-line work, the native daily verbs, and workspace-spec-from-topology —
are present in this repository and absent from anything `cargo install gitgrip` gives you. The
entries below cover the five gr1 units only. Read the git range if you want the gr2 work.

### Fixed
- **`gr pr merge` and `gr checkout` exit nonzero when part of a batch fails** — a multi-repo
operation that failed in some repos and succeeded in others previously exited 0, so a caller
or CI step reading the exit code saw success over a partial failure.

**This does not close the exit-code-honesty class, and should not be read as doing so.**
The class is tracked in `grip#886`, which records 9+ known instances; two are fixed here.
**`gr push` — the most consequential instance, the one that can silently lose work — is
untouched by this release.**
- **`gr link --apply` reports a stale source instead of composing it silently** and
**gripspace pins are checked for freshness**.

**Two measured holes in this remain open** (`grip#891`), and this feature ships with **no
user-facing documentation** — `README.md` has no `gr link` section, only a one-line table
mention. Link freshness is **not** guaranteed by this release; what changed is that one class
of stale composition now reports rather than proceeding quietly.

### Known gaps at this release
- The new exit-code semantics (`EXIT_REFUSED=2`, operational failure `1`, success `0`) are not
documented in `README.md` for either `gr checkout` or `gr pr merge`.
- `CONTRIBUTING.md` does not mention gr2 anywhere, despite **9 of the 14 first-parent units in
`v1.1.0..6192e8c5` touching only `gr2/`** — the same first-parent classification used
throughout this entry. A contributor following that document has no path to discovering gr2
exists or how to set up its separate Python environment.

**Two earlier drafts of this line carried a commit-level statistic; both are withdrawn, and
the second is the more instructive.** The first said gr2 was "roughly a third of this
repository's active commit volume" — no range, no metric, nothing to reproduce. The second
replaced it with "22 of 42 commits (52%), measured by first-parent diff," which named a range
and a method and was **still wrong, because the number did not come from the method it
named**: 22 is `git rev-list --count -- gr2/`, whose path-history simplification silently drops
eight merge commits that *do* touch `gr2/` against their first parent. Under the stated method
the figure is 30 of 42 (71%). A sourced number can be more misleading than an unsourced one,
because citing a method invites trust the number has not earned. The unit measure above needs
no footnote, so it is the only one kept.

## [1.1.0] - 2026-08-13

These entries sat under an `[Unreleased]` heading until 2026-08-21. **That heading was false:**
this work shipped in `v1.1.0`, tagged 2026-08-13 and published to crates.io. Verified by
checking the feature's own symbols into the `v1.1.0` tree rather than by commit archaeology,
with a negative control. A reader trusting the old heading would have believed a shipped
feature was still pending.

### Fixed
- **`gr pr merge` no longer defaults to squash** (#829) — with no `--method`, the command queried the host for its allowed methods and took the first of squash > merge > rebase, so on any repo permitting squash the tool actively chose it. A workspace whose policy is merge-commit-only got squashes from its own tooling, and on a private repo — where hosting rulesets are unavailable on most plans — nothing downstream could reject the result. The default is now a real merge commit, configurable via `settings.merge_method` in the manifest. Choosing is the workspace's job; the host is asked only whether the choice is permitted.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "gitgrip"
version = "1.1.0"
version = "1.2.0"
edition = "2021"
rust-version = "1.80"
description = "Multi-repo workflow tool - manage multiple git repositories as one"
Expand Down
2 changes: 1 addition & 1 deletion docs/IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Pass `--hostname` to `gh` CLI in `src/platform/github.rs` `enable_auto_merge()`

### Phase 3: Repo iteration helper
- [x] New `src/cli/repo_iter.rs`: `RepoVisitResult`, `RepoOpSummary`, `for_each_repo()`, `for_each_repo_path()`
- [ ] Wire into commands — Deferred: most commands accumulate custom state that doesn't fit the simple Success/Skipped/Error enum
- [ ] Wire into commands — Partially done: `checkout` adopted it (2026-08-21). The remaining commands are blocked on `?` propagation out of the loop, three disagreeing skip taxonomies, and per-arm payload types — not on the enum alone

### Phase 4-6: Migrate all commands to WorkspaceContext
- [x] 28/30 commands in main.rs use `load_workspace_context()` (Init, Completions, Bench don't need workspace)
Expand Down
6 changes: 4 additions & 2 deletions docs/PLAN-p2-maintainability.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ No conflicts expected — P0/P1 didn't touch `main.rs` or `cli/mod.rs`.
```
cargo build && cargo test && cargo clippy && cargo fmt --check
```
Watch for clippy warnings on unused `repo_iter` imports. If flagged, the `pub` visibility from `cli/mod.rs` → `lib.rs` should suppress it.
~~Watch for clippy warnings on unused `repo_iter` imports. If flagged, the `pub` visibility from `cli/mod.rs` → `lib.rs` should suppress it.~~

**Struck.** This prescribed silencing the one instrument that would have reported the problem. `pub` does not resolve a dead-code warning, it disables the analysis — an unused private item warns, an unused `pub` item does not — so following this step left `repo_iter` with zero callers and nothing anywhere going red for as long as it existed. A warning is a detector; suppressing it to reach a clean build is not a fix, and writing the suppression down as a step made it the default for whoever came next. If an item has no consumer, give it one or delete it.

### 3. Commit the refactor
Stage all 4 files and commit:
Expand Down Expand Up @@ -59,7 +61,7 @@ gr pr create -t "refactor: P2 maintainability — WorkspaceContext and load_grip
| Command signature migration to `&WorkspaceContext` | Would touch every command file + every test; current ctx field extraction in dispatch works fine |
| Compact dispatch function (Phase 7) | 612-line match for 30 commands is standard; only one dispatch site |
| sync.rs / release.rs decomposition (Phase 8) | Already have helpers (`sync_single_repo`, `execute_post_sync_hooks`, etc.) |
| Wiring `for_each_repo()` into commands | Most commands accumulate custom state that doesn't fit the simple Success/Skipped/Error enum |
| Wiring `for_each_repo()` into commands | Largely accurate, and now measured rather than assumed. Of the 9 command files that hand-roll these counters, `sync`/`pull` do not iterate repos at all, `pr/merge` iterates PRs and awaits, and `push`/`commit`/`forall` propagate `?` out of the loop — which the closure's return type cannot express. `checkout` was the one clean fit and has adopted it. |

## Files touched

Expand Down
17 changes: 17 additions & 0 deletions gr2/docs/HOOK-EVENT-CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,20 @@ and `lease.force_broken` (which fires when a live lease is broken with
| `workspace.materialized` | `gr2 workspace materialize` or `gr2 apply` | `{repos: [{repo, first_materialize: bool}]}` |
| `workspace.file_projected` | File link/copy applied | `{repo, kind, src, dest}` |

#### Propagation

| Type | Trigger | Payload |
|------|---------|---------|
| `propagation.receipt` | The propagation daemon (`gr2/prototypes/propagation_daemon.py`) completed one operation against its declared managed replica, in any terminal state (acknowledged, refused, partial, unverifiable) | `{summary, state, pending_id, operation_id, source_rev, expected_base, after, replayed, receipt_path}` |

`propagation.receipt` is emitted once per operation; a tick that finds the cursor
already at the source revision is not an operation and emits nothing. `summary` is
the one-line notification a channel consumer relays verbatim. `receipt_path` is the
full path of the receipt file the daemon wrote, an explicit exception to the
relative-path rule in 3.3: the receipt store lives under the daemon's declared
`state_dir`, which is not a workspace file and may sit outside `workspace_root`.
`after` is `null` when the operation did not reach `applied`.

### 3.3 Payload Conventions

- All paths in payloads are relative to `workspace_root`, never absolute.
Expand Down Expand Up @@ -471,6 +485,9 @@ class EventType(str, Enum):
# Workspace operations
WORKSPACE_MATERIALIZED = "workspace.materialized"
WORKSPACE_FILE_PROJECTED = "workspace.file_projected"

# Propagation (one event per receipt from the propagation daemon)
PROPAGATION_RECEIPT = "propagation.receipt"
```

### 7.3 Implementation Location
Expand Down
40 changes: 40 additions & 0 deletions gr2/prototypes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,3 +406,43 @@ This keeps the cache discussion grounded in evidence:
- cache remains the optimization
- the prototype should tell us whether the optimization is material enough to
justify building it into `apply`

## Propagation State Machine (Prototype 0)

Before any daemon is allowed to move changes between workspaces, the state
contract for one propagation is proven on synthetic repositories:

```bash
python3 -m pytest gr2/tests/test_propagation_state_machine.py -q
```

`gr2/prototypes/propagation_state_machine.py` drives one change through
`observed -> fetched -> planned -> applied -> verified -> acknowledged`, with
`refused`, `partial`, and `unverifiable` reachable from any of them. Every
transition names the observation that established it, and every receipt names
the exact source and destination revisions. Everything it carries is an opaque
identifier; it holds no notion of agent or workspace identity and no policy
content (the allowed directions are a required input with no default).

The tests build a bare source remote and three destinations (a clean replica, a
dirty authoring clone, an authoring clone that is ahead of the source) and
prove, each as its own witness:

- killing the sink between each pair of states and replaying applies the change
exactly once (measured from the destination's own reflog and the journal)
- the cursor advances only on `acknowledged`
- a moved expected base refuses with the observed base in the receipt; nothing
is merged, forced, or retried against the new base
- a dirty or diverged authoring clone is refused and left byte-for-byte
untouched (refs, HEAD, index, worktree, reflog)
- a destination that cannot be read back after the apply verb is recorded as
`unverifiable`, never collapsed into `refused` or `applied`, and resolves on
replay
- an acknowledged operation replays as a no-op returning the original outcome;
a refused one starts a new attempt, because a refusal describes a moment

It is also the home of a born-red witness for the existing event outbox:
`read_events()` advances the consumer cursor before the caller performs its
effect, so a consumer that fails after reading loses the event. The test is
marked `xfail(strict=True)` and turns the marker into a failure the moment
acknowledgment moves after the effect.
Loading
Loading