From d7091395134a3bbecf688db0329b9fe8dd4495b7 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 21 May 2026 09:37:00 -0700 Subject: [PATCH] fix(retro): stale-base + bad-today-anchor pre-flight guard (#1624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /retro silently produced confidently-wrong output when "today" drifted (model session-context error) or when origin/ was materially behind the actual remote — git log --since returned zero or near-zero commits and the narrative was fabricated from nothing. Adds Step 0.5 with four ordered pre-check branches before any window analysis: A. No 'origin' remote → skip with "base freshness not verified" note B. Detached HEAD → skip with "base freshness not verified" note C. `git fetch origin ` fails (offline) → warn, proceed against last-known origin/ D. Fetch succeeded → compare today vs latest origin/ commit; if gap > window-days, BLOCK with explicit citation of latest-commit date. Skip paths still proceed to Step 1, but the disclosure is carried into the retro narrative ("offline run, window not freshness-verified") so the output is never silently confidently-wrong. Atomic .tmpl + gen:skill-docs regen commit (T-Codex-3 pattern). Co-Authored-By: Claude Opus 4.7 (1M context) --- retro/SKILL.md | 57 +++++++++++++++++++++++++++++++++++++++++++++ retro/SKILL.md.tmpl | 57 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/retro/SKILL.md b/retro/SKILL.md index 92d58f7b8..f75097669 100644 --- a/retro/SKILL.md +++ b/retro/SKILL.md @@ -888,6 +888,63 @@ Check for non-git context that should be included in the retro: If `RETRO_CONTEXT_FOUND`: read `~/.gstack/retro-context.md`. This file is user-authored and may contain meeting notes, calendar events, decisions, and other context that doesn't appear in git history. Incorporate this context into the retro narrative where relevant. +### Step 0.5: Stale-base + bad-today-anchor pre-flight guard + +The retro skill computes a window from "today" and queries `git log --since= origin/`. If "today" drifts (model session-context error) or the local worktree's `origin/` is materially behind the actual remote, the window can return zero or near-zero commits and the retro will fabricate a coherent-looking narrative from nothing. This guard prevents silent confidently-wrong output. + +Run the pre-flight in this exact order. The first branch that matches wins: + +```bash +# Pre-check A: no remote configured? +_RETRO_HAS_REMOTE=$(git remote 2>/dev/null | grep -c '^origin$' || echo 0) +if [ "$_RETRO_HAS_REMOTE" = "0" ]; then + echo "RETRO_GUARD: no 'origin' remote, base freshness not verified — proceeding" + _RETRO_GUARD_VERDICT="skip-no-remote" +fi + +# Pre-check B: detached HEAD or no current base? +if [ -z "$_RETRO_GUARD_VERDICT" ]; then + _RETRO_HEAD_REF=$(git symbolic-ref --quiet HEAD 2>/dev/null || echo "") + if [ -z "$_RETRO_HEAD_REF" ]; then + echo "RETRO_GUARD: detached HEAD, base freshness not verified — proceeding" + _RETRO_GUARD_VERDICT="skip-detached" + fi +fi + +# Pre-check C: fetch origin ; if it fails, warn but proceed. +if [ -z "$_RETRO_GUARD_VERDICT" ]; then + if ! git fetch origin --quiet 2>/dev/null; then + echo "RETRO_GUARD: 'git fetch origin ' failed (offline?) — proceeding against last-known origin/" + _RETRO_GUARD_VERDICT="warn-fetch-failed" + fi +fi + +# Pre-check D: BLOCK only when fetch succeeded AND the latest origin/ +# commit predates the retro window. Today's date should be loaded from the +# user-visible "## currentDate" tag in the session reminder; if the gap between +# origin/'s newest commit and today exceeds the window, the model's +# "today" is almost certainly stale (or the worktree is wildly behind). +if [ -z "$_RETRO_GUARD_VERDICT" ]; then + _RETRO_LATEST_ISO=$(git log -1 --format=%ci origin/ 2>/dev/null | awk '{print $1}') + if [ -n "$_RETRO_LATEST_ISO" ]; then + # The model computes today from the session reminder (NEVER from `date` — + # the system clock can be hours off in containerized harnesses). + # Compute window in DAYS (default 7): if today - latest-commit-date > window-days, + # BLOCK. If the model cannot reliably compute "today", it MUST stop here and + # ask the user via AskUserQuestion rather than proceeding. + echo "RETRO_GUARD: latest origin/ commit on $_RETRO_LATEST_ISO" + _RETRO_GUARD_VERDICT="check-gap" + fi +fi +``` + +After running the bash block, the model evaluates `RETRO_GUARD: latest origin/ commit on ` against today and the window: + +- If the **latest-commit date is older than (today − window-days)**, BLOCK with: "Retro window is stale. Latest commit on `origin/` was ``, but the window covers `` to ``. This usually means either (a) today's date is wrong in this session or (b) `origin/` is materially behind the remote. Confirm today's date via the session reminder; if today is correct, run `git fetch origin ` manually and re-run /retro." Stop the skill until the user resolves. +- Otherwise, write: "RETRO_GUARD: latest commit `` within window — proceeding." + +Skip paths (`skip-no-remote`, `skip-detached`, `warn-fetch-failed`) all proceed to Step 1 with the cited reason on a single stderr line so the retro narrative carries the disclosure ("offline run, window not freshness-verified") rather than silently misreporting. + ### Step 1: Gather Raw Data First, fetch origin and identify the current user: diff --git a/retro/SKILL.md.tmpl b/retro/SKILL.md.tmpl index 93aed3d43..b0819c8a6 100644 --- a/retro/SKILL.md.tmpl +++ b/retro/SKILL.md.tmpl @@ -95,6 +95,63 @@ Check for non-git context that should be included in the retro: If `RETRO_CONTEXT_FOUND`: read `~/.gstack/retro-context.md`. This file is user-authored and may contain meeting notes, calendar events, decisions, and other context that doesn't appear in git history. Incorporate this context into the retro narrative where relevant. +### Step 0.5: Stale-base + bad-today-anchor pre-flight guard + +The retro skill computes a window from "today" and queries `git log --since= origin/`. If "today" drifts (model session-context error) or the local worktree's `origin/` is materially behind the actual remote, the window can return zero or near-zero commits and the retro will fabricate a coherent-looking narrative from nothing. This guard prevents silent confidently-wrong output. + +Run the pre-flight in this exact order. The first branch that matches wins: + +```bash +# Pre-check A: no remote configured? +_RETRO_HAS_REMOTE=$(git remote 2>/dev/null | grep -c '^origin$' || echo 0) +if [ "$_RETRO_HAS_REMOTE" = "0" ]; then + echo "RETRO_GUARD: no 'origin' remote, base freshness not verified — proceeding" + _RETRO_GUARD_VERDICT="skip-no-remote" +fi + +# Pre-check B: detached HEAD or no current base? +if [ -z "$_RETRO_GUARD_VERDICT" ]; then + _RETRO_HEAD_REF=$(git symbolic-ref --quiet HEAD 2>/dev/null || echo "") + if [ -z "$_RETRO_HEAD_REF" ]; then + echo "RETRO_GUARD: detached HEAD, base freshness not verified — proceeding" + _RETRO_GUARD_VERDICT="skip-detached" + fi +fi + +# Pre-check C: fetch origin ; if it fails, warn but proceed. +if [ -z "$_RETRO_GUARD_VERDICT" ]; then + if ! git fetch origin --quiet 2>/dev/null; then + echo "RETRO_GUARD: 'git fetch origin ' failed (offline?) — proceeding against last-known origin/" + _RETRO_GUARD_VERDICT="warn-fetch-failed" + fi +fi + +# Pre-check D: BLOCK only when fetch succeeded AND the latest origin/ +# commit predates the retro window. Today's date should be loaded from the +# user-visible "## currentDate" tag in the session reminder; if the gap between +# origin/'s newest commit and today exceeds the window, the model's +# "today" is almost certainly stale (or the worktree is wildly behind). +if [ -z "$_RETRO_GUARD_VERDICT" ]; then + _RETRO_LATEST_ISO=$(git log -1 --format=%ci origin/ 2>/dev/null | awk '{print $1}') + if [ -n "$_RETRO_LATEST_ISO" ]; then + # The model computes today from the session reminder (NEVER from `date` — + # the system clock can be hours off in containerized harnesses). + # Compute window in DAYS (default 7): if today - latest-commit-date > window-days, + # BLOCK. If the model cannot reliably compute "today", it MUST stop here and + # ask the user via AskUserQuestion rather than proceeding. + echo "RETRO_GUARD: latest origin/ commit on $_RETRO_LATEST_ISO" + _RETRO_GUARD_VERDICT="check-gap" + fi +fi +``` + +After running the bash block, the model evaluates `RETRO_GUARD: latest origin/ commit on ` against today and the window: + +- If the **latest-commit date is older than (today − window-days)**, BLOCK with: "Retro window is stale. Latest commit on `origin/` was ``, but the window covers `` to ``. This usually means either (a) today's date is wrong in this session or (b) `origin/` is materially behind the remote. Confirm today's date via the session reminder; if today is correct, run `git fetch origin ` manually and re-run /retro." Stop the skill until the user resolves. +- Otherwise, write: "RETRO_GUARD: latest commit `` within window — proceeding." + +Skip paths (`skip-no-remote`, `skip-detached`, `warn-fetch-failed`) all proceed to Step 1 with the cited reason on a single stderr line so the retro narrative carries the disclosure ("offline run, window not freshness-verified") rather than silently misreporting. + ### Step 1: Gather Raw Data First, fetch origin and identify the current user: