Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3c2a4b4
feat(core): enforce the classifier contract, format invariants, and d…
Aug 26, 2026
80ec0ae
feat: expose resolveGeneratedFiles through .gitwandrc and the CLI
Aug 26, 2026
a0f1106
plan: merge context (accuracy lot C)
Aug 26, 2026
2392a13
feat(core,cli,mcp,desktop): merge context — the engine finally knows …
Aug 26, 2026
8f55a05
docs+tests: close out merge context (lot C task 6)
Aug 26, 2026
45c7827
feat(core): key-wise merge for JSON manifest fragments (lot E)
Aug 26, 2026
e5947b5
roadmap: make Engine Accuracy a first-class release; free the version…
Aug 26, 2026
c25fd10
fix(cli): make the merge-context detection tests hermetic
Aug 26, 2026
cc20576
plan: repo conventions (accuracy lot F) — measure the team's policies…
Aug 26, 2026
2acb06e
feat(core,cli): repo conventions measured from history (accuracy lot …
Aug 26, 2026
2f745a7
fix(cli): give the git-integration tests a real timeout budget
Aug 26, 2026
5652582
benchmark: corpus v2 (measured selection) + CI agreement gate (lot G)
Aug 26, 2026
a457ac9
benchmark: regenerate the v2 baseline with per-repo agreement detail
Aug 26, 2026
ffd11f3
docs: implementation plan for the regenerate tier (accuracy lot D, full)
Aug 26, 2026
9967603
feat(core): regenerate tier plumbing — plan emission for generated fi…
Aug 27, 2026
a33aafc
fix(core): attach regeneration plan at all three generated-file decli…
Aug 27, 2026
54d3ddd
fix(core): remove dead minConfidence site, harden script-suppression …
Aug 27, 2026
e59c926
feat(cli): regenerate-tier executor (accuracy lot D, task 2)
Aug 27, 2026
30a8cd6
fix(cli): sibling-map clean-file gap + env allowlist (regenerate tier…
Aug 27, 2026
a5dc392
feat(cli,mcp): conventions/.gitwandrc wiring + MCP regenerate reporti…
Aug 27, 2026
e15fdd9
fix(mcp): guard regenerate-report sibling-map against narrowed files:…
Aug 27, 2026
6addbdc
feat(scripts): regenerate-tier measurement harness (accuracy lot D, t…
Aug 27, 2026
8c62a7c
fix: final-review fix wave for the regenerate tier (accuracy lot D)
Aug 27, 2026
bde4df3
test(mcp): add explicit 30s timeout to regenerate-report's real-git-r…
Aug 27, 2026
d181fdf
docs: implementation plan for the regenerate tier follow-up (merge-in…
Aug 28, 2026
224a2bb
docs(website): document --regenerate/--resolve-generated and gitwand …
Aug 28, 2026
687b318
fix(cli): seed the disposable regeneration worktree from the real mer…
Aug 28, 2026
87ed859
feat(scripts): seed the measurement harness's worktree from the real …
Aug 28, 2026
b62d03c
benchmark: full corpus sweep for the regenerate-tier gate, post merge…
Aug 28, 2026
43be17e
fix: harden regenerate-tier merge-index seeding after whole-branch re…
Aug 28, 2026
a09e5b1
docs(benchmark): resolve the gate-verdict section's remaining contrad…
Aug 28, 2026
9613b5c
benchmark: real re-run of the regenerate-tier sweep against the fixed…
Aug 28, 2026
099f535
fix(benchmark): rebuild regenerate-tier scratch index via ls-tree/mkt…
Aug 28, 2026
01251ce
docs(benchmark): fix honesty gaps in the conclusive sweep's write-up
Aug 28, 2026
e2973c8
fix(benchmark): C-quoted merge-tree skip paths + blobless mktree regr…
Aug 28, 2026
11e465f
docs: fix generated-file/lockfile claims left stale by the accuracy work
Aug 28, 2026
35383d6
chore: gitignore the Impeccable local session-lease cache
Aug 31, 2026
5b8035a
Merge remote-tracking branch 'origin/main' into feat/conflict-engine-…
Aug 31, 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
80 changes: 80 additions & 0 deletions .github/workflows/benchmark-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Lot G — l'accord avec les merges humains en garde-fou de régression.
#
# Rejoue le corpus épinglé (benchmark/corpus.json) contre le moteur du PR et
# échoue si l'accord baisse au-delà du bruit par rapport à la baseline commitée
# (benchmark/results/*-baseline.json). Généralise le procès de
# token_level_merge (PR #117) : aucun pattern n'entre si le corpus dit qu'il
# rend le moteur moins juste.
#
# Coût maîtrisé : les clones (bare + blobless, ~1,5 Go) sont mis en cache avec
# pour clé le hash de corpus.json — seul le premier run après un re-pin paie le
# clonage. Le replay lui-même prend quelques minutes.

name: benchmark-gate

on:
pull_request:
paths:
- "packages/core/**"
- "benchmark/**"
- "scripts/replay-conflicts.mjs"
- ".github/workflows/benchmark-gate.yml"
workflow_dispatch:

concurrency:
group: benchmark-gate-${{ github.ref }}
cancel-in-progress: true

jobs:
gate:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

# merge-tree --write-tree exige git >= 2.38 ; ubuntu-latest est bien au-delà,
# mais on échoue explicitement plutôt que de mesurer zéro conflit en silence.
- name: Check git version
run: |
git --version
v=$(git --version | grep -oE '[0-9]+\.[0-9]+' | head -1)
if [ "$(printf '%s\n2.38\n' "$v" | sort -V | head -1)" != "2.38" ]; then
echo "::error::git >= 2.38 required (merge-tree --write-tree)"; exit 1
fi

- name: Install & build the engine
run: |
pnpm install --frozen-lockfile --filter @gitwand/core
pnpm --filter @gitwand/core build

# La clé de cache est le hash du corpus : un re-pin invalide le cache,
# tout le reste le réutilise. restore-keys volontairement absent — un
# cache partiel d'un ancien corpus fausserait la mesure.
- name: Cache the pinned corpus clones
uses: actions/cache@v4
with:
path: benchmark/.cache
key: benchmark-corpus-${{ hashFiles('benchmark/corpus.json') }}

- name: Replay the corpus
run: node benchmark/run.mjs --out results/ci.json

- name: Gate on agreement vs the committed baseline
run: |
baseline=$(ls benchmark/results/*-baseline.json | sort | tail -1)
echo "baseline: $baseline"
node benchmark/compare.mjs "$baseline" results/ci.json

- name: Upload fresh results
if: always()
uses: actions/upload-artifact@v4
with:
name: benchmark-results
path: results/ci.json
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ apps/desktop/public/grammars/

# Superpowers
.superpowers/

# Impeccable (local tool-lease cache, no repo content)
.impeccable/
website/.vitepress/cache
website/.vitepress/dist
research/
Expand Down
23 changes: 10 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **The Merge Room on `/agent`.** The page is now a shared workspace rather than a tool listing. Tool handlers no longer just return text to the agent: they file a case into live page state that the person sitting there watches fill up. The engine settles every hunk that carries no decision; every hunk where the two branches genuinely disagree queues for a human, who picks a side and gets the assembled file back, ready to paste. No tool on the page can make that call, which is the boundary the whole thing is built around. A live journal records who did what, agent and human alike.
- **`list_cases`**, a third WebMCP tool, lets an agent read the room back: what is filed, what the engine settled, what is still waiting on a person, what they have already decided. It is what turns a stateless calculator into a workspace an agent can pick back up.

### Fixed

- **`parse_git_error` did not recognise a rebase that stopped on a conflict.** The catalogue only knew the phrases git prints when you try to *start* a rebase while one is already unfinished (`rebase-merge directory`), not the ones it prints when a rebase *halts*, which is the far more common paste. Reported by an agent audit of the live page. It now keys on the rebase-specific commands, and a halted cherry-pick gets its own entry rather than being mislabelled a rebase: both print `could not apply`, so matching on that phrase would have handed out `git rebase --continue` to someone mid-cherry-pick.

### Added

- **`/agent`, a WebMCP page.** gitwand.app/agent exposes two read-only git tools to any agent browsing it, over the W3C WebMCP standard: `parse_git_error` explains a failing git command and gives the commands that fix it, `resolve_conflict` runs the deterministic engine over a conflicted file and reports per hunk what was resolved and what still needs a human. Both execute in the visitor's tab, so nothing is uploaded and there is no backend. `@gitwand/core` is imported on demand rather than at module scope, which keeps it out of the shared theme chunk every page downloads.
- The page states its own registration status live, including when the browser has no WebMCP at all, and writes both tool contracts out in HTML and JSON-LD for the majority of visitors and crawlers that will never run the script.
- **A try-it panel on `/agent`.** Both tools can be run by hand from the page, against editable sample inputs, so the majority of visitors whose browser has no WebMCP can still see what an agent gets back. It calls the same `execute` an agent calls rather than a mock, and deliberately does not feed the agent call counter.
- **The Merge Room on `/agent`.** The page is now a shared workspace rather than a tool listing. Tool handlers no longer just return text to the agent: they file a case into live page state that the person sitting there watches fill up. The engine settles every hunk that carries no decision; every hunk where the two branches genuinely disagree queues for a human, who picks a side and gets the assembled file back, ready to paste. No tool on the page can make that call, which is the boundary the whole thing is built around. A live journal records who did what, agent and human alike.
- **`list_cases`**, a third WebMCP tool, lets an agent read the room back: what is filed, what the engine settled, what is still waiting on a person, what they have already decided. It is what turns a stateless calculator into a workspace an agent can pick back up.
- **The engine now knows what merge it is in.** A new optional `mergeContext` (operation + which side is the target branch) flows from the CLI, the MCP server and the desktop into `@gitwand/core`. Its first use: a version scalar set differently on both sides — `'13.x-dev'` vs `'12.54.1'` — resolves to the **target branch's value**, which is what teams actually ship. Measured by replaying laravel/framework's real merges: agreement with the human resolution on fully-resolved files jumps from 36.6 % to **81.9 %**. Without context, that case is now *proposed* instead of applied — the old fallback was a coin flip measured wrong three times out of four. Orderable dependency bumps deliberately keep "newest wins": flipping those to the target regressed three other corpora, and the benchmark caught it before it shipped.
- **`format_semantic` classification.** A hunk the textual classifier calls `complex` but a format-aware resolver (JSON, Markdown, YAML, lockfiles…) can merge semantically is now reclassified, scored and traced like every other pattern — no more files reported as containing `complex` hunks that were silently applied without a confidence score.
- **Format invariants in post-merge validation.** A resolution that would produce a changelog with two `Unreleased` sections, a duplicated version heading, or a JSON object with duplicate keys is retracted — syntax validation alone passed all three.
- **Key-wise merging for manifest fragments.** Conflicts in `package.json` / `composer.json` are almost always fragments — a few `"key": value,` lines — which the line-level engine merged at exactly the wrong granularity. Those fragments are now merged **by key** (three-way, deletions and one-sided changes handled), with one bounded arbitration: two constraints on the same operator (`^7.23.0` vs `^7.23.3`) resolve to the newer, which is what teams ship. Anything else — operator changes, `workspace:*` migrations — is a human decision and is declined. On the benchmark this is the first change that raises coverage *and* agreement at once, on all four measured repositories.
- **`resolveGeneratedFiles` option** (`.gitwandrc`, and `--resolve-generated` on the CLI).

### Changed

- **The engine speaks English.** Every explanation, resolution reason, decision-trace step, confidence booster and penalty `@gitwand/core` produces was written in French. None of the consumers translate them, so the desktop merge editor, the CLI summary and the `@gitwand/mcp` `explanation` / `resolutionReason` fields have been handing French text to every user and every agent, whatever their locale. 194 strings translated across 38 files. Comments and test names stay French: this is only about what leaves the engine.
- A regression guard (`__tests__/english-output.test.ts`) runs the engine over the whole corpus and asserts that no string it hands back is French, checking real output rather than scanning source so it cannot be fooled by how a string is assembled. It caught four strings a source scan had missed, including one with no accented characters in it.
- **Generated files decline by default.** Lockfiles, minified bundles and `dist/` outputs are regenerated by tools, not merged — measured on 1,662 real merges, auto-merging them diverged from what teams shipped in almost every case. GitWand now explains what to regenerate instead of writing a plausible-but-wrong merge; only the patterns that fabricate nothing (same change, one-sided change, deletion, whitespace) still apply. The previous behaviour is one `.gitwandrc` key away.
- **A reproducible benchmark now backs every accuracy claim** — `benchmark/` pins 8 public repositories to exact commits, replays ~1,700 merges through the engine and compares the output byte-for-byte with what the teams actually committed. The engine changes above were driven, and one of them corrected, by its numbers.

### Fixed

- **`parse_git_error` did not recognise a rebase that stopped on a conflict.** The catalogue only knew the phrases git prints when you try to *start* a rebase while one is already unfinished (`rebase-merge directory`), not the ones it prints when a rebase *halts*, which is the far more common paste. Reported by an agent audit of the live page. It now keys on the rebase-specific commands, and a halted cherry-pick gets its own entry rather than being mislabelled a rebase: both print `could not apply`, so matching on that phrase would have handed out `git rebase --continue` to someone mid-cherry-pick.
- **`pnpm test` was non-deterministic (#172).** Suites that use real git repositories are subprocess-bound, not CPU-bound, and were timing out under the load of the whole monorepo testing at once. Every git-backed suite now shares a 60s timeout, chosen to catch a hang rather than to enforce a performance budget. Running workspaces one at a time turned out to be **faster** as well as deterministic (49s against 119s), because five packages each fanning out to one worker per core oversubscribes the machine several times over, so `pnpm test` now passes `--workspace-concurrency=1`. Eleven consecutive full runs green, against roughly one failure in three before.

- **Site-wide WebMCP tools went dark on browsers without `navigator.modelContext`.** The registration script bailed out entirely unless the deprecated `navigator` location existed, so the three documentation tools would disappear the day Chrome removes the alias it deprecated in 150. It now prefers `document.modelContext`, where the spec has put the entry point since 27 May 2026, and falls back to `navigator` only when that is all the browser offers. It registers once either way: on the versions exposing both names they alias the same object, so registering on both would have duplicated every tool.
- **WebMCP tools could never be unregistered.** `signal` was passed as a property of the tool dictionary, which declares no such member, so it was silently dropped. It now goes in the options argument where `ModelContextRegisterToolOptions` expects it.

- **Signed commits failed from the GUI with a gpg-agent/ssh-agent socket error (#171)**, even though the identical `git commit` succeeded from Terminal. On macOS, a Finder/Dock-launched GitWand backfills its minimal `launchd` environment by reading `$SHELL -l -c env` once at startup, but `-l` (login) alone does not make zsh source `~/.zshrc` — only an interactive shell does, and setup guides for gpg/ssh agents (`export GPG_TTY=$(tty)`, agent-socket exports for tools like 1Password/YubiKey) conventionally live there. The probe now runs `$SHELL -i -l -c env`, so those exports are captured like any other shell-rc variable.

## [3.8.0] - 2026-08-24
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ The registry holds **12 patterns**, of which **8 auto-apply**. The other four ei

The confidence column is indicative: every hunk carries a computed `ConfidenceScore` (see below), not a fixed label. `value_only_change`, for instance, scores on the ratio of volatile tokens to total tokens and rejects the hunk outright below its threshold.

**Not a registry pattern:** `generated_file` is a separate reclassification pass that runs after classification. When a hunk lands in `complex` and its path matches a generated-file glob (lockfiles, bundles, `dist/`, plus anything in `generatedFiles`), the resolver rewrites it to `generated_file` and resolves to *theirs*, on the assumption the file will be regenerated. It appears in `ConflictType` but never in the classifier registry.
**Not a registry pattern:** `generated_file` is a separate reclassification pass that runs after classification. When a hunk's path matches a generated-file glob (lockfiles, bundles, `dist/`, plus anything in `generatedFiles`), the resolver rewrites it to `generated_file` and, by default, declines with an actionable reason rather than guessing — [measured on 1,662 real merges](https://github.com/devlint/GitWand/tree/main/benchmark), auto-merging generated files diverged from what teams actually shipped in almost every case. GitWand tells you to resolve the source file and re-run the installer/build instead. Opt back into the old accept-theirs/semantic-merge behavior with `.gitwandrc`'s `resolveGeneratedFiles: true` or `gitwand resolve --resolve-generated`. `generated_file` appears in `ConflictType` but never in the classifier registry.

### Composite confidence score

Expand All @@ -192,6 +192,8 @@ Every resolution carries a `ConfidenceScore` object rather than a simple label:
}
```

(Shape shown for a `generated_file` hunk with `resolveGeneratedFiles: true` — the default is to decline generated files rather than score and apply them; see the pattern table above.)

Score formula: `score = typeClassification − dataRisk×0.4 − scopeImpact×0.15`

### Format-aware resolvers
Expand Down
Loading
Loading