From 4b51ba146dc56385d029480581b59e2f5ef4ec64 Mon Sep 17 00:00:00 2001 From: Shay Date: Sat, 20 Jun 2026 19:58:33 -0700 Subject: [PATCH] chore(dev): enforce fresh-base startup guard for CORE agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add scripts/agent_startup.sh — a sourced bash guard that prevents stale-base worktrees from silently replaying or creating conflict PRs. Root cause: PR #844 was opened from a worktree whose HEAD was behind origin/main. The resulting replay produced a conflict-laden PR that required manual reconciliation (see PR #847). ## Behaviour Default (new implementation task): HEAD must equal origin/main Working tree must be clean → source scripts/agent_startup.sh PR-resume task: CODEX_ALLOW_NON_MAIN_BASE=1 source scripts/agent_startup.sh origin/main must be a strict ancestor of HEAD (enforced via merge-base) Debug-only dirty override: CODEX_ALLOW_DIRTY=1 source scripts/agent_startup.sh ## Steps performed by the script 1. git fetch origin --prune 2. Print HEAD / branch / origin/main / merge-base 3. Fail if dirty (unless CODEX_ALLOW_DIRTY=1) 4. Fail if HEAD != origin/main (unless CODEX_ALLOW_NON_MAIN_BASE=1) 5. In resume mode, verify origin/main is ancestor of HEAD via merge-base 6. Print diff --name-status vs origin/main 7. Verify uv exists 8. uv sync --frozen only when uv.lock is present 9. Print final git status ## Escape hatches CODEX_ALLOW_NON_MAIN_BASE=1 PR-resume, ancestry enforced CODEX_ALLOW_DIRTY=1 Debug/WIP sessions, documented in session log ## Validation bash -n scripts/agent_startup.sh → OK git diff --check → OK (no trailing whitespace) shellcheck not available locally; shebang is bash, bash -n passed ## Non-goals Does not touch runtime, evals, reports, teaching proposals, packs, policy, identity, recall, vault, field, algebra, or serving paths. --- .github/copilot-instructions.md | 9 +- GPT55.md | 11 +- GROK.md | 12 ++- scripts/agent_startup.sh | 178 ++++++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 5 deletions(-) create mode 100755 scripts/agent_startup.sh diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 68826d88..3b0b6b60 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -112,7 +112,14 @@ Learning is reviewed mutation: ## Validation -Use CLI suites: +Before starting any task, run the startup guard to ensure a fresh base: + +```bash +source scripts/agent_startup.sh +# For PR-resume tasks: CODEX_ALLOW_NON_MAIN_BASE=1 source scripts/agent_startup.sh +``` + +Then use CLI suites to validate your work: ```bash core test --suite smoke -q diff --git a/GPT55.md b/GPT55.md index f1842906..86b9926f 100644 --- a/GPT55.md +++ b/GPT55.md @@ -15,13 +15,18 @@ generic chatbot, not an infrastructure playground. 1. **Read this file in full.** 2. **Read `AGENTS.md` in full.** 3. **Read `docs/runtime_contracts.md` in full.** -4. **Run the smoke suite:** +4. **Run the startup guard** — enforces fresh-base and clean-tree invariants: + ```bash + source scripts/agent_startup.sh + ``` + For a PR-resume task: `CODEX_ALLOW_NON_MAIN_BASE=1 source scripts/agent_startup.sh` +5. **Run the smoke suite:** ```bash core test --suite smoke -q ``` -5. **Check for a handoff doc** — read the most recent `HANDOFF-*.md` if one +6. **Check for a handoff doc** — read the most recent `HANDOFF-*.md` if one exists dated within the last 3 days. -6. **State your task scope** — before editing, name the module(s) and the +7. **State your task scope** — before editing, name the module(s) and the invariant you will prove was not violated. --- diff --git a/GROK.md b/GROK.md index 46fa263c..2046535d 100644 --- a/GROK.md +++ b/GROK.md @@ -104,7 +104,16 @@ Never use `git reset --hard`, broad `git checkout .`, broad `git restore .`, `gi ### 2. Establish a clean, current baseline -After the tree is clean or unknown work is safely stashed: +**Run the startup guard first** — it automates steps 2–4 and will hard-stop if the worktree is stale: + +```bash +source scripts/agent_startup.sh +``` + +For a new task (default, no env vars) the script requires `HEAD == origin/main` and a clean tree. +For a PR-resume task, set `CODEX_ALLOW_NON_MAIN_BASE=1`; the script then verifies `origin/main` is a strict ancestor of `HEAD`. + +If you cannot source the script, perform the equivalent steps manually: ```bash git fetch origin --prune @@ -115,6 +124,7 @@ git status --short --branch If `main` cannot fast-forward, stop and report the exact state. Do not merge, rebase, or resolve conflicts unless explicitly instructed. + ### 3. Prefer a new worktree for non-trivial implementation For non-trivial runtime, reasoning, eval, teaching, pack, or multi-file work, create a fresh worktree from current `origin/main`: diff --git a/scripts/agent_startup.sh b/scripts/agent_startup.sh new file mode 100755 index 00000000..241fc98e --- /dev/null +++ b/scripts/agent_startup.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# ============================================================================= +# scripts/agent_startup.sh +# +# Codex / Gemini environment startup guard for CORE. +# +# PURPOSE +# Catch stale-base worktrees before they create replay/conflict PRs. +# Every new implementation task must run from HEAD == origin/main on a +# clean tree. PR-resume tasks may run from a branch, but only when +# origin/main is a proper ancestor of HEAD. +# +# USAGE +# source scripts/agent_startup.sh # default (strict) +# CODEX_ALLOW_DIRTY=1 source ... # allow dirty worktree (debug only) +# CODEX_ALLOW_NON_MAIN_BASE=1 source ... # allow HEAD ahead of origin/main +# # (PR-resume; ancestry still checked) +# +# EXIT CODES +# 0 All checks passed — safe to proceed. +# 1 Guard failed — do NOT start the task. +# +# ENVIRONMENT CONTROLS +# CODEX_ALLOW_DIRTY=1 Bypass dirty-tree check (debug/wip only). +# CODEX_ALLOW_NON_MAIN_BASE=1 Allow HEAD ahead of origin/main, but only +# when origin/main is a strict ancestor of HEAD. +# +# NON-GOALS +# This script does NOT touch runtime, evals, reports, teaching proposals, +# packs, policy, identity, recall, vault, field, algebra, or serving paths. +# ============================================================================= + +set -euo pipefail + +# ── helpers ────────────────────────────────────────────────────────────────── + +_PASS="✓" +_FAIL="✗" +_WARN="!" + +_ok() { printf '%s %s\n' "$_PASS" "$*"; } +_fail() { printf '%s %s\n' "$_FAIL" "$*" >&2; } +_warn() { printf '%s %s\n' "$_WARN" "$*" >&2; } +_sep() { printf '%s\n' "────────────────────────────────────────────────────────────────────────"; } + +_die() { + _fail "$*" + _fail "Aborting. Fix the issue above, then re-run this script." + # Use 'return' rather than 'exit' so sourced scripts don't kill the shell. + return 1 +} + +# ── header ─────────────────────────────────────────────────────────────────── + +_sep +printf 'CORE agent startup guard | %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" +_sep + +# ── 0. repository root ──────────────────────────────────────────────────────── + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || { + _die "Not inside a git repository." +} +_ok "repo root: $REPO_ROOT" + +# ── 1. fetch origin --prune ─────────────────────────────────────────────────── + +printf 'Fetching origin --prune …\n' +if git fetch origin --prune --quiet; then + _ok "fetch origin --prune" +else + _die "git fetch origin --prune failed. Check network / remote config." +fi + +# ── 2. diagnostic: HEAD, branch, origin/main, merge-base ────────────────────── + +HEAD_SHA="$(git rev-parse HEAD)" +BRANCH="$(git rev-parse --abbrev-ref HEAD)" +ORIGIN_MAIN_SHA="$(git rev-parse origin/main 2>/dev/null)" || { + _die "origin/main does not exist. Ensure origin is correctly configured." +} +MERGE_BASE="$(git merge-base HEAD origin/main 2>/dev/null)" || { + _die "Could not compute merge-base between HEAD and origin/main." +} + +printf '\n' +printf ' HEAD : %s\n' "$HEAD_SHA" +printf ' branch : %s\n' "$BRANCH" +printf ' origin/main : %s\n' "$ORIGIN_MAIN_SHA" +printf ' merge-base : %s\n' "$MERGE_BASE" +printf '\n' + +# ── 3. dirty-tree check ──────────────────────────────────────────────────────── + +DIRTY="$(git status --porcelain)" +if [ -n "$DIRTY" ]; then + if [ "${CODEX_ALLOW_DIRTY:-0}" = "1" ]; then + _warn "Dirty worktree detected (CODEX_ALLOW_DIRTY=1, continuing in debug mode)." + else + _fail "Dirty worktree detected. Uncommitted changes are present:" + printf '%s\n' "$DIRTY" >&2 + _die "Set CODEX_ALLOW_DIRTY=1 only for explicit debug sessions. For a new task, start from a clean tree." + fi +else + _ok "working tree is clean" +fi + +# ── 4. base guard: HEAD == origin/main (default) or ancestry check (resume) ─── + +if [ "$HEAD_SHA" = "$ORIGIN_MAIN_SHA" ]; then + _ok "HEAD == origin/main (fresh base, new task allowed)" +else + if [ "${CODEX_ALLOW_NON_MAIN_BASE:-0}" = "1" ]; then + # PR-resume mode: origin/main must be an ancestor of HEAD. + if [ "$MERGE_BASE" = "$ORIGIN_MAIN_SHA" ]; then + _ok "CODEX_ALLOW_NON_MAIN_BASE=1 — origin/main is ancestor of HEAD (PR-resume allowed)" + else + _fail "CODEX_ALLOW_NON_MAIN_BASE=1 is set, but origin/main is NOT an ancestor of HEAD." + _fail " HEAD : $HEAD_SHA" + _fail " origin/main : $ORIGIN_MAIN_SHA" + _fail " merge-base : $MERGE_BASE" + _die "Your branch has diverged from origin/main. Rebase onto origin/main before resuming." + fi + else + _fail "HEAD is not origin/main." + _fail " HEAD : $HEAD_SHA (branch: $BRANCH)" + _fail " origin/main : $ORIGIN_MAIN_SHA" + _fail "" + _fail "For a new implementation task, the worktree must start from origin/main." + _fail "If you are resuming a PR, set CODEX_ALLOW_NON_MAIN_BASE=1 and ensure" + _fail "origin/main is an ancestor of your branch." + _die "Stale base detected. This is the condition that caused #844 conflict PRs." + fi +fi + +# ── 5. changed files vs origin/main ─────────────────────────────────────────── + +printf '\nChanged files vs origin/main:\n' +DIFF_FILES="$(git diff --name-status origin/main HEAD 2>/dev/null)" +if [ -z "$DIFF_FILES" ]; then + printf ' (none — HEAD is origin/main)\n' +else + printf '%s\n' "$DIFF_FILES" | sed 's/^/ /' +fi +printf '\n' + +# ── 6. uv availability ──────────────────────────────────────────────────────── + +if command -v uv >/dev/null 2>&1; then + UV_VERSION="$(uv --version 2>/dev/null || echo 'unknown')" + _ok "uv found: $UV_VERSION" +else + _die "uv is not installed or not in PATH. Install uv before proceeding: https://docs.astral.sh/uv/" +fi + +# ── 7. uv sync --frozen (only when uv.lock exists) ──────────────────────────── + +LOCKFILE="$REPO_ROOT/uv.lock" +if [ -f "$LOCKFILE" ]; then + printf 'Running uv sync --frozen …\n' + if uv sync --frozen; then + _ok "uv sync --frozen succeeded" + else + _die "uv sync --frozen failed. The lock file may be out of date or the environment is broken." + fi +else + _warn "uv.lock not found — skipping uv sync (expected for projects without a lock file)" +fi + +# ── 8. final git status ──────────────────────────────────────────────────────── + +_sep +printf 'Final git status:\n' +git status --short --branch +_sep + +_ok "Startup guard passed — safe to begin task." +printf '\n'