swain-session

Installation
SKILL.md

Session

Manages session identity, preferences, and context continuity across agent sessions. This skill is agent-agnostic — it relies on AGENTS.md for auto-invocation.

Auto-run behavior

This skill is invoked automatically at session start (see AGENTS.md). When auto-invoked:

  1. Restore tab name — run the tab-naming script
  2. Load preferences — read session.json and apply any stored preferences
  3. Show context bookmark — if a previous session left a context note, display it

When invoked manually, the user can change preferences or bookmark context.

Session purpose text

When the operator launches with free text (e.g., swain new bug about timestamps), the launcher exports SWAIN_PURPOSE and — for runtimes that accept an initial prompt — also passes it inline as /swain-session Session purpose: new bug about timestamps.

The launcher is responsible for choosing the checkout that will own that bookmark:

  • If the operator starts from the main checkout, the launcher opens a new worktree first and only then passes the session purpose.
  • If the operator starts inside a linked worktree that already has a bookmark, the launcher should steer them to resume/finish that worktree or open a different worktree before reusing the purpose text.

The greeting script (swain-session-greeting.sh) reads $SWAIN_PURPOSE and writes the bookmark deterministically (SPEC-297). The greeting JSON exposes the captured text as the purpose field.

When the greeting JSON's purpose field is non-null:

  • Display it to the operator: **Session purpose:** <text>.

Do not re-parse the initial prompt or call swain-bookmark.sh yourself — the greeting already did both. The inline prompt text is for display context only; the env var is the source of truth.

Preflight

Before any step, run the preflight script to gather all session state in a single pass. This replaces the old subprocess chain (greeting → bootstrap → tab-name) with one read-only script.

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
PREFLIGHT_SCRIPT="$(find "$REPO_ROOT" -path '*/swain-session/scripts/swain-session-preflight.sh' -print -quit 2>/dev/null)"
PREFLIGHT_JSON=$( bash "$PREFLIGHT_SCRIPT" --repo-root "$REPO_ROOT" 2>/dev/null )
echo "$PREFLIGHT_JSON"

Store PREFLIGHT_JSON for use in all steps below. Every decision references a field from this JSON — do not run additional check commands unless performing a mutation.

Step 1 — Fast Greeting (SPEC-194)

Run the greeting script. It calls the preflight internally and applies lightweight mutations (tab naming, lock cleanup, .agents dir creation). It does not invoke specgraph, GitHub API, or the full status dashboard.

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-session-greeting.sh" --json

The greeting emits structured JSON:

{
  "greeting": true,
  "branch": "trunk",
  "dirty": false,
  "isolated": false,
  "bookmark": "Left off implementing the bootstrap script",
  "focus": "VISION-001",
  "tab": "project @ branch",
  "warnings": []
}

The preflight JSON also includes previous session state under prev_session, which eliminates the need for a separate swain-session-state.sh resume call:

{
  "prev_session": {
    "exists": true,
    "status": "closed",
    "session_id": "session-20260404-215204-9576",
    "focus_lane": "INITIATIVE-002",
    "phase": "closed",
    "start_time": "2026-04-05T01:52:04Z",
    "end_time": "2026-04-06T04:10:09Z",
    "decisions_made": 0,
    "walkaway": "Reviewed and fixed SPIKE-058"
  }
}

After receiving the greeting JSON:

  1. Present the greeting to the operator — branch, dirty state, bookmark (if any), focus lane (if any), and warnings.

  2. If prev_session.exists is true, display the previous session context (from the preflight JSON) so the operator can decide whether to continue or start fresh.

  3. If isolated is false and the operator has not started work yet, do not create a worktree now — worktree creation is handled by bin/swain pre-launch (SPEC-245). If a worktree name is needed for reference, generate it:

    bash "$REPO_ROOT/.agents/bin/swain-worktree-name.sh" "context"
    

    Then re-run the greeting with --path to refresh tab name and context:

    bash "$REPO_ROOT/.agents/bin/swain-session-greeting.sh" --path "$(pwd)" --json
    
  4. If bookmark is not null, display it:

    Resuming session — Last time: {bookmark}

  5. The session is now ready for work. The full status dashboard is available on-demand (see Status Dashboard).

If $TMUX is NOT set (detected by absence of tab in the JSON), check whether tmux is installed:

  • tmux not installed: Offer to install it (brew install tmux).
  • tmux installed but not in a session: Show: [note] Not in a tmux session — session tab and pane features unavailable

The operator can say "exit worktree" or "back to main" at any time — this ends the session. bin/swain handles worktree cleanup after the runtime exits (SPEC-245).

Worktree / branch changes (agent-agnostic)

When an agent enters a worktree or switches branches, re-run the bootstrap with --path to update the tab name:

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-session-bootstrap.sh" --path "$NEW_WORKDIR" --skip-worktree --auto

This is agent-agnostic — works in Claude Code, opencode, gemini cli, codex, copilot, or any agent that reads AGENTS.md and can run bash commands.

Session.json schema

{
  "lastBranch": "trunk",
  "lastContext": "Working on swain-session skill",
  "preferences": {
    "verbosity": "concise"
  },
  "bookmark": {
    "note": "Left off implementing the bootstrap script",
    "files": ["SKILL.md"],
    "timestamp": "2026-03-10T14:32:00Z"
  }
}

Migration: If .agents/session.json does not exist but the old global location (~/.claude/projects/<project-path-slug>/memory/session.json) does, the bootstrap script copies it automatically.

README Reconciliation Checkpoint (SPEC-209)

After the greeting and before work begins, compare README.md against the artifact tree. This runs once per session, at focus lane selection time.

Trigger

When a focus lane is set (either restored from a previous session or newly selected), and README.md exists:

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
[ -f "$REPO_ROOT/README.md" ] && echo "has_readme" || echo "no_readme"

If no README exists, skip reconciliation silently — swain-doctor will flag the missing README.

Process

  1. Read README.md and extract claims — any statement about what the project does, who it's for, how it works, what it supports, or what behavior it exhibits. Read the entire README as prose; no markers or section conventions required.
  2. Compare claims against current Active Visions, Designs, Journeys, and Persona artifacts. Look for:
    • Stale promises — README claims a feature or behavior that an artifact explicitly dropped or superseded.
    • Missing coverage — an artifact describes a capability the README doesn't mention.
    • Contradictions — README and artifact disagree on behavior, audience, or scope.
  3. For each mismatch, surface a specific question to the operator:

    "README says '{claim}' but {artifact-id} {describes the conflict}. Which is right?"

Reconciliation direction

Bidirectional. Drift does not assume artifacts are right:

  • A new Vision may mean the README needs updating.
  • The README may be right and the Vision needs reshaping.
  • A promise may have been intentionally dropped and needs removing from both.

Deferral tracking

The operator can defer any mismatch. Deferrals are tracked in .agents/session.json under a readme_deferrals key:

{
  "readme_deferrals": [
    {
      "claim": "real-time sync",
      "conflict_artifact": "VISION-003",
      "deferred_at": "2026-03-31T14:00:00Z"
    }
  ]
}

Deferred items are raised again at the next session start. When the operator resolves a deferral (updates README or artifact), remove it from the list.

Silent pass

If no drift is detected, the reconciliation check passes silently — no output to the operator.

Session Lifecycle (SPEC-119)

swain-session owns a bounded session lifecycle: start → work → close → resume. Session state is tracked in .agents/session-state.json via the swain-session-state.sh script.

Session start

After bootstrap completes and the worktree is ready, initialize the session lifecycle:

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-session-state.sh" init --focus "<FOCUS-ID>" --session-roadmap "$(pwd)/SESSION-ROADMAP.md" --repo-root "$REPO_ROOT"

This:

  1. Creates .agents/session-state.json with focus lane, decision budget (default 5), and start time
  2. Generates SESSION-ROADMAP.md via chart.sh session --focus <ID>

The focus lane defaults to the previous session's lane (from bootstrap JSON session.focus). Confirm with the operator or accept their redirect.

Custom decision budget: --budget 7

During work — recording decisions

When the operator or agent makes a decision (approves a spec, chooses an approach, sets direction), record it:

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-session-state.sh" record-decision --note "Approved SPEC-119 implementation approach"

Session close

When the operator says "done", "wrap up", "close session", or the decision budget is reached, execute this close sequence. Critical: swain-retro must run while the session is still active so it can read session state. Do not close the session before running retro.

Step 1 — Generate session digest and progress logs

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-session-digest.sh" --session-id "$(jq -r .session_id "$REPO_ROOT/.agents/session-state.json")" --output "$REPO_ROOT/.agents/session-log.jsonl"
bash "$REPO_ROOT/.agents/bin/swain-progress-log.sh" --digest "$REPO_ROOT/.agents/session-log.jsonl"

This appends a JSONL digest entry and updates each touched EPIC/Initiative's progress.md and ## Progress section.

Step 2 — Run retro (session still active)

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
SWAIN_RETRO_SKILL="$REPO_ROOT/.claude/skills/swain-retro/SKILL.md"
Skill("$SWAIN_RETRO_SKILL", "Session close — session is closing. Run /swain-retro to capture session learnings before the session state is cleared.")

Important: Retro reads session.json and session-state.json while they are still populated. Do not call session-state.sh close before this step.

Step 3 — Close the session

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-session-state.sh" close --walkaway "Completed SPEC-119 tests and state management" --session-roadmap "$(pwd)/SESSION-ROADMAP.md"

This sets session phase to closed with end time and appends the walk-away signal to SESSION-ROADMAP.md.

Step 4 — Run session teardown

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
SWAIN_TEARDOWN_SKILL="$REPO_ROOT/.claude/skills/swain-teardown/SKILL.md"
Skill("$SWAIN_TEARDOWN_SKILL", "Session teardown — --session-chain flag passed from swain-session close handler.")

This runs orphan worktree checks, git dirty-state check, ticket sync prompt, and writes a handoff summary. The --session-chain flag tells teardown to skip the redundant session-active check since the handler already confirmed session state.

Step 5 — Commit SESSION-ROADMAP.md

Finally, commit SESSION-ROADMAP.md to git.

Session resume

On the next session start, read prev_session from the preflight JSON (see Preflight). This includes the previous session's focus lane, walkaway note, decision count, and staleness status — no separate script call needed.

If you need to call session-state.sh directly (e.g., from a script that doesn't have the preflight JSON):

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-session-state.sh" resume

Display the previous session context so the operator can decide whether to continue or start fresh.

Session state schema

{
  "session_id": "session-20260328-220634-4ad1",
  "focus_lane": "INITIATIVE-019",
  "phase": "active",
  "start_time": "2026-03-28T22:06:34Z",
  "last_activity_time": "2026-03-28T22:06:34Z",
  "end_time": null,
  "decision_budget": 5,
  "decisions_made": 0,
  "decisions": [],
  "walkaway": null
}

Manual invocation commands

When invoked explicitly by the user, support these operations:

Set tab name

User says something like "set tab name to X" or "rename tab":

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-tab-name.sh" "Custom Name"

Bookmark context

User says "remember where I am" or "bookmark this":

  • Infer what they're working on from conversation context, or use the note they provided — do not prompt the user
  • Write to session.json bookmark field with note, relevant files, and timestamp
  • If a bookmark already exists, overwrite it silently without asking for confirmationswain-bookmark.sh handles atomic writes

Clear bookmark

User says "clear bookmark" or "fresh start":

  • Remove the bookmark field from session.json

Show session info

User says "session info" or "what's my session":

  • Display current tab name, branch, preferences, bookmark status
  • If the bookmark note contains an artifact ID (e.g., SPEC-052, EPIC-018), show the Vision ancestry breadcrumb for strategic context. Run bash "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents/bin/chart.sh" scope <ID> 2>/dev/null | head -5 to get the parent chain. Display as: Context: Swain > Operator Situational Awareness > Vision-Rooted Chart Hierarchy

Set preference

User says "set preference X to Y":

  • Update preferences in session.json

Post-operation bookmark (auto-update protocol)

Other swain skills update the session bookmark after operations. Read references/bookmark-protocol.md for the protocol, invocation patterns, and examples.

Focus Lane

The operator can set a focus lane to scope recommendations within a single vision or initiative. This is a steering mechanism — it doesn't hide other work, but frames recommendations around the operator's current focus.

Setting focus: When the operator says "focus on security" or "I'm working on VISION-001", resolve the name to an artifact ID and invoke the focus script.

Name-to-ID resolution: If the operator uses a name instead of an ID (e.g., "security" instead of "VISION-001"), search Vision and Initiative artifact titles for the best match using swain chart:

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/chart.sh" --ids --flat 2>/dev/null | grep -i "<name>"

If exactly one match, use it. If multiple matches, ask the operator to clarify. If no match, tell the operator no Vision or Initiative matches that name and offer to create one.

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-focus.sh" set <RESOLVED-ID>

Clearing focus:

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-focus.sh" clear

Checking focus:

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-focus.sh"

Display the focus artifact as a context line by calling artifact-context.sh on the focus ID. Fall back to the bare ID if the utility is unavailable.

Focus lane is stored in .agents/session.json under the focus_lane key. It persists across status checks within a session. The status dashboard reads it to filter recommendations and show peripheral awareness for non-focus visions.

Status Dashboard (SPEC-122)

swain-session now owns the project status dashboard. When the operator says "status", "what's next", "dashboard", "overview", "where are we", "what should I work on", or "show me priorities", run the status script:

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
STATUS_SCRIPT="$REPO_ROOT/.agents/bin/swain-status.sh"
[ -f "$STATUS_SCRIPT" ] && bash "$STATUS_SCRIPT" --refresh || echo "status dashboard script not found"

For compact mode (MOTD): bash "$STATUS_SCRIPT" --compact

After the script runs, present a structured agent summary following references/agent-summary-template.md.

Cache

Status writes to .agents/status-cache.json with 120-second TTL. Use --refresh to bypass, --json for raw output.

Recommendation

Read .priority.recommendations[0] from the JSON cache. When a focus lane is set, recommendations scope to that vision/initiative.

Context-rich display

When presenting artifacts to the operator (recommendations, focus lane, decisions needed), use the artifact-context utility instead of bare IDs:

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
CONTEXT=$(bash "$REPO_ROOT/.agents/bin/artifact-context.sh" <ARTIFACT-ID> 2>/dev/null)

If the utility is available and returns output, use the context line. If unavailable or empty, fall back to <ID> — <title> (current behavior).

Display format: title ID — scope. progress.

Mode Inference

  1. Both specs in review AND strategic decisions pending → ask operator
  2. Specs awaiting review → detail mode
  3. Focus lane + pending decisions → vision mode
  4. Nothing actionable → vision mode (master plan mirror)

Decisions Needed (roadmap integration)

Uses chart.sh roadmap --json for Eisenhower classification. Show top 5 items from "Do First" and "Schedule" quadrants that need operator decisions.

Settings

This skill reads from swain.settings.json (project root) and ~/.config/swain/settings.json (user override). User settings take precedence.

Relevant settings:

  • terminal.tabNameFormat — format string for tab names. Supports {project} and {branch} placeholders. Default: {project} @ {branch}

Error handling

  • If jq is not available, warn the user and skip JSON operations. Tab naming still works without jq.
  • If git is not available, use the directory name as the project name and skip branch detection.
  • Never fail hard — session management is a convenience, not a gate.
Related skills

More from cristoslc/swain

Installs
104
Repository
cristoslc/swain
GitHub Stars
2
First Seen
Mar 11, 2026