gabriel / musehub public
mwp-qa.md markdown
426 lines 16.6 KB
Raw
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 8 days ago

MWP QA — Verifying the Muse Wire Protocol (clone / push / fetch / pull)

Background

The Muse Wire Protocol (MWP) is how muse clone, muse push, muse fetch, and muse pull move commits, snapshots, and blobs between your machine and a MuseHub server. It's functionally equivalent to git's smart-HTTP protocol, rebuilt on a binary packfile format (mpack) with content-addressed objects and presigned uploads direct to object storage (R2 on staging/prod, MinIO on localhost).

This mist is a copy-paste-able verification pass for anyone picking up work against https://staging.musehub.ai for the first time. Run it top to bottom once when you first get access — if every step below matches its expected result, the wire protocol is working correctly for you and you're clear to start real work. If anything deviates from the expected result, stop and report it (don't work around it silently) — see Troubleshooting at the bottom.

This guide was produced from muse#63, a full black-box verification pass of the wire protocol MVP against real staging, run after closing muse#58's 8 sub-tickets (MWP-1..MWP-8). Every recipe below was actually executed against staging or localhost and its exact result recorded (with <you> substituted back in for whichever handle actually ran it) — this is not speculative documentation.

Wherever you see <you>, substitute your own handle or a unique scratch-repo suffix — every recipe below uses it consistently for both the repo-name suffix and the owner segment of the URL, since they're the same value once you're authenticated as yourself.


Prerequisites

1. Install the CLI:

curl -fsSL https://staging.musehub.ai/install.sh | sh
muse --version

2. Register your identity (Ed25519 keypair, no password/JWT):

muse auth keygen --hub https://staging.musehub.ai
muse auth register --hub https://staging.musehub.ai --handle <you>
muse auth whoami

keygen stores a BIP-39 mnemonic in your OS keychain and never prints it to the terminal. Back it up immediately:

# macOS
security find-generic-password -s muse -a mnemonic -w | pbcopy   # paste into your password manager

Losing the mnemonic means permanent loss of your derived keys.


Architecture at a glance

Command What it does Server endpoint
muse push Uploads only the commits/blobs the remote doesn't already have POST /push/mpack-presign → PUT to object storage → POST /push/unpack-mpack
muse clone Downloads a complete repo — there's no separate clone endpoint, it's a fetch with have=[] POST /fetch/mpack
muse fetch Downloads new commits/blobs into your local repo, without touching your working tree or branch pointer POST /fetch/mpack
muse pull fetch + fast-forward merge (or a reported conflict if history diverged) POST /fetch/mpack (then local merge)

Objects are content-addressed (sha256:<hex> is the integrity check), so push/fetch only ever transfer the true delta — you'll see this verified concretely in Step 2 below.

A convenience worth knowing up front: muse init auto-seeds three remotes — local, staging, production — pointing at <host>/<you>/<directory-name>. So as long as the hub repo you create has the same name as your local directory, you never need muse remote add at all — muse push staging main just works out of the box. All recipes below rely on this; if you name your hub repo differently from your directory, you'll need muse remote set-url staging <url> first.

--json output below is piped through jq for readability — that's a convention, not a requirement; drop | jq any time you want raw output.


Quick Start — 5 minute smoke test

Confirms all 4 verbs work end to end against staging.

# Create and push a brand-new repo — directory name and hub repo name must
# match so the auto-seeded 'staging' remote resolves correctly
cd /tmp && rm -rf mwp-smoke-<you> && mkdir mwp-smoke-<you> && cd mwp-smoke-<you>
muse init --json | jq
{
  "muse_version": "0.2.0rc15",
  "schema": 1,
  "exit_code": 0,
  "duration_ms": 1.945,
  "warnings": [],
  "status": "ok",
  "repo_id": "sha256:ff51dfb3547e43fdfde6d080e59e7b25047c1a72e3bdebf8c140d8c6f1c7c6b8",
  "branch": "main",
  "domain": "code",
  "path": "/private/tmp/mwp-smoke-<you>/.muse",
  "reinitialized": false,
  "bare": false,
  "remotes": {
    "local": "https://localhost:1337/<you>/mwp-smoke-<you>",
    "staging": "https://staging.musehub.ai/<you>/mwp-smoke-<you>",
    "production": "https://musehub.ai/<you>/mwp-smoke-<you>"
  }
}
echo "hello" > README.md && muse code add README.md
muse commit -m "init" --agent-id <you> --model-id manual
muse hub repo create --name mwp-smoke-<you> --no-init --hub https://staging.musehub.ai --json | jq
{
  "muse_version": "0.2.0rc15",
  "schema": 1,
  "exit_code": 0,
  "duration_ms": 451.494,
  "warnings": [],
  "repo_id": "sha256:932c532eb980f812828f2822f512b21d8821403b28f992206497e413e0d59dc4",
  "name": "mwp-smoke-<you>",
  "owner": "<you>",
  "slug": "mwp-smoke-<you>",
  "visibility": "public",
  "default_branch": "main",
  "clone_url": "https://staging.musehub.ai/<you>/mwp-smoke-<you>",
  "url": "https://staging.musehub.ai/<you>/mwp-smoke-<you>"
}
muse push staging main --json | jq
{
  "muse_version": "0.2.0rc15",
  "schema": 1,
  "exit_code": 0,
  "duration_ms": 2142.301,
  "warnings": [],
  "status": "pushed",
  "remote": "staging",
  "branch": "main",
  "head": "sha256:d05f7e8bdfeee15f79635cd962fee48de3ab7967c5c0bc706385a567f725318e",
  "commits_sent": 1,
  "objects_sent": 1,
  "force": false,
  "dry_run": false
}

Expect: "status": "pushed", exit_code: 0. (No muse remote add step — staging was already configured by muse init.)

# Clone it back down
cd /tmp && rm -rf mwp-smoke-clone
muse clone https://staging.musehub.ai/<you>/mwp-smoke-<you> mwp-smoke-clone --json | jq
{
  "muse_version": "0.2.0rc15",
  "schema": 1,
  "exit_code": 0,
  "duration_ms": 1418.982,
  "warnings": [],
  "status": "cloned",
  "url": "https://staging.musehub.ai/<you>/mwp-smoke-<you>",
  "directory": "/private/tmp/mwp-smoke-clone",
  "branch": "main",
  "commits_received": 1,
  "blobs_written": 1,
  "skipped_blobs": 0,
  "head": "sha256:d05f7e8bdfeee15f79635cd962fee48de3ab7967c5c0bc706385a567f725318e",
  "dry_run": false,
  "shallow_commits": []
}
cat mwp-smoke-clone/README.md

Expect: "status": "cloned", exit_code: 0, README.md contains hello.

# Push a second commit, then fetch + pull it into the clone
cd /tmp/mwp-smoke-<you>
echo "v2" >> README.md && muse code add README.md
muse commit -m "v2" --agent-id <you> --model-id manual
muse push staging main --json | jq

cd /tmp/mwp-smoke-clone
muse fetch origin --branch main --json | jq   # downloads the new commit, does NOT update your working tree
cat README.md                                  # still shows only "hello" — fetch != merge, this is correct
muse pull origin main --json | jq              # fetch + fast-forward merge
cat README.md                                  # now shows "hello" and "v2"

Expect: after fetch, the working tree is unchanged (proves fetch and pull are genuinely different operations, not aliases). After pull, README.md shows both lines.

If all of the above matches, clean up and move to the detailed recipes below only if you want deeper confidence, or skip straight to real work:

cd /tmp/mwp-smoke-<you> && muse hub repo delete --hub https://staging.musehub.ai --yes
rm -rf /tmp/mwp-smoke-<you> /tmp/mwp-smoke-clone

Careful: muse hub repo delete with no target resolves the repo from your current directory's remote config — always run it from inside the repo you mean to delete, or pass the name explicitly (muse hub repo delete <you>/<name> --yes) if you're elsewhere.


Detailed verb-by-verb recipes

Each recipe below was run against real staging as part of muse#63's verification pass, with the exact result recorded. Use <you> as a placeholder for your handle / a unique scratch-repo suffix throughout, so your scratch repos don't collide with anyone else's.

muse push — uploads only the true delta

cd /tmp && rm -rf mwp-qa-push-<you> && mkdir mwp-qa-push-<you> && cd mwp-qa-push-<you>
muse init --json | jq
echo "base" > file.txt && muse code add file.txt
muse commit -m "base" --agent-id <you> --model-id manual
muse hub repo create --name mwp-qa-push-<you> --no-init --hub https://staging.musehub.ai --json | jq
muse push staging main --json | jq '{objects_sent, commits_sent}'

Expect: first push sends commits_sent: 1, objects_sent: 1 (the one new blob).

# Push again with no changes — should send nothing
muse push staging main --json | jq '{objects_sent, commits_sent}'

Expect: commits_sent: 0 (nothing new; muse detects the remote is already up to date and no-ops cleanly, not an error).

# Two branches sharing a base — the second branch's push should exclude
# blobs already reachable via the first branch's tip
muse checkout -b feature-a && echo "fa" > fa.txt && muse code add fa.txt
muse commit -m "feature a" --agent-id <you> --model-id manual
muse push staging feature-a --json | jq

muse checkout main && muse checkout -b feature-b && echo "fb" > fb.txt && muse code add fb.txt
muse commit -m "feature b" --agent-id <you> --model-id manual
muse push staging feature-b --json | jq '.objects_sent'

Expect: objects_sent: 1 — only fb.txt's blob, not file.txt's (which the remote already has via both main and feature-a).

muse clone — always a complete, correct working tree

cd /tmp && rm -rf mwp-qa-clone-<you>
time muse clone https://staging.musehub.ai/<you>/mwp-qa-push-<you> mwp-qa-clone-<you> --json | jq

Expect: "status": "cloned", exit_code: 0. First clone of a repo right after it was created may take a few seconds longer (server builds a prebuilt packfile in the background) — if the server isn't ready yet, muse clone automatically retries with backoff; you'll see a ⏳ remote preparing fetch data... message rather than an error, and it converges on its own.

# Push then clone again immediately — this exact sequence was once a real bug
# (musehub#113/MWP-1) where the clone could silently miss the newest commit
cd /tmp/mwp-qa-push-<you>
muse checkout main   # the push recipe above leaves you on feature-b — switch back
echo "v2" >> file.txt && muse code add file.txt
muse commit -m "v2" --agent-id <you> --model-id manual
muse push staging main --json | jq

cd /tmp && rm -rf mwp-qa-clone-2-<you>
muse clone https://staging.musehub.ai/<you>/mwp-qa-push-<you> mwp-qa-clone-2-<you> --json | jq
diff /tmp/mwp-qa-push-<you>/file.txt /tmp/mwp-qa-clone-2-<you>/file.txt

Expect: diff produces no output (files identical, v2 line present in both).

muse fetch — downloads without touching your working tree

cd /tmp && rm -rf mwp-qa-fetch-x-<you> mwp-qa-fetch-y-<you>
muse clone https://staging.musehub.ai/<you>/mwp-qa-push-<you> mwp-qa-fetch-x-<you> --json | jq
muse clone https://staging.musehub.ai/<you>/mwp-qa-push-<you> mwp-qa-fetch-y-<you> --json | jq

cd /tmp/mwp-qa-fetch-x-<you>
echo "from-x" >> file.txt && muse code add file.txt
muse commit -m "from x" --agent-id <you> --model-id manual
muse push origin main --json | jq

cd /tmp/mwp-qa-fetch-y-<you>
muse fetch origin --branch main --json | jq
cat file.txt   # should NOT show "from-x" yet

Expect: fetch exits 0 and downloads the new commit's data, but your local branch pointer and working tree are untouched — file.txt still shows the old content. This is the correct, intentional difference between fetch and pull.

muse pull — fetch + merge, and what happens on a real conflict

cd /tmp/mwp-qa-fetch-y-<you>
muse pull origin main --json | jq
cat file.txt   # NOW shows "from-x"

Expect: "status": "fast_forward", working tree converges.

Conflict case — if two people edit the same file on diverging branches, a pull will not silently pick a side or corrupt anything — it stops and surfaces the conflict:

# From two independent clones, each commits a different change to the same line,
# then the second one to pull sees:
muse pull origin main --json | jq
# {"status": "conflict", "exit_code": 1, "conflict_paths": ["file.txt"], ...}
muse conflicts --json | jq   # inspect both sides before resolving

If you ever see "status": "conflict", do not run muse checkout --ours or --theirs blindly — read both sides (muse conflicts --json, then open the file and read the <<<<<<< ours / ======= theirs markers) and resolve manually with muse resolve <path>, then commit.


Advanced: repeated push/clone loop (confidence check)

If you want extra confidence before relying on this for real work, this loop mirrors what was run 10 times against staging with zero staleness and flat upload size on every iteration:

cd /tmp && rm -rf mwp-qa-loop-<you> && mkdir mwp-qa-loop-<you> && cd mwp-qa-loop-<you>
muse init --json | jq
muse hub repo create --name mwp-qa-loop-<you> --no-init --hub https://staging.musehub.ai --json | jq
for i in $(seq 1 5); do
  echo "iteration $i" >> log.txt
  muse code add log.txt
  muse commit -m "iter $i" --agent-id <you> --model-id manual
  muse push staging main --json | jq "{i: $i, objects_sent}"
  rm -rf /tmp/mwp-qa-loop-clone-$i
  muse clone https://staging.musehub.ai/<you>/mwp-qa-loop-<you> /tmp/mwp-qa-loop-clone-$i --json | jq
  tail -1 /tmp/mwp-qa-loop-clone-$i/log.txt
done

Expect: objects_sent stays 1 every iteration (never grows), and each clone's last line always matches that iteration's commit — no staleness, no manual cache-busting needed at any point.

A ~30s wait on some iterations is normal, not a hang. Against real staging, cloning immediately after a push can hit the server's async prebuild job before it's finished — you'll see ⏳ remote preparing fetch data (server busy, attempt 1); retrying in 30s … on stderr, then the clone completes correctly once the retry lands. This is expected roughly half the time in a tight push→clone loop on staging (localhost doesn't show this, since there's no real prebuild latency to race against). It always converges well within the 120s retry budget — if you ever see ❌ Remote still preparing clone data after 120s, that's the one case worth reporting, not the 30s wait itself.


Troubleshooting

Symptom Meaning Fix
⏳ remote preparing fetch data (server busy, attempt N); retrying in 30s … Server hasn't finished building the fetch cache for a very recent push yet Normal — the client retries automatically for up to 120s. If it eventually fails with ❌ Remote still preparing clone data after 120s, wait a bit and retry manually; if it happens repeatedly, report it.
❌ Repository not found on remote (push) The repo doesn't exist on the hub yet muse hub repo create --name <name> --no-init --hub <url> --json, then retry the push.
Remote '<name>' is not configured muse clone always names the remote origin; a manually-added remote keeps whatever name you gave it muse remote --json to see configured names, or muse remote set-url <name> <url>.
push rejected: ... is not a fast-forward ... use --force to override Someone else pushed to the same branch first (or you rewrote history locally) This is correct, safe behavior — pull/rebase first. Only use --force if you're deliberately rewriting shared history, and never on a shared branch without coordinating with whoever else is using it.
"status": "conflict" on pull Two branches diverged with real conflicting changes See the conflict-case recipe above — read both sides, muse resolve, never blind --ours/--theirs.
muse fetch <remote> <branch>unrecognized arguments fetch takes --branch/-b, not a positional branch arg muse fetch <remote> --branch <branch> --json

If something doesn't match its expected result above and none of these explain it, stop and report the exact command, its JSON output, and which step it was — don't work around it silently.


Cleanup

Scratch repos created while running this guide should be deleted when you're done so they don't accumulate on staging:

cd /tmp/mwp-qa-push-<you> && muse hub repo delete --hub https://staging.musehub.ai --yes
cd /tmp/mwp-qa-loop-<you> && muse hub repo delete --hub https://staging.musehub.ai --yes
rm -rf /tmp/mwp-qa-*-<you> /tmp/mwp-smoke-<you> /tmp/mwp-smoke-clone
File History 3 commits
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 8 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226 Merge branch 'fix/two-column-scroll-layout' into dev Human 8 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454 chore: bump version to 0.2.0.dev2 — nightly.2, matching muse Sonnet 4.6 patch 11 days ago