diff --git a/CHANGELOG.md b/CHANGELOG.md index f30e14555..ba8d4fb42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **Modular `teach` dispatcher** — `lib/dispatchers/teach-dispatcher.zsh` is now a 307-line loader that sources 10 focused modules under `lib/dispatchers/teach/` (main, content, help, init-config, slides, style, backup, status, archive, map). The monolithic 5,611-line file is gone; characterization tests in `tests/test-teach-dispatcher-characterization.zsh` guard routing behavior. +- **`flow handoff `** — scaffolds a structured `docs/planning/HANDOFF-.md` for + transferring context between Claude chat/planning sessions and Claude Code sessions (or + between Claude Code sessions across a context reset). Pre-fills the Relevant Files section + from `git diff --name-only` against a base branch, refuses to overwrite an existing handoff, + and optionally files a GitHub issue (`--issue`) from the same content. See + `docs/commands/handoff.md` and `docs/planning/PROPOSAL-claude-chat-to-code-handoff.md` for + the rationale. ## [7.15.0] — 2026-07-02 — Homebrew distribution health + doc gap fills diff --git a/commands/flow.zsh b/commands/flow.zsh index 415fcdec8..bc0b08fc6 100644 --- a/commands/flow.zsh +++ b/commands/flow.zsh @@ -77,6 +77,9 @@ flow() { status) status "$@" ;; + handoff) + _flow_handoff "$@" + ;; # ── Timer ─────────────────────────────────────────────────────────────── timer) diff --git a/completions/_flow b/completions/_flow index bf351e84a..431af2251 100644 --- a/completions/_flow +++ b/completions/_flow @@ -34,6 +34,7 @@ _flow() { 'inbox:View your inbox' 'win:Log a win' 'status:View/update .STATUS file' + 'handoff:Scaffold a structured Claude-chat-to-Claude-Code handoff document' # Actions 'test:Run tests (context-aware)' 'build:Build project (context-aware)' diff --git a/docs/commands/handoff.md b/docs/commands/handoff.md new file mode 100644 index 000000000..c0dfbca5f --- /dev/null +++ b/docs/commands/handoff.md @@ -0,0 +1,116 @@ +# Command Reference: flow handoff + +> **Scaffold a structured handoff document for transferring context from a Claude chat/planning session into a repo for a Claude Code session to pick up** + +Complete reference for the `flow handoff` command - generate a structured handoff file and +optionally file it as a GitHub issue. + +--- + +## Synopsis + +```bash +flow handoff +flow handoff --base +flow handoff --issue +flow handoff --help +``` + +--- + +## Description + +The `flow handoff` command scaffolds `docs/planning/HANDOFF-.md` using a fixed schema +designed for context transfer between AI sessions — whether that's a Claude.ai chat session +handing off to a local Claude Code session, or one Claude Code session handing off to the next +after a context reset. + +It solves a problem observed in practice: without a template, handoffs tend to sprawl across +several ad-hoc documents (a spec, a feature request, a free-form note) with overlapping content +and no consistent structure for the next session to rely on. `flow handoff` produces one file, +in one place, every time. + +See [docs/planning/PROPOSAL-claude-chat-to-code-handoff.md](../planning/PROPOSAL-claude-chat-to-code-handoff.md) +for the research behind the template's structure, and +[docs/specs/SPEC-flow-handoff-command.md](../specs/SPEC-flow-handoff-command.md) for the full +implementation spec. + +--- + +## Options + +| Option | Description | +|---|---| +| `` | Required. Used as the filename suffix: `HANDOFF-.md` | +| `--base ` | Branch to diff against for the Relevant Files pre-fill. Defaults to `dev`, falling back to `main` if `dev` doesn't exist | +| `--issue` | After creating the file, also run `gh issue create` using the handoff file as the issue body. Requires `gh` to be installed and authenticated | +| `--help`, `-h` | Show usage | + +--- + +## Generated file structure + +```markdown +# Handoff: + +**Date:** ... +**Branch:** ... +**Base for diff:** ... + +## Summary +## Key Decisions +## Traps to Avoid +## Working Agreements +## Relevant Files +## Open Work +## Verification Note +## Origin +``` + +**Relevant Files** is the only section pre-filled automatically, from +`git diff --name-only ...HEAD`. Every other section is a placeholder for you (or the +authoring session) to fill in before the handoff is used. + +**Open Work** should be written as status, not instructions — "X is not yet implemented," not +"implement X next" — so the receiving session isn't nudged toward a specific next step it hasn't +independently evaluated. + +**Verification Note** is included by default and should not be removed: it instructs the +receiving session to treat the handoff's claims as things to check against the actual repo +state, not as facts to trust unconditionally. + +--- + +## Behavior notes + +- **Refuses to overwrite.** If `docs/planning/HANDOFF-.md` already exists, the command + exits non-zero and does not touch the file. Pick a different slug, or edit the existing file + directly. +- **Empty diff is handled.** If there's no diff against the base branch yet (e.g., a fresh + branch with no commits), the Relevant Files section gets a placeholder line instead of being + blank. +- **`--issue` needs `gh` on PATH.** Without it, the command fails clearly rather than silently + skipping the issue-filing step. + +--- + +## Examples + +```bash +# Basic usage — diffs against dev (or main) automatically +flow handoff ai-rewrite-trigger + +# Diff against a specific base branch +flow handoff ai-rewrite-trigger --base main + +# Also file a GitHub issue with the handoff content as the body +flow handoff ai-rewrite-trigger --issue +``` + +--- + +## See also + +- [`wt`](../reference/REFCARD-WORKTREE-DISPATCHER.md) — worktree management, typically used + alongside `flow handoff` when a feature is scoped to its own branch/worktree +- [`status`](status.md) — for ongoing `.STATUS` tracking, distinct from one-time handoffs diff --git a/docs/planning/PROPOSAL-claude-chat-to-code-handoff.md b/docs/planning/PROPOSAL-claude-chat-to-code-handoff.md new file mode 100644 index 000000000..fcc98bbc7 --- /dev/null +++ b/docs/planning/PROPOSAL-claude-chat-to-code-handoff.md @@ -0,0 +1,94 @@ +# Proposal: Standardized Handoff Protocol — Claude Chat → Repo → Claude Code + +**Date:** 2026-07-04 +**Author:** Davood Tofighi (research + drafting assisted by Claude) +**Status:** Implemented — see `docs/specs/SPEC-flow-handoff-command.md` and the +`feature/flow-handoff-command` branch/worktree for the resulting `flow handoff` command. + +--- + +## 1. Executive Summary + +An earlier handoff (spec + feature-request + handoff note + GitHub issue, done ad hoc for the +`feature/ai-rewrite-trigger` branch) worked but was more scattered than necessary — four +documents with overlapping content and no template. This proposal researches how the broader +Claude community structures chat-to-Claude-Code handoffs, compares that against what was +actually done, and proposes (and now implements, via `flow handoff`) a single standardized +protocol. + +**Core recommendation:** collapse the four-document pattern into one structured handoff file per +feature, following a format the community has already converged on, plus a lightweight +CLAUDE.md-level distinction between stable and session state. + +--- + +## 2. Research: What the Claude Community Actually Does + +### 2.1 The stable-vs-session-state split + +The most consistent pattern across independent sources is a two-tier memory model: a stable +file (`CLAUDE.md`) holding things that rarely change — working style, standing preferences, +architectural conventions, constraints — and a session/dashboard file holding things that go +stale within days: current task, decisions made this session, open threads. Letting +session-specific state accumulate inside CLAUDE.md pollutes every future session with outdated +context. + +### 2.2 The structured `/transfer-context` pattern + +A community-published Claude Code skill defines an opinionated schema for handoff content: +Summary (completed work only), Key Decisions (with why), Traps to Avoid (dead ends already +tried), Working Agreements (interaction preferences), Relevant Files (path + line range + why), +Open Work (framed as status, not instructions — "X is not yet implemented," never "implement X +next"), and a closing instruction telling the next session to verify every claim against the +actual code rather than trust the handoff at face value. + +### 2.3 Write the handoff before compaction, not after + +A separate community pattern favors explicitly writing a handoff document before context runs +out, rather than relying on automatic compaction. The next session starts with only the plan, +not the accumulated back-and-forth of the prior conversation; the old transcript is linked, not +embedded, so it's available on demand without cluttering fresh context. + +### 2.4 Anthropic's own documented mechanism + +Claude Code's official docs describe a "plan locally, execute remotely" pattern: collaborate on +an approach locally, commit the plan to the repo, then launch a separate execution context +against that committed artifact. The transferable principle: the plan must be committed before +the next session begins, since that session has no access to the prior conversation — only to +what's on disk. + +### 2.5 Handoff bundles as a validated general concept + +Anthropic's own Claude Design → Claude Code handoff confirms that a purpose-built, structured +handoff format outperforms an unstructured context dump. + +--- + +## 3. Gap Analysis (original ad-hoc handoff vs. best practice) + +| Dimension | Ad-hoc approach | Best practice | Resolution | +|---|---|---|---| +| Number of documents | 4, overlapping | 1 structured file | `flow handoff` produces exactly one | +| Stable vs. session state | No distinction | CLAUDE.md vs. per-feature handoff | Handoff scoped to `docs/planning/`, never promoted into CLAUDE.md | +| Open work framing | Mixed status/instructions | Status only | Template enforces status-only phrasing | +| Verify-don't-trust instruction | Missing | Explicit | Built into the template's Verification Note | +| Committed before handoff assumed complete | Done correctly | Matches best practice | No change needed | + +--- + +## 4. Implementation + +Implemented as `flow handoff ` — see `docs/specs/SPEC-flow-handoff-command.md` for the +full spec, `docs/commands/handoff.md` for the command reference, and `lib/handoff-helpers.zsh` +for the implementation. + +--- + +## 5. Sources + +- jdhodges.com — "Claude Handoff Prompt: How to Keep Context Across Sessions" (2026) +- artemxtech.substack.com — "Never lose your work between Claude Code sessions" (2026) +- GitHub gist (BexTuychiev) — `/transfer-context` skill definition +- github.com/ykdojo/claude-code-tips — community tips repo, `/dx:handoff` plugin command +- code.claude.com/docs — "Use Claude Code on the web" (official docs, plan-locally/execute-remotely pattern) +- claudefa.st/blog — "Claude Design to Claude Code: AI Design Handoff" diff --git a/docs/specs/SPEC-flow-handoff-command.md b/docs/specs/SPEC-flow-handoff-command.md new file mode 100644 index 000000000..eb52eefab --- /dev/null +++ b/docs/specs/SPEC-flow-handoff-command.md @@ -0,0 +1,138 @@ +# SPEC: `flow handoff` — Structured Claude Chat → Repo → Claude Code Handoff Command + +**Status:** DRAFT → Implementing +**Date:** 2026-07-04 +**Author:** Davood Tofighi (spec drafted with Claude) +**Branch:** `feature/flow-handoff-command` +**Worktree:** `~/.git-worktrees/flow-cli/feature-flow-handoff-command` +**Origin:** `docs/planning/PROPOSAL-claude-chat-to-code-handoff.md` (research + rationale) + +--- + +## 1. Goal + +A `flow handoff ` command that scaffolds a structured handoff document (per the schema +in the origin proposal), pre-fills the "Relevant Files" section from `git diff --stat` against +a base branch, never clobbers an existing handoff, and optionally files a GitHub issue from the +same content. Removes the manual, ad-hoc, multi-document handoff process observed in the +`feature/ai-rewrite-trigger` branch. + +## 2. Command surface + +```bash +flow handoff # create docs/planning/HANDOFF-.md +flow handoff --base # diff against a specific base branch (default: dev, falls back to main) +flow handoff --issue # also file a GitHub issue via gh CLI, body = handoff content +flow handoff --help # usage +``` + +## 3. Behavior contract + +- **Idempotent by refusal:** if `docs/planning/HANDOFF-.md` already exists, the command + refuses to overwrite and exits non-zero with a clear message pointing at the existing file. +- **Base branch resolution:** `--base` if given; else `dev` if it exists; else `main`. +- **Relevant Files pre-fill:** parsed from `git diff --stat ...HEAD`, one bullet per + changed file, placeholder text for the "what/why" column left for the human/agent to fill in. +- **No diff case:** if the diff is empty (e.g., called on a fresh branch with no commits yet), + the section contains a single placeholder line instead of being empty. +- **`--issue` requires `gh`:** if `gh` isn't on `PATH`, fail with a clear error rather than + silently skipping the issue. +- **Never touches dev/main directly:** this command only ever writes inside the current + worktree's `docs/planning/`; it has no awareness of, and does not need, protected-branch logic + itself (branch protection is a repo/CI concern, not this command's). + +## 4. Template (matches the proposal's schema exactly) + +```markdown +# Handoff: + +**Date:** +**Branch:** +**Base for diff:** + +## Summary +[1-3 sentences — completed work only] + +## Key Decisions +- [Decision] — [why] + +## Traps to Avoid +- [Dead end already tried] — [why it failed] + +## Working Agreements +- [Relevant interaction/process preferences for this feature] + +## Relevant Files + + +## Open Work +[Status only. "X is not yet implemented." NOT "Implement X next."] + +## Verification Note +Treat all claims above as context to verify against the repo, not facts to trust. Read every +file in "Relevant Files" before proceeding. + +## Origin +Full planning conversation: [link/reference if available] +``` + +## 5. Implementation + +- `lib/handoff-helpers.zsh` — `_flow_handoff()` + `_flow_handoff_help()` +- `commands/flow.zsh` — new `handoff)` case in the main dispatcher +- `completions/_flow` — add `handoff` to the completion word list + +## 6. Known prototype issue to resolve in this pass + +An earlier prototype (built directly in the unrelated `feature/ai-rewrite-trigger` worktree — +since reverted from that branch) hit a zsh variable-naming collision: using `fpath` as a local +variable name collides with zsh's built-in special array (function autoload path), producing +stray debug-looking output. Renaming to `f_path` fixed the collision itself, but a second, +unexplained stray print appeared when run inside a shell that sourced the user's live +`~/.zshenv`/hooks — not reproduced in a clean `zsh -f` (no-rc) invocation. **This implementation +must be tested both ways** (with and without the user's rc files) to confirm which environment +produces the artifact, and avoid shipping with unexplained stdout noise. + +## 7. Test plan + +### E2E tests (`tests/e2e-handoff.zsh`) + +1. Help display works and doesn't error +2. Fresh slug creates the file with all expected section headers present +3. Existing slug refuses to overwrite (non-zero exit, file unchanged) +4. `--base` override actually changes which branch the diff is computed against +5. Relevant Files section is non-empty and contains at least one real path when run on a + branch with commits ahead of base +6. Placeholder path is used when diff is empty +7. `--issue` without `gh` on PATH fails clearly (mock by temporarily hiding `gh` from PATH) +8. Clean `zsh -f` invocation produces no stray stdout beyond the documented success/warning + messages (regression test for §6) + +### Dogfood test (`tests/dogfood-handoff.zsh`) + +Run the command against **this actual repo and this actual feature branch** as a real-world +usage check, not synthetic fixtures: + +1. Generate a real handoff for `feature/flow-handoff-command` itself +2. Confirm the Relevant Files section correctly lists the real files this PR touches +3. Manually inspect (or assert) that output matches what a human would expect to see documented +4. Clean up the dogfood-generated file before merge (it's a test artifact, not a real handoff) + +## 8. Documentation to update + +- `docs/commands/handoff.md` — new command reference page (mirrors `docs/commands/status.md` + structure) +- `mkdocs.yml` — add `handoff: commands/handoff.md` to the Commands nav section +- `docs/index.md` — check whether commands are enumerated there; add if so +- `CHANGELOG.md` — new entry under Unreleased/next version +- `docs/planning/PROPOSAL-claude-chat-to-code-handoff.md` — mark proposal as "Implemented" with + a pointer to this spec and branch + +## 9. Merge readiness checklist + +- [ ] All e2e tests pass +- [ ] Dogfood test passes and its artifact is cleaned up +- [ ] `scripts/check-links.js` / `scripts/lint-docs.sh` pass (if run as part of pre-commit/CI) +- [ ] `npm test` passes (existing suite unaffected) +- [ ] Docs + mkdocs nav + CHANGELOG all updated +- [ ] PR opened against `dev` with a summary referencing this spec diff --git a/flow.plugin.zsh b/flow.plugin.zsh index c17756d3e..e4dcc40d1 100644 --- a/flow.plugin.zsh +++ b/flow.plugin.zsh @@ -56,6 +56,7 @@ source "$FLOW_PLUGIN_DIR/lib/em-render.zsh" source "$FLOW_PLUGIN_DIR/lib/em-ics.zsh" source "$FLOW_PLUGIN_DIR/lib/em-watch.zsh" source "$FLOW_PLUGIN_DIR/lib/tok-sync.zsh" +source "$FLOW_PLUGIN_DIR/lib/handoff-helpers.zsh" # ============================================================================ # COMMANDS diff --git a/lib/handoff-helpers.zsh b/lib/handoff-helpers.zsh new file mode 100644 index 000000000..560282cd7 --- /dev/null +++ b/lib/handoff-helpers.zsh @@ -0,0 +1,170 @@ +#!/usr/bin/env zsh +# ══════════════════════════════════════════════════════════════════════════════ +# HANDOFF - Structured Claude-chat-to-Claude-Code handoff generator +# ══════════════════════════════════════════════════════════════════════════════ +# +# File: lib/handoff-helpers.zsh +# Version: 1.0 +# Date: 2026-07-04 +# Pattern: command + keyword + options (matches wt-dispatcher.zsh style) +# +# Usage: flow handoff [--issue] [--base ] +# +# Examples: +# flow handoff ai-rewrite-trigger +# flow handoff ai-rewrite-trigger --base dev +# flow handoff ai-rewrite-trigger --issue +# +# Spec: docs/specs/SPEC-flow-handoff-command.md +# Origin: docs/planning/PROPOSAL-claude-chat-to-code-handoff.md +# +# ══════════════════════════════════════════════════════════════════════════════ + +if [[ -z "$_C_BOLD" ]]; then + _C_BOLD='\033[1m' + _C_DIM='\033[2m' + _C_NC='\033[0m' + _C_RED='\033[31m' + _C_GREEN='\033[32m' + _C_YELLOW='\033[33m' + _C_BLUE='\033[34m' + _C_MAGENTA='\033[35m' + _C_CYAN='\033[36m' +fi + +_flow_handoff() { + local slug="" + local do_issue=false + local base_branch="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --issue) do_issue=true ;; + --base) shift; base_branch="$1" ;; + --help|-h) _flow_handoff_help; return 0 ;; + -*) echo -e "${_C_RED}✗ Unknown option: $1${_C_NC}"; return 1 ;; + *) slug="$1" ;; + esac + shift + done + + if [[ -z "$slug" ]]; then + echo -e "${_C_RED}✗ Feature slug required${_C_NC}" + echo "Usage: flow handoff [--issue] [--base ]" + return 1 + fi + + local git_root + git_root=$(git rev-parse --show-toplevel 2>/dev/null) + if [[ -z "$git_root" ]]; then + echo -e "${_C_RED}✗ Not in a git repository${_C_NC}" + return 1 + fi + + local planning_dir="$git_root/docs/planning" + local handoff_path="$planning_dir/HANDOFF-${slug}.md" + mkdir -p "$planning_dir" + + if [[ -f "$handoff_path" ]]; then + echo -e "${_C_YELLOW}⚠ Handoff already exists: $handoff_path${_C_NC}" + echo -e "${_C_DIM}Not overwriting. Edit it directly, or pick a different slug.${_C_NC}" + return 1 + fi + + if [[ -z "$base_branch" ]]; then + if git show-ref --verify --quiet refs/heads/dev 2>/dev/null; then + base_branch="dev" + else + base_branch="main" + fi + fi + + local current_branch + current_branch=$(git branch --show-current 2>/dev/null) + + # Pre-fill "Relevant Files" using --name-only (simpler + no per-line parsing + # of a special-variable-adjacent name; avoids the fpath collision hit during + # prototyping — see spec §6) + local relevant_files="" + local changed_files + changed_files=$(git diff --name-only "${base_branch}...HEAD" -- 2>/dev/null) + + if [[ -n "$changed_files" ]]; then + local this_file + for this_file in ${(f)changed_files}; do + relevant_files+="- \`${this_file}\` — [what changed and why]"$'\n' + done + else + relevant_files="- [no diff vs ${base_branch} yet — fill in manually]"$'\n' + fi + + cat > "$handoff_path" </dev/null; then + echo -e "${_C_RED}✗ gh CLI not found — cannot file issue${_C_NC}" + return 1 + fi + echo "" + echo -e "${_C_BLUE}Filing GitHub issue from handoff content...${_C_NC}" + gh issue create --title "handoff: ${slug}" --body-file "$handoff_path" + fi +} + +_flow_handoff_help() { + echo -e " +${_C_BOLD}flow handoff${_C_NC} - Structured Claude-chat-to-Claude-Code handoff generator + +${_C_YELLOW}USAGE${_C_NC}: + ${_C_CYAN}flow handoff ${_C_NC} Create docs/planning/HANDOFF-.md + ${_C_CYAN}flow handoff --issue${_C_NC} Also file a GitHub issue from it + ${_C_CYAN}flow handoff --base ${_C_NC} Diff against a specific base branch + +${_C_YELLOW}WHAT IT DOES${_C_NC}: + 1. Scaffolds a structured handoff file (Summary, Key Decisions, Traps to Avoid, + Working Agreements, Relevant Files, Open Work, Verification Note, Origin) + 2. Pre-fills Relevant Files from 'git diff --name-only' against the base branch + 3. Never overwrites an existing handoff file for the same slug + 4. Optionally files a GitHub issue with the handoff content as the body + +${_C_YELLOW}SEE ALSO${_C_NC}: + ${_C_DIM}docs/specs/SPEC-flow-handoff-command.md${_C_NC} for the full spec + ${_C_DIM}docs/planning/PROPOSAL-claude-chat-to-code-handoff.md${_C_NC} for the rationale + ${_C_DIM}wt${_C_NC} for worktree management this pairs with +" +} diff --git a/mkdocs.yml b/mkdocs.yml index 26a003c92..e6771fcc8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -299,6 +299,7 @@ nav: - morning: commands/morning.md - dash: commands/dash.md - status: commands/status.md + - handoff: commands/handoff.md - sync: commands/sync.md - doctor: commands/doctor.md - config: commands/config.md diff --git a/tests/dogfood-handoff.zsh b/tests/dogfood-handoff.zsh new file mode 100755 index 000000000..abb1f8de6 --- /dev/null +++ b/tests/dogfood-handoff.zsh @@ -0,0 +1,67 @@ +#!/usr/bin/env zsh +# dogfood-handoff.zsh - Real-world usage check for `flow handoff` +# +# Unlike e2e-handoff.zsh (isolated temp repo), this runs the command against +# THIS actual repo and the actual feature/flow-handoff-command branch, to +# verify it behaves sensibly on real data before merge. Cleans up its own +# generated artifact - this is a test run, not a real handoff. +# +# Usage: zsh tests/dogfood-handoff.zsh + +SCRIPT_DIR="${0:A:h}" +PROJECT_ROOT="${SCRIPT_DIR:h}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +RESET='\033[0m' + +cd "$PROJECT_ROOT" || exit 1 +source "$PROJECT_ROOT/lib/handoff-helpers.zsh" + +DOGFOOD_SLUG="dogfood-self-test-$$" +DOGFOOD_FILE="$PROJECT_ROOT/docs/planning/HANDOFF-${DOGFOOD_SLUG}.md" + +cleanup() { + rm -f "$DOGFOOD_FILE" +} +trap cleanup EXIT + +echo "${CYAN}Dogfooding: flow handoff against real repo, real branch${RESET}" +echo "" + +current_branch=$(git branch --show-current) +echo " Branch under test: $current_branch" + +_flow_handoff "$DOGFOOD_SLUG" --base dev +rc=$? + +if [[ $rc -ne 0 ]]; then + echo "${RED}✗ Command exited non-zero ($rc) on real repo${RESET}" + exit 1 +fi + +if [[ ! -f "$DOGFOOD_FILE" ]]; then + echo "${RED}✗ Expected file not created: $DOGFOOD_FILE${RESET}" + exit 1 +fi + +echo "" +echo "${CYAN}Generated Relevant Files section (real diff against dev):${RESET}" +sed -n '/## Relevant Files/,/## Open Work/p' "$DOGFOOD_FILE" | sed '$d' | sed 's/^/ /' + +echo "" +actual_changed_files=$(git diff --name-only dev...HEAD -- 2>/dev/null | wc -l | tr -d ' ') +listed_files=$(grep -c '^- `' "$DOGFOOD_FILE") + +if [[ "$actual_changed_files" -gt 0 && "$listed_files" -ne "$actual_changed_files" ]]; then + echo "${YELLOW}⚠ Listed file count ($listed_files) doesn't match actual diff count ($actual_changed_files)${RESET}" + echo " ${YELLOW}(not a hard failure - review manually if this looks wrong)${RESET}" +fi + +echo "${GREEN}✓ Dogfood run completed and artifact cleaned up${RESET}" +echo "" +echo "${CYAN}Reminder:${RESET} this test's generated file is deleted automatically on exit -" +echo "it is a test run, not a real handoff for this branch." +exit 0 diff --git a/tests/e2e-handoff.zsh b/tests/e2e-handoff.zsh new file mode 100755 index 000000000..bb457e291 --- /dev/null +++ b/tests/e2e-handoff.zsh @@ -0,0 +1,164 @@ +#!/usr/bin/env zsh +# e2e-handoff.zsh - End-to-end tests for `flow handoff` command +# +# Tests the full flow handoff path against an isolated temp git repo (never +# touches the real flow-cli repo state): +# - Help display +# - Fresh slug creates file with expected sections +# - Existing slug refuses to overwrite +# - --base override changes the diff base +# - Relevant Files pre-fill (non-empty diff, and empty-diff placeholder) +# - --issue fails clearly without `gh` on PATH +# - Clean `zsh -f` invocation produces no unexpected stray stdout +# +# Usage: zsh tests/e2e-handoff.zsh + +SCRIPT_DIR="${0:A:h}" +PROJECT_ROOT="${SCRIPT_DIR:h}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +DIM='\033[2m' +RESET='\033[0m' + +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 + +run_test() { + local test_name="$1" + local test_func="$2" + + TESTS_RUN=$((TESTS_RUN + 1)) + echo -n " ${CYAN}[$TESTS_RUN] $test_name...${RESET} " + + local output + output=$(eval "$test_func" 2>&1) + local rc=$? + + if [[ $rc -eq 0 ]]; then + echo "${GREEN}PASS${RESET}" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + echo "${RED}FAIL${RESET}" + echo "${DIM}$output${RESET}" | sed 's/^/ /' + TESTS_FAILED=$((TESTS_FAILED + 1)) + fi +} + +# ── Isolated temp repo setup ──────────────────────────────────────────────── + +TMP_REPO=$(mktemp -d) +trap 'rm -rf "$TMP_REPO"' EXIT + +setup_temp_repo() { + cd "$TMP_REPO" || return 1 + git init -q -b dev + git config user.email "test@example.com" + git config user.name "Test" + echo "seed" > seed.txt + git add seed.txt + git commit -qm "seed" +} + +setup_temp_repo + +source "$PROJECT_ROOT/lib/handoff-helpers.zsh" + +echo "${CYAN}Running e2e tests for flow handoff (temp repo: $TMP_REPO)${RESET}" +echo "" + +test_help_display() { + cd "$TMP_REPO" || return 1 + local out + out=$(_flow_handoff --help 2>&1) + [[ "$out" == *"USAGE"* ]] || return 1 + return 0 +} + +test_fresh_slug_creates_file() { + cd "$TMP_REPO" || return 1 + _flow_handoff e2e-fresh-slug >/dev/null 2>&1 + local f="$TMP_REPO/docs/planning/HANDOFF-e2e-fresh-slug.md" + [[ -f "$f" ]] || return 1 + grep -q "## Summary" "$f" || return 1 + grep -q "## Relevant Files" "$f" || return 1 + grep -q "## Verification Note" "$f" || return 1 + return 0 +} + +test_existing_slug_refuses() { + cd "$TMP_REPO" || return 1 + local f="$TMP_REPO/docs/planning/HANDOFF-e2e-fresh-slug.md" + local before_mtime + before_mtime=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null) + _flow_handoff e2e-fresh-slug >/dev/null 2>&1 + local rc=$? + [[ $rc -ne 0 ]] || return 1 + local after_mtime + after_mtime=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null) + [[ "$before_mtime" == "$after_mtime" ]] || return 1 + return 0 +} + +test_relevant_files_nonempty_on_real_diff() { + cd "$TMP_REPO" || return 1 + git checkout -qb e2e-diff-branch + echo "change" > changed.txt + git add changed.txt + git commit -qm "add changed file" + _flow_handoff e2e-diff-check --base dev >/dev/null 2>&1 + local f="$TMP_REPO/docs/planning/HANDOFF-e2e-diff-check.md" + grep -q "changed.txt" "$f" || return 1 + git checkout -q dev + git branch -D e2e-diff-branch -q + return 0 +} + +test_empty_diff_placeholder() { + cd "$TMP_REPO" || return 1 + _flow_handoff e2e-empty-diff --base dev >/dev/null 2>&1 + local f="$TMP_REPO/docs/planning/HANDOFF-e2e-empty-diff.md" + grep -q "no diff vs dev yet" "$f" || return 1 + return 0 +} + +test_issue_fails_without_gh() { + cd "$TMP_REPO" || return 1 + local fake_path + fake_path=$(mktemp -d) + # PATH with no gh binary reachable + local out + out=$(PATH="$fake_path:/usr/bin:/bin" _flow_handoff e2e-issue-test --issue 2>&1) + local rc=$? + rm -rf "$fake_path" + [[ $rc -ne 0 ]] || return 1 + [[ "$out" == *"gh"* ]] || return 1 + return 0 +} + +test_clean_zshf_no_stray_output() { + cd "$TMP_REPO" || return 1 + local out + out=$(zsh -f -c "source '$PROJECT_ROOT/lib/handoff-helpers.zsh'; cd '$TMP_REPO'; _flow_handoff e2e-clean-run --base dev" 2>&1) + # Only expect our own known ✓/⚠ lines and the dim sub-line - no bare + # "varname=value" style stray assignment echoes + if echo "$out" | grep -qE '^[a-zA-Z_]+=[^ ]'; then + return 1 + fi + return 0 +} + +run_test "help display works" test_help_display +run_test "fresh slug creates file with expected sections" test_fresh_slug_creates_file +run_test "existing slug refuses to overwrite" test_existing_slug_refuses +run_test "relevant files populated on real diff" test_relevant_files_nonempty_on_real_diff +run_test "empty diff uses placeholder line" test_empty_diff_placeholder +run_test "--issue fails clearly without gh on PATH" test_issue_fails_without_gh +run_test "clean zsh -f run has no stray stdout" test_clean_zshf_no_stray_output + +echo "" +echo "${CYAN}Results:${RESET} $TESTS_PASSED/$TESTS_RUN passed, $TESTS_FAILED failed" +[[ $TESTS_FAILED -eq 0 ]] && exit 0 || exit 1