Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
58 changes: 53 additions & 5 deletions .session-continuity/SESSION_PRIMER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<system-reminder>` 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
Expand Down
22 changes: 20 additions & 2 deletions hooks/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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. `<system-reminder>`
# is the convention Claude Code uses for system-injected context that is
Expand All @@ -97,7 +115,7 @@ Primer status (auto):
- Last primer change: $status_mtime
- Outstanding items: $status_outstanding
- Learnings: $status_learnings
</system-reminder>
${outstanding_block}</system-reminder>
EOF

# Weekly freshness check (best-effort, silent on failure). Runs AFTER the
Expand Down
249 changes: 249 additions & 0 deletions meta/superpowers/plans/2026-08-12-session-start-outstanding-items.md
Original file line number Diff line number Diff line change
@@ -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 `<system-reminder>` 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 `</system-reminder>` 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 <desc> <expected-substr-or-EMPTY> <actual>
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 <desc> <forbidden-substr> <actual>
assert_not() {
local desc="$1" forbidden="$2" act="$3"
[[ "$act" != *"$forbidden"* ]] && ok "$desc" || bad "$desc (found forbidden '*$forbidden*')"
}

# payload <cwd> -> 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</system-reminder>' "$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 <<EOF
<system-reminder>
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
</system-reminder>
EOF
```

to:

```bash
cat <<EOF
<system-reminder>
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}</system-reminder>
EOF
```

(Note: `${outstanding_block}` sits directly adjacent to `</system-reminder>` 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)"
```
Loading