diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2c0d0e51a..04fd7d9a7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -162,6 +162,12 @@ "source": "./plugins/code-tidying", "category": "review", "tags": ["tidy", "refactoring", "simplify", "boy-scout", "maintenance", "skill"] + }, + { + "name": "session-flow", + "source": "./plugins/session-flow", + "category": "productivity", + "tags": ["workflow", "handoff", "retro", "retrospective", "session", "orchestration", "skill"] } ] } diff --git a/README.md b/README.md index a135408bb..96046d3af 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Browse and manage with `/plugin`. To refresh after updates: `/plugin marketplace | [`discovery`](plugins/discovery) | Skills | Structured discovery before changes. Ships four skills: `/discovery:explore` and `/discovery:explore-deep` (six-dimension codebase exploration, inline or in a forked subagent) plus `/discovery:research` and `/discovery:research-deep` (three-phase multi-source external research with source tiers, falsification, and recency gates). | | [`playwright`](plugins/playwright) | Skill | Live E2E browser automation through Microsoft's `@playwright/cli` — named sessions, accessibility-ref snapshots (click/fill by ref), screenshots, console/network capture, mocking, tracing, video, and auth state, with artifacts written to disk so only paths enter context, plus Windows/orchestrator overlays and a gated maintainer update flow tracking the upstream npm skill. | | [`code-tidying`](plugins/code-tidying) | Skills | Structure-only codebase improvement per Beck's *Tidy First?*: `/code-tidying:tidy` proactively hunts a rotated, glob-scoped lane for named tidyings under a research-backed scope budget (consumer-defined lanes in `.claude/tidy-lanes/` override bundled ones) and ships one tight PR; `/code-tidying:batch-simplify` sweeps recently changed files through grouped, dependency-ordered simplification waves with a never-drop deferred-items contract. | +| [`session-flow`](plugins/session-flow) | Skills | Session-lifecycle toolkit of four skills: `workflow` (navigate a staged dev workflow and suggest the next stage), `handoff` (write a save-point + resume prompt for the `/clear`-and-resume pattern), `retro` (structured retrospective with a bundled transcript-metrics parser and learning codification), and `orchestration-brief` (arm a session or spawned worker with proactive-orchestration imperatives). | Install one: `/plugin install @melodic-software`. diff --git a/plugins/session-flow/.claude-plugin/plugin.json b/plugins/session-flow/.claude-plugin/plugin.json new file mode 100644 index 000000000..bc5c41b67 --- /dev/null +++ b/plugins/session-flow/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", + "name": "session-flow", + "version": "0.1.0", + "description": "Session-lifecycle toolkit of four skills: workflow (navigate a staged dev workflow and suggest the next stage), handoff (write a save-point and resume prompt for /clear), retro (structured session retrospective with transcript metrics and learning codification), and orchestration-brief (arm a session or worker with proactive-orchestration imperatives).", + "author": { + "name": "Melodic Software", + "email": "info@melodicsoftware.com" + }, + "license": "MIT", + "keywords": ["workflow", "handoff", "retrospective", "session", "checkpoint", "orchestration", "skill"] +} diff --git a/plugins/session-flow/README.md b/plugins/session-flow/README.md new file mode 100644 index 000000000..2789f483f --- /dev/null +++ b/plugins/session-flow/README.md @@ -0,0 +1,97 @@ +# session-flow + +A Claude Code plugin bundling four skills for one cohesive capability: managing the lifecycle of a +working session — where you are in the work, how to pause and resume it, what to learn from it, and +how to arm it for delegation-heavy tasks. + +| Skill | Question it answers | +|---|---| +| `/session-flow:workflow` | Where am I in the staged dev workflow, and what comes next? | +| `/session-flow:handoff` | How do I save this session's state so a fresh `/clear` session resumes without rediscovery? | +| `/session-flow:retro` | What happened this session, what did we learn, and how do we codify it? | +| `/session-flow:orchestration-brief` | How do I arm this session (or a spawned worker) with proactive-orchestration imperatives? | + +## What each skill does + +### workflow + +The map for a staged development workflow (contract → explore → research → plan → implement → test +→ review → verify → retro). Detects the current position from conversation evidence, suggests the +next stage, and serves ordered checklists for the pre-PR sequence and end-of-session wrap-up. When +the consuming repo defines its own stage skills, it routes to them; otherwise stages execute +inline. + +```shell +/session-flow:workflow # position + next-stage suggestion +/session-flow:workflow steps # full stage definitions +/session-flow:workflow pre-pr # ordered pre-PR gate checklist +/session-flow:workflow wrap-up # end-of-session checklist +/session-flow:workflow spec-first # stage-by-stage execution with /clear between stages +``` + +### handoff + +Writes a mid-session save-point for the `/clear`-and-resume pattern: a durable handoff file (task, +progress, decisions, files modified, tried-and-ruled-out, next steps, TaskList snapshot) plus a +copy-paste resume prompt — or prompt-only when follow-ups are small. Handoff files chain via +`session_id` / `previous_handoff` frontmatter so `retro` can analyze the whole session chain. The +skill always STOPS after emitting the save-point — continuing would defeat the purpose. + +```shell +/session-flow:handoff # auto-detect full vs prompt-only +/session-flow:handoff prompt # force prompt-only +/session-flow:handoff file phase-3 # force full handoff, topic "phase-3" +``` + +### retro + +Structured session retrospective: extracts transcript metrics via a bundled stdlib-only parser +(multi-session-aware — walks the handoff chain), assesses quality across five dimensions against +the consuming repo's own conventions, checks Claude Code auto-memory for feedback regressions, and +codifies user-approved learnings. Health scores persist across sessions for trend analysis. + +```shell +/session-flow:retro # full 5-phase analysis (default) +/session-flow:retro codify # persist a specific mid-session learning +/session-flow:retro trends # cross-session score history +/session-flow:retro quick # abbreviated, for limited context +``` + +### orchestration-brief + +Arms the current session for an orchestration-heavy task by loading six proactive-orchestration +imperatives (delegate/fan-out, spec-every-spawn, fresh-context verify, run-workers-well, nested +subagents, surface drift) as standing instructions — or exports them as a paste-ready, tool-agnostic +brief for a spawned worker or fresh session. + +```shell +/session-flow:orchestration-brief # prime this session +/session-flow:orchestration-brief worker # paste-ready worker brief +/session-flow:orchestration-brief handoff compact # headline-only fresh-session brief +``` + +## Consumer conventions + +The skills adapt to the consuming repo rather than imposing structure: + +- **Stage skills** — `workflow` routes to the repo's own stage skills when they exist; every stage + degrades gracefully to inline execution. +- **Artifact location** — `handoff` and `workflow` honor a repo-documented convention for + save-points/work journals (declared in the repo's `CLAUDE.md` / rules); the default is + `.claude/handoffs/` in the project. +- **Quality gates and conventions** — build/test/lint commands, review criteria, and codification + targets all come from the consuming repo's own instruction files. + +## Install + +```shell +/plugin marketplace add melodic-software/claude-code-plugins +/plugin install session-flow@melodic-software +``` + +## Configuration + +No `userConfig`. State: retro score history persists under the plugin's `${CLAUDE_PLUGIN_DATA}` +directory (per-project files) — never in the consumer's repo. Handoff save-points are project files +by design (they travel with the repo). Network: none — the bundled transcript parser is +stdlib-only Python 3.10+ reading local `~/.claude/projects/` transcripts. diff --git a/plugins/session-flow/skills/handoff/SKILL.md b/plugins/session-flow/skills/handoff/SKILL.md new file mode 100644 index 000000000..6cac5f63e --- /dev/null +++ b/plugins/session-flow/skills/handoff/SKILL.md @@ -0,0 +1,187 @@ +--- +name: handoff +description: "Write a mid-session save-point for /clear-and-resume — a durable handoff file (default) or a copy-paste resume prompt when follow-ups are small. Use when: 'handoff', 'save state', 'checkpoint this', 'pause', 'come back later', context is heavy, or quality is degrading." +argument-hint: "[file|prompt] [topic] (e.g., /handoff, /handoff prompt, /handoff file phase-3)" +user-invocable: true +disable-model-invocation: false +--- + +## Pre-computed context + +Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` +Claude session: !`echo "${CLAUDE_CODE_SESSION_ID:-unknown}"` +Uncommitted changes: !`git status --porcelain 2>/dev/null | head -20 || echo "clean"` +Recent commits: !`git log --oneline -5 2>/dev/null || echo "no commits"` + +## Purpose + +Context bloat is expensive and quality degrades as context rots. When a task has room left but +context is heavy, capture a save-point — a handoff document, or just a copy-paste resume prompt when +follow-ups are small — and `/clear`. + +Based on the canonical pattern Anthropic recommends for the `/clear` workflow: put the rest of the +plan in a handoff file; explain what you tried, what worked, and what didn't, so the next agent with +fresh context can load that file and nothing else. The save-point captures a *snapshot* of in-flight +state — including what was tried and ruled out — so the next session doesn't waste effort +rediscovering dead ends. + +## Where handoffs live + +Honor the consuming repo's documented convention for session save-points / work journals (check +`CLAUDE.md` / `.claude/rules/`) if one exists. Otherwise default to **`.claude/handoffs/`** in the +project — files named `-handoff-.md` with `TS = date -u +%Y%m%dT%H%M%SZ` (ISO basic, +Windows-safe, sortable). Handoffs are project files by design: they travel with the repo and any +session or machine can resume from them. + +## Arguments + +`$ARGUMENTS` carries `[file|prompt] [topic]` — both optional, positional: + +- **Method** (`file` | `prompt`) — recognized ONLY as the first token. `file` forces the full + durable handoff; `prompt` forces prompt-only. Omitted → auto-detect (see "Choosing the path"). +- **Topic** — short kebab slug for the filename. When the first token is not a method keyword it IS + the topic (`/handoff phase-3`); with a method present it is the second token. Omitted → inferred + from context. + +## Hard rule — handoff ALWAYS terminates current execution + +**The whole point of `/handoff` is `/clear` + fresh-session resume.** The skill produces the +save-point, THEN STOPS. It does NOT keep executing the underlying task in the current session; that +defeats the purpose. STOP is the default and near-universal outcome — NEVER unlocked by the user +having listed multiple steps, nor by the remaining work being "small". + +**Mandatory STOP gate (walk every box):** + +- [ ] Path chosen (full vs prompt-only) per "Choosing the path" +- [ ] Copy/paste resume prompt emitted between two dashed rails (see "Final step") +- [ ] `/clear`-then-paste instruction surfaced to the user +- [ ] **STOP.** No further work items, no next phase, no follow-on skill, no commit/push. The + session ends as far as the task is concerned + +**NOT authorization to continue (these all STOP):** + +- A multi-step pipeline naming `/handoff` (e.g. "handoff, then verify, then PR") → the listed steps + run in the FRESH session AFTER `/clear`. Naming `/handoff` names a `/clear` boundary, not a waiver +- "do all of it" → authorizes executing the phases across the session chain, but each `/handoff` + between them still enforces its `/clear` boundary (that is WHY the handoffs get written) +- A standalone user-invoked `/handoff` → always STOP, regardless of surrounding instructions + +The only exception: the user's prior turn used explicit stay-in-session language about handoffs +specifically (e.g. "don't `/clear` between phases, keep going"). + +## When to invoke + +- Mid-task, context heavy (check `/context` output or user report) +- Quality degrading (context rot) — responses drifting, repeating, or looping +- About to pause for hours/overnight; want a clean resume +- About to switch to a different task; this one isn't done +- Last turn had an unexpected compaction +- Sharing state with another session or machine + +## Locate the position first + +Before emitting anything, establish where the work stands: if a plan or checklist artifact backs +the work (see the sibling `workflow` skill), read it THIS turn and name the next unfinished stage — +the resume prompt points at the next stage, not just "continue here". Ground every status claim in +a fresh read, never a prior session's assertion. With no plan artifact, name the next concrete +action from the conversation. + +## Choosing the path: full handoff vs prompt-only + +**A resume prompt is ALWAYS emitted.** The only decision is whether to ALSO write a durable handoff +file. Full handoff = prompt + file (prompt `@`-references the file); prompt-only = the same prompt +carrying its detail inline, no file. + +**Default: write the file.** Skip it only when NO plan artifact backs the work AND all of these +clearly hold: + +- Remaining follow-ups fit as a short bullet list in the prompt +- The work is straightforward, not exploratory +- No "tried and ruled out" dead-ends worth preserving +- No load-bearing decision + rationale a future session must not rediscover +- No non-trivial task list to reconstitute + +ANY doubt → full handoff. A wrongly-skipped file loses state the fresh session must rediscover; a +wrongly-written one costs nothing. The explicit method argument overrides auto-detect — but note +`prompt` leaves a gap in the session-id chain that `/retro` walks (no file, no chain pointer). + +## Writing the handoff (full path) + +The document structure (seven body sections — Task / Progress / Decisions made / Files modified / +Tried and ruled out / Open questions / Files to review), the TaskList snapshot + reconstitute +format, and the frontmatter shape (including the `session_id` / `previous_handoff` / +`previous_session_id` chain fields that `/retro` walks) live in `context/structure.md` — walk it +while writing the file. + +## Final step: emit the copy/paste resume prompt + +**Copy-region clarity (both paths) — two dashed rails, no fence:** + +- The prompt sits between two full-width `─` (U+2500) rails — top rail, prompt, bottom rail. Use + literal `─`, NOT markdown `---` (turns the adjacent line into a heading) and NOT a code fence + (the user copies the text between the rails, not fence markers). +- The ONLY thing between the rails is the prompt — no labels, no padding lines. Commentary sits + above the top rail or below the bottom rail, never between. +- One plain-language instruction sits directly ABOVE the top rail: "`/clear`, then copy everything + between the dashed lines." +- **Goal-aware re-arm:** if a `/goal` is active this session (infer from conversation), the FIRST + line between the rails starts with literal `/goal` — `/clear` destroys an active goal, so the + pasted block must re-arm it. When unsure, omit it and note below the bottom rail: "if a goal was + active, prepend `/goal `." + +Full-path shape (minimum form — live: bare `─` rails, no fence; shown inside a fence here for +display): + +```text +`/clear`, then copy everything between the dashed lines: + +────────────────────────────────────────────────────────── +Read @.claude/handoffs/-handoff-.md and continue per its "Open questions / next steps". +Prior session: . +────────────────────────────────────────────────────────── +``` + +When the next stage is a specific skill in the consuming repo, swap the directive to +`Read @… and execute /.` The `@`-reference is mandatory on the full path — the fresh session +loads it; do NOT inline the file's detail in the prompt. Prompt-only carries its remaining-work +bullets inline between the rails instead. + +`` = this session's `$CLAUDE_CODE_SESSION_ID` (the frontmatter `session_id`) — it lets a +fresh session or `/retro` chain-walker locate the transcript later. + +## Post-write enforcement checklist + +Tick each item in the response so the user can verify the exit shape. Missing any tick = handoff +incomplete. + +**Full path:** + +- [ ] Position located + next stage named (fresh reads this turn) +- [ ] Handoff file written to the handoff location with frontmatter per `context/structure.md` +- [ ] `previous_handoff` + `previous_session_id` present IF this session continued a prior + handoff's task (chain continuity per `context/structure.md`); omitted otherwise — including when + the directory holds only unrelated-task handoffs +- [ ] All seven body sections present +- [ ] TaskList snapshot + Reconstitute sections present (OR explicit "exception: 0 active tasks") +- [ ] Resume prompt emitted between dashed rails, `@`-referencing the file; copy instruction above + the top rail; `/goal` first line if a goal is active +- [ ] **EXECUTION STOPS HERE** + +**Prompt-only path:** + +- [ ] Prompt-only justified (all auto-detect criteria hold, OR `prompt` explicitly passed) +- [ ] Self-contained resume prompt between dashed rails — remaining-work bullets inline +- [ ] Copy instruction above the rails; `/goal` first line if a goal is active +- [ ] **EXECUTION STOPS HERE** — "small enough" means the prompt captures the work, NOT "small + enough to skip `/clear` and finish in-session" + +## What this skill does NOT do + +- **Does not commit** — handoff docs are durable task state, not source code. Commit ready code + changes separately; describe uncommitted work in "Progress" +- **Does not invoke `/clear`** — the user types `/clear`. The skill produces the save-point, emits + the resume prompt, and stops +- **Does not continue executing the underlying task** — per the hard rule above. Prompt-only does + NOT relax this +- **Does not replace a contract or plan** — it captures in-flight state at any point +- **Does not summarize the whole conversation** — task-relevant state only diff --git a/plugins/session-flow/skills/handoff/context/structure.md b/plugins/session-flow/skills/handoff/context/structure.md new file mode 100644 index 000000000..8a8c8d607 --- /dev/null +++ b/plugins/session-flow/skills/handoff/context/structure.md @@ -0,0 +1,156 @@ +# Handoff document structure + full-path write procedure + +Reference consulted while WRITING a full-path handoff (the decision logic — STOP gate, path choice, +exit checklists — stays in `SKILL.md`). Walk every section; be specific — vague handoffs cost the +next session a re-investigation. + +## The seven body sections + +### Task + +One paragraph. What are we working on? What does "done" look like? If a contract or approved plan +exists on disk, reference it rather than restating. + +### Progress + +What's already done: + +- **Committed** — list commit SHAs and one-line summaries +- **Uncommitted but working** — files changed and why +- **In progress** — what's partially done (never commit partial work — describe it in prose) + +### Decisions made + +Bullet list. Non-obvious choices + rationale (often a past incident or research finding): + +- Decision → rationale +- Example: "Using `Result` instead of exceptions for not-found → railway-oriented style is the + project default per its CLAUDE.md" + +Goal: the next session trusts prior decisions without rediscovering them. + +### Files modified + +List, one-line rationale each: + +- `path/to/file` — added handler X, wired into DI +- `path/to/test` — covers happy path; still need edge cases + +### Tried and ruled out + +The most valuable section. Failed approaches and WHY they failed: + +- Approach → why it didn't work (specific error, constraint, trade-off) + +Without this the next session repeats the dead end. Budget detail generously here. + +### Open questions / next steps + +Actionable items ordered by priority: + +1. [ ] Next immediate action +2. [ ] Question blocking progress (who to ask, how to test) +3. [ ] Follow-up once the blocker resolves + +### Files to review on resume + +Entry points the next session reads first: + +- Primary: the file holding the current focus +- Related: its tests, the relevant convention/rule files +- Any plan/checklist artifacts and prior handoff entries + +## TaskList snapshot + reconstitute instructions + +TaskList state is in-memory only — `/clear` blows it away. Capture it verbatim so the resuming +session recreates it via `TaskCreate`. + +**Before writing this section:** call `TaskList` to fetch live state. Render every task with its +current status: + +| Glyph | Status | +|-------|--------| +| `[x]` | completed | +| `[~]` | in_progress | +| `[ ]` | pending | +| `[!]` | blocked | + +Body shape: + +````markdown +### TaskList snapshot + +- [x] Profile update — done; unclear handles investigated. +- [~] Full scan — wave 1 initialized (0/178 complete). Drive the loop in the fresh session. +- [ ] Full pipeline run — collection → dedup → ranking → synthesis. + +### Reconstitute on resume + +Fresh session MUST recreate this TaskList before resuming. Run the TaskCreate calls in this exact +order (preserves numbering): + +```text +TaskCreate(subject="Profile update", description="...") → status=completed via TaskUpdate +TaskCreate(subject="Full scan", description="...") → status=in_progress via TaskUpdate +TaskCreate(subject="Full pipeline run", description="...") → status=pending (default) +``` +```` + +**Exception:** when 0 active tasks OR all `completed`, omit this section — nothing to reconstitute. + +## Full-path write procedure + +Write the file into the handoff location (SKILL.md "Where handoffs live"): + +```bash +TS=$(date -u +%Y%m%dT%H%M%SZ) # ISO basic — Windows-safe, no colons +TOPIC= # e.g. plan-rev2, retry-loop, post-merge +DIR=.claude/handoffs # or the consuming repo's documented location +SESSION_ID="${CLAUDE_CODE_SESSION_ID:-unknown}" + +# Candidate prior handoff (newest by timestamp) for the chain pointer — but +# only USE it when this session is a continuation of that handoff's task +# (see "Chain continuity" below). +PRIOR=$(ls -1 "$DIR"/*-handoff-*.md 2>/dev/null | sort | tail -1) +PRIOR_SID="" +if [[ -n "$PRIOR" ]]; then + PRIOR_SID=$(awk '/^session_id:/ {print $2; exit}' "$PRIOR") +fi + +mkdir -p "$DIR" +# Write: $DIR/${TS}-handoff-${TOPIC}.md +``` + +**Frontmatter shape:** + +```yaml +--- +type: handoff +date: # date -u +%Y-%m-%dT%H:%M:%SZ +topic: +session_id: # REQUIRED — $CLAUDE_CODE_SESSION_ID at write time; + # literal "unknown" when unset +previous_handoff: .md # CONDITIONAL — omit when no prior handoff exists +previous_session_id: # CONDITIONAL — omit when no prior handoff exists +--- +``` + +`session_id` captures the current session for downstream chain-walkers (the sibling `retro` skill's +transcript parser). `previous_handoff` (the prior file's name, relative to the handoff directory) + +`previous_session_id` create the backward chain pointer. + +**Chain continuity — same task only.** Emit `previous_handoff` / `previous_session_id` ONLY when +this session actually continued the prior handoff's work: it resumed from that handoff (the resume +prompt loaded it), or the task/topic clearly matches. A shared handoff directory accumulates +entries from unrelated tasks — pointing at the newest file regardless would splice unrelated +sessions into one chain, and a later `/retro` would aggregate stale transcripts and decisions as if +they belonged to the current work. The first handoff of a NEW task has neither field, even when +older, unrelated handoffs exist in the directory. Older entries lacking `session_id` cause +chain-walkers to break cleanly at the first absent field. + +`CLAUDE_CODE_SESSION_ID` is set in the Bash tool subprocess (Claude Code v2.1.132+). Resolve it via +Bash — skill markdown does not template-expand env vars. + +Multiple handoffs accumulate in the directory — fine; ISO timestamps keep them ordered, and the +newest entry is the resume point. The newest handoff reads the previous one's "Next steps" as its +"Progress" starting point. diff --git a/plugins/session-flow/skills/orchestration-brief/SKILL.md b/plugins/session-flow/skills/orchestration-brief/SKILL.md new file mode 100644 index 000000000..41b537d6e --- /dev/null +++ b/plugins/session-flow/skills/orchestration-brief/SKILL.md @@ -0,0 +1,108 @@ +--- +name: orchestration-brief +description: "Arm the CURRENT session for an orchestration-heavy task by loading six proactive-orchestration imperatives (delegate/fan-out, spec-every-spawn, fresh-context verify, run-workers-well, nested subagents, surface drift) as active standing instructions; optionally export them as a paste-ready brief for a spawned worker or fresh session. Use when: 'orchestration brief', 'prime this session', 'arm for orchestration', 'about to do heavy delegation', 'worker spawn prompt', 'delegation preamble'." +argument-hint: "[] | handoff [compact] | worker [compact]" +user-invocable: true +disable-model-invocation: false +--- + +## Purpose + +Invoking this skill **arms the current session** for an orchestration-heavy task: it loads the +expanded six-imperative operational form into active working context, and it declares deliberate +intent to orchestrate the work about to start, so the triggers get evaluated actively rather than +sitting passively in background rules. That is the default — no paste, no rails, just preloaded +context. + +The same imperatives also **export** as a self-contained, paste-ready brief for targets that LEAVE +the session and therefore inherit none of its context: a spawned subagent/teammate, a fresh session +you will `/clear` into, or a non-Claude-Code tool. Export is model- and tool-agnostic by +construction — nothing in the pasted text depends on a specific model, env var, or repo file. + +The official sources and quotes behind each imperative: `context/sources.md` (read only when +judging a coverage question or extending the skill). + +## Actions + +| Action | What it does | +|---|---| +| *(default — optional ``)* | **Prime THIS session.** The standing instructions below are now active for the upcoming task; respond with a terse acknowledgment and, if `` is given, one line orienting to it. Do NOT re-emit the imperatives — loading them IS the priming. | +| `handoff [compact]` | **Export** the imperatives as a paste-ready dashed-rail brief framed for a fresh session. `compact` = headlines only. | +| `worker [compact]` | **Export** framed for a spawned worker (prepends the did-not-inherit-context line). `compact` = headlines only. | + +## Orchestration imperatives — standing instructions + +At each decision boundary in this task, evaluate these and ACT on a match without waiting to be +told: + +1. DELEGATE / FAN OUT — start with one agent (a single agent goes further than you expect); + delegate only when work would flood context, fans across genuinely independent paths, or needs a + tool-restricted specialist. Decompose by what CONTEXT each piece needs, not by head-count or + work-type — sequential or shared-context steps stay in one agent. Coding parallelizes less than + research: never split one feature across agents. Multi-agent costs 3–10× the tokens (returns + cost context too), so spend it on value + parallelism, not convenience. +2. SPEC EVERY SPAWN — give each worker an objective, an output format, the tools/sources to use, + and explicit task boundaries. Vague delegation makes workers duplicate each other, leave gaps, + or wander. +3. FRESH-CONTEXT VERIFY — after an edit batch or a finding set, hand it to a SEPARATE verifier; + never self-audit in the context that produced it. Give the verifier concrete pass/fail criteria + ("run the full suite, report all failures"), scope it to correctness/requirements (not style), + and judge the final STATE, not the process — an uncriteriaed verifier just rubber-stamps. +4. RUN WORKERS WELL — prefer non-blocking dispatch: keep working while independent workers run. + Reuse a long-lived worker across subtasks when your runtime supports it (saves cost via cache). + Watch running workers and intervene the moment one drifts or is missing context. +5. NESTED SUBAGENTS — a worker may spawn its own workers when a delegated task itself subdivides + AND the depth is non-load-bearing. This is a shipped feature, not experimental — but reliability + degrades with depth and platforms cap it, so never author a tree that needs a specific or deep + nesting level. +6. SURFACE DRIFT — the moment you notice a stale reference, broken citation, or convention + conflict adjacent to your task, flag it in one line; don't fix it silently, don't deep-dive. + +Discipline: trigger-evaluation is mandatory; the ACTION stays calibrated (delegate on value + +parallelism, not convenience). Treat every worker's return as unverified synthesis — verify +load-bearing claims against a primary source before acting. Cite sources you actually fetched; +never label a claim "known" / "from memory" / "obvious". + +**Priming addendum (current session only).** As the main session — not a spawned worker — you may +also reach orchestration surfaces a worker cannot: agent teams (lead-only) and dynamic workflows +(main-session-only). The export modes omit this line because a pasted target cannot reach those +surfaces. + +## Export modes (handoff / worker) — paste-ready brief + +Only for a target that LEAVES the session. Emit the six imperatives above between two full-width +`─` (U+2500) dashed rails — top rail, brief, bottom rail, nothing else between them; the +`/clear`/paste instruction or any commentary sits above the top rail or below the bottom rail, +never between (NOT a code fence — the user copies the text between the rails, not fence markers). + +Live shape: bare `─` rails, no fence — shown inside a fence here for display only. + +```text +────────────────────────────────────────────────────────── +ORCHESTRATION BRIEF — standing instructions for the whole task, regardless of which model or tool runs you. + +At each decision boundary, evaluate these and ACT on a match without waiting to be told: +[the six numbered imperatives above, verbatim] + +Discipline: [the Discipline line above, verbatim] +────────────────────────────────────────────────────────── +``` + +- `handoff` — the opening line above already fits a fresh session; emit as-is. +- `worker` — insert as the FIRST line between the rails: `You are a spawned worker and did NOT + inherit the parent session's context or the repo's conditional rules — these instructions are + your only copy.` +- `compact` — emit only the six numbered HEADLINES (`1. DELEGATE / FAN OUT`, `2. SPEC EVERY + SPAWN`, …) plus the closing Discipline line; drop every sub-clause. + +## What this skill does NOT do + +- **Default does not emit paste-text.** Priming the current session is a terse acknowledgment — + the work happens because the imperatives loaded into context, not because anything was printed. + Use `handoff` / `worker` only when the target LEAVES the session. +- **Not a surface-selection guide.** Which parallel-execution surface to pick (subagents vs nested + vs teams vs workflows) is a judgment the main session makes against current official docs; the + export brief deliberately omits agent teams + dynamic workflows because a spawned worker cannot + reach either. +- **Does not delegate, verify, nest, or spawn anything itself** — it arms the session or emits + instruction text. diff --git a/plugins/session-flow/skills/orchestration-brief/context/sources.md b/plugins/session-flow/skills/orchestration-brief/context/sources.md new file mode 100644 index 000000000..90f43344c --- /dev/null +++ b/plugins/session-flow/skills/orchestration-brief/context/sources.md @@ -0,0 +1,90 @@ +# Sources behind the orchestration brief + +Official sources backing each imperative in the brief. **URLs are authoritative; fetch them to +confirm.** Lines marked *(paraphrase)* are summarizer renderings captured during research +(2026-06-14), concept-faithful but not byte-exact — re-fetch the URL for verbatim wording. Lines +marked *(verbatim, verified)* were confirmed against the raw doc at capture time. + +## Imperative 1 — DELEGATE / FAN OUT + +- **Start simple; a single agent goes far.** "Start with the simplest approach that works, and add + complexity only when evidence supports it"; "A well-designed single agent with appropriate tools + can accomplish far more than many developers expect." *(paraphrase)* — + +- **Decompose by context boundary, not work type.** "Group work by what context it requires, not + by what kind of work it is"; sequential phases of one feature "share too much context." + *(paraphrase)* — same URL +- **Coding is less parallelizable than research.** "Most coding tasks involve fewer truly + parallelizable tasks than research." *(paraphrase)* — + +- **Cost multipliers.** Multi-agent "typically use 3–10× more tokens than single-agent approaches"; + the research system reports ~4× per agent vs chat and ~15× for multi-agent; "token usage by + itself explains 80% of the variance." *(paraphrase)* — both URLs above +- **Use multi-agent only for context-protection / parallelization / specialization; outside these + "coordination costs typically exceed the benefits."** *(paraphrase)* — + building-multi-agent-systems (URL above) + +## Imperative 2 — SPEC EVERY SPAWN + +- "Each subagent needs an objective, an output format, guidance on the tools and sources to use, + and clear task boundaries." Without it, agents "duplicate work, leave gaps, or fail to find + necessary information." *(paraphrase)* — + +- Scale effort to complexity: "Simple fact-finding requires just 1 agent with 3–10 tool calls … + complex research might use more than 10 subagents." *(paraphrase)* — same URL + +## Imperative 3 — FRESH-CONTEXT VERIFY + +- **Fresh context beats self-review.** A reviewer "running in a fresh subagent context sees only + the diff and the criteria you give it, not the reasoning that produced the change." + *(paraphrase)* — +- **Verifier needs explicit criteria or it rubber-stamps.** "A verifier told only to check whether + output is good, with no further criteria, will rubber-stamp the generator's output"; specify + "Run the full test suite and report all failures" rather than "make sure it works." + *(paraphrase)* — + best-practices + (URL above) +- **Scope the reviewer.** "Tell the reviewer to flag only gaps that affect correctness or the + stated requirements." *(paraphrase)* — best-practices (URL above) +- **Judge final state, not process.** "Evaluate whether it achieved the correct final state" + rather than "whether the agent followed a specific process." *(paraphrase)* — + +- Fable-5 verifier guidance (verbatim, verified): "Separate, fresh-context verifier subagents tend + to outperform self-critique." — + + +## Imperative 4 — RUN WORKERS WELL + +All three sub-behaviors are from the Fable 5 prompting guide (verbatim, verified) — +: + +- **Async over blocking:** "prefer asynchronous communication between orchestrator and subagents + over blocking until each subagent returns"; "Delegate independent subtasks to subagents and keep + working while they run." +- **Long-lived subagents:** "Long-lived subagents that keep their context across subtasks save + time and cost through cache reads and avoid bottlenecking on the slowest subagent." +- **Monitor and steer:** "Intervene if a subagent goes off track or is missing relevant context." + +The brief states these model-agnostically on purpose: they are correct standing imperatives for an +under-delegating model too. + +## Imperative 5 — NESTED SUBAGENTS + +Verbatim, verified against ("Spawn nested subagents", +min-version 2.1.172): + +- Shipped, **not** experimental: "As of Claude Code v2.1.172, a subagent can spawn its own + subagents." +- Gating: "listing `Agent` in `tools` lets that subagent spawn nested subagents"; "If `Agent` is + omitted … the agent cannot spawn any subagents." +- Depth: foreground "can spawn at any depth … self-limiting"; background "a background subagent at + depth five does not receive the Agent tool and cannot spawn further. The limit is fixed and not + configurable." + +The brief's "never author a tree that needs a specific or deep nesting level" is justified by +reliability degradation with depth plus the platform caps above. + +## Imperative 6 — SURFACE DRIFT + +Authoring convention, NOT canonical Anthropic orchestration guidance (it appears in none of the +multi-agent sources). Kept in the brief because drift-flagging is useful for any worker: a one-line +flag preserves the signal without derailing the task. diff --git a/plugins/session-flow/skills/retro/SKILL.md b/plugins/session-flow/skills/retro/SKILL.md new file mode 100644 index 000000000..47b238a24 --- /dev/null +++ b/plugins/session-flow/skills/retro/SKILL.md @@ -0,0 +1,114 @@ +--- +name: retro +description: "Run a structured session retrospective: extract transcript metrics, assess quality across five dimensions, check feedback-memory regressions, and codify learnings durably. Use when: 'retro', 'retrospective', 'what did we learn', 'how did I do', 'codify learnings', 'show trends', or at end of session; modes: session (default), codify, trends, quick." +argument-hint: "[mode] (e.g., /retro, /retro session, /retro codify, /retro trends, /retro quick)" +user-invocable: true +disable-model-invocation: false +--- + +## Pre-computed context + +Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` +Claude session: !`echo "${CLAUDE_CODE_SESSION_ID:-unknown}"` +Recent commits: !`git log --oneline -5 2>/dev/null || echo "no commits"` +Working tree status: !`git status --porcelain 2>/dev/null | head -20 || echo "clean"` +Changed files (staged+unstaged): !`git diff --name-only HEAD 2>/dev/null || echo "none"` + +## Purpose + +The self-improvement loop. Answers: "What happened, what did we learn, and how do we prevent the +same mistakes next time?" Every other workflow stage is about the current task; this skill is about +the next task — and every task after that. + +**Three concepts this skill enforces:** + +1. **Analyze** — examine what happened with evidence (transcript metrics, conversation context, + feedback regressions) +2. **Identify** — find errors, behavioral adjustments, process improvements, and skill/tool + candidates +3. **Codify** — persist learnings durably (the consuming repo's instruction files and rules, or + Claude Code auto-memory for contributor-specific facts) + +**What this skill is NOT:** not a code review (design judgment on current work), not outcome +verification (does the change match intent), and not Claude Code's built-in `/insights` +(cross-session usage analytics) — this is structured quality analysis with codification. + +## Paths + +Resolve at runtime — never hardcode machine-specific paths: + +- **Session data root** — `~/.claude/projects//`, where `` is the + project's absolute path with every character outside `[A-Za-z0-9]` replaced by `-`: + + ```bash + PROJECT_SLUG=$(pwd -W 2>/dev/null || pwd) # Windows drive form when available + PROJECT_SLUG=$(printf '%s' "$PROJECT_SLUG" | sed 's/[^A-Za-z0-9]/-/g') + SESSION_DATA_DIR="$HOME/.claude/projects/$PROJECT_SLUG" + ``` + + Verify the directory exists before use; if the computed slug misses, find it by locating the + current session's JSONL: `ls "$HOME/.claude/projects"/*/"${CLAUDE_CODE_SESSION_ID}.jsonl"`. +- Transcript: `/.jsonl`; subagents: + `//subagents/` +- **Auto-memory** (feedback regression check): `/memory/` — present only when the + consumer uses Claude Code auto-memory; degrade gracefully when absent +- **Score history** (plugin state): `${CLAUDE_PLUGIN_DATA}/scores/.md` — survives + plugin updates, never lands in the consumer's repo +- Parser: `${CLAUDE_PLUGIN_ROOT}/skills/retro/scripts/parse_transcript.py` (stdlib-only, + Python 3.10+) + +## Step 0: Detect mode + +| Signal | Mode | Context file | +|--------|------|-------------| +| End of session, bare `/retro`, post-merge | **session** | `context/session.md` — full 5-phase analysis | +| "codify", "save learnings", mid-session learning | **codify** | `context/codify.md` — targeted codification only | +| "trends", "scores", "how am I doing" | **trends** | `context/trends.md` — cross-session score history | +| "quick retro", short session, limited context | **quick** | `context/quick.md` — abbreviated pass | + +If `$ARGUMENTS` specifies a mode, use it. Otherwise infer from context; when context is >75% used +or compaction has occurred, prefer `quick`; ambiguous → `session`. Read the mode's context file +before proceeding. + +## Step 1: Execute the mode + +Follow the selected context file. Each mode has its own phases, outputs, and interactive +checkpoints. + +## Step 2: Handoff + +After the retrospective: + +| Condition | Suggestion | +|-----------|-----------| +| End-of-session, retro complete | Suggest any wrap-up steps the consuming repo defines | +| Codify mode, learnings saved | Return to the task at hand | +| Trends mode, analysis presented | Suggest focus areas for next session | +| Session mode, follow-ups queued | Suggest filing them in the consumer's work-item tracker | + +## Multi-session awareness + +When the sibling `handoff` skill's save-points exist (`.claude/handoffs/` or the consuming repo's +documented location), the retro spans the whole session CHAIN, not just the current session: the +parser's `--chain-from` walks `previous_handoff` / `previous_session_id` frontmatter pointers +backwards from the newest handoff file and aggregates metrics across every chained transcript. See +`context/session.md` Phase 1. + +## What this skill does NOT do + +- **Does not run builds or tests** — that's the consuming repo's verify stage +- **Does not review code quality** — that's its review stage +- **Does not write scores or reports into the consumer's repo** — plugin state stays in + `${CLAUDE_PLUGIN_DATA}`; only user-approved codifications (rule edits, memory entries) land + outside it + +## Gotchas + +- **Run all phases by default** in session mode — skip metrics only when the parser fails or the + user asks +- **Always include skill-candidate and follow-up-candidate analysis** — even when the conclusion is + "no candidates this session" +- **Codify follows the workflow too** — adding a bullet to a rules file requires verification, not + just pasting +- **Phase 4 is an interactive checkpoint** — never persist codifications without explicit user + approval diff --git a/plugins/session-flow/skills/retro/context/codify.md b/plugins/session-flow/skills/retro/context/codify.md new file mode 100644 index 000000000..a2284e38f --- /dev/null +++ b/plugins/session-flow/skills/retro/context/codify.md @@ -0,0 +1,63 @@ +# Codify Mode — Targeted Learning Capture + +Persist specific learnings from the current session without running the full retrospective. Use +mid-session when a valuable learning emerges, or any time something should be saved before it is +lost to context compaction. + +## When to use + +- A gotcha was discovered that future sessions need to know +- A convention was established through implementation that should be documented +- A correction happened that should prevent the same mistake next time +- Tool/library behavior was verified that should be recorded +- The user says "remember this" or "save this learning" + +## Process + +### 1. Identify what to codify + +Scan the recent conversation for learnings: + +| Category | Target | Example | +|----------|--------|---------| +| Behavioral correction | Auto-memory feedback entry | "Always verify API versions before using features" | +| Convention established | The repo's rules file or `CLAUDE.md` | "This repo uses pattern X, not Y" | +| External reference | Auto-memory reference entry | "Framework testing docs at " | +| Project context | Auto-memory project entry | "Middleware rewrite driven by compliance, not tech debt" | +| Tool/API discovery | The repo's rules gotcha section | "Flag Z breaks the test runner" | + +### 2. Apply the placement decision tree + +1. Would another contributor on a fresh clone need this? → **project** (the repo's tracked + instruction files) +2. Does it protect the accuracy of a git-tracked artifact? → **project** (in the artifact) +3. Is it about how this specific user wants the agent to behave? → **personal** (feedback memory) +4. Is it about the user's role or expertise? → **personal** (user memory) +5. Is it about ongoing work status? → **personal** (project memory) +6. Is it a pointer to external information? → **personal** (reference memory) + +When in doubt, prefer project scope — a tracked rule is reviewable and portable; a personal memory +is neither. + +### 3. Verify before persisting + +Every codification is itself a technical claim: + +- **Memory entries**: verify content is accurate against current codebase state; update an + existing entry rather than duplicating; delete entries this session's evidence falsified +- **Rules / CLAUDE.md edits**: verify the claim (even if observed in conversation), cross-reference + existing content for consistency and duplication + +### 4. Present and confirm + +Present proposed codifications grouped by scope (personal vs project) as tables with a one-line +content summary each. Ask for approval before executing. Then return to the current work. + +## What this mode does NOT do + +- No transcript metrics, no behavioral assessment or scoring +- No feedback regression history check +- No skill/follow-up candidate generation +- No health score + +It's surgical: identify, verify, persist, return. diff --git a/plugins/session-flow/skills/retro/context/quick.md b/plugins/session-flow/skills/retro/context/quick.md new file mode 100644 index 000000000..64f382bdd --- /dev/null +++ b/plugins/session-flow/skills/retro/context/quick.md @@ -0,0 +1,61 @@ +# Quick Mode — Abbreviated Retrospective + +Lightweight retrospective when the full 5-phase analysis isn't appropriate. Use when context is +limited (post-compaction, short session), or the user explicitly requests a quick pass. + +## When to use + +- Context window >75% used or compaction has occurred +- Short session (single task, <30 minutes) +- User says "quick retro" or "abbreviated retro" +- Post-merge when a full retro would exceed the remaining context budget + +## Process + +### 1. Skip metrics extraction + +Do NOT run the parser. Use conversation context only. + +### 2. Behavioral quick-check + +Assess against the staged workflow as a checklist — not full dimensional analysis: + +| Stage | Done? | Note | +|------|-------|------| +| 1. Explore | Yes/No/Partial | (one line) | +| 2. Research | Yes/No/Partial | | +| 3. Plan | Yes/No/Partial/N/A | | +| 4. Implement | Yes/No/Partial | | +| 5. Test | Yes/No/Partial/N/A | | +| 6. Review | Yes/No/Partial | | +| 7. Verify | Yes/No/Partial | | +| 8. Retro | In progress | (this) | + +### 3. Top findings (max 3) + +Only errors, regressions, or significant behavioral gaps — skip minor issues. + +### 4. Recommendations (max 3) + +Highest-priority only, same format as session mode Phase 3 but capped. + +### 5. Quick score + +> **Session score: X/10** — (one sentence justification) + +Append to the score history (`${CLAUDE_PLUGIN_DATA}/scores/.md`) using the session- +mode format. + +### 6. Feedback regression spot-check + +If auto-memory exists, read up to 10 recent `feedback_*.md` files (not all — budget constraint) and +flag any regression prominently. + +## What this mode does NOT do + +- No parser run (no Phase 1), no full 5-dimension analysis +- No skill/follow-up candidate generation (unless something jumps out) +- No interactive Phase 4 approval gate — present recommendations and execute approved items + directly + +It's fast: scan, flag, score, move on. diff --git a/plugins/session-flow/skills/retro/context/session.md b/plugins/session-flow/skills/retro/context/session.md new file mode 100644 index 000000000..6ca4979c9 --- /dev/null +++ b/plugins/session-flow/skills/retro/context/session.md @@ -0,0 +1,251 @@ +# Session Mode — Full 5-Phase Retrospective + +Comprehensive post-session analysis. Default mode and most thorough — use at end of session or +after a PR merges. + +## Phase 1: Extract (automated metrics) + +> Skip this phase if the user explicitly requests it, or if the parser errors (exit 2) — report the +> error and continue with conversation-context analysis only. + +### Phase 1.0: Discover the session chain (multi-session-aware) + +When handoff save-points exist (the sibling `handoff` skill's directory — `.claude/handoffs/` or +the consuming repo's documented location), the retro analyzes EVERY chained session across +`/handoff` + `/clear` cycles, not just the current one. The parser walks the chain itself via +`--chain-from` (newest handoff file → its `previous_handoff` pointer → repeat; breaks cleanly at +the first entry lacking `session_id`). + +**Continuity gate first.** Use `--chain-from` ONLY when the newest handoff belongs to the current +work: this session resumed from it (the resume prompt loaded it), this session wrote it, or its +`topic`/Task section clearly matches the current task. A shared directory can hold save-points +from completed or abandoned tasks — chaining from an unrelated newest file would splice stale +sessions into this retro's aggregate. When continuity is absent or unclear, fall back to the +single-session form. + +### Phase 1.1: Parse the transcript(s) + +Resolve `SESSION_DATA_DIR` per SKILL.md "Paths", then: + +```bash +PARSER="${CLAUDE_PLUGIN_ROOT}/skills/retro/scripts/parse_transcript.py" + +# Pick an interpreter that is actually Python 3.10+ (a bare `python` may be older): +PY="" +for c in python3 python; do + if command -v "$c" >/dev/null 2>&1 \ + && "$c" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)' 2>/dev/null; then + PY="$c"; break + fi +done + +# Single-session form: +"$PY" "$PARSER" --sessions "${CLAUDE_CODE_SESSION_ID}" --base "$SESSION_DATA_DIR" + +# Multi-session form (handoff chain exists). Set HANDOFF_DIR to the consuming +# repo's documented save-point location when it declares one (see the handoff +# skill's "Where handoffs live"); the plugin default is .claude/handoffs/. +HANDOFF_DIR=.claude/handoffs +NEWEST=$(ls -1 "$HANDOFF_DIR"/*-handoff-*.md 2>/dev/null | sort | tail -1) +"$PY" "$PARSER" --chain-from "$NEWEST" --current-session "${CLAUDE_CODE_SESSION_ID}" --base "$SESSION_DATA_DIR" +``` + +If `PY` resolves empty (no Python 3.10+ available), skip metrics extraction and note why. The +parser is stdlib-only. + +### Script contract + +JSON to stdout: `status` / `summary`, plus per-session `data` (session info, turns, tokens, tool +usage + rejections, compactions, turn durations, stop reasons, files modified, subagents, errors) +and — in multi-session form — an `aggregate` block. Exit codes: 0 = success, 1 = warning, +2 = error. + +### Present metrics + +Format as two GFM tables — **Session Summary** (duration, model, assistant turns, human messages, +compactions, total context tokens, tool rejections, subagent count) and **Tool Distribution** +(tool / count / %, sorted descending). + +--- + +## Session type detection + +Before analysis, identify the session type from conversation context — it calibrates Phase 5 +scoring: + +- **Coding** — code changes made. Score Technical quality on code quality +- **Planning/Design** — architecture decisions, documentation, API design. Score on design + reasoning and decision quality +- **Research** — investigation, comparison, learning. Score on research rigor and conclusion + quality +- **Mixed** — score each task individually, then aggregate + +--- + +## Phase 2: Analyze (qualitative assessment) + +Analyze across five dimensions using BOTH Phase 1 metrics AND conversation context. Derive the +convention baseline from the consuming repo's own instruction files: its `CLAUDE.md`, the +`.claude/rules/` files relevant to the ecosystems touched this session, and any review-criteria +docs it names. Read only the relevant ones. + +### 2A. Error analysis + +- Mistakes made and corrected; failed approaches and wasted cycles +- Incorrect assumptions that had to be revised +- Build or test failures caused by changes +- Stale information used without verification + +### 2B. Behavioral assessment + +Check adherence to the staged workflow (the sibling `workflow` skill, or the consuming repo's own +documented workflow if it defines one): + +- Which stages were followed? Which were skipped, and was the skip justified? +- Was research performed for load-bearing claims, with current authoritative sources? +- Was a plan written and approved for non-trivial work? Stress-tested when blast radius was wide? +- Was uncertainty flagged when verification wasn't possible? + +### 2C. Feedback regression check + +One of the most valuable parts — prevents repeating previously corrected mistakes. If the consumer +uses Claude Code auto-memory (`/memory/` exists), read the `feedback_*.md` files +and check whether this session violated any saved guidance: + +| Memory file | Violated? | Evidence | +| --- | --- | --- | +| `feedback_example.md` | YES / No / N/A | (specific session behavior) | + +Flag regressions prominently — a regression means a previously corrected behavior has resurfaced. +No memory directory → note "auto-memory not in use" and move on. + +### 2D. Technical assessment + +Evaluate code changes (if any) against the consuming repo's own engineering conventions. If no +code changes were made, note "N/A" and skip. + +### 2E. Efficiency assessment + +- Compaction count — were compactions avoidable (earlier `/handoff`, tighter reads)? +- Parallel tool-call opportunities missed; redundant file reads +- Subagent usage — appropriate delegation? +- Longest/slowest turns — what caused them? + +### Phase 2 output + +Present findings as a GFM table per dimension: + +| # | Severity | Finding | Evidence | Impact | +| --- | --- | --- | --- | --- | + +--- + +## Phase 3: Recommend (improvements) + +Map each Phase 2 finding to an improvement target. Also identify improvements not tied to specific +findings. + +**Research before recommending.** For any recommendation involving skills, hooks, agents, or Claude +Code configuration: verify it against current official docs before presenting — never recommend +features from training-data assumptions. + +Present as a GFM table with a **Scope** column distinguishing: + +- **project** — git-tracked, shared with the team (the repo's `CLAUDE.md`, rules, skills, settings) +- **personal** — machine-specific, NOT committed (auto-memory, user settings) + +| # | Target | Scope | Type | Recommendation | Justification | Priority | +| --- | --- | --- | --- | --- | --- | --- | + +### Skill candidate analysis (REQUIRED — always include) + +Evaluate whether the session revealed a genuinely repeatable multi-step workflow worth +encapsulating as a skill: + +| Factor | Minimum for a skill | Skip if | +| --- | --- | --- | +| Steps | 3+ distinct phases | Linear, 1-2 step process | +| Reuse | Likely monthly+ | Truly one-off | +| Complexity | Requires judgment or branching | Simple command alias | +| Context | Needs reference files or rubrics | Self-evident workflow | + +Always present the subsection — either candidate(s) with name/description/rationale, or "no +candidates" with a one-line explanation of what was considered. + +### Follow-up candidates (REQUIRED — always include) + +Evaluate whether the session produced follow-up work for the consumer's work-item tracker: +deferred research, discovered gaps (missing tests, undocumented conventions), research context +worth preserving. Present as a table, or "no candidates — session work was self-contained." + +--- + +## Phase 4: Act (with user approval) + +Group Phase 3 recommendations by action type, then **explicitly ask the user** which items to +execute — do not proceed without their response. + +- **Personal (not committed):** proposed auto-memory entries — create only on approval +- **Project (already validated this session):** rule/instruction-file updates codifying what + HAPPENED (a gotcha discovered through failures, a convention established through implementation). + Apply on approval +- **Queue for follow-up (needs further research):** recommendations beyond what this session + validated — list them; do NOT make those changes now + +Apply the team-shared-first lens: if a learning generalizes to ANY contributor, it belongs in a +tracked surface (the repo's instruction files), not personal memory. Reserve auto-memory for facts +true only for this machine/person. + +**Every approved codification follows the workflow** — verify the claim, cross-reference existing +content for duplication, then edit. No "just save it" shortcut. + +End Phase 4 with an explicit question, e.g.: "Which of these recommendations should I execute now? +Reply with the numbers, 'all', or 'skip' to proceed to the summary." + +--- + +## Phase 5: Summary + +### Accomplishments + +1-3 bullets of what the session achieved. + +### Key learnings + +- **Behavioral:** what to do differently next time +- **Technical:** patterns learned, gotchas discovered + +### Actions taken + +Checklist of memory saved / rules edited / items queued. + +### Session health score + +**Calibration:** read the score history at `${CLAUDE_PLUGIN_DATA}/scores/.md` if it +exists and note trends alongside this session's scores. + +| Dimension | Score | Notes | +| --- | --- | --- | +| Workflow adherence | /10 | | +| Technical quality | /10 | calibrate to session type | +| Alignment | /10 | convention compliance + memory utilization | +| Efficiency | /10 | | +| Error rate | /10 | 10 = no errors | +| **Overall** | **/10** | weighted average | + +**Scoring anchors** (consistency across sessions): 9-10 exemplary (all stages followed, no errors, +feedback respected); 7-8 good (minor gaps); 5-6 mixed (some stages skipped, recoverable errors); +3-4 below standard (multiple skips, regressions, avoidable errors); 1-2 poor (fundamental process +failures or incorrect code shipped). + +Every dimension gets a numeric score; use N/A only when truly irrelevant. + +### Score tracking + +Append this session's scores to `${CLAUDE_PLUGIN_DATA}/scores/.md` (create the +directory and file with a header row on first use): + +```markdown +| Date | Session | Type | Workflow | Technical | Alignment | Efficiency | Errors | Overall | +| 2026-03-21 | session-id-short | Coding | 8 | 7 | 8 | 8 | 9 | 8 | +``` diff --git a/plugins/session-flow/skills/retro/context/trends.md b/plugins/session-flow/skills/retro/context/trends.md new file mode 100644 index 000000000..a93723e1d --- /dev/null +++ b/plugins/session-flow/skills/retro/context/trends.md @@ -0,0 +1,60 @@ +# Trends Mode — Cross-Session Performance Analysis + +Analyze historical session health scores to identify patterns, improvements, and areas needing +attention. No current-session analysis — purely retrospective across sessions. + +## Data source + +Score history persisted by session/quick mode: + +```text +${CLAUDE_PLUGIN_DATA}/scores/.md +``` + +Format: `| Date | Session | Type | Workflow | Technical | Alignment | Efficiency | Errors | Overall |` + +## Process + +### 1. Load and parse the score history + +If the file doesn't exist, report "No historical scores found. Run `/retro session` to build +history." and exit. + +### 2. Compute aggregate statistics + +| Dimension | Mean | Median | Min | Max | Trend (last 10) | +|-----------|------|--------|-----|-----|-----------------| +| Workflow | | | | | improving/stable/declining | +| Technical | | | | | | +| Alignment | | | | | | +| Efficiency | | | | | | +| Errors | | | | | | +| **Overall** | | | | | | + +### 3. Identify patterns + +- **Strongest dimensions** — consistently 8+, established habits +- **Weakest dimensions** — consistently below 7, need focus +- **Volatility** — high variance suggests inconsistent application +- **Session type correlation** — do scores vary by session type? +- **Time trends** — improving, stable, or declining over the recorded span? + +### 4. Generate actionable insights + +Suggest 2-3 specific focus areas for the next session: + +| # | Focus area | Evidence | Suggested action | +|---|-----------|----------|-----------------| + +### 5. Notable sessions + +Highlight outliers — best sessions (overall 9+, what made them great) and worst (overall <6, what +went wrong). + +## What this mode does NOT do + +- No current-session analysis, no transcript metrics +- No memory writes or rule edits +- No skill/follow-up candidate generation + +It's analytical: load data, find patterns, suggest focus areas. diff --git a/plugins/session-flow/skills/retro/scripts/parse-transcript.test.sh b/plugins/session-flow/skills/retro/scripts/parse-transcript.test.sh new file mode 100755 index 000000000..f70c73cec --- /dev/null +++ b/plugins/session-flow/skills/retro/scripts/parse-transcript.test.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Contract tests for parse_transcript.py — delegates to the pytest suite. +# +# SKIPs (exit 0) when Python 3.10+ or pytest is unavailable, matching the +# repo test-runner convention for optional toolchains. +set -uo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1 + +PY="" +for candidate in python3 python; do + if command -v "$candidate" >/dev/null 2>&1; then + if "$candidate" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)' 2>/dev/null; then + PY="$candidate" + break + fi + fi +done + +if [[ -z "$PY" ]]; then + echo "SKIP: Python 3.10+ not found" + exit 0 +fi + +if ! "$PY" -c 'import pytest' 2>/dev/null; then + echo "SKIP: pytest not installed for $PY" + exit 0 +fi + +exec "$PY" -m pytest test_parse_transcript.py -q diff --git a/plugins/session-flow/skills/retro/scripts/parse_transcript.py b/plugins/session-flow/skills/retro/scripts/parse_transcript.py new file mode 100755 index 000000000..e2e2ecba5 --- /dev/null +++ b/plugins/session-flow/skills/retro/scripts/parse_transcript.py @@ -0,0 +1,805 @@ +#!/usr/bin/env python3 +"""Parse Claude Code JSONL transcript(s) and extract quantitative metrics. + +Zero external dependencies — stdlib only. + +Usage (single-session, backward compatible): + python parse_transcript.py + +Usage (multi-session, new): + python parse_transcript.py --sessions ... --base + python parse_transcript.py --chain-from --base + +Arguments: + session_id Single UUID session identifier (positional, backward compat) + base_path Base directory containing session JSONL files (positional) + --sessions Multiple UUIDs to parse + aggregate (first = current, rest = chained prior) + --chain-from Walk session chain via handoff frontmatter (previous_handoff / previous_session_id) + starting from the given handoff journal entry file; emits aggregated multi-session output + --base Base directory containing session JSONL files (required with --sessions / --chain-from) + --help Print help and exit 0 + +Output: + JSON to stdout. + + Single-session shape (existing — preserved): + {"status": "...", "summary": "...", "data": {...}} + + Multi-session shape: + { + "status": "...", + "summary": "...", + "sessions": [ + {"id": "", "role": "current"|"previous", + "transcript_present": true|false, "subagents_present": true|false, + "data": {...}, # or "error": "..." when transcript missing + }, + ... + ], + "aggregate": { + "total_assistant_turns": N, "total_human_messages": N, + "total_input_tokens": N, "total_output_tokens": N, + "total_compactions": N, "total_tool_rejections": N, + "all_tools": {"ToolName": count, ...}, + "all_subagents": [{"id", "type", "description", "session_id"}, ...], + "all_models": [...], "all_branches": [...], + }, + } + +Exit codes: + 0 = success (or partial chain — some SIDs missing transcripts, but at + least one transcript or subagent record found) + 1 = warnings (e.g., no transcript found for the only SID but subagents exist) + 2 = error (e.g., invalid arguments, no files at all — including a + multi-session parse where every SID resolves to nothing) +""" + +from __future__ import annotations + +import argparse +import json +import sys +from collections import Counter +from datetime import datetime +from pathlib import Path +from typing import Any, NoReturn, cast + + +def parse_timestamp(ts_str: str) -> datetime | None: + """Parse ISO 8601 timestamp string to datetime.""" + if not ts_str: + return None + try: + return datetime.fromisoformat(ts_str.replace("Z", "+00:00")) + except (ValueError, TypeError): + return None + + +# Tools whose `input.file_path` is a write target. Read/Grep/Glob also have +# file_path inputs but should not count as "files modified". +_FILE_MODIFYING_TOOLS = frozenset({"Write", "Edit", "MultiEdit", "NotebookEdit"}) + +# Truncation length for tool-rejection error snippets in the output. +_REJECTION_SNIPPET_LEN = 200 + + +def _normalize_path(path: str, cwd: str | None) -> str: + """Strip cwd prefix from absolute paths so relative and absolute references dedup.""" + if not path: + return path + + norm_path = path.replace("\\", "/").rstrip("/") + + if not cwd: + return norm_path + + norm_cwd = cwd.replace("\\", "/").rstrip("/") + if not norm_cwd: + return norm_path + + if norm_path == norm_cwd: + return "." + + prefix = norm_cwd + "/" + if norm_path.startswith(prefix): + return norm_path[len(prefix) :] + + return norm_path + + +def _as_dict(value: Any) -> dict[str, Any]: + """Return value as a dict when it is one, else an empty dict.""" + return cast(dict[str, Any], value) if isinstance(value, dict) else {} + + +def _as_list(value: Any) -> list[Any]: + """Return value as a list when it is one, else an empty list.""" + return cast(list[Any], value) if isinstance(value, list) else [] + + +def _count_tools_and_extract_paths(content: list[Any], metrics: dict[str, Any]) -> None: + """Single-pass: count tool uses and extract file paths from Write/Edit.""" + for raw_item in content: + if not isinstance(raw_item, dict): + continue + item = cast(dict[str, Any], raw_item) + if item.get("type") != "tool_use": + continue + name = str(item.get("name", "unknown")) + metrics["tool_usage"][name] += 1 + if name in _FILE_MODIFYING_TOOLS: + fp_candidate = _as_dict(item.get("input")).get("file_path") + if isinstance(fp_candidate, str) and fp_candidate: + metrics["files_modified"].add(fp_candidate) + + +def parse_main_transcript(filepath: Path) -> dict[str, Any] | None: + """Stream-parse main JSONL transcript and collect metrics.""" + metrics: dict[str, Any] = { + "assistant_turns": 0, + "user_turns": 0, + "human_messages": 0, + "tool_result_messages": 0, + "tool_usage": Counter(), + "tool_rejections": [], + "compactions": [], + "assistant_timestamps": [], + "turn_durations_ms": [], + "hook_summaries": [], + "errors": [], + "first_timestamp": None, + "last_timestamp": None, + "models": set(), + "stop_reasons": Counter(), + "git_branches": set(), + "versions": set(), + "files_modified": set(), + "cwd": None, + "total_input_tokens": 0, + "total_output_tokens": 0, + "cache_creation_tokens": 0, + "cache_read_tokens": 0, + "queued_messages": 0, + } + + if not filepath.is_file(): + return None + + with filepath.open(encoding="utf-8", errors="replace") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + loaded = json.loads(line) + except json.JSONDecodeError: + metrics["errors"].append( + { + "type": "parse_error", + "detail": "Malformed JSON line", + } + ) + continue + + if not isinstance(loaded, dict): + continue + + event = cast(dict[str, Any], loaded) + timestamp = event.get("timestamp") + if isinstance(timestamp, str) and timestamp: + if metrics["first_timestamp"] is None: + metrics["first_timestamp"] = timestamp + metrics["last_timestamp"] = timestamp + + if branch := event.get("gitBranch"): + metrics["git_branches"].add(branch) + if version := event.get("version"): + metrics["versions"].add(version) + if metrics["cwd"] is None and (cwd := event.get("cwd")): + # First cwd is session context; mid-session directory changes are ignored. + metrics["cwd"] = cwd + + match event.get("type"): + case "assistant": + metrics["assistant_turns"] += 1 + msg = _as_dict(event.get("message")) + + if isinstance(timestamp, str) and timestamp: + metrics["assistant_timestamps"].append(timestamp) + + if model := msg.get("model"): + metrics["models"].add(model) + + if stop := msg.get("stop_reason"): + metrics["stop_reasons"][stop] += 1 + + usage = _as_dict(msg.get("usage")) + metrics["total_input_tokens"] += usage.get("input_tokens", 0) + metrics["total_output_tokens"] += usage.get("output_tokens", 0) + metrics["cache_creation_tokens"] += usage.get( + "cache_creation_input_tokens", 0 + ) + metrics["cache_read_tokens"] += usage.get( + "cache_read_input_tokens", 0 + ) + + _count_tools_and_extract_paths( + _as_list(msg.get("content")), metrics + ) + + case "user": + metrics["user_turns"] += 1 + content_raw = _as_dict(event.get("message")).get("content", []) + + if isinstance(content_raw, str): + metrics["human_messages"] += 1 + continue + + for raw_item in _as_list(content_raw): + if isinstance(raw_item, str): + metrics["human_messages"] += 1 + continue + if not isinstance(raw_item, dict): + continue + item = cast(dict[str, Any], raw_item) + match item.get("type"): + case "tool_result": + metrics["tool_result_messages"] += 1 + if item.get("is_error"): + metrics["tool_rejections"].append( + { + "tool_use_id": item.get("tool_use_id", ""), + "snippet": str(item.get("content", ""))[ + :_REJECTION_SNIPPET_LEN + ], + } + ) + case "text": + metrics["human_messages"] += 1 + case _: + continue + + case "system": + match event.get("subtype", ""): + case "compact_boundary": + compact_meta = event.get("compactMetadata", {}) + metrics["compactions"].append( + { + "timestamp": timestamp, + "trigger": compact_meta.get("trigger", "unknown"), + "pre_tokens": compact_meta.get("preTokens", 0), + } + ) + + case "stop_hook_summary": + metrics["hook_summaries"].append( + { + "hook_count": event.get("hookCount", 0), + "prevented_continuation": event.get( + "preventedContinuation", False + ), + "has_output": event.get("hasOutput", False), + "hook_errors": event.get("hookErrors", []), + } + ) + case _: + pass + + case "queue-operation": + if event.get("operation") == "enqueue": + metrics["queued_messages"] += 1 + + case "file-history-snapshot": + backups = _as_dict( + _as_dict(event.get("snapshot")).get("trackedFileBackups") + ) + for fpath_key in backups: + metrics["files_modified"].add(str(fpath_key)) + + case _: + pass + + metrics["files_modified"] = { + _normalize_path(p, metrics["cwd"]) for p in metrics["files_modified"] + } + + metrics["turn_durations_ms"] = _compute_turn_deltas_ms( + metrics["assistant_timestamps"] + ) + + return metrics + + +def _compute_turn_deltas_ms(timestamps: list[str]) -> list[int]: + """Wall-clock deltas between consecutive assistant events in milliseconds. + + CC's system.turn_duration events are emitted sparsely (1-5 per session) + and don't reflect every assistant turn. Deriving from assistant-event + timestamps gives one delta per turn-pair (N-1 deltas for N turns) and + measures end-to-end cycle time (user wait + tool exec + agent thinking). + """ + deltas: list[int] = [] + prev: datetime | None = None + for ts in timestamps: + cur = parse_timestamp(ts) + if cur is None: + continue + if prev is not None: + delta_ms = round((cur - prev).total_seconds() * 1000) + if delta_ms >= 0: + deltas.append(delta_ms) + prev = cur + return deltas + + +def parse_subagents(subagent_dir: Path) -> list[dict[str, str]]: + """Parse subagent meta files for inventory.""" + if not subagent_dir.is_dir(): + return [] + + subagents: list[dict[str, str]] = [] + for meta_file in sorted(subagent_dir.glob("*.meta.json")): + try: + meta = json.loads(meta_file.read_text(encoding="utf-8", errors="replace")) + agent_id = meta_file.stem.replace(".meta", "") + subagents.append( + { + "id": agent_id, + "type": meta.get("agentType", "unknown"), + "description": meta.get("description", ""), + } + ) + except (json.JSONDecodeError, OSError): + continue + + return subagents + + +def compute_duration_stats(durations_ms: list[int]) -> dict[str, int]: + """Compute stats from a list of durations in milliseconds.""" + if not durations_ms: + return { + "count": 0, + "avg_ms": 0, + "min_ms": 0, + "max_ms": 0, + "total_ms": 0, + } + total = sum(durations_ms) + return { + "count": len(durations_ms), + "avg_ms": round(total / len(durations_ms)), + "min_ms": min(durations_ms), + "max_ms": max(durations_ms), + "total_ms": total, + } + + +def compute_session_duration( + first_ts: str | None, + last_ts: str | None, +) -> dict[str, str | float | None]: + """Compute session duration from first and last timestamp strings.""" + if not first_ts or not last_ts: + return {"start": None, "end": None, "duration_minutes": 0} + + start = parse_timestamp(first_ts) + end = parse_timestamp(last_ts) + + if not start or not end: + return {"start": None, "end": None, "duration_minutes": 0} + + duration = (end - start).total_seconds() / 60.0 + + return { + "start": start.isoformat(), + "end": end.isoformat(), + "duration_minutes": round(duration, 1), + } + + +def _emit_and_exit( + status: str, summary: str, data: dict[str, Any], code: int +) -> NoReturn: + print(json.dumps({"status": status, "summary": summary, "data": data})) + sys.exit(code) + + +def build_session_data( + session_id: str, metrics: dict[str, Any], subagents: list[dict[str, Any]] +) -> dict[str, Any]: + """Convert parsed metrics + subagents into the `data` block emitted in single-session + output AND embedded per-session in multi-session output.""" + session_duration = compute_session_duration( + metrics["first_timestamp"], metrics["last_timestamp"] + ) + turn_stats = compute_duration_stats(metrics["turn_durations_ms"]) + return { + "session": { + "id": session_id, + "start": session_duration["start"], + "end": session_duration["end"], + "duration_minutes": session_duration["duration_minutes"], + "models": sorted(metrics["models"]), + "versions": sorted(metrics["versions"]), + "git_branches": sorted(metrics["git_branches"]), + }, + "turns": { + "assistant": metrics["assistant_turns"], + "user": metrics["user_turns"], + "human_messages": metrics["human_messages"], + "tool_result_messages": metrics["tool_result_messages"], + }, + "tokens": { + "total_input": metrics["total_input_tokens"], + "total_output": metrics["total_output_tokens"], + "cache_creation": metrics["cache_creation_tokens"], + "cache_read": metrics["cache_read_tokens"], + "total_context": ( + metrics["total_input_tokens"] + + metrics["cache_creation_tokens"] + + metrics["cache_read_tokens"] + ), + }, + "queued_user_messages": metrics["queued_messages"], + "tools": { + "usage": dict(metrics["tool_usage"].most_common()), + "rejections": metrics["tool_rejections"], + "rejection_count": len(metrics["tool_rejections"]), + }, + "compactions": { + "count": len(metrics["compactions"]), + "details": metrics["compactions"], + }, + "turn_durations": turn_stats, + "stop_reasons": dict(metrics["stop_reasons"]), + "hook_summaries": { + "count": len(metrics["hook_summaries"]), + "prevented_continuations": sum( + 1 for h in metrics["hook_summaries"] if h["prevented_continuation"] + ), + "hook_errors": sum( + len(h["hook_errors"]) for h in metrics["hook_summaries"] + ), + }, + "files_modified": sorted(metrics["files_modified"]), + "subagents": subagents, + "errors": metrics["errors"], + } + + +def parse_one_session(session_id: str, base_path: Path) -> dict[str, Any]: + """Parse a single session's transcript + subagents. Returns either a fully + populated `data` block per build_session_data() OR an error stub + {"transcript_present": False, ...}.""" + main_jsonl = base_path / f"{session_id}.jsonl" + subagent_dir = base_path / session_id / "subagents" + + metrics = parse_main_transcript(main_jsonl) + subagents = parse_subagents(subagent_dir) + + if metrics is None: + return { + "id": session_id, + "transcript_present": False, + "subagents_present": bool(subagents), + "subagents": subagents, + "error": f"No transcript found at {main_jsonl}", + } + + data = build_session_data(session_id, metrics, subagents) + return { + "id": session_id, + "transcript_present": True, + "subagents_present": bool(subagents), + "data": data, + } + + +def _frontmatter_value(line: str) -> str: + """Extract a scalar frontmatter value: text after the first colon, with any + trailing inline YAML comment (` # ...`) and surrounding quotes stripped.""" + value = line.split(":", 1)[1].strip() + value = value.split(" #", 1)[0].strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}: + value = value[1:-1] + return value + + +def extract_chain_from_handoff( + handoff_file: Path, base_path: Path, limit: int = 1000 +) -> list[str]: + """Walk handoff journal-entry chain backwards via previous_handoff / previous_session_id + frontmatter fields. Returns ordered list of session-ids (current handoff's session_id + first, then prior). NOTE: callers separately prepend the current $CLAUDE_CODE_SESSION_ID + if they want it; this function only walks the chain starting from the given file. + + Forward-only convention: older entries lacking session_id break the walk cleanly. + Pointer cycles are detected via visited files and break the walk; `limit` is a + runaway safety bound far above any real chain — hitting it emits a stderr warning + rather than truncating silently.""" + sids: list[str] = [] + seen: set[str] = set() + visited_files: set[Path] = set() + cursor: Path | None = handoff_file + depth = 0 + + while cursor and cursor.is_file(): + resolved = cursor.resolve() + if resolved in visited_files: + print( + f"warning: handoff chain pointer cycle at {cursor}; stopping walk", + file=sys.stderr, + ) + break + visited_files.add(resolved) + if depth >= limit: + print( + f"warning: handoff chain longer than {limit} entries; " + "older sessions truncated", + file=sys.stderr, + ) + break + + sid = None + prev_handoff_rel = None + try: + with cursor.open(encoding="utf-8", errors="replace") as f: + # Read only the frontmatter (first --- block). + in_fm = False + for line in f: + line = line.rstrip("\n") + if line == "---": + if not in_fm: + in_fm = True + continue + break + if not in_fm: + continue + if line.startswith("session_id:"): + sid = _frontmatter_value(line) + elif line.startswith("previous_handoff:"): + prev_handoff_rel = _frontmatter_value(line) + except OSError: + break + + if sid and sid != "unknown" and sid not in seen: + sids.append(sid) + seen.add(sid) + + if not prev_handoff_rel: + break + # Resolve the pointer relative to the handoff file's own directory + # (flat layout: a sibling filename); fall back to the directory's + # parent for layouts whose pointers keep a subdir prefix + # (e.g. journal/). + candidate = cursor.parent / prev_handoff_rel + if not candidate.is_file(): + candidate = cursor.parent.parent / prev_handoff_rel + cursor = candidate + depth += 1 + + return sids + + +def build_multi_session_output( + session_ids: list[str], base_path: Path +) -> dict[str, Any]: + """Parse each session-id and aggregate.""" + if not session_ids: + return { + "status": "error", + "summary": "No session-ids supplied for multi-session parse", + "sessions": [], + "aggregate": {}, + } + + sessions_out: list[dict[str, Any]] = [] + agg_tools: Counter[str] = Counter() + agg_subagents: list[dict[str, Any]] = [] + agg_models: set[str] = set() + agg_branches: set[str] = set() + total_assistant = 0 + total_human = 0 + total_input = 0 + total_output = 0 + total_compactions = 0 + total_rejections = 0 + transcripts_present = 0 + + for idx, sid in enumerate(session_ids): + result = parse_one_session(sid, base_path) + result["role"] = "current" if idx == 0 else "previous" + sessions_out.append(result) + + if result.get("transcript_present"): + transcripts_present += 1 + data = result["data"] + agg_tools.update(data["tools"]["usage"]) + subagents = data["subagents"] + agg_models.update(data["session"]["models"]) + agg_branches.update(data["session"]["git_branches"]) + total_assistant += data["turns"]["assistant"] + total_human += data["turns"]["human_messages"] + total_input += data["tokens"]["total_input"] + total_output += data["tokens"]["total_output"] + total_compactions += data["compactions"]["count"] + total_rejections += data["tools"]["rejection_count"] + else: + subagents = result.get("subagents", []) + + for sub in subagents: + tagged = dict(sub) + tagged["session_id"] = sid + agg_subagents.append(tagged) + + if transcripts_present == len(session_ids): + status = "pass" + elif transcripts_present > 0 or agg_subagents: + # Genuine partial chain: some transcripts, or subagent evidence only. + status = "warning" + else: + # Nothing found at all — a bad base dir, stale chain, or wrong SIDs + # must fail loudly, not read as a successful all-zero retro. + status = "error" + summary = ( + f"Multi-session retro: {len(session_ids)} chained session(s) " + f"({transcripts_present} with transcript), " + f"{total_assistant} assistant turns total, " + f"{total_human} human messages, " + f"{total_compactions} compaction(s)" + ) + + return { + "status": status, + "summary": summary, + "sessions": sessions_out, + "aggregate": { + "total_assistant_turns": total_assistant, + "total_human_messages": total_human, + "total_input_tokens": total_input, + "total_output_tokens": total_output, + "total_compactions": total_compactions, + "total_tool_rejections": total_rejections, + "all_tools": dict(agg_tools.most_common()), + "all_subagents": agg_subagents, + "all_models": sorted(agg_models), + "all_branches": sorted(agg_branches), + }, + } + + +def _parse_legacy_or_argparse(argv: list[str]) -> argparse.Namespace: + """Support BOTH legacy positional 2-arg form AND new argparse-flag form. + + Legacy: parse_transcript.py + New: parse_transcript.py --sessions SID1 SID2 ... --base + New: parse_transcript.py --chain-from --base + """ + # Detect legacy form: exactly 2 positional args, neither starts with -- + if len(argv) == 2 and not argv[0].startswith("--") and not argv[1].startswith("--"): + return argparse.Namespace( + session_id=argv[0], + base=Path(argv[1]), + sessions=None, + chain_from=None, + ) + + parser = argparse.ArgumentParser( + prog="parse_transcript.py", + description="Parse Claude Code JSONL transcript(s) and emit metrics JSON.", + ) + parser.add_argument( + "--base", type=Path, help="Base directory containing session JSONL files" + ) + parser.add_argument( + "--sessions", + nargs="+", + help="Multiple UUIDs to parse + aggregate (first = current, rest = chained prior)", + ) + parser.add_argument( + "--chain-from", + type=Path, + dest="chain_from", + help="Walk session chain via handoff frontmatter starting from the given log file", + ) + parser.add_argument( + "--current-session", + dest="current_session", + help="Optional current session-id to prepend when using --chain-from", + ) + ns = parser.parse_args(argv) + ns.session_id = None + return ns + + +def main() -> None: + argv = sys.argv[1:] + if not argv: + # Emit usage error before argparse can produce its own; --help/-h fall through. + _emit_and_exit( + "error", + "Usage: parse_transcript.py | " + "parse_transcript.py --sessions --base | " + "parse_transcript.py --chain-from --base ", + {}, + 2, + ) + + ns = _parse_legacy_or_argparse(argv) + + if not ns.base or not ns.base.is_dir(): + _emit_and_exit( + "error", + f"--base must point to an existing directory (got: {ns.base})", + {}, + 2, + ) + + # Multi-session: --chain-from + if ns.chain_from is not None: + if not ns.chain_from.is_file(): + _emit_and_exit( + "error", + f"--chain-from file not found: {ns.chain_from}", + {}, + 2, + ) + chain_sids = extract_chain_from_handoff(ns.chain_from, ns.base) + # Prepend current session-id if supplied AND not already first + all_sids: list[str] = [] + if ns.current_session and ns.current_session not in chain_sids: + all_sids.append(ns.current_session) + all_sids.extend(chain_sids) + output = build_multi_session_output(all_sids, ns.base) + print(json.dumps(output, indent=2)) + sys.exit(0 if output["status"] in {"pass", "warning"} else 2) + + # Multi-session: --sessions + if ns.sessions: + output = build_multi_session_output(list(ns.sessions), ns.base) + print(json.dumps(output, indent=2)) + sys.exit(0 if output["status"] in {"pass", "warning"} else 2) + + # Single-session: legacy positional path + session_id = ns.session_id + if not session_id: + _emit_and_exit( + "error", + "No session-id supplied (positional or --sessions / --chain-from)", + {}, + 2, + ) + + result = parse_one_session(session_id, ns.base) + + if not result.get("transcript_present"): + # Existing single-session warning vs error semantics preserved. + if result.get("subagents"): + _emit_and_exit( + "warning", + ( + f"No main transcript found, " + f"but {len(result['subagents'])} subagent(s) discovered" + ), + {"subagents": result["subagents"]}, + 1, + ) + _emit_and_exit("error", result["error"], {}, 2) + + data = result["data"] + output = { + "status": "pass", + "summary": ( + f"Session: {data['turns']['assistant']} assistant turns, " + f"{data['turns']['human_messages']} human messages, " + f"{data['compactions']['count']} compaction(s), " + f"{data['session']['duration_minutes']}min" + ), + "data": data, + } + print(json.dumps(output, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/plugins/session-flow/skills/retro/scripts/test_parse_transcript.py b/plugins/session-flow/skills/retro/scripts/test_parse_transcript.py new file mode 100644 index 000000000..1744771e9 --- /dev/null +++ b/plugins/session-flow/skills/retro/scripts/test_parse_transcript.py @@ -0,0 +1,779 @@ +"""Tests for parse_transcript.py — output contract and edge case coverage. + +Runs the script as a subprocess to test the CLI interface, not internals. +Following python/testing.md "Testing standalone scripts" pattern. +""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +SCRIPT = Path(__file__).parent / "parse_transcript.py" +SESSION_ID = "test-session" + + +def _run_script(session_id: str, base_path: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(SCRIPT), session_id, base_path], + capture_output=True, + text=True, + timeout=10, + ) + + +def _run_with_event(tmp_path: Path, event: dict) -> dict: + """Write a single JSONL event and return the parsed `data` dict from stdout.""" + (tmp_path / f"{SESSION_ID}.jsonl").write_text(json.dumps(event) + "\n") + result = _run_script(SESSION_ID, str(tmp_path)) + result.check_returncode() + return json.loads(result.stdout)["data"] + + +def test_missing_args(): + """No arguments produces error status with exit code 2.""" + result = subprocess.run( + [sys.executable, str(SCRIPT)], + capture_output=True, + text=True, + timeout=10, + ) + output = json.loads(result.stdout) + assert output["status"] == "error" + assert result.returncode == 2 + + +def test_nonexistent_session(tmp_path): + """Nonexistent session ID produces error status.""" + result = _run_script("nonexistent-session", str(tmp_path)) + output = json.loads(result.stdout) + assert output["status"] == "error" + assert result.returncode == 2 + + +def test_empty_jsonl(tmp_path): + """Empty JSONL file produces pass status with zero counts.""" + jsonl = tmp_path / f"{SESSION_ID}.jsonl" + jsonl.write_text("") + result = _run_script(SESSION_ID, str(tmp_path)) + output = json.loads(result.stdout) + assert output["status"] == "pass" + assert output["data"]["turns"]["assistant"] == 0 + + +def test_single_assistant_turn_extracts_cache_tokens(tmp_path): + """Cache tokens are extracted from usage data.""" + data = _run_with_event( + tmp_path, + { + "type": "assistant", + "timestamp": "2026-03-23T18:00:00Z", + "message": { + "model": "claude-opus-4-6", + "content": [{"type": "tool_use", "name": "Read", "id": "123"}], + "stop_reason": "tool_use", + "usage": { + "input_tokens": 100, + "output_tokens": 50, + "cache_creation_input_tokens": 500, + "cache_read_input_tokens": 10000, + }, + }, + "version": "2.1.81", + "gitBranch": "main", + }, + ) + assert data["tokens"]["cache_creation"] == 500 + assert data["tokens"]["cache_read"] == 10000 + assert data["tokens"]["total_context"] == 10600 + assert data["tokens"]["total_input"] == 100 + assert data["tokens"]["total_output"] == 50 + assert data["tools"]["usage"]["Read"] == 1 + + +def test_assistant_turn_with_null_usage(tmp_path): + """Null usage field does not crash token extraction.""" + data = _run_with_event( + tmp_path, + { + "type": "assistant", + "timestamp": "2026-03-23T18:00:00Z", + "message": { + "model": "claude-opus-4-6", + "content": [{"type": "text", "text": "hi"}], + "stop_reason": "end_turn", + "usage": None, + }, + "version": "2.1.81", + "gitBranch": "main", + }, + ) + assert data["turns"]["assistant"] == 1 + assert data["tokens"]["total_input"] == 0 + assert data["tokens"]["total_output"] == 0 + + +def test_write_edit_file_paths_extracted(tmp_path): + """File paths from Write/Edit tool_use inputs are captured in files_modified.""" + data = _run_with_event( + tmp_path, + { + "type": "assistant", + "timestamp": "2026-03-23T18:00:00Z", + "message": { + "content": [ + { + "type": "tool_use", + "name": "Write", + "id": "w1", + "input": {"file_path": "/tmp/test.py", "content": "hello"}, + } + ], + "usage": {"input_tokens": 0, "output_tokens": 0}, + }, + }, + ) + assert "/tmp/test.py" in data["files_modified"] + + +def test_files_modified_dedups_relative_and_absolute_posix(tmp_path): + """Same file referenced as relative + absolute under cwd dedups to one entry.""" + cwd = str(tmp_path) + data = _run_with_event( + tmp_path, + { + "type": "assistant", + "timestamp": "2026-03-23T18:00:00Z", + "cwd": cwd, + "message": { + "content": [ + { + "type": "tool_use", + "name": "Write", + "id": "w1", + "input": {"file_path": "CLAUDE.md", "content": "x"}, + }, + { + "type": "tool_use", + "name": "Edit", + "id": "e1", + "input": { + "file_path": f"{cwd}/CLAUDE.md", + "old_string": "a", + "new_string": "b", + }, + }, + ], + "usage": {"input_tokens": 0, "output_tokens": 0}, + }, + }, + ) + assert data["files_modified"] == ["CLAUDE.md"] + + +def test_files_modified_dedups_windows_backslash_absolute(tmp_path): + """Windows-style absolute path collapses to relative form against Windows cwd.""" + data = _run_with_event( + tmp_path, + { + "type": "assistant", + "timestamp": "2026-03-23T18:00:00Z", + "cwd": "C:\\Projects\\example", + "message": { + "content": [ + { + "type": "tool_use", + "name": "Write", + "id": "w1", + "input": {"file_path": "CLAUDE.md", "content": "x"}, + }, + { + "type": "tool_use", + "name": "Edit", + "id": "e1", + "input": { + "file_path": "C:\\Projects\\example\\CLAUDE.md", + "old_string": "a", + "new_string": "b", + }, + }, + ], + "usage": {"input_tokens": 0, "output_tokens": 0}, + }, + }, + ) + assert data["files_modified"] == ["CLAUDE.md"] + + +def test_files_modified_keeps_path_outside_cwd(tmp_path): + """Absolute path outside cwd is preserved (not stripped or dropped).""" + data = _run_with_event( + tmp_path, + { + "type": "assistant", + "timestamp": "2026-03-23T18:00:00Z", + "cwd": str(tmp_path), + "message": { + "content": [ + { + "type": "tool_use", + "name": "Write", + "id": "w1", + "input": {"file_path": "/etc/hosts", "content": "x"}, + }, + ], + "usage": {"input_tokens": 0, "output_tokens": 0}, + }, + }, + ) + assert "/etc/hosts" in data["files_modified"] + + +def test_files_modified_snapshot_dedups_with_write_event(tmp_path): + """file-history-snapshot absolute path deduplicates against a Write relative path.""" + cwd = str(tmp_path) + events = [ + json.dumps( + { + "type": "assistant", + "timestamp": "2026-03-23T18:00:00Z", + "cwd": cwd, + "message": { + "content": [ + { + "type": "tool_use", + "name": "Write", + "id": "w1", + "input": {"file_path": "README.md", "content": "x"}, + } + ], + "usage": {"input_tokens": 0, "output_tokens": 0}, + }, + } + ), + json.dumps( + { + "type": "file-history-snapshot", + "timestamp": "2026-03-23T18:00:01Z", + "snapshot": { + "trackedFileBackups": { + f"{cwd}/README.md": "backup-content", + } + }, + } + ), + ] + jsonl = tmp_path / f"{SESSION_ID}.jsonl" + jsonl.write_text("\n".join(events) + "\n") + data = json.loads(_run_script(SESSION_ID, str(tmp_path)).stdout)["data"] + assert data["files_modified"] == ["README.md"] + + +def test_files_modified_no_cwd_keeps_paths_unchanged(tmp_path): + """No cwd in any event: paths survive normalization unchanged.""" + data = _run_with_event( + tmp_path, + { + "type": "assistant", + "timestamp": "2026-03-23T18:00:00Z", + "message": { + "content": [ + { + "type": "tool_use", + "name": "Write", + "id": "w1", + "input": {"file_path": "/tmp/test.py", "content": "x"}, + }, + ], + "usage": {"input_tokens": 0, "output_tokens": 0}, + }, + }, + ) + assert "/tmp/test.py" in data["files_modified"] + + +def test_queue_operation_counted(tmp_path): + """queue-operation enqueue events are counted.""" + events = [ + json.dumps( + { + "type": "queue-operation", + "operation": "enqueue", + "timestamp": "2026-03-23T18:00:00Z", + "content": "user message", + } + ), + json.dumps( + { + "type": "queue-operation", + "operation": "remove", + "timestamp": "2026-03-23T18:00:01Z", + } + ), + ] + jsonl = tmp_path / f"{SESSION_ID}.jsonl" + jsonl.write_text("\n".join(events) + "\n") + data = json.loads(_run_script(SESSION_ID, str(tmp_path)).stdout)["data"] + assert data["queued_user_messages"] == 1 + + +def test_except_syntax_handles_non_string_timestamp(tmp_path): + """Non-string timestamp doesn't crash (Python 2 except syntax regression).""" + _run_with_event( + tmp_path, + { + "type": "assistant", + "timestamp": 12345, + "message": {"content": [], "usage": {}}, + }, + ) + + +def test_turn_durations_derived_from_assistant_timestamps(tmp_path): + """N assistant events yield N-1 turn durations derived from timestamp deltas. + + Replaces reliance on CC's sparse system.turn_duration events + (empirically 0-5 per session vs hundreds of assistant turns). + """ + events = [ + json.dumps( + { + "type": "assistant", + "timestamp": f"2026-05-23T01:38:{secs:02d}.000Z", + "message": {"content": [], "usage": {}}, + } + ) + for secs in (10, 15, 30, 45) + ] + jsonl = tmp_path / f"{SESSION_ID}.jsonl" + jsonl.write_text("\n".join(events) + "\n") + data = json.loads(_run_script(SESSION_ID, str(tmp_path)).stdout)["data"] + assert data["turns"]["assistant"] == 4 + assert data["turn_durations"]["count"] == 3 + assert data["turn_durations"]["min_ms"] == 5000 + assert data["turn_durations"]["max_ms"] == 15000 + assert data["turn_durations"]["total_ms"] == 35000 + + +def test_turn_durations_zero_when_single_turn(tmp_path): + """A single assistant turn yields zero deltas (no pairs).""" + data = _run_with_event( + tmp_path, + { + "type": "assistant", + "timestamp": "2026-05-23T01:38:00.000Z", + "message": {"content": [], "usage": {}}, + }, + ) + assert data["turn_durations"]["count"] == 0 + + +def test_output_contract_has_required_keys(tmp_path): + """Output JSON has all required top-level and nested keys.""" + # Re-run to capture full stdout (including status/summary), since + # _run_with_event only returns the inner data dict. + (tmp_path / f"{SESSION_ID}.jsonl").write_text( + json.dumps( + { + "type": "assistant", + "timestamp": "2026-03-23T18:00:00Z", + "message": { + "model": "claude-opus-4-6", + "content": [], + "stop_reason": "end_turn", + "usage": {"input_tokens": 10, "output_tokens": 5}, + }, + } + ) + + "\n" + ) + output = json.loads(_run_script(SESSION_ID, str(tmp_path)).stdout) + + assert "status" in output + assert "summary" in output + assert "data" in output + data = output["data"] + assert "session" in data + assert "turns" in data + assert "tokens" in data + assert "tools" in data + assert "compactions" in data + assert "files_modified" in data + assert "subagents" in data + assert "queued_user_messages" in data + + tokens = data["tokens"] + assert "total_input" in tokens + assert "total_output" in tokens + assert "cache_creation" in tokens + assert "cache_read" in tokens + assert "total_context" in tokens + + assert "cwd" not in data["session"] + + +# ----------------------------------------------------------------------------- +# Multi-session tests (Phase C — retro-pre-commit-chain) +# ----------------------------------------------------------------------------- + + +def _write_assistant_event( + tmp_path: Path, session_id: str, ts: str = "2026-05-23T00:00:00Z" +): + """Write a minimal valid JSONL for the given session.""" + event = { + "type": "assistant", + "timestamp": ts, + "message": { + "model": "claude-opus-4-7", + "content": [{"type": "tool_use", "name": "Read", "id": "x"}], + "stop_reason": "tool_use", + "usage": { + "input_tokens": 10, + "output_tokens": 5, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + }, + }, + } + (tmp_path / f"{session_id}.jsonl").write_text(json.dumps(event) + "\n") + + +def _run_multi(args: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(SCRIPT), *args], + capture_output=True, + text=True, + timeout=10, + ) + + +def test_multi_session_all_present(tmp_path): + """--sessions with N transcripts present → status=pass, aggregate populated.""" + _write_assistant_event(tmp_path, "sid-curr") + _write_assistant_event(tmp_path, "sid-prev") + result = _run_multi(["--sessions", "sid-curr", "sid-prev", "--base", str(tmp_path)]) + result.check_returncode() + output = json.loads(result.stdout) + assert output["status"] == "pass" + assert len(output["sessions"]) == 2 + assert output["sessions"][0]["role"] == "current" + assert output["sessions"][0]["id"] == "sid-curr" + assert output["sessions"][1]["role"] == "previous" + assert output["sessions"][1]["id"] == "sid-prev" + assert output["sessions"][0]["transcript_present"] is True + assert output["sessions"][1]["transcript_present"] is True + agg = output["aggregate"] + # 2 sessions x 1 assistant turn each = 2 + assert agg["total_assistant_turns"] == 2 + assert agg["total_input_tokens"] == 20 + assert agg["total_output_tokens"] == 10 + assert "Read" in agg["all_tools"] + + +def test_multi_session_one_missing(tmp_path): + """--sessions with one transcript absent → status=warning, partial aggregate.""" + _write_assistant_event(tmp_path, "sid-curr") + # sid-prev intentionally missing + result = _run_multi( + ["--sessions", "sid-curr", "sid-prev-missing", "--base", str(tmp_path)] + ) + assert result.returncode == 0 # warning still exits 0 + output = json.loads(result.stdout) + assert output["status"] == "warning" + assert output["sessions"][0]["transcript_present"] is True + assert output["sessions"][1]["transcript_present"] is False + assert "error" in output["sessions"][1] + # Aggregate counts only the present transcript + assert output["aggregate"]["total_assistant_turns"] == 1 + + +def test_multi_session_all_missing_is_error(tmp_path): + """--sessions where every transcript is absent → status=error, exit 2.""" + result = _run_multi( + ["--sessions", "sid-gone-a", "sid-gone-b", "--base", str(tmp_path)] + ) + assert result.returncode == 2 + output = json.loads(result.stdout) + assert output["status"] == "error" + assert all(s["transcript_present"] is False for s in output["sessions"]) + + +def test_notebook_edit_counts_as_file_modification(tmp_path): + """NotebookEdit tool_use file_path lands in files_modified.""" + data = _run_with_event( + tmp_path, + { + "type": "assistant", + "timestamp": "2026-03-23T18:00:00Z", + "message": { + "content": [ + { + "type": "tool_use", + "name": "NotebookEdit", + "id": "n1", + "input": {"file_path": "analysis.ipynb", "new_source": "x"}, + } + ], + "usage": {"input_tokens": 0, "output_tokens": 0}, + }, + }, + ) + assert "analysis.ipynb" in data["files_modified"] + + +def test_chain_from_strips_inline_yaml_comments(tmp_path): + """Inline YAML comments in handoff frontmatter values don't corrupt the chain.""" + handoffs_dir = tmp_path / ".claude" / "handoffs" + handoffs_dir.mkdir(parents=True) + + (handoffs_dir / "20260520T100000Z-handoff-alpha.md").write_text( + "---\ntype: handoff\nsession_id: sid-oldest # REQUIRED\n---\nbody\n" + ) + (handoffs_dir / "20260521T110000Z-handoff-beta.md").write_text( + "---\ntype: handoff\nsession_id: sid-middle # REQUIRED\n" + "previous_handoff: 20260520T100000Z-handoff-alpha.md # CONDITIONAL\n" + "previous_session_id: sid-oldest\n---\nbody\n" + ) + + base = tmp_path / "session-data" + base.mkdir() + _write_assistant_event(base, "sid-middle") + _write_assistant_event(base, "sid-oldest") + + handoff_file = handoffs_dir / "20260521T110000Z-handoff-beta.md" + result = _run_multi(["--chain-from", str(handoff_file), "--base", str(base)]) + assert result.returncode == 0, result.stderr + output = json.loads(result.stdout) + sids = [s["id"] for s in output["sessions"]] + assert "sid-middle" in sids + assert "sid-oldest" in sids + assert all(s["transcript_present"] for s in output["sessions"]) + + +def test_chain_from_breaks_on_pointer_cycle(tmp_path): + """A previous_handoff cycle stops the walk instead of looping forever.""" + handoffs_dir = tmp_path / ".claude" / "handoffs" + handoffs_dir.mkdir(parents=True) + + (handoffs_dir / "20260520T100000Z-handoff-alpha.md").write_text( + "---\ntype: handoff\nsession_id: sid-a\n" + "previous_handoff: 20260521T110000Z-handoff-beta.md\n" + "previous_session_id: sid-b\n---\nbody\n" + ) + (handoffs_dir / "20260521T110000Z-handoff-beta.md").write_text( + "---\ntype: handoff\nsession_id: sid-b\n" + "previous_handoff: 20260520T100000Z-handoff-alpha.md\n" + "previous_session_id: sid-a\n---\nbody\n" + ) + + base = tmp_path / "session-data" + base.mkdir() + _write_assistant_event(base, "sid-a") + _write_assistant_event(base, "sid-b") + + handoff_file = handoffs_dir / "20260521T110000Z-handoff-beta.md" + result = _run_multi(["--chain-from", str(handoff_file), "--base", str(base)]) + assert result.returncode == 0, result.stderr + assert "cycle" in result.stderr + output = json.loads(result.stdout) + sids = [s["id"] for s in output["sessions"]] + assert sids.count("sid-a") == 1 + assert sids.count("sid-b") == 1 + + +def test_multi_edit_counts_as_file_modification(tmp_path): + """MultiEdit tool_use file_path lands in files_modified.""" + data = _run_with_event( + tmp_path, + { + "type": "assistant", + "timestamp": "2026-03-23T18:00:00Z", + "message": { + "content": [ + { + "type": "tool_use", + "name": "MultiEdit", + "id": "m1", + "input": {"file_path": "src/app.py", "edits": []}, + } + ], + "usage": {"input_tokens": 0, "output_tokens": 0}, + }, + }, + ) + assert "src/app.py" in data["files_modified"] + + +def test_multi_session_subagent_per_session_tagging(tmp_path): + """Subagents from each session are tagged with session_id in the aggregate.""" + _write_assistant_event(tmp_path, "sid-a") + _write_assistant_event(tmp_path, "sid-b") + + # Seed subagents per session — parse_subagents reads .meta.json files, + # mirroring ~/.claude/projects///subagents/.meta.json shape + for sid in ("sid-a", "sid-b"): + sub_dir = tmp_path / sid / "subagents" + sub_dir.mkdir(parents=True, exist_ok=True) + (sub_dir / f"sub-{sid}.meta.json").write_text( + json.dumps( + { + "agentType": "explore", + "description": f"explore-from-{sid}", + } + ) + ) + + result = _run_multi(["--sessions", "sid-a", "sid-b", "--base", str(tmp_path)]) + result.check_returncode() + output = json.loads(result.stdout) + all_subs = output["aggregate"]["all_subagents"] + # Each subagent entry MUST carry session_id of its origin + sids_seen = {sub["session_id"] for sub in all_subs} + assert "sid-a" in sids_seen + assert "sid-b" in sids_seen + + +def test_chain_from_walks_handoff_pointers(tmp_path): + """--chain-from reads handoff journal-entry frontmatter + walks backwards.""" + # Subdir-prefixed layout: pointers keep a journal/ prefix + slug = "test-slug" + journal_dir = tmp_path / "work-notes" / slug / "journal" + journal_dir.mkdir(parents=True) + + # Older handoff first + (journal_dir / "20260520T100000Z-handoff-alpha.md").write_text( + "---\ntype: handoff\nsession_id: sid-oldest\n---\nbody\n" + ) + # Newer handoff points at the older one + (journal_dir / "20260521T110000Z-handoff-beta.md").write_text( + "---\ntype: handoff\nsession_id: sid-middle\n" + "previous_handoff: journal/20260520T100000Z-handoff-alpha.md\n" + "previous_session_id: sid-oldest\n---\nbody\n" + ) + + # Seed transcripts for chained SIDs + base = tmp_path / "session-data" + base.mkdir() + _write_assistant_event(base, "sid-middle") + _write_assistant_event(base, "sid-oldest") + + handoff_file = journal_dir / "20260521T110000Z-handoff-beta.md" + result = _run_multi(["--chain-from", str(handoff_file), "--base", str(base)]) + assert result.returncode == 0, result.stderr + output = json.loads(result.stdout) + # Chain-walker emits the newest handoff's session_id first, then walks back + sids = [s["id"] for s in output["sessions"]] + assert "sid-middle" in sids + assert "sid-oldest" in sids + + +def test_chain_from_walks_flat_layout_pointers(tmp_path): + """--chain-from resolves previous_handoff as a sibling filename (flat handoffs dir).""" + handoffs_dir = tmp_path / ".claude" / "handoffs" + handoffs_dir.mkdir(parents=True) + + (handoffs_dir / "20260520T100000Z-handoff-alpha.md").write_text( + "---\ntype: handoff\nsession_id: sid-oldest\n---\nbody\n" + ) + (handoffs_dir / "20260521T110000Z-handoff-beta.md").write_text( + "---\ntype: handoff\nsession_id: sid-middle\n" + "previous_handoff: 20260520T100000Z-handoff-alpha.md\n" + "previous_session_id: sid-oldest\n---\nbody\n" + ) + + base = tmp_path / "session-data" + base.mkdir() + _write_assistant_event(base, "sid-middle") + _write_assistant_event(base, "sid-oldest") + + handoff_file = handoffs_dir / "20260521T110000Z-handoff-beta.md" + result = _run_multi(["--chain-from", str(handoff_file), "--base", str(base)]) + assert result.returncode == 0, result.stderr + output = json.loads(result.stdout) + sids = [s["id"] for s in output["sessions"]] + assert "sid-middle" in sids + assert "sid-oldest" in sids + + +def test_chain_from_with_current_session_prepend(tmp_path): + """--current-session prepends as first SID when not already in chain.""" + slug = "test-slug" + journal_dir = tmp_path / "work-notes" / slug / "journal" + journal_dir.mkdir(parents=True) + (journal_dir / "20260521T110000Z-handoff-alpha.md").write_text( + "---\ntype: handoff\nsession_id: sid-prior\n---\nbody\n" + ) + + base = tmp_path / "session-data" + base.mkdir() + _write_assistant_event(base, "sid-current") + _write_assistant_event(base, "sid-prior") + + handoff_file = journal_dir / "20260521T110000Z-handoff-alpha.md" + result = _run_multi( + [ + "--chain-from", + str(handoff_file), + "--current-session", + "sid-current", + "--base", + str(base), + ] + ) + assert result.returncode == 0, result.stderr + output = json.loads(result.stdout) + assert output["sessions"][0]["id"] == "sid-current" + assert output["sessions"][0]["role"] == "current" + assert output["sessions"][1]["id"] == "sid-prior" + + +def test_chain_from_handoff_without_session_id(tmp_path): + """Handoff entry without session_id frontmatter → chain breaks at that entry.""" + slug = "test-slug" + journal_dir = tmp_path / "work-notes" / slug / "journal" + journal_dir.mkdir(parents=True) + (journal_dir / "20260521T110000Z-handoff-preshape.md").write_text( + "---\ntype: handoff\n---\nbody\n" + ) + + base = tmp_path / "session-data" + base.mkdir() + + handoff_file = journal_dir / "20260521T110000Z-handoff-preshape.md" + result = _run_multi(["--chain-from", str(handoff_file), "--base", str(base)]) + output = json.loads(result.stdout) + # No SIDs extracted from the pre-shape entry → empty sessions list → error status + assert output["status"] == "error" + + +def test_chain_from_nonexistent_file_errors(tmp_path): + """Missing --chain-from file → exit 2.""" + result = _run_multi( + [ + "--chain-from", + str(tmp_path / "nonexistent.md"), + "--base", + str(tmp_path), + ] + ) + assert result.returncode == 2 + + +def test_legacy_positional_still_works(tmp_path): + """Positional form preserves existing single-session output shape.""" + _write_assistant_event(tmp_path, "legacy-sid") + result = subprocess.run( + [sys.executable, str(SCRIPT), "legacy-sid", str(tmp_path)], + capture_output=True, + text=True, + timeout=10, + ) + result.check_returncode() + output = json.loads(result.stdout) + # Single-session shape has "data" key (NOT "sessions" + "aggregate") + assert "data" in output + assert "sessions" not in output + assert "aggregate" not in output diff --git a/plugins/session-flow/skills/workflow/SKILL.md b/plugins/session-flow/skills/workflow/SKILL.md new file mode 100644 index 000000000..9dd39c318 --- /dev/null +++ b/plugins/session-flow/skills/workflow/SKILL.md @@ -0,0 +1,122 @@ +--- +name: workflow +description: "Navigate a staged development workflow (explore → research → plan → implement → test → review → verify → retro) and suggest the next stage. Use when: 'workflow', 'what step am I on', 'what comes next', 'pre-pr sequence', 'wrap up', at session start, or whenever the next step is unclear." +argument-hint: "[mode] (e.g., /workflow, /workflow steps, /workflow pre-pr, /workflow wrap-up, /workflow philosophy, /workflow spec-first)" +user-invocable: true +disable-model-invocation: false +--- + +## Pre-computed context + +Current branch: !`git branch --show-current 2>/dev/null || echo "not a git repository"` +Working tree: !`git status --porcelain 2>/dev/null | head -20 || echo "clean"` +Recent commits: !`git log --oneline -5 2>/dev/null || echo "no commits"` + +## Purpose + +The reference and navigator for a staged development workflow. Individual stages are executed by +whatever means the consuming repo provides (its own stage skills, or inline work); this skill is the +map — it defines the stages, detects the current position, and suggests what comes next. + +**Three roles:** + +1. **Reference** — stage definitions and how stages compose (`context/steps.md`) +2. **Navigator** — session-aware guidance on which stage comes next based on what's been done +3. **Checklist** — pre-PR sequence and end-of-session wrap-up as structured checklists + +## Consumer conventions + +This skill adapts to the consuming repo rather than imposing structure: + +- **Stage execution.** When the consuming repo defines a skill for a stage (its skill listing or + `CLAUDE.md` names one — e.g. an explore, research, plan/architect, or implement skill), suggest + invoking that skill. Otherwise execute the stage inline following its definition in + `context/steps.md`. Never invent skill names — check what actually exists. +- **Artifact location.** When persisting stage outputs or checklists, honor the consuming repo's + documented convention for work/planning artifacts (check `CLAUDE.md` / `.claude/rules/`). When no + convention exists, default to `.claude/handoffs/` in the project (shared with the sibling + `handoff` skill). +- **Quality gates.** The consuming repo's own build/test/lint commands and review criteria govern; + this skill names WHERE gates belong in the sequence, not what they contain. + +## Argument parsing + +Parse the first argument to determine mode: + +| Argument | Mode | Action | +|----------|------|--------| +| *(none)* | **Default** | Show compact stage overview + detect current position + suggest next stage | +| `steps` | **Steps** | Load `context/steps.md` — full stage definitions | +| `pre-pr` | **Pre-PR** | Load `context/pre-pr.md` — pre-PR sequence checklist | +| `wrap-up` | **Wrap-up** | Load `context/wrap-up.md` — end-of-session checklist | +| `philosophy` | **Philosophy** | Load `context/philosophy.md` — depth expectations and verification rigor | +| `spec-first` | **Spec-first** | Load `context/spec-first.md` — stage-by-stage execution with `/clear` between stages | + +## Default mode (no arguments) + +### 1. Show the workflow at a glance + +```text +0. Contract (optional — lock goal, constraints, acceptance criteria before building) +1. Explore → 2. Research → 3. Plan (+ stress-test) → 4. Implement +5. Test → 6. Review → 7. Verify outcome → 8. Retrospective (/retro) +PR lifecycle: prep → create → monitor CI → merge (runs after step 7) +``` + +### 2. Detect current position + +Check conversation context for evidence of completed stages: + +- Is the goal/constraints/acceptance-criteria contract crisp (stated by the user, or in a plan + artifact on disk)? → Stage 0 satisfied +- Has the relevant code been read or the codebase surveyed? → Stage 1 done +- Have external sources been consulted for load-bearing technical claims? → Stage 2 done +- Has a plan been written and approved? → Stage 3 done +- Has code been written via Write/Edit? → Stage 4 in progress or done +- Have tests been run? → Stage 5 done +- Has a self-review or delegated review happened? → Stage 6 done +- Has the outcome been verified against intent with evidence? → Stage 7 done +- Is there a PR? → PR lifecycle in progress + +Verify a stage from its artifact or output — a plan file, cited sources, green test output — not +from conversation vibes. + +### 3. Suggest next stage + +Based on what's been done, recommend the next stage with rationale. If the consuming repo has a +skill for that stage, name it; otherwise describe the inline work. + +### 4. Track progress (tasks ≥3 stages) + +For work expected to span 3+ stages, create a task per applicable stage via TaskCreate, mark +completed stages `completed` and the current one `in_progress`. For durable cross-`/clear` tracking, +also copy `templates/checklist.md` into the artifact location (see "Consumer conventions") as +`workflow-checklist.md` and tick boxes as stages produce their outputs. Skip the file when the +consuming repo already tracks the same stages in its own plan artifact — never mirror progress in +two files. + +## Key principles (always apply, regardless of mode) + +- **Verification rigor is size-independent** — a one-line config change gets the same rigor as a + multi-file feature (`context/philosophy.md`) +- **This skill navigates; stages execute elsewhere** — route to the stage work once position is + known, don't re-run it here +- **Verify stage completion from artifacts** — a stage is done when its output exists, not when it + was mentioned + +## Gotchas + +- **Marking a stage done from conversation vibes** — verify the artifact or output exists before + suggesting the next stage. +- **Skipping the contract stage on behavior-changing work** — fuzzy intent becomes silent plan + assumptions; lock the goal and acceptance criteria first. +- **Opening a PR before the verify stage** — the pre-PR sequence (`context/pre-pr.md`) is ordered + for a reason; verification evidence comes before the PR, not after. + +## What this skill does NOT do + +- **Does not execute stages** — it is the map, not the territory +- **Does not replace the consuming repo's own gates** — build/test/lint commands, review criteria, + and commit conventions stay repo-owned +- **Does not require any specific stage skills to exist** — every stage degrades gracefully to + inline execution diff --git a/plugins/session-flow/skills/workflow/context/philosophy.md b/plugins/session-flow/skills/workflow/context/philosophy.md new file mode 100644 index 000000000..c0e463a36 --- /dev/null +++ b/plugins/session-flow/skills/workflow/context/philosophy.md @@ -0,0 +1,41 @@ +# Workflow Philosophy & Depth Expectations + +## Universal applicability + +**This applies to ALL technical claims** — including "just adding a bullet point" to a rules file, +writing a comment, or answering a question. No size threshold below which verification is skipped. + +- **Task size does NOT reduce research depth** — a one-line config change gets the same + verification rigor as a multi-file feature +- **Analyzing existing research is not summarizing it** — restating a document's conclusions skips + the research stage. Analysis requires independent verification of the claims and a fit check + against the current codebase + +## Philosophy + +More tokens and more time are acceptable — even encouraged — when they produce more accuracy and +prevent rework. Insufficient research is a leading source of rework. If context is healthy, invest +in depth; context pressure (approaching compaction) is the budget constraint, not effort. + +## No assumptions + +Default to HIGH confidence, HIGH accuracy, HIGH attention to detail before making claims or +submitting code. Dig into the details, verify the specifics, confirm the edge cases. + +## Task tracking + +For non-trivial work (3+ stages), create tasks at the START, update status as you go. Tasks make +progress visible; for state that must survive `/clear`, use the durable checklist or a `/handoff` +save-point — in-memory tasks do not persist. + +## Current information is non-negotiable + +**Never operate on stale knowledge.** Exploration establishes what IS; research establishes what +SHOULD BE. Together they are the knowledge-gathering prerequisite for every task. + +- **When in doubt, look it up** — a quick doc fetch is near-free; acting on outdated information is + expensive +- **Flag uncertainty explicitly** — if current information cannot be obtained, say so; never + present training-data-era knowledge as current fact +- **File/directory placement is a technical claim** — when creating files for a specific tool, + research that tool's official directory conventions before placing files diff --git a/plugins/session-flow/skills/workflow/context/pre-pr.md b/plugins/session-flow/skills/workflow/context/pre-pr.md new file mode 100644 index 000000000..5388eae28 --- /dev/null +++ b/plugins/session-flow/skills/workflow/context/pre-pr.md @@ -0,0 +1,30 @@ +# Pre-PR Sequence + +Ordered checklist for code changes heading to a pull request. Complete in order; each step gates the +next. Use the consuming repo's own commands and review criteria at each gate. + +1. **Test thoroughly** — run all affected tests, smoke test new functionality, verify edge cases. + No PR without evidence the changes work +2. **Review** — self-review the full diff (`git diff HEAD` catches staged + unstaged) against the + repo's conventions, or dispatch a fresh-context reviewer. Resolve blocking findings before + proceeding +3. **Stage surgically** — `git add ` for specific files, never `git add -A` or `git add .` + (risk of including secrets, build artifacts, or unrelated changes) +4. **Simplify** — pass over the changed code for reuse, clarity, and unnecessary complexity +5. **Review the simplify diff** — inspect what changed; approve or revert each edit individually +6. **Re-test after simplify** — cleanup edits can introduce issues; run the tests again +7. **Verify outcome** — confirm the result matches the original intent with evidence (see + `steps.md` stage 7). Never claim improvement without measurements +8. **Open the PR** — only after steps 1–7 pass + +## Reviewing incoming findings (CI + bot review) + +- **Research before fixing CI failures** — diagnose the root cause from logs; never guess-fix and + re-push in a loop +- **Evaluate review comments before acting** — verify each claim against the code; classify + VALID / INCORRECT / UNCERTAIN with evidence, and fix only the valid ones + +## Scope tips + +- Docs/config-only changes may skip steps 4–6 when there is no code to simplify +- Keep the PR small and cohesive — split unrelated changes into separate PRs diff --git a/plugins/session-flow/skills/workflow/context/spec-first.md b/plugins/session-flow/skills/workflow/context/spec-first.md new file mode 100644 index 000000000..08c87cad0 --- /dev/null +++ b/plugins/session-flow/skills/workflow/context/spec-first.md @@ -0,0 +1,65 @@ +# Spec-first workflow (context-budget-aware) + +Alternative execution mode for the staged workflow. Instead of running every stage in ONE long +session — where every turn re-processes the growing conversation — each stage persists its output to +disk and the next stage starts fresh via `/clear`. + +**Why:** long sessions compound per-turn token cost and invite context rot; clearing between stages +trades a small re-read cost for tight, purpose-built context per stage. + +**When to use:** multi-phase features spanning hours, work known in advance to have distinct +explore + research + plan + implement stages, cross-session work that may pause overnight. + +**When NOT to use:** one-line fixes, quick config tweaks, tightly-coupled +exploration+implementation (e.g. debugging where findings shape the fix in real time). The default +is still the single-session pattern — spec-first is opt-in. + +## How stage handoffs work + +Each stage writes its output to the repo's work-artifact location (the consuming repo's documented +convention, or the plugin default `.claude/handoffs/` — see the workflow skill's "Consumer +conventions"). The next stage reads only that artifact. + +| Stage | Writes | Next stage reads | +|-------|--------|------------------| +| 0 Contract | brief/plan file (goal, constraints, acceptance criteria) | contract for explore/research/plan | +| 1 Explore | exploration findings file | context for research | +| 2 Research | research findings file (cited sources) | evidence for plan | +| 3 Plan | plan file (phases + verification criteria), user-approved | roadmap for implement | +| any | `/handoff` save-point | mid-task snapshot for the fresh session | + +## Execution pattern + +```text +contract → writes the brief → /clear +explore → writes findings → /clear +research → writes cited evidence → /clear +plan → writes the approved plan → /clear +implement → ships code, commits per phase → (optional /handoff if context bloats) +test → review → verify → /retro ← the back half often runs in one session +``` + +`/clear` between every stage is the maximum-reduction pattern. In practice, collapse adjacent +stages when context is still small — but commit to clearing at least between research and plan, and +between plan and implement. Those are the biggest re-processing wins. + +## Why it saves context + +A single-session workflow re-processes the entire growing conversation on every turn. By the +implement stage, each turn carries every explore finding, every research pass, every plan +iteration — even though implementation only needs the approved plan. With `/clear` between stages, +each stage's context is tight and purpose-built, and compaction is rarely reached. + +## /handoff: the escape hatch + +Mid-stage, if context grows heavy or quality degrades, invoke `/handoff` to snapshot the current +state (what's done, decisions, what was tried and ruled out, next steps) and `/clear`. Multiple +save-points accumulate; timestamps keep them ordered. + +## Trade-offs + +**Wins:** fewer tokens re-processed per stage, cleaner model focus, resilience to compaction, +cross-session resumability. + +**Costs:** slight overhead writing + reading artifacts; stages must be artifact-complete (anything +left implicit in conversation is lost to the next stage); over-clearing on tiny tasks is noise. diff --git a/plugins/session-flow/skills/workflow/context/steps.md b/plugins/session-flow/skills/workflow/context/steps.md new file mode 100644 index 000000000..867ff3bea --- /dev/null +++ b/plugins/session-flow/skills/workflow/context/steps.md @@ -0,0 +1,92 @@ +# Workflow Stages — Full Definitions + +The staged development workflow plus the optional contract stage. When the consuming repo defines a +skill for a stage, invoke it; otherwise execute the stage inline per its definition here. + +## 0. Contract (optional — lock the brief before building) + +Drive fuzzy intent to a zero-ambiguity contract before behavior-changing work: goal, constraints, +acceptance criteria, captured assumptions. Persist it (a plan file in the repo's artifact location) +so later stages aim at an explicit target instead of inferring one mid-task. + +- Trigger conditions: intent is fuzzy, scope is uncalibrated, or the work changes behavior, + structure, or contracts +- Skip conditions: one-line bug fixes, or follow-ups where the contract IS the conversation +- Front-loads clarification cost in one round-trip; ask load-bearing questions one at a time, + highest architectural blast radius first + +## 1. Explore + +Structured local codebase exploration: read the relevant code, git history, file layout, tests, and +dependencies. Understand current state before changing anything. + +- Survey breadth-first (glob/grep), confirm the load-bearing files, then read those in full +- When files referenced in git status or history don't exist on disk, ask before investigating — + they may be intentionally deleted + +## 2. Research + +External verification of technical claims: official docs, primary sources, current versions. + +- No load-bearing claim accepted without verified, current information from authoritative sources +- **Task size does NOT reduce research depth** — a one-line config change gets the same + verification rigor as a multi-file feature +- Reading a document and restating its conclusions is not research — analysis requires independent + verification + +## 3. Plan + +Structured plan with rationale, test strategy, and a user approval gate before execution begins. + +- Include what will change, why, in what order, and how success is verified +- Plan depth scales to blast radius — a wide-impact change earns an adversarial stress-test pass + (assumptions, failure scenarios, operational gotchas) before approval +- **Not the same as Claude Code's built-in plan mode** — that is a read-only permission mode; this + stage is a planning discipline that can run in any mode +- For non-trivial work, decompose into phases with per-phase verifiable completion criteria + +## 4. Implement + +Structured execution with incremental validation and commit checkpoints. + +- Validate (build/test) after each logical block using the consuming repo's own commands +- Commit after green — small, frequent commits are save points +- If implementation diverges from the approved plan or hits unexpected complexity, stop and + re-plan rather than pushing through a broken approach +- At phase boundaries on long work, write a save-point (`/handoff`) so a fresh session can resume + +## 5. Test + +Testing discipline: write or extend tests for the change, run the affected suite, investigate +failures to root cause. + +- Never retry a failing test blindly — reproduce, diagnose, fix, retest +- Test the change's observable behavior, not its implementation detail + +## 6. Review + +Quality checks before verification: self-review the diff against the consuming repo's conventions +and review criteria, or delegate to a fresh-context reviewer. + +- A reviewer in a fresh context sees only the diff and the criteria — it is not anchored by the + reasoning that produced the change; prefer that over pure self-audit for non-trivial diffs + +## 7. Verify outcome + +Prove the change achieved its intent, with evidence. + +- Mechanical pass first: build + test + lint per the consuming repo's commands +- Then outcome confirmation: does the result match the contract/plan? Exercise the affected flow, + not just the compiler +- **Never claim improvement without before/after measurements** — baselines first, measure deltas, + report with data + +## 8. Retrospective + +Session analysis, learning codification, trend tracking — the self-improvement loop. Invoke the +sibling `retro` skill (`/retro`, or `/retro quick` under context pressure). + +## PR lifecycle (after step 7) + +Prep (review + verify evidence) → create → monitor CI → address review findings → merge. Standalone +sequence, not a numbered stage — see `context/pre-pr.md` for the ordered gate checklist. diff --git a/plugins/session-flow/skills/workflow/context/wrap-up.md b/plugins/session-flow/skills/workflow/context/wrap-up.md new file mode 100644 index 000000000..18bdf374d --- /dev/null +++ b/plugins/session-flow/skills/workflow/context/wrap-up.md @@ -0,0 +1,26 @@ +# End-of-Session Wrap-up + +When a task or conversation appears complete, proactively suggest these before the user leaves. +Don't wait to be asked — suggest as soon as primary work is done. + +## Checklist + +1. **PR lifecycle** — if code was modified and a PR is planned, run the pre-PR sequence + (`context/pre-pr.md`) and open the PR; if one exists, check CI status and outstanding review + comments before leaving + +2. **Save-point** — if the work is unfinished, write a `/handoff` so a fresh session resumes + without rediscovery + +3. **Retrospective** — `/retro` for substantive sessions (full analysis), `/retro quick` when + context is limited, `/retro codify` when a specific learning surfaced mid-session + +## When to suggest each item + +| Condition | Suggest | +|-----------|---------| +| Code modified, no PR exists | Pre-PR sequence, then create the PR | +| PR exists, CI running | Monitor CI before leaving | +| Work unfinished, session ending | `/handoff` | +| Any session with substantive work | `/retro` (full or quick based on context budget) | +| Session had errors or surprises | `/retro codify` (capture specific learnings immediately) | diff --git a/plugins/session-flow/skills/workflow/templates/checklist.md b/plugins/session-flow/skills/workflow/templates/checklist.md new file mode 100644 index 000000000..c0096fa48 --- /dev/null +++ b/plugins/session-flow/skills/workflow/templates/checklist.md @@ -0,0 +1,42 @@ +# Workflow Checklist + +Copy this checklist into the repo's work-artifact location as `workflow-checklist.md` (see the +workflow skill's "Consumer conventions" for where that is). Tick each box as the corresponding +stage produces its output. The ticked artifact is the durable proof-of-stage for `/clear` resume +and `/retro` analysis. + +## Stages + +- [ ] 0. Contract — goal, constraints, acceptance criteria locked (SKIP when intent is already + crisp from the user's request) +- [ ] 1. Explore — relevant code, tests, and history read → findings noted +- [ ] 2. Research — load-bearing claims verified against current authoritative sources +- [ ] 3. Plan — plan written with phases + verification criteria, user-approved (stress-tested + when blast radius is wide) +- [ ] 4. Implement — plan executed, incremental validation, commits per green phase +- [ ] 5. Test — affected suite green; new behavior covered +- [ ] 6. Review — diff reviewed against repo conventions; blocking findings resolved +- [ ] 7. Verify — outcome matches intent, with evidence (measurements where improvement is claimed) +- [ ] 8. Retrospective — `/retro` run; learnings codified + +## PR lifecycle (after step 7) + +- [ ] PR prep — pre-PR sequence complete (`context/pre-pr.md`) +- [ ] PR created +- [ ] CI green; review comments addressed +- [ ] Merged + +## Skip criteria + +A stage may be SKIPPED with explicit justification recorded next to its box. Typical skips: stage 0 +when intent is already crisp; stage 5 for doc-only changes with no behavior delta. Never skip +stages 1, 2, 3, 6, or 7 for code changes. + +## How to use + +1. At task start, copy this template into the work-artifact location. +2. As each stage produces its output, tick the box — the tick is the commitment that the stage ran + AND produced its artifact. +3. At `/clear` or session end, the ticked state is durable — the next session reads the file to + resume. +4. `/retro` analyzes ticks + skips for codification opportunities.