diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index e8bb6e3..1ba9cd0 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "session-continuity", - "version": "0.12.2", + "version": "0.12.3", "description": "Cross-session memory for Claude Code projects via two in-repo docs: SESSION_PRIMER.md (current state) and LEARNINGS.md (hard-won bugs).", "author": { "name": "Tal Golan" diff --git a/.session-continuity/SESSION_PRIMER.md b/.session-continuity/SESSION_PRIMER.md index ce88499..2a9899f 100644 --- a/.session-continuity/SESSION_PRIMER.md +++ b/.session-continuity/SESSION_PRIMER.md @@ -74,6 +74,54 @@ No external credentials or costs. ## Current state +- **v0.12.3 shipped** (branch `session-start-outstanding-items`, commit pushed, + tag `v0.12.3` pending). SessionStart hook now surfaces outstanding items from + the primer: extracts the "Outstanding items" section, lists the first line of + each numbered item (sub-bullets dropped), and injects into the SessionStart + reminder with an instruction asking which to tackle. When the section is empty + or missing, no block is added — the output remains identical to prior versions. + New hermetic smoke runner + `meta/superpowers/validation/2026-08-12-session-start-smoke.zsh` validates 11 + test cases covering both paths (`.session-continuity/` and legacy `docs/`), + multi-line items, empty sections, and missing sections; all pass. Shellcheck + clean on the modified hook script. Also fixes a pre-existing `grep -c` exit-code + bug: `grep -c` exits 1 when no matches are found (even though it outputs "0"), + so a fallback `|| echo '0'` would output both the grep "0" and the fallback "0", + causing spurious output duplication in the status line. Fixed by using `|| true` + as the fallback and explicit empty-case handling. `plugin.json` 0.12.2→0.12.3, + CHANGELOG entry added. +- **v0.12.2 shipped** (branch `fix/hook-json-escaping`, PR #11, tag `v0.12.2` + pushed, GitHub Release published, live plugin install refreshed and + verified). Fixes `proven-gate.sh` and `smoke-gate.sh` emitting malformed + JSON on deny: a reason string containing a literal `"` broke their + hand-built `printf` JSON, so the block worked but the reason never + parsed — undiagnosable rather than unsafe. Root cause: every existing + per-gate runner asserted with a substring match on `deny`, which passes on + malformed JSON too, so this shipped green. Fix, in order: + 1. New hermetic runner + `meta/superpowers/validation/2026-08-12-hook-json-contract-smoke.zsh` + parses each gate's deny output with a real JSON parser (`python3 -m + json`) instead of substring-matching it, and fails if any + `hooks/*-gate.sh` has no fixture — so a future gate can't skip the + check. Red on landing (4 failures: `proven-gate` + 3 `smoke-gate` + cases), 16/16 green after the fix. + 2. All six gates' `deny()` (not just the two broken ones) now route the + reason through a `json_escape()` helper: backslash-then-quote + escaping, full C0 control-byte range (`0x00`-`0x1F`, not just + `\n\t\r`) folded to a space. `smoke-gate.sh`'s now-redundant + `offender_esc` pre-escape removed. + 3. `.session-continuity/LEARNINGS.md` entry #1 gained a "Second trap" + addendum: a well-formed JSON *shape* isn't the same as parseable + JSON, and a substring assert can't tell the difference. + 4. `plugin.json` 0.12.1→0.12.2, CHANGELOG entry added. + Full validation suite 8 runners / 101 checks green; shellcheck clean on + all six gates. Built via `superpowers:subagent-driven-development` + (fresh implementer + reviewer per task, final whole-branch review on the + most capable model — 1 Minor finding, non-blocking: the new runner's + coverage check verifies gate-name list membership, not fixture + existence, so CHANGELOG/LEARNINGS phrasing slightly overstates what's + machine-enforced). Plan: + `meta/superpowers/plans/2026-08-12-hook-json-escaping-fix.md`. - **v0.12.1 shipped** (branch `fix/smoke-gate-false-positive`). Fixes a line-level false positive in `hooks/smoke-gate.sh`: the weak-smoke branch denied any line where `smoke` co-occurred with a weak-word @@ -150,11 +198,11 @@ No external credentials or costs. **Current `git log --oneline -5` (primary branch):** ``` -986b22a feat(end-session): report outstanding-items verdicts in Step 3 checklist -dd59d4a feat(end-session): verify outstanding items against code (Step 1) -a822087 docs(plan): address review of outstanding-items verification plan -eb4e7ba docs(plan): outstanding-items verification implementation plan -1e8944e docs(spec): address review of outstanding-items verification +(commit to be regenerated after merge) +b75af34 feat(learnings): backfill Trigger lines, require them going forward +8f05d0e fix(hooks): JSON-escape deny reasons (v0.12.2) (#11) +4eb8d5c fix(hooks): scope smoke-gate weak-word to adjacency + honor MANDATORY (v0.12.1) (#10) +5e3426c feat: outstanding-items code verification in end-session (v0.12.0) (#9) ``` Regenerate this block whenever you commit — see "Primer maintenance" below. diff --git a/CHANGELOG.md b/CHANGELOG.md index db88de3..2cc2d8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.12.3] — 2026-08-12 + +### Added +- **SessionStart hook surfaces outstanding items.** The `hooks/session-start.sh` + hook now extracts top-level numbered items from the primer's "Outstanding + items" section (first line only; sub-bullets dropped) and injects them into + the SessionStart `` block, followed by an instruction asking + the user which (if any) they want to tackle. When the section is empty or + missing, no block is added and the reminder remains identical to prior output. + +### Fixed +- **Spurious output duplication from `grep -c` exit code.** The `status_learnings` + computation used `grep -cE ... || echo '0'` to count LEARNINGS entries. When + `grep` finds no matches, it outputs "0" but exits with code 1 (not 0), causing + the fallback `echo '0'` to also run. The command substitution captured both + outputs, resulting in "0\n0" being assigned to the variable and later printed + to the reminder. Fixed by using `|| true` as the fallback and adding explicit + empty-case handling with `${status_learnings:-0}`. + ## [0.12.2] — 2026-08-12 ### Fixed diff --git a/hooks/session-start.sh b/hooks/session-start.sh index e9ad69b..39a9f75 100755 --- a/hooks/session-start.sh +++ b/hooks/session-start.sh @@ -83,7 +83,25 @@ status_outstanding="$(awk ' inside && /^[0-9]+\. / { count++ } END { print count+0 } ' "$cwd/$primer_path" 2>/dev/null || echo '?')" -status_learnings="$(grep -cE '^### [0-9]+\.' "$cwd/$learnings_path" 2>/dev/null || echo '0')" +status_learnings="$(grep -cE '^### [0-9]+\.' "$cwd/$learnings_path" 2>/dev/null || true)" +status_learnings="${status_learnings:-0}" + +# Outstanding items: extract the first line only of each top-level numbered +# item (sub-bullets and continuation lines are intentionally dropped — see +# spec's Decision section for why no truncation heuristics are added). +# Empty when the section is missing or has no numbered items, which keeps +# the reminder identical to today's output in that case. +outstanding_items="$(awk ' + /^## Outstanding items/ { inside=1; next } + inside && /^## / { exit } + inside && /^[0-9]+\. / { print } +' "$cwd/$primer_path" 2>/dev/null || true)" + +if [ -n "$outstanding_items" ]; then + outstanding_block=$'\nOutstanding items:\n'"$outstanding_items"$'\n\nAsk the user which of these (if any) they want to tackle this session.\n' +else + outstanding_block="" +fi # Inject the reminder into Claude's SessionStart context. `` # is the convention Claude Code uses for system-injected context that is @@ -97,7 +115,7 @@ Primer status (auto): - Last primer change: $status_mtime - Outstanding items: $status_outstanding - Learnings: $status_learnings - +${outstanding_block} EOF # Weekly freshness check (best-effort, silent on failure). Runs AFTER the diff --git a/meta/superpowers/plans/2026-08-12-session-start-outstanding-items.md b/meta/superpowers/plans/2026-08-12-session-start-outstanding-items.md new file mode 100644 index 0000000..5e1559c --- /dev/null +++ b/meta/superpowers/plans/2026-08-12-session-start-outstanding-items.md @@ -0,0 +1,249 @@ +# SessionStart Outstanding-Items Surfacing Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** `hooks/session-start.sh` lists the primer's outstanding items (first line each) in its `` and instructs Claude to ask the user which one to tackle this session. + +**Architecture:** One new awk pass in the existing bash script extracts the first line of each top-level numbered item under `## Outstanding items`, builds a pre-formatted text block (empty string if no items), and splices it into the existing heredoc immediately before the closing `` tag — no items means no block, no extra blank line, identical output to today. + +**Tech Stack:** bash (`set -euo pipefail`), awk, zsh (test runner, matching existing `meta/superpowers/validation/*-smoke.zsh` convention), shellcheck. + +## Global Constraints + +- No new files besides the test runner — same plain-stdout contract, no JSON, no `hooks.json` change (per spec `## Implementation notes`). +- First line only per item — no multi-line/sub-bullet capture (per spec `## Decision`). +- Silent omission when zero outstanding items — no "Outstanding items: 0" noise, and no stray blank line in that case (per spec `## Decision` + review finding rolled into `## Testing`). +- Both `.session-continuity/` and legacy `docs/` primer paths must get the feature; Task 1's smoke test covers both paths (per spec `## Implementation notes` + `## Testing`). +- No trimming/reformatting of items whose first line ends mid-sentence (e.g. trailing colon) — accepted tradeoff, do not add heuristics (per spec `## Decision`). +- Spec: `meta/superpowers/specs/2026-08-12-session-start-outstanding-items-design.md`. + +--- + +### Task 1: Extract and surface outstanding items in `hooks/session-start.sh` + +**Files:** +- Modify: `hooks/session-start.sh` (insert new awk pass + `outstanding_block` construction after the existing status computations at L86, splice into the heredoc at L91-100) +- Create: `meta/superpowers/validation/2026-08-12-session-start-smoke.zsh` +- Modify: `.claude-plugin/plugin.json`, `CHANGELOG.md` (version bump — see Step 6) + +**Interfaces:** +- Consumes: nothing new from other tasks — this is the only task. +- Produces: nothing consumed by later tasks — this is the only task. + +- [ ] **Step 1: Write the failing smoke test** + +Create `meta/superpowers/validation/2026-08-12-session-start-smoke.zsh`: + +```zsh +#!/usr/bin/env zsh +# Smoke runner for hooks/session-start.sh's outstanding-items surfacing. +# Hermetic: builds a scratch fixture repo per case, feeds a synthetic +# SessionStart payload on stdin, asserts on stdout. No live session, no +# network (SESSION_CONTINUITY_SKIP_UPDATE_CHECK=1 short-circuits +# version-check.sh's GitHub call). +set -uo pipefail + +here="${0:A:h}" +repo="${here:h:h:h}" # validation -> superpowers -> meta -> repo root +hook="$repo/hooks/session-start.sh" + +export SESSION_CONTINUITY_SKIP_UPDATE_CHECK=1 + +pass=0; fail=0 +ok() { print -P "%F{green}✓%f $1"; (( pass++ )); return 0; } +bad() { print -P "%F{red}✗%f $1"; (( fail++ )); return 0; } + +# assert +assert() { + local desc="$1" exp="$2" act="$3" + if [[ "$exp" == "EMPTY" ]]; then + [[ -z "$act" ]] && ok "$desc" || bad "$desc (expected empty, got: $act)" + else + [[ "$act" == *"$exp"* ]] && ok "$desc" || bad "$desc (expected '*$exp*', got: $act)" + fi +} + +# assert_not +assert_not() { + local desc="$1" forbidden="$2" act="$3" + [[ "$act" != *"$forbidden"* ]] && ok "$desc" || bad "$desc (found forbidden '*$forbidden*')" +} + +# payload -> a SessionStart JSON payload naming that cwd +payload() { printf '{"cwd":"%s"}' "$1"; } + +# --- Case set 1: canonical .session-continuity/ path, multi-line item --- +d1="$(mktemp -d)" +mkdir -p "$d1/.session-continuity" +cat > "$d1/.session-continuity/SESSION_PRIMER.md" <<'PRIMER' +# Session Primer + +## Outstanding items + +1. First item, single line. +2. Second item header text: (rejected — details below) + - sub-bullet A + - sub-bullet B +3. Third item, single line. + +## Workflow conventions +PRIMER +touch "$d1/.session-continuity/LEARNINGS.md" + +out1="$(payload "$d1" | bash "$hook")" +assert "1a lists item 1 first line" '1. First item, single line.' "$out1" +assert "1b lists item 2 first line only" '2. Second item header text: (rejected — details below)' "$out1" +assert_not "1c drops item 2 sub-bullets" 'sub-bullet A' "$out1" +assert "1d lists item 3 first line" '3. Third item, single line.' "$out1" +assert "1e includes ask-the-user instruction" 'Ask the user which of these' "$out1" +rm -rf "$d1" + +# --- Case set 2: legacy docs/ path gets the same treatment --- +d2="$(mktemp -d)" +mkdir -p "$d2/docs" +cat > "$d2/docs/SESSION_PRIMER.md" <<'PRIMER' +# Session Primer + +## Outstanding items + +1. Only item on the legacy path. + +## Workflow conventions +PRIMER +touch "$d2/docs/LEARNINGS.md" + +out2="$(payload "$d2" | bash "$hook")" +assert "2a legacy docs/ path also lists items" '1. Only item on the legacy path.' "$out2" +assert "2b legacy docs/ path also gets instruction" 'Ask the user which of these' "$out2" +rm -rf "$d2" + +# --- Case set 3: empty Outstanding items section -> no block, no noise --- +d3="$(mktemp -d)" +mkdir -p "$d3/.session-continuity" +cat > "$d3/.session-continuity/SESSION_PRIMER.md" <<'PRIMER' +# Session Primer + +## Outstanding items + +## Workflow conventions +PRIMER +touch "$d3/.session-continuity/LEARNINGS.md" + +out3="$(payload "$d3" | bash "$hook")" +assert_not "3a no Outstanding items: header block" 'Outstanding items:' "$out3" +assert_not "3b no ask-the-user instruction" 'Ask the user which of these' "$out3" +assert "3c closing tag immediately follows Learnings line (no stray blank line)" $'- Learnings: 0\n' "$out3" +rm -rf "$d3" + +# --- Case set 4: missing Outstanding items section entirely -> no block --- +d4="$(mktemp -d)" +mkdir -p "$d4/.session-continuity" +cat > "$d4/.session-continuity/SESSION_PRIMER.md" <<'PRIMER' +# Session Primer + +## Workflow conventions +PRIMER +touch "$d4/.session-continuity/LEARNINGS.md" + +out4="$(payload "$d4" | bash "$hook")" +assert_not "4a no Outstanding items: header block when section absent" 'Outstanding items:' "$out4" +rm -rf "$d4" + +print "" +print -P "Result: %F{green}$pass passed%f, %F{red}$fail failed%f" +(( fail == 0 )) +``` + +Make it executable: + +```bash +chmod +x meta/superpowers/validation/2026-08-12-session-start-smoke.zsh +``` + +- [ ] **Step 2: Run the test to check it fails as expected** + +Run: `zsh meta/superpowers/validation/2026-08-12-session-start-smoke.zsh` +Expected: cases 1a, 1b, 1d, 1e, 2a, 2b FAIL (current script has no `Outstanding items:` section or instruction line at all). Cases 1c, 3a, 3b, 3c, 4a currently pass by accident (nothing to find/nothing to break yet) — Step 4 re-checks all of them together once the implementation lands. + +- [ ] **Step 3: Implement the awk pass and heredoc splice** + +Open `hooks/session-start.sh`. Locate the existing status computation block (currently lines 77-86, ending at `status_learnings=...`) and insert a new block immediately after it, before the `# Inject the reminder...` comment: + +```bash +# Outstanding items: extract the first line only of each top-level numbered +# item (sub-bullets and continuation lines are intentionally dropped — see +# spec's Decision section for why no truncation heuristics are added). +# Empty when the section is missing or has no numbered items, which keeps +# the reminder identical to today's output in that case. +outstanding_items="$(awk ' + /^## Outstanding items/ { inside=1; next } + inside && /^## / { exit } + inside && /^[0-9]+\. / { print } +' "$cwd/$primer_path" 2>/dev/null || true)" + +if [ -n "$outstanding_items" ]; then + outstanding_block=$'\nOutstanding items:\n'"$outstanding_items"$'\n\nAsk the user which of these (if any) they want to tackle this session.\n' +else + outstanding_block="" +fi +``` + +Then change the existing heredoc (currently ending): + +```bash +cat < +This project has $primer_path. Read it before any work — it's the fastest path to context. Also check $learnings_path if anything surprises you. + +Primer status (auto): +- HEAD: $status_sha +- Last primer change: $status_mtime +- Outstanding items: $status_outstanding +- Learnings: $status_learnings + +EOF +``` + +to: + +```bash +cat < +This project has $primer_path. Read it before any work — it's the fastest path to context. Also check $learnings_path if anything surprises you. + +Primer status (auto): +- HEAD: $status_sha +- Last primer change: $status_mtime +- Outstanding items: $status_outstanding +- Learnings: $status_learnings +${outstanding_block} +EOF +``` + +(Note: `${outstanding_block}` sits directly adjacent to `` in the template, with no line break between them. When `outstanding_block` is empty this collapses to exactly today's output. When non-empty, the block's own leading/trailing `\n` supply the blank-line spacing and the newline before the closing tag.) + +- [ ] **Step 4: Run the test and confirm all cases pass** + +Run: `zsh meta/superpowers/validation/2026-08-12-session-start-smoke.zsh` +Expected: `Result: 12 passed, 0 failed` (all of 1a-1e, 2a-2b, 3a-3c, 4a). + +- [ ] **Step 5: shellcheck** + +Run: `shellcheck hooks/session-start.sh` +Expected: no warnings. If shellcheck flags the `$'...'` ANSI-C quoting or the `${outstanding_block}` splice, fix the reported line directly — do not add a `# shellcheck disable` without first trying a straightforward fix. + +- [ ] **Step 6: Bump version and update the primer** + +Every prior hook-script change (v0.9.0 through v0.12.2) bumped `plugin.json` + added a `CHANGELOG.md` entry, including behavior-only tweaks like v0.12.1's smoke-gate scoping fix — no ambiguity here, this change follows the same precedent: + +1. In `.claude-plugin/plugin.json`, bump `"version"` from `"0.12.2"` to `"0.12.3"`. +2. In `CHANGELOG.md`, add a `[0.12.3]` entry describing the SessionStart outstanding-items surfacing (mirror the format of the existing `[0.12.2]`/`[0.12.1]` entries). +3. In `.session-continuity/SESSION_PRIMER.md`'s `## Outstanding items` section, remove item #5 ("SessionStart should restate outstanding items and ask which to work on") — it's done, and this repo's "Primer maintenance" convention requires removing finished items in the same commit as the change that finished them. +4. Regenerate the `git log --oneline -5` block in the primer's `## Current state` section, and add a new `## Current state` bullet describing what shipped (mirror the style of the existing v0.12.x bullets), naming the `0.12.3` version bump. + +- [ ] **Step 7: Commit** + +```bash +git add hooks/session-start.sh meta/superpowers/validation/2026-08-12-session-start-smoke.zsh .claude-plugin/plugin.json CHANGELOG.md .session-continuity/SESSION_PRIMER.md +git commit -m "feat(hooks): surface outstanding items in SessionStart reminder (v0.12.3)" +``` diff --git a/meta/superpowers/specs/2026-08-12-session-start-outstanding-items-design.md b/meta/superpowers/specs/2026-08-12-session-start-outstanding-items-design.md new file mode 100644 index 0000000..e87dcf4 --- /dev/null +++ b/meta/superpowers/specs/2026-08-12-session-start-outstanding-items-design.md @@ -0,0 +1,106 @@ +# Design — SessionStart surfaces outstanding items + +Date: 2026-08-12 +Status: approved (brainstorming), pending implementation plan + +## Problem + +`hooks/session-start.sh` already computes an `Outstanding items: N` count in +its 4-line status block, but never shows *what* those items are, and never +prompts the user to pick one. Outstanding item #5 in the primer names this +gap explicitly: SessionStart should restate the list and ask which (if any) +to work on this session, instead of leaving that entirely to the model's +judgment. + +## Decision + +Extend the existing `` block with a new "Outstanding items" +section, appended *after* the existing 4-line status block, populated by a +new awk pass that extracts the **first line only** of each top-level +numbered item (`^[0-9]+\. `) inside `## Outstanding items` — same +section-boundary logic already used for `status_outstanding`. Full +multi-line detail (e.g. item 2's ten sub-bullets) is dropped; the first line +alone is enough for the model to relay to the user and enough to keep the +SessionStart context injection small. This mirrors the existing 4-line +status block's philosophy: compact, best-effort, never crashes the hook. + +Accepted tradeoff: an item whose first line ends mid-sentence (e.g. item 2's +"...deemed high-value):" — the colon dangles into the now-dropped sub-list) +will render as a truncated-looking line. No trimming/reformatting logic is +added for this — the model relaying the list to the user can paraphrase, and +adding text-shape heuristics (detect trailing colon, etc.) is exactly the +kind of speculative complexity this hook has avoided elsewhere. + +The new section is appended to the reminder text, followed by an explicit +instruction line telling Claude to ask the user which item (if any) to +tackle — closing the gap that item #5 identifies (nudge-to-read-primer +today does not equal prompt-to-pick-work). + +If the primer has zero outstanding items (missing section, or section with +no numbered lines), the new block is omitted entirely — no "Outstanding +items: 0" noise, matching the silent-omit pattern already used elsewhere in +the script (e.g. exit 0 on missing primer). + +## Output shape + +``` + +This project has .session-continuity/SESSION_PRIMER.md. Read it before any work — it's the fastest path to context. Also check .session-continuity/LEARNINGS.md if anything surprises you. + +Primer status (auto): +- HEAD: f9b75cd +- Last primer change: 2026-08-12 11:59 +- Outstanding items: 5 +- Learnings: 7 + +Outstanding items: +1. Submit to the Anthropic marketplace. +2. Deferred recommendations from `meta/superpowers/recommendations/improvements_20260521.md` (rejected or not-yet-prioritized — v0.5.1 + v0.6.0 shipped the items deemed high-value): +3. Automated integration tests. +4. Plan to drop the `docs/` fallback in hooks. +5. SessionStart should restate outstanding items and ask which to work on. + +Ask the user which of these (if any) they want to tackle this session. + +``` + +## Implementation notes + +- New awk block runs after the existing `status_outstanding` counter, same + section-boundary pattern (`/^## Outstanding items/ {inside=1}` / + `inside && /^## / {exit}` / `inside && /^[0-9]+\. /`), but prints the + matched line instead of counting it. +- Guard: only emit the "Outstanding items:" sub-block (and its trailing + instruction line) if the extracted list is non-empty. +- No change to the existing 4-line status block, no new files, no + `hooks.json` wiring change, no JSON — same plain-stdout contract. +- `docs/` legacy-path branch gets the same treatment as `.session-continuity/` + (the script already branches on `primer_path`/`learnings_path` for both; + the new awk pass reads from whichever `primer_path` was resolved, so both + branches get the feature for free). + +## Testing + +No `session-start.sh` smoke runner exists yet (checked +`meta/superpowers/validation/` — none named `session-start`). This is a +**new** hermetic runner, not an extension, following the same pattern as +`*-gate-smoke.zsh`: `meta/superpowers/validation/2026-08-12-session-start-smoke.zsh`. +Cases: + +- Fixture primer at `.session-continuity/SESSION_PRIMER.md` with several + outstanding items, including one multi-line item — assert only the first + line is captured and the trailing instruction line is present. +- Same fixture shape at the legacy `docs/SESSION_PRIMER.md` / + `docs/LEARNINGS.md` path — assert the feature works on that branch too, + not just asserted true in prose. +- Primer with an empty/missing `## Outstanding items` section — assert no + "Outstanding items:" block and no instruction line appear. + +## Out of scope + +- Does not change `/session-continuity:end-session`'s existing + outstanding-items verification (v0.12.0) — that's a separate, unrelated + code path (Step 1 of end-session, not SessionStart). +- Does not add interactivity to the hook itself — hooks only inject text; + the actual "ask the user" step happens in the model turn that follows, + same as today's primer-read nudge. diff --git a/meta/superpowers/validation/2026-08-12-session-start-smoke.zsh b/meta/superpowers/validation/2026-08-12-session-start-smoke.zsh new file mode 100755 index 0000000..bd1d76c --- /dev/null +++ b/meta/superpowers/validation/2026-08-12-session-start-smoke.zsh @@ -0,0 +1,117 @@ +#!/usr/bin/env zsh +# Smoke runner for hooks/session-start.sh's outstanding-items surfacing. +# Hermetic: builds a scratch fixture repo per case, feeds a synthetic +# SessionStart payload on stdin, asserts on stdout. No live session, no +# network (SESSION_CONTINUITY_SKIP_UPDATE_CHECK=1 short-circuits +# version-check.sh's GitHub call). +set -uo pipefail + +here="${0:A:h}" +repo="${here:h:h:h}" # validation -> superpowers -> meta -> repo root +hook="$repo/hooks/session-start.sh" + +export SESSION_CONTINUITY_SKIP_UPDATE_CHECK=1 + +pass=0; fail=0 +ok() { print -P "%F{green}✓%f $1"; (( pass++ )); return 0; } +bad() { print -P "%F{red}✗%f $1"; (( fail++ )); return 0; } + +# assert +assert() { + local desc="$1" exp="$2" act="$3" + if [[ "$exp" == "EMPTY" ]]; then + [[ -z "$act" ]] && ok "$desc" || bad "$desc (expected empty, got: $act)" + else + [[ "$act" == *"$exp"* ]] && ok "$desc" || bad "$desc (expected '*$exp*', got: $act)" + fi +} + +# assert_not +assert_not() { + local desc="$1" forbidden="$2" act="$3" + [[ "$act" != *"$forbidden"* ]] && ok "$desc" || bad "$desc (found forbidden '*$forbidden*')" +} + +# payload -> a SessionStart JSON payload naming that cwd +payload() { printf '{"cwd":"%s"}' "$1"; } + +# --- Case set 1: canonical .session-continuity/ path, multi-line item --- +d1="$(mktemp -d)" +mkdir -p "$d1/.session-continuity" +cat > "$d1/.session-continuity/SESSION_PRIMER.md" <<'PRIMER' +# Session Primer + +## Outstanding items + +1. First item, single line. +2. Second item header text: (rejected — details below) + - sub-bullet A + - sub-bullet B +3. Third item, single line. + +## Workflow conventions +PRIMER +touch "$d1/.session-continuity/LEARNINGS.md" + +out1="$(payload "$d1" | bash "$hook")" +assert "1a lists item 1 first line" '1. First item, single line.' "$out1" +assert "1b lists item 2 first line only" '2. Second item header text: (rejected — details below)' "$out1" +assert_not "1c drops item 2 sub-bullets" 'sub-bullet A' "$out1" +assert "1d lists item 3 first line" '3. Third item, single line.' "$out1" +assert "1e includes ask-the-user instruction" 'Ask the user which of these' "$out1" +rm -rf "$d1" + +# --- Case set 2: legacy docs/ path gets the same treatment --- +d2="$(mktemp -d)" +mkdir -p "$d2/docs" +cat > "$d2/docs/SESSION_PRIMER.md" <<'PRIMER' +# Session Primer + +## Outstanding items + +1. Only item on the legacy path. + +## Workflow conventions +PRIMER +touch "$d2/docs/LEARNINGS.md" + +out2="$(payload "$d2" | bash "$hook")" +assert "2a legacy docs/ path also lists items" '1. Only item on the legacy path.' "$out2" +assert "2b legacy docs/ path also gets instruction" 'Ask the user which of these' "$out2" +rm -rf "$d2" + +# --- Case set 3: empty Outstanding items section -> no block, no noise --- +d3="$(mktemp -d)" +mkdir -p "$d3/.session-continuity" +cat > "$d3/.session-continuity/SESSION_PRIMER.md" <<'PRIMER' +# Session Primer + +## Outstanding items + +## Workflow conventions +PRIMER +touch "$d3/.session-continuity/LEARNINGS.md" + +out3="$(payload "$d3" | bash "$hook")" +assert_not "3a no Outstanding items: header block" $'\nOutstanding items:\n' "$out3" +assert_not "3b no ask-the-user instruction" 'Ask the user which of these' "$out3" +assert "3c closing tag immediately follows Learnings line (no stray blank line)" $'- Learnings: 0\n' "$out3" +rm -rf "$d3" + +# --- Case set 4: missing Outstanding items section entirely -> no block --- +d4="$(mktemp -d)" +mkdir -p "$d4/.session-continuity" +cat > "$d4/.session-continuity/SESSION_PRIMER.md" <<'PRIMER' +# Session Primer + +## Workflow conventions +PRIMER +touch "$d4/.session-continuity/LEARNINGS.md" + +out4="$(payload "$d4" | bash "$hook")" +assert_not "4a no Outstanding items: header block when section absent" $'\nOutstanding items:\n' "$out4" +rm -rf "$d4" + +print "" +print -P "Result: %F{green}$pass passed%f, %F{red}$fail failed%f" +(( fail == 0 ))