Skip to content

Commit 742102b

Browse files
Enforce canonical root checkout semantics
Co-authored-by: Andrew <andrewxhill@gmail.com>
1 parent 6634e79 commit 742102b

12 files changed

Lines changed: 306 additions & 11 deletions

File tree

.agent-hooks/guard_mainline_git.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ def main():
9898
"make commits there, then use `mq submit --check-only --json` and either "
9999
"`mq submit --wait --timeout 15m --json` or `mq submit --json` followed by "
100100
"`mq wait --submission <id> --for landed --json --timeout 30m`.\n"
101+
"Keep /Users/devrel/Projects/recallnet/mainline clean on branch main; "
102+
"that root checkout is the canonical protected checkout humans inspect.\n"
101103
"If you are the controller agent, use `mq land --json --timeout 30m` or let "
102104
"one machine-global `mainlined --all --json` drainer own registered repos.\n"
103105
"Allowed on main: read-only inspection and worktree creation. "

.agents/skills/worktree/SKILL.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ that work back through `mq`.
1111
The goal is simple:
1212

1313
- `main` stays clean
14+
- the repo root checkout stays boring and trustworthy
1415
- each task gets its own worktree
1516
- all commits happen in the feature worktree
1617
- landing happens through `mq`, not manual merge into `main`
@@ -50,7 +51,7 @@ positive to work around. Move to a feature worktree and continue there.
5051

5152
### 1. Start from a clean protected branch worktree
5253

53-
Use the canonical repo worktree as the protected branch worktree.
54+
Use the repo root checkout as the canonical protected branch worktree.
5455

5556
Example:
5657

@@ -64,6 +65,7 @@ Expected:
6465

6566
- worktree is clean
6667
- branch is `main`
68+
- this root checkout is the one humans inspect and wrappers build from
6769

6870
### 2. Create a dedicated feature worktree
6971

@@ -203,6 +205,8 @@ Expected:
203205
- `status --json` can correlate a succeeded submission to `publish_request_id`,
204206
`publish_status`, and `outcome`
205207
- `unmerged` is empty once the branch is truly reachable from protected `main`
208+
- `mq repo show` and `mq doctor` do not warn that the repo root checkout is
209+
dirty or non-canonical
206210

207211
## Review and fix loop
208212

AGENTS.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ instructions.
55

66
## Protected Main Worktrees
77

8-
- Treat `/Users/devrel/Projects/_wt/recallnet/mainline/protected-main` as the
9-
canonical protected `main` worktree for this repo.
10-
- Treat `/Users/devrel/Projects/recallnet/mainline` as protected too unless you
11-
have explicitly confirmed it is a disposable feature checkout. Do not assume
12-
the root checkout is safe for feature commits.
8+
- Treat `/Users/devrel/Projects/recallnet/mainline` as the canonical protected
9+
`main` checkout for this repo.
10+
- Treat `/Users/devrel/Projects/_wt/recallnet/mainline/protected-main` as a
11+
protected mirror, not as the human-facing source of truth.
12+
- The root checkout must stay clean and on branch `main`. Humans inspect it,
13+
wrappers build from it, and docs refer to it.
1314
- Do not use native mutating `git` commands from that worktree while on branch
1415
`main`.
1516
- Blocked examples: `git commit`, `git merge`, `git rebase`, `git push`,
@@ -31,6 +32,8 @@ instructions.
3132
- Controllers and factory-style daemons should prefer:
3233
- `mq land --json --timeout 30m`
3334
- or one machine-global `mainlined --all --json`
35+
- If `mq repo show` or `mq doctor` warns that the root checkout is dirty or not
36+
canonical, fix that before trusting local binaries or local docs.
3437
- Plain `mq submit` now opportunistically tries to drain after queueing. If the
3538
integration lock is already held, it exits cleanly and the active worker keeps
3639
draining.

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
## Repo Workflow
1111

1212
- treat `main` as protected
13+
- keep the repo root checkout clean on `main`; that is the canonical protected checkout humans inspect and wrappers build from
1314
- do feature work in a dedicated topic worktree
1415
- initialize new clones with `mq repo init`, commit `mainline.toml`, and run `./scripts/install-hooks.sh`
1516
- most agents should finish from a topic worktree with `mq submit --check-only --json` and `mq submit --wait --timeout 15m --json`

PLAN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Deliverables:
5757
- detect repo root from current directory
5858
- inspect worktrees and refs using `go-git`-backed repository access
5959
- identify canonical protected branch worktree
60+
- for ordinary repos, treat the repo root checkout as the canonical protected `main`
6061
- detect and model bare repo storage path separately from worktree path
6162
- config file support
6263
- `mainline repo init`

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ There is one rule that matters:
5353

5454
Once that line is real, the rest follows:
5555

56+
- the repo root checkout stays as the canonical protected `main`
5657
- feature work happens in topic worktrees
5758
- submissions are recorded durably
5859
- integration is serialized
@@ -208,6 +209,10 @@ That init commit matters. It turns the repo’s queue policy into versioned,
208209
reviewable state instead of one more local convention that agents have to infer.
209210
`mq repo init` also registers the repo for `mainlined --all`, so one machine
210211
daemon can drain many repos without one idle process per repo.
212+
For normal repos, the root checkout should be the canonical protected `main`.
213+
Keep it clean and boring. Humans inspect that path, and the machine wrapper
214+
builds `mq` and `mainlined` from it. If it is dirty, the wrapper should refuse
215+
to build rather than silently drift.
211216

212217
## The Core Commands
213218

SPEC.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ MVP checks:
297297

298298
- worktree clean
299299
- protected branch worktree clean
300+
- for ordinary repos, the repository root checkout should be the canonical protected `main` checkout that humans inspect and local wrappers build from
300301
- branch not already integrated
301302
- branch HEAD still matches submitted SHA unless `--allow-newer-head` is set;
302303
when that flag is set, newer queued branch heads are allowed only if the new

docs/FLOWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ mq doctor --repo /path/to/main
4949
mq status --repo /path/to/main
5050
```
5151

52+
For ordinary repos, `/path/to/main` should be the repo root checkout that
53+
humans inspect and local wrappers build from. Keep that checkout clean and on
54+
the protected branch. Topic worktrees are where feature edits belong.
55+
5256
Submit from any linked worktree in the same repo:
5357

5458
```bash

docs/install.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ git commit -m "Initialize mainline repo policy"
6565

6666
`mq repo init` automatically registers the repo for the machine-global daemon
6767
registry used by `mainlined --all`.
68+
For ordinary repos, treat that root checkout as the canonical protected `main`.
69+
Keep it clean and on `main`, because humans inspect it and the machine wrappers
70+
build from it.
71+
The machine-level `mq` and `mainlined` wrappers refuse to build from a dirty
72+
root checkout so local binaries cannot silently drift away from the code humans
73+
are reading.
6874

6975
State compatibility:
7076

internal/app/registry.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package app
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"errors"
67
"os"
@@ -61,7 +62,13 @@ func loadRegisteredReposFromPath(path string) ([]registeredRepo, error) {
6162

6263
var registry registryFile
6364
if err := json.Unmarshal(data, &registry); err != nil {
64-
return nil, err
65+
sanitized, sanitizeErr := firstJSONObject(data)
66+
if sanitizeErr != nil {
67+
return nil, err
68+
}
69+
if unmarshalErr := json.Unmarshal(sanitized, &registry); unmarshalErr != nil {
70+
return nil, err
71+
}
6572
}
6673
sort.Slice(registry.Repositories, func(i, j int) bool {
6774
return registry.Repositories[i].RepositoryRoot < registry.Repositories[j].RepositoryRoot
@@ -131,3 +138,43 @@ func canonicalRegistryPath(path string) string {
131138
}
132139
return filepath.Clean(path)
133140
}
141+
142+
func firstJSONObject(data []byte) ([]byte, error) {
143+
trimmed := bytes.TrimSpace(data)
144+
if len(trimmed) == 0 || trimmed[0] != '{' {
145+
return nil, errors.New("registry does not start with json object")
146+
}
147+
148+
depth := 0
149+
inString := false
150+
escaped := false
151+
for i, b := range trimmed {
152+
if inString {
153+
if escaped {
154+
escaped = false
155+
continue
156+
}
157+
if b == '\\' {
158+
escaped = true
159+
continue
160+
}
161+
if b == '"' {
162+
inString = false
163+
}
164+
continue
165+
}
166+
167+
switch b {
168+
case '"':
169+
inString = true
170+
case '{':
171+
depth++
172+
case '}':
173+
depth--
174+
if depth == 0 {
175+
return trimmed[:i+1], nil
176+
}
177+
}
178+
}
179+
return nil, errors.New("registry json object did not terminate cleanly")
180+
}

0 commit comments

Comments
 (0)