diff --git a/retro/SKILL.md b/retro/SKILL.md index f750976698..25a5d3c28b 100644 --- a/retro/SKILL.md +++ b/retro/SKILL.md @@ -888,11 +888,17 @@ 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 +### Date/window pre-flight 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: +Run this dedicated pre-flight after resolving the requested window and before the main data-gathering commands. For `/retro compare`, run it against the current window before computing the prior window. For `/retro global`, run it once per discovered repo after detecting that repo's default branch and before collecting its git-log data. + +Resolve: +- `` to the exact `--since` value used for the current window, such as `2026-05-13T00:00:00`. +- `` to the numeric current-window length in days. Use `7` for the default window, `N` for `Nd`, `N * 7` for `Nw`, and `ceil(N / 24)` with a minimum of `1` for `Nh`. + +Run the pre-flight in this exact order. The first skip branch that matches proceeds with a disclosure; otherwise the freshness metrics must be printed: ```bash # Pre-check A: no remote configured? @@ -919,32 +925,39 @@ if [ -z "$_RETRO_GUARD_VERDICT" ]; then 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). +# Pre-check D: print date/window freshness metrics when base freshness is known. 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" + _RETRO_SYSTEM_DATE=$(date +%Y-%m-%d) + _RETRO_WINDOW_DAYS="" + case "$_RETRO_WINDOW_DAYS" in ''|*[!0-9]*) _RETRO_WINDOW_DAYS=7 ;; esac + if [ "$_RETRO_WINDOW_DAYS" -lt 1 ]; then _RETRO_WINDOW_DAYS=1; fi + _ORIGIN_LATEST_UNIX=$(git log origin/ -1 --format="%at" 2>/dev/null || echo 0) + _ORIGIN_LATEST_DATE=$(git log origin/ -1 --format="%ad" --date=short 2>/dev/null || echo unknown) + case "$_ORIGIN_LATEST_UNIX" in ''|*[!0-9]*) _ORIGIN_LATEST_UNIX=0 ;; esac + if [ "$_ORIGIN_LATEST_UNIX" -gt 0 ]; then + _ORIGIN_GAP_DAYS=$(( ($(date +%s) - _ORIGIN_LATEST_UNIX) / 86400 )) + else + _ORIGIN_GAP_DAYS=9999 + fi + _WINDOW_COMMITS=$(git rev-list --count origin/ --since="" 2>/dev/null || echo 0) + case "$_WINDOW_COMMITS" in ''|*[!0-9]*) _WINDOW_COMMITS=0 ;; esac + echo "RETRO_SYSTEM_DATE: $_RETRO_SYSTEM_DATE" + echo "RETRO_WINDOW_DAYS: $_RETRO_WINDOW_DAYS" + echo "ORIGIN_DEFAULT_LATEST: $_ORIGIN_LATEST_DATE (${_ORIGIN_GAP_DAYS}d before system date)" + echo "RETRO_WINDOW_COMMIT_COUNT: $_WINDOW_COMMITS" + if [ "$_ORIGIN_GAP_DAYS" -gt "$_RETRO_WINDOW_DAYS" ]; then + echo "STALE-BASE WARNING: latest origin/ commit is ${_ORIGIN_GAP_DAYS} days before the system date, which is older than the ${_RETRO_WINDOW_DAYS}d retro window. Confirm the 'today' anchor and whether origin/ is current before writing the retro." + fi + if [ "$_WINDOW_COMMITS" -eq 0 ]; then + echo "EMPTY-WINDOW WARNING: origin/ has zero commits in the requested retro window. Treat this as a possible wrong today/window anchor, not proof that no work happened." 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. +If the date you used to compute a day/week window does not match `RETRO_SYSTEM_DATE`, print `TODAY-ANCHOR WARNING`, recompute the window from `RETRO_SYSTEM_DATE`, and rerun the pre-flight. If `STALE-BASE WARNING` or `EMPTY-WINDOW WARNING` appears, call it out before any normal retro narrative. Do not produce a clean-looking "nothing happened" retro without explaining the suspect system date/window/base-branch evidence. This window-aware threshold intentionally catches the #1624 9-day-gap/7d reproducer: if the latest default-branch commit is 9 days before the system date and the current retro window is 7 days, the pre-flight must print `STALE-BASE WARNING`. + ### 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 b0819c8a6b..3c34b4d1b4 100644 --- a/retro/SKILL.md.tmpl +++ b/retro/SKILL.md.tmpl @@ -95,11 +95,17 @@ 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 +### Date/window pre-flight 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: +Run this dedicated pre-flight after resolving the requested window and before the main data-gathering commands. For `/retro compare`, run it against the current window before computing the prior window. For `/retro global`, run it once per discovered repo after detecting that repo's default branch and before collecting its git-log data. + +Resolve: +- `` to the exact `--since` value used for the current window, such as `2026-05-13T00:00:00`. +- `` to the numeric current-window length in days. Use `7` for the default window, `N` for `Nd`, `N * 7` for `Nw`, and `ceil(N / 24)` with a minimum of `1` for `Nh`. + +Run the pre-flight in this exact order. The first skip branch that matches proceeds with a disclosure; otherwise the freshness metrics must be printed: ```bash # Pre-check A: no remote configured? @@ -126,32 +132,39 @@ if [ -z "$_RETRO_GUARD_VERDICT" ]; then 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). +# Pre-check D: print date/window freshness metrics when base freshness is known. 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" + _RETRO_SYSTEM_DATE=$(date +%Y-%m-%d) + _RETRO_WINDOW_DAYS="" + case "$_RETRO_WINDOW_DAYS" in ''|*[!0-9]*) _RETRO_WINDOW_DAYS=7 ;; esac + if [ "$_RETRO_WINDOW_DAYS" -lt 1 ]; then _RETRO_WINDOW_DAYS=1; fi + _ORIGIN_LATEST_UNIX=$(git log origin/ -1 --format="%at" 2>/dev/null || echo 0) + _ORIGIN_LATEST_DATE=$(git log origin/ -1 --format="%ad" --date=short 2>/dev/null || echo unknown) + case "$_ORIGIN_LATEST_UNIX" in ''|*[!0-9]*) _ORIGIN_LATEST_UNIX=0 ;; esac + if [ "$_ORIGIN_LATEST_UNIX" -gt 0 ]; then + _ORIGIN_GAP_DAYS=$(( ($(date +%s) - _ORIGIN_LATEST_UNIX) / 86400 )) + else + _ORIGIN_GAP_DAYS=9999 + fi + _WINDOW_COMMITS=$(git rev-list --count origin/ --since="" 2>/dev/null || echo 0) + case "$_WINDOW_COMMITS" in ''|*[!0-9]*) _WINDOW_COMMITS=0 ;; esac + echo "RETRO_SYSTEM_DATE: $_RETRO_SYSTEM_DATE" + echo "RETRO_WINDOW_DAYS: $_RETRO_WINDOW_DAYS" + echo "ORIGIN_DEFAULT_LATEST: $_ORIGIN_LATEST_DATE (${_ORIGIN_GAP_DAYS}d before system date)" + echo "RETRO_WINDOW_COMMIT_COUNT: $_WINDOW_COMMITS" + if [ "$_ORIGIN_GAP_DAYS" -gt "$_RETRO_WINDOW_DAYS" ]; then + echo "STALE-BASE WARNING: latest origin/ commit is ${_ORIGIN_GAP_DAYS} days before the system date, which is older than the ${_RETRO_WINDOW_DAYS}d retro window. Confirm the 'today' anchor and whether origin/ is current before writing the retro." + fi + if [ "$_WINDOW_COMMITS" -eq 0 ]; then + echo "EMPTY-WINDOW WARNING: origin/ has zero commits in the requested retro window. Treat this as a possible wrong today/window anchor, not proof that no work happened." 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. +If the date you used to compute a day/week window does not match `RETRO_SYSTEM_DATE`, print `TODAY-ANCHOR WARNING`, recompute the window from `RETRO_SYSTEM_DATE`, and rerun the pre-flight. If `STALE-BASE WARNING` or `EMPTY-WINDOW WARNING` appears, call it out before any normal retro narrative. Do not produce a clean-looking "nothing happened" retro without explaining the suspect system date/window/base-branch evidence. This window-aware threshold intentionally catches the #1624 9-day-gap/7d reproducer: if the latest default-branch commit is 9 days before the system date and the current retro window is 7 days, the pre-flight must print `STALE-BASE WARNING`. + ### Step 1: Gather Raw Data First, fetch origin and identify the current user: diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 0a0c9741ba..c39b9cc71e 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -1085,6 +1085,22 @@ describe('Retro plan completion section', () => { expect(retroSkill).toContain('plan_items_total'); expect(retroSkill).toContain('Plan Completion This Period'); }); + + test('retro SKILL.md warns on stale base or empty windows', () => { + expect(retroSkill).toContain('### Date/window pre-flight'); + expect(retroSkill).toContain('RETRO_SYSTEM_DATE'); + expect(retroSkill).toContain('RETRO_WINDOW_DAYS'); + expect(retroSkill).toContain('ORIGIN_DEFAULT_LATEST'); + expect(retroSkill).toContain('RETRO_WINDOW_COMMIT_COUNT'); + expect(retroSkill).toContain('TODAY-ANCHOR WARNING'); + expect(retroSkill).toContain('STALE-BASE WARNING'); + expect(retroSkill).toContain('EMPTY-WINDOW WARNING'); + expect(retroSkill).toContain('$_ORIGIN_GAP_DAYS" -gt "$_RETRO_WINDOW_DAYS'); + expect(retroSkill).toContain('#1624 9-day-gap/7d reproducer'); + expect(retroSkill).toContain('For `/retro compare`, run it against the current window'); + expect(retroSkill).toContain('For `/retro global`, run it once per discovered repo'); + expect(retroSkill).toContain('git rev-list --count origin/ --since=""'); + }); }); // --- Plan status footer in preamble ---