Skip to content

Commit d3657e9

Browse files
heavygeecursoragent
andcommitted
docs(tooling): add hapi-sync-fork-main and enforce mirror before driver rebuild
Fork main must track upstream/main plus fork-only docs. New sync script; driver rebuild fails when primary main is behind upstream. Document cadence in driver-soup and meta bot README. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8aae594 commit d3657e9

6 files changed

Lines changed: 288 additions & 8 deletions

File tree

docs/operator/AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ upstream → https://github.com/tiann/hapi.git
2929
origin → https://github.com/heavygee/hapi.git
3030
```
3131

32+
**Fork `main` mirror:** after upstream activity run `hapi-sync-fork-main` (in `scripts/tooling/`) and `git push origin main`. Primary checkout `~/coding/hapi` must contain **upstream product code + fork docs** — not docs-only drift. `hapi-driver-rebuild` refuses if `main` is behind `upstream/main`.
33+
3234
- Extend upstream; PR-sized slices; default path unchanged when new code off
3335
- **Never modify maintainer canon** in upstream PRs - see § Upstream file boundaries
3436
- **Upstream PR branches** start from `upstream/main` only - product code diffs, nothing fork-local

docs/tooling/README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Feature agents should **read the relevant doc below at session start**; meta bot
2727
| Doc | Purpose |
2828
|-----|---------|
2929
| [new-feature-intake.md](./new-feature-intake.md) | **Operator requests new behavior** — discovery, playback, soup vs clean demo, gates before dogfood, PR after approval |
30+
| `scripts/tooling/hapi-sync-fork-main.sh` | Keep `~/coding/hapi` `main` = upstream + fork docs |
3031
| [worktree-testing.md](./worktree-testing.md) | `hapi-active` symlink, `hapi-use-worktree`, service swing |
3132
| [driver-soup.md](./driver-soup.md) | Daily driver manifest, merge-train PR worktrees, garden vs soup |
3233
| [pr-review-loop.md](./pr-review-loop.md) | Pre-PR verification + cold review; pre-push open-PR gate; post-push PR comment poll |
@@ -103,7 +104,16 @@ cd web && bun run build # before hub UI test
103104

104105
Meta bot does not merge or declare "done" without evidence from these (or the repo's documented subset for a given change).
105106

106-
### 5. Documentation maintenance
107+
### 5. Fork `main` sync (mandatory cadence)
108+
109+
```bash
110+
hapi-sync-fork-main # after upstream merges
111+
hapi-sync-fork-main --check-only # before driver rebuild / intake (also enforced by hapi-driver-rebuild)
112+
```
113+
114+
Push `origin main` after sync. Fork `main` must stay **upstream/main + fork-only docs** — see [driver-soup.md](./driver-soup.md).
115+
116+
### 6. Documentation maintenance (tooling docs)
107117

108118
When tooling behavior changes, meta bot updates **in the same change**:
109119

@@ -113,7 +123,7 @@ When tooling behavior changes, meta bot updates **in the same change**:
113123

114124
Keep [pr-review-loop.md](./pr-review-loop.md) honest about IDE vs CLI hook support.
115125

116-
### 6. Worktree lifecycle
126+
### 7. Worktree lifecycle
117127

118128
After merge:
119129

docs/tooling/driver-manifest.example.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
base: upstream/main
88

99
layers:
10-
# Current daily-driver soup: pluggable voice + session list attention
10+
# Open upstream PRs / fork branches only (drop when merged to upstream/main)
1111
- branch: feat/pluggable-voice-backend
12-
- pr: 699
1312

1413
# Add unmerged PRs / branches below (top merges last):

docs/tooling/driver-soup.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,16 @@ hapi-use-driver # restarts hapi-hub + hapi-runner together
7979

8080
### When upstream moves
8181

82-
1. Edit manifest (drop merged PRs, add new ones)
83-
2. `hapi-driver-rebuild --build-web --verify`
84-
3. Garden smoke: `curl -sf http://127.0.0.1:3006/health` + quick VR/web check
85-
4. Log drift in `~/coding/hapi-garden/GARDEN_LOGBOOK.md` if API changed
82+
1. **Sync fork mirror:** `hapi-sync-fork-main` then `git push origin main`
83+
2. Edit manifest — drop layers merged to `upstream/main`
84+
3. `hapi-driver-rebuild --build-web --verify` (refuses if fork `main` is behind upstream)
85+
4. `hapi-use-driver` when ready
86+
5. Garden smoke: `curl -sf http://127.0.0.1:3006/health` + quick web/VR check
87+
6. Log drift in `~/coding/hapi-garden/GARDEN_LOGBOOK.md` if API changed
88+
89+
### Keeping fork `main` truthful
90+
91+
Fork `main` = **`upstream/main` + fork-only docs/plans**. After upstream merges: run `hapi-sync-fork-main`. Meta bot: weekly `--check-only` even if idle.
8692

8793
---
8894

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env bash
2+
# Rebuild ~/coding/hapi-driver from ~/.config/hapi/driver-manifest.yaml
3+
#
4+
# ~/coding/hapi-driver is READ-ONLY between rebuilds — this script is the only
5+
# supported way to change it. Hand-edits and cp-from-other-worktrees are forbidden.
6+
#
7+
# Usage:
8+
# hapi-driver-rebuild # rebuild only (no hub restart)
9+
# hapi-driver-rebuild --build-web # also rebuild web/dist
10+
# hapi-driver-rebuild --verify # run typecheck + test after merge
11+
# hapi-driver-rebuild --activate # swing hapi-active + restart hub (DESTRUCTIVE to live sessions)
12+
#
13+
set -euo pipefail
14+
15+
PRIMARY="${HAPI_PRIMARY:-$HOME/coding/hapi}"
16+
DRIVER="${HAPI_DRIVER:-$HOME/coding/hapi-driver}"
17+
MANIFEST="${HAPI_DRIVER_MANIFEST:-$HOME/.config/hapi/driver-manifest.yaml}"
18+
PARSE="$PRIMARY/scripts/tooling/parse-driver-manifest.mjs"
19+
DRIVER_BRANCH="${HAPI_DRIVER_BRANCH:-driver/integration}"
20+
BUN="${BUN:-$HOME/.bun/bin/bun}"
21+
22+
BUILD_WEB=0
23+
VERIFY=0
24+
ACTIVATE=0
25+
26+
while [[ $# -gt 0 ]]; do
27+
case "$1" in
28+
--build-web) BUILD_WEB=1; shift ;;
29+
--verify) VERIFY=1; shift ;;
30+
--activate) ACTIVATE=1; shift ;;
31+
-h|--help)
32+
sed -n '2,12p' "$0"
33+
exit 0
34+
;;
35+
*) echo "Unknown option: $1" >&2; exit 2 ;;
36+
esac
37+
done
38+
39+
if [[ ! -f "$MANIFEST" ]]; then
40+
echo "ERROR: manifest not found: $MANIFEST" >&2
41+
echo "Copy example: mkdir -p ~/.config/hapi && cp $PRIMARY/docs/tooling/driver-manifest.example.yaml $MANIFEST" >&2
42+
exit 1
43+
fi
44+
45+
if [[ ! -x "$PARSE" ]] && [[ ! -f "$PARSE" ]]; then
46+
echo "ERROR: parser missing: $PARSE" >&2
47+
exit 1
48+
fi
49+
50+
mkdir -p "$(dirname "$MANIFEST")"
51+
52+
echo "Fetching upstream..."
53+
git -C "$PRIMARY" fetch upstream
54+
55+
SYNC_SCRIPT="$PRIMARY/scripts/tooling/hapi-sync-fork-main.sh"
56+
if [[ -x "$SYNC_SCRIPT" ]]; then
57+
if ! "$SYNC_SCRIPT" --check-only 2>/dev/null; then
58+
echo "ERROR: fork main is behind upstream/main. Run: hapi-sync-fork-main && git push origin main" >&2
59+
exit 1
60+
fi
61+
elif git -C "$PRIMARY" show-ref --verify --quiet refs/heads/main; then
62+
behind_main="$(git -C "$PRIMARY" rev-list --count main..upstream/main 2>/dev/null || echo 0)"
63+
if [[ "${behind_main:-0}" -gt 0 ]]; then
64+
echo "ERROR: $PRIMARY main is ${behind_main} commit(s) behind upstream/main" >&2
65+
echo " Run: hapi-sync-fork-main" >&2
66+
exit 1
67+
fi
68+
fi
69+
70+
if [[ -d "$DRIVER" ]] && [[ -n "$(git -C "$DRIVER" status --porcelain)" ]]; then
71+
echo "WARNING: $DRIVER has local changes — rebuild will reset the tree (stash or commit them elsewhere first)." >&2
72+
echo " Only manifest-driven rebuilds belong on the driver tree. See docs/tooling/driver-soup.md" >&2
73+
fi
74+
75+
if [[ ! -d "$DRIVER" ]]; then
76+
echo "Creating driver worktree at $DRIVER (branch $DRIVER_BRANCH)..."
77+
git -C "$PRIMARY" worktree add -b "$DRIVER_BRANCH" "$DRIVER" upstream/main
78+
fi
79+
80+
if [[ ! -e "$DRIVER/hub/.env" ]]; then
81+
echo "Linking hub/.env → ~/.hapi/hub.env"
82+
ln -s "$HOME/.hapi/hub.env" "$DRIVER/hub/.env"
83+
fi
84+
85+
manifest_json="$("$BUN" run "$PARSE" "$MANIFEST")"
86+
base_ref="$(echo "$manifest_json" | jq -r '.base')"
87+
layer_count="$(echo "$manifest_json" | jq '.layers | length')"
88+
89+
if echo "$manifest_json" | jq -e '.layers[] | select(.ref == "fix/web-scroll-guard-unwrap-race")' >/dev/null; then
90+
pr722_state="$(gh pr view 722 --repo "${HAPI_PR_REPO:-tiann/hapi}" --json state --jq '.state' 2>/dev/null || true)"
91+
if [[ "$pr722_state" == "MERGED" ]]; then
92+
echo "WARNING: upstream PR #722 is merged — drop fix/web-scroll-guard-unwrap-race from $MANIFEST" >&2
93+
fi
94+
fi
95+
96+
if [[ -n "$upstream_tip" && "$base_ref" == "upstream/main" ]]; then
97+
echo "Base: upstream/main @ $(git -C "$PRIMARY" log -1 --oneline "$upstream_tip")"
98+
fi
99+
100+
echo "Resetting $DRIVER to $base_ref ($layer_count layer(s))..."
101+
git -C "$DRIVER" checkout -B "$DRIVER_BRANCH" "$base_ref"
102+
103+
resolve_merge_ref() {
104+
local type="$1" ref="$2"
105+
case "$type" in
106+
branch|integrate)
107+
if git -C "$PRIMARY" rev-parse --verify "${ref}^{commit}" >/dev/null 2>&1; then
108+
echo "$ref"
109+
else
110+
echo "ERROR: layer ref not found: $ref" >&2
111+
exit 1
112+
fi
113+
;;
114+
pr)
115+
local head_branch pr_repo="${HAPI_PR_REPO:-tiann/hapi}"
116+
head_branch="$(gh pr view "$ref" --repo "$pr_repo" --json headRefName --jq '.headRefName' 2>/dev/null || true)"
117+
if [[ -z "$head_branch" || "$head_branch" == "null" ]]; then
118+
echo "ERROR: could not resolve PR #$ref via gh (repo: $pr_repo)" >&2
119+
exit 1
120+
fi
121+
git -C "$PRIMARY" fetch origin "$head_branch" 2>/dev/null || true
122+
echo "origin/$head_branch"
123+
;;
124+
*)
125+
echo "ERROR: unknown layer type: $type" >&2
126+
exit 1
127+
;;
128+
esac
129+
}
130+
131+
for i in $(seq 0 $((layer_count - 1))); do
132+
type="$(echo "$manifest_json" | jq -r ".layers[$i].type")"
133+
ref="$(echo "$manifest_json" | jq -r ".layers[$i].ref")"
134+
merge_ref="$(resolve_merge_ref "$type" "$ref")"
135+
136+
echo "Layer $((i + 1))/$layer_count: merging $merge_ref ..."
137+
if ! git -C "$DRIVER" merge --no-edit "$merge_ref"; then
138+
echo "ERROR: merge conflict merging $merge_ref into $DRIVER_BRANCH" >&2
139+
echo "Resolve in $DRIVER, commit, or fix manifest order." >&2
140+
exit 1
141+
fi
142+
done
143+
144+
echo "Driver HEAD: $(git -C "$DRIVER" log -1 --oneline)"
145+
146+
if [[ "$BUILD_WEB" -eq 1 ]] || [[ ! -f "$DRIVER/web/dist/index.html" ]]; then
147+
echo "Building web..."
148+
if [[ ! -d "$DRIVER/node_modules" ]]; then
149+
echo "Installing dependencies (first driver build)..."
150+
(cd "$DRIVER" && "$BUN" install)
151+
fi
152+
(cd "$DRIVER/web" && "$BUN" run build)
153+
fi
154+
155+
if [[ "$VERIFY" -eq 1 ]]; then
156+
echo "Running typecheck..."
157+
(cd "$DRIVER" && "$BUN" typecheck)
158+
echo "Running tests..."
159+
(cd "$DRIVER" && "$BUN" run test)
160+
fi
161+
162+
echo ""
163+
echo "Driver rebuild complete: $DRIVER @ $(git -C "$DRIVER" rev-parse --short HEAD)"
164+
echo "Manifest: $MANIFEST"
165+
echo "Active hub: $(readlink -f "$HOME/coding/hapi-active" 2>/dev/null || echo '(no symlink)')"
166+
167+
if [[ "$ACTIVATE" -eq 1 ]]; then
168+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
169+
echo " ACTIVATE: restarts hapi-hub + hapi-runner (kills sessions)"
170+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
171+
if [[ -t 0 ]]; then
172+
read -rp "Proceed with hapi-use-worktree $DRIVER? [y/N] " yn
173+
[[ "${yn,,}" == "y" ]] || { echo "Skipped activate."; exit 0; }
174+
else
175+
echo "Non-interactive: skipping activate (re-run with TTY or use hapi-use-worktree manually)." >&2
176+
exit 0
177+
fi
178+
exec hapi-use-worktree "$DRIVER"
179+
fi
180+
181+
echo ""
182+
echo "To swing live hub (restarts service — kills sessions):"
183+
echo " hapi-use-worktree $DRIVER"
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env bash
2+
# Sync fork main with upstream/main, keeping fork-only commits on top.
3+
#
4+
# Fork main should always be: upstream/main + operator docs/plans (never product-only drift).
5+
#
6+
# Usage:
7+
# hapi-sync-fork-main # fetch + merge if behind
8+
# hapi-sync-fork-main --check-only # exit 1 if primary main is behind upstream
9+
# hapi-sync-fork-main --rebase # rebase fork-only commits onto upstream (linear history)
10+
#
11+
set -euo pipefail
12+
13+
PRIMARY="${HAPI_PRIMARY:-$HOME/coding/hapi}"
14+
UPSTREAM_REF="${HAPI_UPSTREAM_REF:-upstream/main}"
15+
MAIN_BRANCH="${HAPI_MAIN_BRANCH:-main}"
16+
17+
CHECK_ONLY=0
18+
USE_REBASE=0
19+
20+
while [[ $# -gt 0 ]]; do
21+
case "$1" in
22+
--check-only) CHECK_ONLY=1; shift ;;
23+
--rebase) USE_REBASE=1; shift ;;
24+
-h|--help)
25+
sed -n '2,12p' "$0"
26+
exit 0
27+
;;
28+
*) echo "Unknown option: $1" >&2; exit 2 ;;
29+
esac
30+
done
31+
32+
if [[ ! -d "$PRIMARY/.git" ]]; then
33+
echo "ERROR: not a git repo: $PRIMARY" >&2
34+
exit 1
35+
fi
36+
37+
echo "Fetching upstream..."
38+
git -C "$PRIMARY" fetch upstream
39+
40+
if ! git -C "$PRIMARY" show-ref --verify --quiet "refs/remotes/$UPSTREAM_REF"; then
41+
echo "ERROR: missing $UPSTREAM_REF" >&2
42+
exit 1
43+
fi
44+
45+
behind="$(git -C "$PRIMARY" rev-list --count "$MAIN_BRANCH..$UPSTREAM_REF" 2>/dev/null || echo 0)"
46+
ahead="$(git -C "$PRIMARY" rev-list --count "$UPSTREAM_REF..$MAIN_BRANCH" 2>/dev/null || echo 0)"
47+
48+
echo "fork $MAIN_BRANCH: ${ahead} ahead, ${behind} behind $UPSTREAM_REF"
49+
50+
if [[ "$behind" -eq 0 ]]; then
51+
echo "OK: $PRIMARY $MAIN_BRANCH is up to date with $UPSTREAM_REF"
52+
exit 0
53+
fi
54+
55+
if [[ "$CHECK_ONLY" -eq 1 ]]; then
56+
echo "FAIL: $PRIMARY $MAIN_BRANCH is $behind commit(s) behind $UPSTREAM_REF" >&2
57+
echo "Run: hapi-sync-fork-main" >&2
58+
exit 1
59+
fi
60+
61+
if [[ -n "$(git -C "$PRIMARY" status --porcelain)" ]]; then
62+
echo "ERROR: $PRIMARY has uncommitted changes — stash or commit before sync" >&2
63+
git -C "$PRIMARY" status --short | head -20
64+
exit 1
65+
fi
66+
67+
git -C "$PRIMARY" checkout "$MAIN_BRANCH"
68+
69+
if [[ "$USE_REBASE" -eq 1 ]]; then
70+
echo "Rebasing $MAIN_BRANCH onto $UPSTREAM_REF..."
71+
git -C "$PRIMARY" rebase "$UPSTREAM_REF"
72+
else
73+
echo "Merging $UPSTREAM_REF into $MAIN_BRANCH..."
74+
git -C "$PRIMARY" merge "$UPSTREAM_REF" -m "merge(upstream): sync tiann/hapi main into fork main ($(git -C "$PRIMARY" rev-parse --short "$UPSTREAM_REF"))"
75+
fi
76+
77+
echo ""
78+
echo "Synced: $(git -C "$PRIMARY" log -1 --oneline)"
79+
echo "Next: review ~/.config/hapi/driver-manifest.yaml — drop layers now on upstream/main"
80+
echo " hapi-driver-rebuild --build-web --verify (if soup layers changed)"

0 commit comments

Comments
 (0)