From 77aefe918fce1285ff4e126dea421d41d91eb770 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 31 Oct 2026 23:28:47 -0700 Subject: [PATCH 1/2] chore(quest): claim quest/m0/helpers/merge-driver.md Quest-Claim: 8f52d7e2104befa3 From 15fab1d4cee7f766256277cc083ccae9b1b13d32 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 31 Oct 2026 23:39:27 -0700 Subject: [PATCH 2/2] feat(resolve): say when the merge driver falls back, and pin quest in the dev shell The quest merge driver fell back to `git merge-file` without a word when `quest resolve` could not run, leaving plain conflict markers in link lists. When that fallback leaves conflicts, it now prints to stderr why and which file's link lists were not auto-resolved. The dev shell now carries a quest built from a pinned, reviewed commit of this repository (a non-flake input, bumped with `nix flake update quest-pinned`), so this repository's own merges resolve link lists without running a PR head's code. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 3 ++- README.md | 6 ++++-- flake.lock | 19 ++++++++++++++++++- flake.nix | 12 ++++++++++++ quest/m0/helpers/README.md | 1 - quest/m0/helpers/merge-driver.md | 28 ---------------------------- quest/m0/required-heading.md | 4 ---- src/cli/main.ts | 3 ++- src/cli/resolve.ts | 12 +++++++++--- test/resolve.test.ts | 7 +++++++ 10 files changed, 54 insertions(+), 41 deletions(-) delete mode 100644 quest/m0/helpers/merge-driver.md diff --git a/AGENTS.md b/AGENTS.md index 3202847b..2b83e5aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,8 @@ Do not edit `CONTRIBUTING.md`, `PROMPTING.md`, `assets/AGENTS.md`, or - Run `quest guide` and read it completely before starting work. - Keep README examples consistent with the CLI's actual `--help` output. - In this checkout, `quest` means `bun run --silent quest`. + In this checkout, `quest` means `bun run --silent quest`; the dev shell's + `quest` is a pinned build for the merge driver, not this checkout. - Use an isolated worktree under `.worktrees/`. Preserve existing changes. - Use the Nix shell so local tooling matches CI: `nix develop` - Run `just install` in a fresh checkout to install the locked dependencies. diff --git a/README.md b/README.md index ed9557bd..2926de12 100644 --- a/README.md +++ b/README.md @@ -271,8 +271,10 @@ opening link. Prose, headings, an entry both sides rewrote, or one side removed while the other edited, keep the usual conflict markers. Any `quest` command registers the driver in a fresh clone or worktree whose `.gitattributes` names it. Without `quest` on the merge's PATH, the driver -falls back to `git merge-file`, and GitHub's merges ignore drivers, so a PR -shows as conflicting until a local merge from its base is pushed. +falls back to `git merge-file` and, when conflicts remain, prints to stderr +that the file's link lists were not auto-resolved. GitHub's merges ignore +drivers, so a PR shows as conflicting until a local merge from its base is +pushed. The registered driver passes Git's conflict marker length and revision labels to `quest resolve`, including the ancestor label for `diff3` conflicts. A direct diff --git a/flake.lock b/flake.lock index f087d34a..bebb50d5 100644 --- a/flake.lock +++ b/flake.lock @@ -34,10 +34,27 @@ "type": "github" } }, + "quest-pinned": { + "flake": false, + "locked": { + "lastModified": 1793505952, + "narHash": "sha256-jDq0fsxzLGIu6FsFfIsnUgvb23Pb+Rja4KZwIxGa34I=", + "owner": "moq-dev", + "repo": "quest", + "rev": "7a191e6197c691f477b4c55edba34cc425c3e77c", + "type": "github" + }, + "original": { + "owner": "moq-dev", + "repo": "quest", + "type": "github" + } + }, "root": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "quest-pinned": "quest-pinned" } }, "systems": { diff --git a/flake.nix b/flake.nix index df7714e7..af36b932 100644 --- a/flake.nix +++ b/flake.nix @@ -4,6 +4,14 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-utils.url = "github:numtide/flake-utils"; + # A reviewed quest for the dev shell's merge driver, so local merges resolve + # link lists without running the checkout's own, possibly unreviewed, code. + # Only its source is pinned, so the lock never chains older pins. Bump with + # `nix flake update quest-pinned`. + quest-pinned = { + url = "github:moq-dev/quest"; + flake = false; + }; }; outputs = @@ -11,6 +19,7 @@ self, nixpkgs, flake-utils, + quest-pinned, ... }: # The pinned nixpkgs no longer evaluates on x86_64-darwin; Intel Macs use the release binary. @@ -29,6 +38,7 @@ config.allowUnfreePredicate = pkg: nixpkgs.lib.getName pkg == "claude-code"; }; quest = pkgs.callPackage ./nix/package.nix { }; + questPinned = pkgs.callPackage "${quest-pinned}/nix/package.nix" { }; # https://downloads.claude.ai/claude-code-releases/2.1.305/manifest.zst.json # Keep the tested runner version independent of nixpkgs. claudeCode = pkgs.claude-code.override { @@ -68,6 +78,8 @@ packages = with pkgs; [ # Installs dependencies, runs every script and tool, and compiles the release binaries. bun + # `quest resolve` for the merge driver; `just` and `bun run quest` run the checkout. + questPinned just jq git diff --git a/quest/m0/helpers/README.md b/quest/m0/helpers/README.md index dbf75eed..790eb3c1 100644 --- a/quest/m0/helpers/README.md +++ b/quest/m0/helpers/README.md @@ -71,7 +71,6 @@ commands from Sep-Oct. Session counts in each child come from that window. ## Required - [quest merge --land gaps](/quest/m0/helpers/merge-land.md) - squash on squash-only repositories, PR URLs outside a checkout, labels released after direct merges, and a warning when auto-merge outlives its head -- [Visible merge-driver fallback](/quest/m0/helpers/merge-driver.md) - a merge without `quest` on PATH says so, and this repository's dev shell provides one - [Review rules](/quest/m0/helpers/review-rules.md) - a head whose diff matches the reviewed diff keeps its review - [Plan delegated follow-ups](/quest/m0/helpers/follow-up-plan.md) - a caller plans a background merge's follow-ups without asking - [Drop review-rounds](/quest/m0/helpers/review-rounds.md) - `quest status` stops counting GitHub reviews now that `quest review` numbers rounds diff --git a/quest/m0/helpers/merge-driver.md b/quest/m0/helpers/merge-driver.md deleted file mode 100644 index 998472f5..00000000 --- a/quest/m0/helpers/merge-driver.md +++ /dev/null @@ -1,28 +0,0 @@ -# [S] Make the merge driver's fallback visible - -## Goal - -A merge that the `quest` merge driver can't run says so, rather than -silently leaving plain conflict markers in link lists. This repository's dev -shell provides a `quest`, so its own merges resolve link lists. - -## Plan - -Decided 2026-10-28 (user, /quest-plan of the follow-ups from a 25-PR /quest-complete run). The driver config runs `quest resolve` only when `quest` is on PATH, and -otherwise falls back to `git merge-file` without a word. In moq-dev/quest -the dev shell has no `quest`, so merge agents hand-resolved link lists. A -hand-resolved conflict needs a fresh review, so link-list conflicts stalled -merges on Codex reviews (#233, #250, #255). The same merges resolved cleanly -once `quest` was put on PATH. - -- The fallback prints to stderr that `quest` wasn't found and link lists - weren't auto-resolved, naming the file. Keep the fallback itself. -- This repository's flake puts a pinned `quest` release in the dev shell, - not a build of the checkout. That way merging a PR head never runs that - head's code. -- Rejected: `quest init` writing an absolute binary path, which breaks on - upgrades and Nix store paths. - -## Related - -- [Sibling PRs through a server-side merge](/quest/m0/helpers/queue-siblings.md) - the same link-list conflict, but in GitHub's merges, which run no driver diff --git a/quest/m0/required-heading.md b/quest/m0/required-heading.md index b3ed7bdd..8e01ac3a 100644 --- a/quest/m0/required-heading.md +++ b/quest/m0/required-heading.md @@ -19,7 +19,3 @@ so it never sees the heading. After resolving, drop a `Required` heading whose section is now empty. Leave other empty sections to `quest check`, which should report them. Test it with both sides each removing one of the last two entries. - -## Related - -- [Visible merge-driver fallback](/quest/m0/helpers/merge-driver.md) - the driver's other gap diff --git a/src/cli/main.ts b/src/cli/main.ts index ee0a587e..0eeee749 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -978,7 +978,8 @@ export function main(argv: string[], out: Output): number | Promise { "or zdiff3. Exits 1 when a conflict remains.\n\n" + "quest init registers it for quest/** as the merge.quest driver. Any other command registers the driver " + "in the clone's config when .gitattributes names merge=quest and no config sets it. Without quest on " + - "the merge's PATH, the driver falls back to git merge-file. GitHub's merges ignore merge drivers.", + "the merge's PATH, the driver falls back to git merge-file, and when conflicts remain it prints to stderr " + + "that the file's link lists were not auto-resolved. GitHub's merges ignore merge drivers.", ) .argument("", "Our version, overwritten with the result (git's %A)") .argument("", "The common ancestor (git's %O)") diff --git a/src/cli/resolve.ts b/src/cli/resolve.ts index 491d2b6c..8477a1b3 100644 --- a/src/cli/resolve.ts +++ b/src/cli/resolve.ts @@ -14,13 +14,19 @@ const MARKER_SIZE = 7; * names for `%A`, `%O`, and `%B` and runs it with a shell. A `quest` missing * from the merge's PATH, or one without `resolve`, falls back to git's own * merge with conflict markers: `quest help ` exits non-zero, while - * `quest --help` prints the top-level help and exits zero. + * `quest --help` prints the top-level help and exits zero. A + * fallback that leaves conflicts says why on stderr, naming the file (`%P`), + * since those markers may be link-list edits `quest resolve` would have + * merged. Git keeps a placeholder it does not know, such as printf's `%s`. */ export const DRIVER = "if quest help resolve >/dev/null 2>&1; " + "then quest resolve %A %O %B --marker-size=%L --ours-label=%X --base-label=%S --theirs-label=%Y; " + - 'quest_merge_status=$?; if [ "$quest_merge_status" -le 1 ]; then exit "$quest_merge_status"; fi; fi; ' + - "git merge-file --marker-size=%L -L %X -L %S -L %Y %A %O %B"; + 'quest_merge_status=$?; if [ "$quest_merge_status" -le 1 ]; then exit "$quest_merge_status"; fi; ' + + "quest_merge_reason='quest resolve failed'; else quest_merge_reason='no quest resolve on PATH'; fi; " + + "git merge-file --marker-size=%L -L %X -L %S -L %Y %A %O %B && exit 0; quest_merge_status=$?; " + + "printf 'quest: %s, so link lists in %s were not auto-resolved\\n' \"$quest_merge_reason\" %P >&2; " + + 'exit "$quest_merge_status"'; export interface ResolveOptions { markerSize?: number; diff --git a/test/resolve.test.ts b/test/resolve.test.ts index 50225e1c..fff79fb0 100644 --- a/test/resolve.test.ts +++ b/test/resolve.test.ts @@ -284,6 +284,9 @@ describe.skipIf(process.platform === "win32")("the merge driver", () => { const merged = mergeMain(dir, tools); expect(merged.status).toBe(1); expect(readFileSync(join(dir, "quest/README.md"), "utf8")).toContain("<<<<<<< HEAD\n"); + expect(merged.stderr).toContain( + "quest: no quest resolve on PATH, so link lists in quest/README.md were not auto-resolved\n", + ); }); for (const mode of ["quest", "missing", "older"] as const) { @@ -320,6 +323,10 @@ describe.skipIf(process.platform === "win32")("the merge driver", () => { const result = mergeMain(dir, mode === "quest" ? `${tools}:${process.env.PATH}` : tools, "their'branch"); expect(result.status, result.stderr).toBe(1); expect(readFileSync(join(dir, "quest/README.md"), "utf8")).toBe(expected); + const reason = { quest: null, missing: "no quest resolve on PATH", older: "quest resolve failed" }[mode]; + const warning = "so link lists in quest/README.md were not auto-resolved"; + if (reason === null) expect(result.stderr).not.toContain(warning); + else expect(result.stderr).toContain(`quest: ${reason}, ${warning}\n`); }); }