diff --git a/AGENTS.md b/AGENTS.md index deba71272..882ad02eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -178,6 +178,7 @@ When the user pivots mid-session, the default failure mode is piling unrelated w 3. `git switch main && git pull && git switch -c feat/` before touching any new code. - **When the new ask genuinely builds on unmerged code**, stack it: `git switch -c feat/b feat/a` off the existing feature branch and open PR #2 targeting `feat/a` (not `main`). Rebase PR #2 onto `main` once PR #1 merges. - **Never `git stash`.** Stashes are invisible, easy to lose, and collide across agents. If you need to pivot without finishing, commit WIP to the current branch (`git add -A && git commit -m "wip"`) and squash later. WIP commits are visible, pushable, recoverable. +- **`~/Code/lobu` is read-only for agents.** All writes — commits, branch creation, submodule bumps, even one-line build fixes — go through a `make task-setup NAME=` worktree. For the trivial "advance a submodule pointer" case, `make bump SUBMODULE= [TARGET=]` is the lightweight shortcut (skips bun install, .env copy, port allocation). The main checkout staying on `main` is the invariant that lets other agents `git worktree add` cleanly — leaving it on `chore/some-fix` silently breaks every parallel agent's `task-setup`. - **Per-agent isolation:** when launching a parallel Claude Code session, use `claude --worktree ` so each agent gets its own checkout + branch. No shared working dir = no cross-agent collisions. - **Subagent isolation (mandatory):** any spawned subagent that may `git switch`, commit, push, or run a destructive command MUST run with `isolation: "worktree"`. Read-only research/exploration agents may share the parent checkout. If unsure, use a worktree — the cost is a temp checkout, the cost of skipping is overwriting the user's working tree. - **Cross-repo dispatch:** owletto changes go through a `make task-setup NAME=` worktree, which fetches a fresh owletto checkout under `.claude/worktrees//packages/owletto` on a real branch (not a detached submodule SHA). The submodule worktree inherits the parent's `.git` and pushes to the wrong remote; an isolation worktree of lobu that needs to edit owletto code ends up with `origin = lobu-ai/owletto` and can't push to lobu. After an owletto PR merges, bump the submodule pointer in lobu in a separate small PR. diff --git a/Makefile b/Makefile index 1a1a4a607..36b4be067 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Development Makefile for Lobu -.PHONY: help setup build test clean dev build-packages ensure-submodule clean-workers test-unit test-integration test-e2e typecheck task-setup task-clean task-use +.PHONY: help setup build test clean dev build-packages ensure-submodule clean-workers test-unit test-integration test-e2e typecheck task-setup task-clean task-use bump # Default target help: @@ -17,6 +17,7 @@ help: @echo " make task-setup NAME= - Create a paired worktree at .claude/worktrees/ (lobu + submodule on real branch, .env copied, ports auto-assigned, Lobu context registered)" @echo " make task-clean NAME= [FORCE=1] - Remove the worktree, both branches, and the Lobu context (refuses if there's uncommitted/unpushed work unless FORCE=1)" @echo " make task-use NAME= - Point Chrome ext / Mac app symlinks at this worktree (or 'main' for the canonical checkout)" + @echo " make bump SUBMODULE= [TARGET=] - Lightweight worktree + commit + PR for a trivial submodule pointer bump (skips bun install, .env, ports)" # Strict typecheck — mirrors the Dockerfile so local matches CI. Catches # what `build-packages` (relaxed, bundler-only) misses. @@ -85,6 +86,14 @@ task-use: @: $${NAME?Usage: make task-use NAME=} @./scripts/task-use.sh "$(NAME)" +# Lightweight shortcut for "trivial submodule pointer bump" work. Creates a +# minimal worktree (no bun install, no .env copy, no port allocation), advances +# the submodule, opens an auto-merge PR. For agent work that also touches +# submodule *code*, use `make task-setup` instead — it sets up the full env. +bump: + @: $${SUBMODULE?Usage: make bump SUBMODULE= [TARGET=] [NAME=]} + @NAME="$(NAME)" ./scripts/bump-submodule.sh "$(SUBMODULE)" "$(TARGET)" + # --- Test pipelines --------------------------------------------------------- # These mirror what CI runs (.github/workflows/ci.yml) so a passing local run # is a strong signal CI will pass. diff --git a/scripts/bump-submodule.sh b/scripts/bump-submodule.sh new file mode 100755 index 000000000..4bb76e70f --- /dev/null +++ b/scripts/bump-submodule.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# bump-submodule.sh — open a one-line submodule pointer PR via a lightweight worktree. +# +# Usage: +# make bump SUBMODULE=packages/owletto # bump to origin/main +# make bump SUBMODULE=packages/owletto TARGET=abc123def # bump to specific SHA / ref +# make bump SUBMODULE=packages/owletto NAME=cancel-button # custom slug +# +# This is the cheap shortcut for the trivial "bump a submodule pointer" case. +# For agent work that also touches submodule code, use `make task-setup` instead, +# which sets up the full dev environment (ports, .env, bun install, etc). +# +# What this script DOES NOT do that task-setup does: +# - bun install (no dev server runs here) +# - .env copy (no secrets needed for a pointer commit) +# - port allocation (no listener) +# - Lobu CLI context (nothing to talk to) +# +# It just creates a worktree off origin/main, advances the submodule pointer, +# commits, pushes, and opens an auto-merge PR. +# +# Why this exists: the convention is `~/Code/lobu` is read-only for agents +# (see AGENTS.md "Scope discipline"). Without this shortcut, agents reach +# for the main checkout to do "trivial" bumps because task-setup feels +# heavyweight for a one-line change. This makes worktree the easy path. +set -euo pipefail + +SUBMODULE=${1:?Usage: bump-submodule.sh [target-sha-or-ref]} +TARGET=${2:-origin/main} + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +# Verify SUBMODULE is actually a configured submodule +if ! grep -qE "^\s*path\s*=\s*${SUBMODULE//\//\\/}$" .gitmodules 2>/dev/null; then + echo "error: '$SUBMODULE' is not a configured submodule (no matching path= entry in .gitmodules)" >&2 + echo "available submodules:" >&2 + grep -E '^\s*path\s*=' .gitmodules | awk -F= '{print " " $2}' >&2 + exit 1 +fi + +NAME=${NAME:-"$(basename "$SUBMODULE")-$(date +%Y%m%d-%H%M%S)"} +SLUG="bump-${NAME}" +BRANCH="chore/${SLUG}" +WT="$REPO_ROOT/.claude/worktrees/$SLUG" + +if [[ -e "$WT" ]]; then + echo "error: worktree $WT already exists; pick a different NAME or run: make task-clean NAME=$SLUG FORCE=1" >&2 + exit 1 +fi + +echo "→ fetching latest main" +git fetch origin main --quiet + +echo "→ creating worktree at $WT on branch $BRANCH" +git worktree add "$WT" -b "$BRANCH" origin/main >/dev/null +# Drop the same .task marker task-setup uses so `git worktree list` can tell +# this apart from agent-* isolation worktrees, and so task-clean knows where to look. +touch "$WT/.task" + +echo "→ initializing $SUBMODULE in the new worktree" +git -C "$WT" submodule update --init -- "$SUBMODULE" >/dev/null + +echo "→ resolving $TARGET in $SUBMODULE" +git -C "$WT/$SUBMODULE" fetch origin --quiet +# Cleanup helper: remove the worktree AND its branch ref so a re-run with the +# same NAME doesn't trip the "worktree already exists" or "branch already exists" +# guard. `git worktree remove` only removes the tree, not the branch. +cleanup_worktree() { + git worktree remove "$WT" --force 2>/dev/null || true + git branch -D "$BRANCH" 2>/dev/null || true +} + +if ! TARGET_SHA=$(git -C "$WT/$SUBMODULE" rev-parse "$TARGET" 2>/dev/null); then + echo "error: can't resolve $TARGET inside $SUBMODULE" >&2 + cleanup_worktree + exit 1 +fi +BEFORE_SHA=$(git -C "$WT/$SUBMODULE" rev-parse HEAD) +if [[ "$BEFORE_SHA" == "$TARGET_SHA" ]]; then + echo "→ $SUBMODULE is already at $TARGET ($BEFORE_SHA); nothing to bump" + cleanup_worktree + exit 0 +fi + +SHORT_BEFORE=$(git -C "$WT/$SUBMODULE" rev-parse --short "$BEFORE_SHA") +SHORT_AFTER=$(git -C "$WT/$SUBMODULE" rev-parse --short "$TARGET_SHA") +TARGET_SUBJECT=$(git -C "$WT/$SUBMODULE" log -1 --format='%s' "$TARGET_SHA") + +echo "→ advancing $SUBMODULE: $SHORT_BEFORE → $SHORT_AFTER" +git -C "$WT/$SUBMODULE" checkout --detach "$TARGET_SHA" >/dev/null 2>&1 + +echo "→ committing pointer bump" +git -C "$WT" add "$SUBMODULE" +git -C "$WT" commit -m "chore: bump $SUBMODULE pointer to $SHORT_AFTER + +Picks up: $TARGET_SUBJECT + +Before: $SHORT_BEFORE +After: $SHORT_AFTER" >/dev/null + +echo "→ pushing $BRANCH" +git -C "$WT" push -u origin "$BRANCH" --quiet + +if command -v gh >/dev/null 2>&1; then + echo "→ opening PR" + PR_URL=$(gh pr create --base main --head "$BRANCH" \ + --title "chore: bump $SUBMODULE pointer to $SHORT_AFTER" \ + --body "Bumps \`$SUBMODULE\` pointer. + +\`\`\` +Before: $SHORT_BEFORE +After: $SHORT_AFTER +\`\`\` + +Picks up: $TARGET_SUBJECT" 2>&1 | tail -1) + echo " $PR_URL" + echo "→ enabling auto-merge (squash)" + gh pr merge "$PR_URL" --auto --squash >/dev/null 2>&1 || \ + echo " (auto-merge enable failed — admin-merge with: gh pr merge $PR_URL --squash --admin)" +else + echo "→ gh CLI not available; open the PR manually for branch $BRANCH" +fi + +echo +echo "✓ done. After the PR merges, clean up:" +echo " make task-clean NAME=$SLUG FORCE=1"