
Swain Init
- 114 installs
- 2 repo stars
- Updated July 24, 2026
- cristoslc/swain
Initialize a new Swain project with required config, directory structure, and defaults so Claude Code can run structured agent sessions in the repo.
About
swain-init from cristoslc/swain sets up a repository for Swain-powered agent development. It creates configuration, folder conventions, and workflow defaults so teams can start structured Claude Code sessions quickly without manually assembling agent tooling from scratch.
- Bootstraps Swain project scaffolding
- Creates default config and session layout
- Standardizes agent workflow entrypoints
- Speeds first usable Swain coding session
Swain Init by the numbers
- 114 all-time installs (skills.sh)
- Ranked #3,946 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cristoslc/swain --skill swain-initAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 114 |
|---|---|
| repo stars | ★ 2 |
| Last updated | July 24, 2026 |
| Repository | cristoslc/swain ↗ |
What it does
Initialize a new Swain project with required config, directory structure, and defaults so Claude Code can run structured agent sessions in the repo.
Files
<!-- swain-model-hint: sonnet, effort: medium -->
Project Onboarding
One-time setup for adopting swain in a project. This skill is not idempotent — it migrates files and installs tools. For per-session health checks, use swain-doctor.
Preflight
Before any phase, run the preflight script to gather environment state. This single call replaces all inline check blocks — phases below read from the JSON output instead of running shell commands.
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
PREFLIGHT_SCRIPT="$(find "$REPO_ROOT" -path '*/swain-init/scripts/swain-init-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 phases below. Every decision references a field from this JSON — do not run additional check commands unless performing a mutation.
Phase 0: Already-initialized detection
Read marker.action from the preflight JSON.
- `"delegate"` — same major version. Tell the user:
Project already initialized (swainmarker.release_version, init vmarker.last_version). Delegating to swain-session.
Skip to Phase 7 (Session Start) below. Do not run Phases 1–6.
- `"upgrade"` — newer major version available. Tell the user:
Project was initialized with swainmarker.last_release_version(init vmarker.last_version). Current:marker.release_version(init vmarker.current_version). Consider running/swain updateto pick up new features.
Starting session.
Skip to Phase 7 (Session Start) below. Do not re-run onboarding — upgrades are handled by swain-update, not swain-init.
- `"onboard"` — no marker found. Proceed with full onboarding (Phases 1–6).
Phase 1: CLAUDE.md → AGENTS.md migration
Goal: establish the @AGENTS.md include pattern so project instructions live in AGENTS.md (which works across Claude Code, GitHub, and other tools that read AGENTS.md natively).
Read migration.state from the preflight JSON.
If "fresh"
Create both files:
- CLAUDE.md:
@AGENTS.md - AGENTS.md:
# AGENTS.md(empty — governance added in Phase 5)
If "migrated"
Skip to Phase 2.
If "standard"
1. Copy CLAUDE.md content to AGENTS.md (preserve everything). 2. If CLAUDE.md contains a <!-- swain governance --> block, strip it from the AGENTS.md copy — it will be re-added cleanly in Phase 5. 3. Replace CLAUDE.md with @AGENTS.md.
Tell the user:
Migrated your CLAUDE.md content to AGENTS.md and replaced CLAUDE.md with @AGENTS.md. Your existing instructions are preserved — Claude Code reads AGENTS.md via the include directive.If "split"
Both files have content. Ask the user:
Both CLAUDE.md and AGENTS.md have content. How should I proceed?
1. Merge — append CLAUDE.md content to the end of AGENTS.md, then replace CLAUDE.md with @AGENTS.md2. Keep AGENTS.md — discard CLAUDE.md content, replace CLAUDE.md with @AGENTS.md3. Abort — leave both files as-is, skip migration
If merge: append CLAUDE.md content (minus any <!-- swain governance --> block) to AGENTS.md, replace CLAUDE.md with @AGENTS.md.
Phase 2: Verify dependencies
Step 2.1 — uv
Read uv.available from the preflight JSON.
If false, install:
curl -LsSf https://astral.sh/uv/install.sh | shIf installation fails, tell the user:
uv installation failed. You can install it manually (https://docs.astral.sh/uv/getting-started/installation/) — swain scripts require uv for Python execution.
Skip the rest of Phase 2 on failure (don't block init on uv, but warn that scripts will not function without it).
Step 2.2 — Vendored tk
Read tk.path and tk.healthy from the preflight JSON.
If tk.path is null or tk.healthy is false, tell the user:
The vendored tk script was not found or is broken. This usually means the swain-do skill was not fully installed. Try running /swain update to reinstall skills.Step 2.3 — Migrate from beads (if applicable)
Read beads.exists and beads.has_backup from the preflight JSON.
If beads.exists is true and beads.has_backup is true, offer migration:
Found existing .beads/ data. Migrate tasks to tk?This will convert.beads/backup/issues.jsonlto.tickets/markdown files.
If user agrees, run migration:
TK_BIN="$(cd "$(dirname "$(find . .claude .agents -path '*/swain-do/bin/tk' -print -quit 2>/dev/null)")" && pwd)"
export PATH="$TK_BIN:$PATH"
cp .beads/backup/issues.jsonl .beads/issues.jsonl 2>/dev/null
ticket-migrate-beads
ls .tickets/*.md 2>/dev/null | wc -lTell the user the results and that .beads/ can be removed after verification.
If beads.exists is false, skip. tk creates .tickets/ on first tk create.
Step 2.4 — Operator bin/ symlinks (SPEC-214, ADR-019)
Read bin_manifests from the preflight JSON. For each entry, create bin/ symlinks:
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
BIN_DIR="$REPO_ROOT/bin"
SKILLS_ROOT="$REPO_ROOT/.agents/skills"
for manifest_dir in "$SKILLS_ROOT"/*/usr/bin; do
[ -d "$manifest_dir" ] || continue
for entry in "$manifest_dir"/*; do
[ -e "$entry" ] || [ -L "$entry" ] || continue
cmd_name="$(basename "$entry")"
script_path="$(cd "$manifest_dir" && readlink -f "$cmd_name" 2>/dev/null || true)"
[ -z "$script_path" ] || [ ! -f "$script_path" ] && continue
rel_path="$(python3 -c "import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))" "$script_path" "$BIN_DIR" 2>/dev/null || echo "")"
[ -z "$rel_path" ] && continue
if [ -L "$BIN_DIR/$cmd_name" ]; then
echo "already linked: $cmd_name"
elif [ -e "$BIN_DIR/$cmd_name" ]; then
echo "conflict — bin/$cmd_name exists as a real file; skipping"
else
mkdir -p "$BIN_DIR"
ln -sf "$rel_path" "$BIN_DIR/$cmd_name"
echo "created bin/$cmd_name"
fi
done
doneTell the user which operator commands are now available in bin/.
If bin_manifests is empty, skip silently.
Phase 3: Pre-commit security hooks
Goal: configure pre-commit hooks for secret scanning so credentials are caught before they enter git history. Default scanner is gitleaks; additional scanners (TruffleHog, Trivy, OSV-Scanner) are opt-in.
Step 3.1 — Check for existing config
Read precommit.config_exists from the preflight JSON.
If true: Present the current .pre-commit-config.yaml content and ask:
Found existing .pre-commit-config.yaml. How should I proceed?1. Merge — add swain's gitleaks hook alongside your existing hooks
2. Skip — leave pre-commit config unchanged
3. Replace — overwrite with swain's default config (your existing hooks will be lost)
If user chooses Skip, skip to Phase 4.
If false: Proceed to Step 3.2.
Step 3.2 — Install pre-commit framework
Read precommit.framework from the preflight JSON.
If false, install:
uv tool install pre-commitIf uv is unavailable or installation fails, warn:
pre-commit framework not available. You can install it manually (uv tool install pre-commitorpip install pre-commit). Skipping hook setup.
Skip to Phase 4 if pre-commit cannot be installed.
Step 3.3 — Create or update .pre-commit-config.yaml
The default config enables gitleaks:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaksIf the user requested additional scanners (via --scanner flags or when asked), add their hooks:
TruffleHog (opt-in):
- repo: https://github.com/trufflesecurity/trufflehog
rev: v3.88.1
hooks:
- id: trufflehog
args: ['--results=verified,unknown']Trivy (opt-in):
- repo: https://github.com/cebidhem/pre-commit-trivy
rev: v1.0.0
hooks:
- id: trivy-fs
args: ['--severity', 'HIGH,CRITICAL', '--scanners', 'vuln,license']OSV-Scanner (opt-in):
- repo: https://github.com/nicjohnson145/pre-commit-osv-scanner
rev: v0.0.1
hooks:
- id: osv-scannerWrite the config file. If merging with an existing config, append the new repo entries to the existing repos: list.
Step 3.4 — Install hooks
Run pre-commit install to activate the hooks.
Step 3.5 — Update swain.settings.json
Read the existing swain.settings.json (if any) and add the sync.scanners key:
{
"sync": {
"scanners": {
"gitleaks": { "enabled": true },
"trufflehog": { "enabled": false },
"trivy": { "enabled": false, "scanners": ["vuln", "license"], "severity": "HIGH,CRITICAL" },
"osv-scanner": { "enabled": false }
}
}
}Set enabled: true for any scanners the user opted into. Merge with existing settings — do not overwrite other keys.
Tell the user:
Pre-commit hooks configured with gitleaks (default). Scanner settings saved toswain.settings.json. To enable additional scanners later, editswain.settings.jsonand re-run/swain-init.
Phase 4: Superpowers companion
Goal: offer to install obra/superpowers if it is not already present.
Step 4.1 — Detect superpowers
Read superpowers.installed from the preflight JSON.
If true, report "Superpowers: already installed" and skip to Phase 4.4.
Step 4.2 — Offer installation
Ask the user:
Superpowers (obra/superpowers) is not installed. It provides TDD, brainstorming, plan writing, and verification skills that swain chains into during implementation and design work.>
Install superpowers now? (yes/no)
If the user says no, note "Superpowers: skipped" and continue to Phase 4.4. They can always install later: npx skills add obra/superpowers.
Step 4.3 — Install
npx skills add obra/superpowersIf the install succeeds, tell the user:
Superpowers installed. Brainstorming, TDD, plan writing, and verification skills are now available.
If it fails, warn:
Superpowers installation failed. You can retry manually: npx skills add obra/superpowersContinue to Phase 4.4 regardless.
Step 4.4 — Tmux
Read tmux.installed from the preflight JSON.
If true, report "tmux: already installed" and continue to Phase 4.5.
If false, ask the user:
tmux is not installed. swain uses tmux for tab naming when available. It is optional — swain works without it, but session tab-naming will be unavailable.
>
Install tmux now? (yes/no)
If yes:
brew install tmuxIf the install succeeds, tell the user:
tmux installed. Workspace layout and tab naming features are now available.
If the install fails, warn:
tmux installation failed. You can install it manually: brew install tmuxIf no, note "tmux: skipped" and continue to Phase 4.5.
Phase 4.5: Shell launcher
Goal: offer to install a swain shell function so the user can launch swain with a single command. Templates are stored per-runtime, per-shell in templates/launchers/{runtime}/swain.{shell} (relative to this skill's directory) — inspect them to see exactly what gets added. Supported runtimes are defined in ADR-017.
Step 4.5.1 — Detect shell
Read launcher.shell from the preflight JSON.
Supported shells: zsh, bash, fish. If the shell is not in this list, tell the user:
Shell launcher templates are available for zsh, bash, and fish. Your shell (launcher.shell) is not yet supported — skipping launcher setup.Skip to Phase 5.
Step 4.5.2 — Check for existing launcher
Read launcher.already_installed from the preflight JSON.
If true, report "Shell launcher: already installed" and skip to Phase 5. Do not modify existing functions.
Step 4.5.3 — Detect runtimes
Read launcher.runtimes from the preflight JSON.
If the array is empty, tell the user:
No supported agentic CLI runtimes found (checked: claude, gemini, codex, copilot, crush). Install one first, then re-run /swain-init.Skip to Phase 5.
Step 4.5.4 — Select runtime
- One runtime found: Offer it directly.
- Multiple runtimes found: Present a numbered list and ask which one to use. Default to
claudeif available.
Read launcher.template_dir from the preflight JSON. Construct the template path:
$TEMPLATE_DIR/$SELECTED_RUNTIME/swain.$SHELL_NAMEStep 4.5.5 — Show template and offer installation
Read the template file content and present it to the user:
Shell launcher — Add a swain command to your shell?>
Detected runtime: [runtime name]. Here's what will be added to <rc-file>:>
```<shell>
<template content>
```
>
Install? (yes/no)
For Crush templates, add a note: "Crush has partial support — it cannot accept an initial prompt, so session initialization relies on AGENTS.md auto-invoke directives."
Step 4.5.6 — Install
If the user accepts, append the template content to the rc file (read launcher.rc_file from preflight JSON, e.g. cat "$TEMPLATE_FILE" >> "$RC_FILE").
Tell the user:
Shell launcher installed. Runsource <rc-file>(or restart your shell) to activate theswaincommand.
If the user declines, note "Shell launcher: skipped" and continue to Phase 5.
Phase 5: Swain governance
Goal: add swain's routing and governance rules to AGENTS.md.
Step 5.1 — Check for existing governance
Read governance.installed from the preflight JSON.
If true, governance is already installed. Tell the user and skip to Phase 5.5.
Step 5.2 — Ask permission
Ask the user:
Ready to add swain governance rules to AGENTS.md. These rules:
- Route artifact requests (specs, stories, ADRs, etc.) to swain-design
- Route task tracking to swain-do (using tk)
- Enforce the pre-implementation protocol (plan before code)
- Prefer swain skills over built-in alternatives
>
Add governance rules to AGENTS.md? (yes/no)
If no, skip to Phase 5.5.
Step 5.3 — Inject governance
Read the canonical governance content from swain-doctor/references/AGENTS.content.md (search .claude/skills, .agents/skills, and skills directories for the file). Append the full contents of that file to AGENTS.md.
Tell the user:
Governance rules added to AGENTS.md. These ensure swain skills are routable and conventions are enforced. You can customize anything outside the <!-- swain governance --> markers.Phase 5.5: README seeding and artifact proposals (SPEC-207)
Goal: ensure every swain project has a README, and offer to bootstrap artifacts from it when the artifact tree is empty.
Step 5.5.1 — Check for README
Read readme.exists from the preflight JSON.
Step 5.5.2 — Seed README if missing
If readme.exists is false, determine the project's context using readme.has_code and readme.has_artifacts from the preflight JSON:
- No code, no artifacts — Interview the operator: "What does this project do?" Write the README from their answer.
- Code exists, no artifacts — Infer project purpose from code (read entry points, package.json/pyproject.toml/go.mod, etc.). Present a draft README to the operator for editing.
- Artifacts exist, no README — Compile from Active Visions, Designs, Journeys, and Personas. Present a draft to the operator for editing.
Present the draft to the operator. They can approve, edit, or skip. If they skip, note "README: skipped" in the summary and swain-doctor will flag it on future sessions.
Step 5.5.3 — Propose seed artifacts from README
Read readme.active_count from the preflight JSON.
If readme.active_count < 3 and README.md exists, read the README and extract intent claims using semantic analysis. Propose seed artifacts:
- Vision — from the README's description of what the project does and why.
- Personas — from who the README addresses and what problems it describes.
- Journeys — from usage flows, examples, or "getting started" paths.
- Designs — from architectural or structural claims.
Present each proposal individually. The operator approves, edits, or rejects each one. Approved artifacts are created via swain-design. Rejected proposals are silently dropped.
Semantic extraction: Read the entire README as prose. No convention-based sections, no operator-placed markers. Any claim in the README is a potential intent source — install instructions, feature descriptions, behavioral claims, architectural statements.
Phase 6: Finalize
Step 6.1 — Create .agents directory
Create .agents/ if it does not exist (mkdir -p .agents). This directory is used by swain-do for configuration and by swain-design scripts for logs.
Step 6.1.1 — Bootstrap .agents/bin/ (ADR-019)
Create .agents/bin/ and populate it with symlinks for all agent-facing scripts in the skill tree:
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
AGENTS_BIN="$REPO_ROOT/.agents/bin"
SKILLS_ROOT="$REPO_ROOT/.agents/skills"
mkdir -p "$AGENTS_BIN"
OPERATOR_SCRIPTS="swain swain-box"
for skill_scripts_dir in "$SKILLS_ROOT"/*/scripts; do
[ -d "$skill_scripts_dir" ] || continue
for script in "$skill_scripts_dir"/*; do
[ -f "$script" ] && [ -x "$script" ] || continue
script_name="$(basename "$script")"
case "$script_name" in test-*) continue ;; esac
echo " $OPERATOR_SCRIPTS " | grep -q " $script_name " && continue
rel_path="$(python3 -c "import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))" "$script" "$AGENTS_BIN" 2>/dev/null)" || continue
ln -sf "$rel_path" "$AGENTS_BIN/$script_name"
done
doneAdd .agents/bin/ and .agents/session.json to .gitignore if not already present (consumer projects should not track these).
Step 6.2 — Run swain-doctor
Invoke the swain-doctor skill. This validates .tickets/ health, checks stale locks, removes legacy skill directories, and ensures governance is correctly installed.
Step 6.3 — Onboarding
Invoke the swain-help skill in onboarding mode to give the user a guided orientation of what they just installed.
Step 6.4 — Write .swain/init.json marker
After all onboarding phases complete, write the .swain/init.json marker file. Read marker.current_version from the preflight JSON for the skill version and marker.release_version for the release version.
If .swain/init.json already exists (partial re-init), read it and append to the history array. Otherwise create a new file:
{
"history": [
{
"version": "4.0.0",
"release": "v0.29.0-alpha",
"timestamp": "2026-03-26T18:30:00Z",
"action": "init"
}
]
}For upgrades (future use by swain-update), append an entry with "action": "upgrade" instead.
Write the file and ensure .swain/ is in .gitignore (it's project-local state, not shared).
Step 6.5 — Summary
Report what was done:
swain init complete.
>
- CLAUDE.md → @AGENTS.md include pattern: [done/skipped/already set up]- tk (ticket) verified: [done/not found]
- Beads migration: [done/skipped/no beads found]
- Pre-commit security hooks: [done/skipped/already configured]
- Superpowers: [installed/skipped/already present]
- tmux: [installed/skipped/already present]
- Shell launcher: [installed (runtime)/skipped/already present/no runtime found/unsupported shell]
- Swain governance in AGENTS.md: [done/skipped/already present]
- README: [seeded/already present/skipped]
- Artifact proposals from README: [N proposed, M accepted/skipped/not applicable]
- Init marker: written (.swain/init.json)
Step 6.6 — Start session
After successful onboarding, proceed to Phase 7 (Session Start) below.
Phase 7: Session Start (ADR-023)
This phase runs every time — both after fresh onboarding (Phase 6) and on the already-initialized fast path (Phase 0). It replaces the former swain-session startup sequence.
Step 7.1 — Fast greeting
Run the fast greeting script. It calls the session preflight internally for all read-only state, then applies lightweight mutations (tab naming, lock cleanup). No subprocess chain — one preflight pass.
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO_ROOT/.agents/bin/swain-session-greeting.sh" --jsonThe 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 session preflight (called internally by the greeting) also gathers previous session state. To access it directly:
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
PREFLIGHT_JSON=$( bash "$REPO_ROOT/.agents/bin/swain-session-preflight.sh" --repo-root "$REPO_ROOT" 2>/dev/null )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 bookmark is not null, display it:
Resuming session — Last time: {bookmark}
3. If isolated is false, do not create a worktree now — worktree creation is deferred to swain-do task dispatch (SPEC-195).
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
Step 7.2 — Session state init
Read prev_session from the session preflight JSON (or call the preflight directly if the greeting didn't expose it). If prev_session.exists is true, display its focus lane, walkaway note, and decision count. Ask the operator: "Continue previous session or start fresh?"
If starting fresh (or no previous session):
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"Step 7.3 — Focus lane
The focus lane scopes recommendations to a single vision or initiative. It is set when the operator decides what to work on.
If the greeting JSON included a `focus` value: Confirm with the operator:
Focus lane: {focus}. Continue with this focus, or change?
If no focus is set: Ask the operator what they want to work on. Resolve names to artifact IDs:
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, ask the operator to clarify. If no match, tell the operator and offer to create one.
Set the focus:
bash "$REPO_ROOT/.agents/bin/swain-focus.sh" set <RESOLVED-ID>Display the focus artifact context:
bash "$REPO_ROOT/.agents/bin/artifact-context.sh" <RESOLVED-ID> 2>/dev/nullThe focus lane is stored in .agents/session.json under the focus_lane key and persists across the session.
Step 7.4 — 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-init Session purpose: new bug about timestamps.
The greeting script (swain-session-greeting.sh) reads $SWAIN_PURPOSE and writes the bookmark deterministically (SPEC-297). The purpose field in the greeting JSON surfaces the captured text.
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.
Worktree / branch changes
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 --autoRe-running init
If the user runs /swain-init on a project that's already set up, Phase 0 reads the preflight JSON's marker.action field and skips to Phase 7 (Session Start) — no onboarding phases run, no interactive prompts appear. This lets users build muscle memory around /swain-init as a single entry point.
To force re-onboarding, delete .swain/init.json and re-run.
#!/usr/bin/env bash
# swain-init-preflight.sh — read-only environment scanner for swain-init
#
# Runs all pre-onboarding checks and emits a single JSON object to stdout.
# This script NEVER mutates state (no file writes, installs, or symlinks).
# The skill file reads this JSON and makes decisions based on the results.
#
# Usage: bash swain-init-preflight.sh [--repo-root /path/to/repo]
#
# JSON schema (all keys present, some may be null on error):
#
# marker.exists bool — .swain/init.json file found
# marker.last_version string — version from last history entry (null if no marker)
# marker.current_version string — version from installed swain-init SKILL.md
# marker.action string — "delegate" | "upgrade" | "onboard"
#
# migration.state string — "fresh" | "migrated" | "standard" | "split"
# migration.claude_md string — "missing" | "empty" | "include_only" | "has_content"
# migration.agents_md string — "missing" | "empty" | "has_content"
#
# uv.available bool — uv binary found in PATH
# uv.path string — path to uv (null if not found)
#
# tk.path string — path to vendored tk (null if not found)
# tk.healthy bool — tk help runs successfully
#
# beads.exists bool — .beads/ directory found
# beads.has_backup bool — .beads/backup/issues.jsonl exists
#
# bin_manifests array — list of {skill, commands[]} for usr/bin/ dirs
#
# precommit.config_exists bool — .pre-commit-config.yaml found
# precommit.framework bool — pre-commit binary found in PATH
#
# superpowers.installed bool — brainstorming SKILL.md found
#
# tmux.installed bool — tmux binary found
#
# launcher.shell string — detected shell name (zsh/bash/fish/unknown)
# launcher.rc_file string — path to rc file
# launcher.already_installed bool — swain function already in rc file
# launcher.runtimes array — list of detected agentic runtimes
# launcher.template_dir string — path to launcher templates (null if not found)
#
# governance.installed bool — swain governance block found in AGENTS.md or CLAUDE.md
#
# readme.exists bool — README.md found
# readme.has_code bool — source code files found in repo
# readme.has_artifacts bool — Active artifacts found in docs/
# readme.active_count int — count of Active VISION/DESIGN/JOURNEY/PERSONA artifacts
#
# agents_dir.exists bool — .agents/ directory exists
#
# Exit: always 0 (partial results on individual check failures)
set -euo pipefail
REPO_ROOT=""
while [ $# -gt 0 ]; do
case "$1" in
--repo-root) REPO_ROOT="$2"; shift 2 ;;
*) shift ;;
esac
done
if [ -z "$REPO_ROOT" ]; then
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
fi
cd "$REPO_ROOT"
# --- Collector variables ---
# Each check function sets variables prefixed with its category.
# We collect everything at the end via python3 JSON serialization.
# --- Marker check ---
check_marker() {
MARKER_EXISTS=false
MARKER_LAST_VERSION=""
MARKER_CURRENT_VERSION=""
MARKER_ACTION="onboard"
if [ -f ".swain/init.json" ]; then
MARKER_EXISTS=true
# Extract last version — try jq first, fall back to python3, then grep
MARKER_LAST_VERSION=$(python3 -c "
import json, sys
try:
d = json.load(open('.swain/init.json'))
print(d['history'][-1]['version'])
except Exception:
sys.exit(1)
" 2>/dev/null || echo "")
fi
# Find current installed version
SKILL_FILE=$(find . .claude .agents -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null || true)
if [ -n "$SKILL_FILE" ] && [ -f "$SKILL_FILE" ]; then
MARKER_CURRENT_VERSION=$(head -20 "$SKILL_FILE" 2>/dev/null | grep 'version:' | awk '{print $2}' || true)
fi
# Determine action
if [ "$MARKER_EXISTS" = true ] && [ -n "$MARKER_LAST_VERSION" ] && [ -n "$MARKER_CURRENT_VERSION" ]; then
LAST_MAJOR="${MARKER_LAST_VERSION%%.*}"
CURRENT_MAJOR="${MARKER_CURRENT_VERSION%%.*}"
if [ "$LAST_MAJOR" = "$CURRENT_MAJOR" ]; then
MARKER_ACTION="delegate"
else
MARKER_ACTION="upgrade"
fi
fi
}
# --- Migration state check ---
check_migration() {
MIGRATION_STATE="fresh"
MIGRATION_CLAUDE_MD="missing"
MIGRATION_AGENTS_MD="missing"
# Classify CLAUDE.md
if [ -f "CLAUDE.md" ]; then
if [ ! -s "CLAUDE.md" ]; then
MIGRATION_CLAUDE_MD="empty"
else
CONTENT=$(cat "CLAUDE.md" 2>/dev/null)
TRIMMED=$(echo "$CONTENT" | sed '/^[[:space:]]*$/d')
if [ "$TRIMMED" = "@AGENTS.md" ]; then
MIGRATION_CLAUDE_MD="include_only"
else
MIGRATION_CLAUDE_MD="has_content"
fi
fi
fi
# Classify AGENTS.md
if [ -f "AGENTS.md" ]; then
if [ ! -s "AGENTS.md" ]; then
MIGRATION_AGENTS_MD="empty"
else
MIGRATION_AGENTS_MD="has_content"
fi
fi
# Determine migration state
if [ "$MIGRATION_CLAUDE_MD" = "include_only" ]; then
MIGRATION_STATE="migrated"
elif [ "$MIGRATION_CLAUDE_MD" = "missing" ] || [ "$MIGRATION_CLAUDE_MD" = "empty" ]; then
if [ "$MIGRATION_AGENTS_MD" = "missing" ] || [ "$MIGRATION_AGENTS_MD" = "empty" ]; then
MIGRATION_STATE="fresh"
else
MIGRATION_STATE="fresh"
fi
elif [ "$MIGRATION_CLAUDE_MD" = "has_content" ]; then
if [ "$MIGRATION_AGENTS_MD" = "has_content" ]; then
MIGRATION_STATE="split"
else
MIGRATION_STATE="standard"
fi
fi
}
# --- uv check ---
check_uv() {
UV_AVAILABLE=false
UV_PATH=""
UV_PATH=$(command -v uv 2>/dev/null || true)
if [ -n "$UV_PATH" ]; then
UV_AVAILABLE=true
fi
}
# --- tk check ---
check_tk() {
TK_PATH=""
TK_HEALTHY=false
TK_PATH=$(find . .claude .agents -path '*/swain-do/bin/tk' -print -quit 2>/dev/null || true)
if [ -n "$TK_PATH" ] && [ -x "$TK_PATH" ]; then
if "$TK_PATH" help >/dev/null 2>&1; then
TK_HEALTHY=true
fi
fi
}
# --- beads check ---
check_beads() {
BEADS_EXISTS=false
BEADS_HAS_BACKUP=false
if [ -d ".beads" ]; then
BEADS_EXISTS=true
if [ -f ".beads/backup/issues.jsonl" ]; then
BEADS_HAS_BACKUP=true
fi
fi
}
# --- bin manifests check ---
check_bin_manifests() {
# Collect as newline-delimited "skill|command" pairs; python3 will parse
BIN_MANIFESTS_RAW=""
SKILLS_ROOT=".agents/skills"
if [ -d "$SKILLS_ROOT" ]; then
for manifest_dir in "$SKILLS_ROOT"/*/usr/bin; do
[ -d "$manifest_dir" ] || continue
skill_name=$(basename "$(dirname "$(dirname "$manifest_dir")")")
for entry in "$manifest_dir"/*; do
[ -e "$entry" ] || [ -L "$entry" ] || continue
cmd_name=$(basename "$entry")
BIN_MANIFESTS_RAW="${BIN_MANIFESTS_RAW}${skill_name}|${cmd_name}
"
done
done
fi
}
# --- pre-commit check ---
check_precommit() {
PRECOMMIT_CONFIG_EXISTS=false
PRECOMMIT_FRAMEWORK=false
if [ -f ".pre-commit-config.yaml" ]; then
PRECOMMIT_CONFIG_EXISTS=true
fi
if command -v pre-commit >/dev/null 2>&1; then
PRECOMMIT_FRAMEWORK=true
fi
}
# --- superpowers check ---
check_superpowers() {
SUPERPOWERS_INSTALLED=false
if ls .agents/skills/brainstorming/SKILL.md .claude/skills/brainstorming/SKILL.md 2>/dev/null | head -1 | grep -q .; then
SUPERPOWERS_INSTALLED=true
fi
}
# --- tmux check ---
check_tmux() {
TMUX_INSTALLED=false
if command -v tmux >/dev/null 2>&1; then
TMUX_INSTALLED=true
fi
}
# --- shell launcher check ---
check_launcher() {
LAUNCHER_SHELL="unknown"
LAUNCHER_RC_FILE=""
LAUNCHER_ALREADY_INSTALLED=false
LAUNCHER_RUNTIMES_RAW=""
LAUNCHER_TEMPLATE_DIR=""
# Detect shell
LAUNCHER_SHELL=$(basename "${SHELL:-unknown}")
# Map to rc file
case "$LAUNCHER_SHELL" in
zsh) LAUNCHER_RC_FILE="$HOME/.zshrc" ;;
bash) LAUNCHER_RC_FILE="$HOME/.bashrc" ;;
fish) LAUNCHER_RC_FILE="$HOME/.config/fish/config.fish" ;;
*) LAUNCHER_RC_FILE="" ;;
esac
# Check for existing launcher
if [ -n "$LAUNCHER_RC_FILE" ] && [ -f "$LAUNCHER_RC_FILE" ]; then
case "$LAUNCHER_SHELL" in
zsh|bash)
if grep -q 'swain\s*()' "$LAUNCHER_RC_FILE" 2>/dev/null; then
LAUNCHER_ALREADY_INSTALLED=true
fi
;;
fish)
if grep -q 'function swain' "$LAUNCHER_RC_FILE" 2>/dev/null; then
LAUNCHER_ALREADY_INSTALLED=true
fi
;;
esac
fi
# Detect runtimes
for rt in claude gemini codex copilot crush; do
if command -v "$rt" >/dev/null 2>&1; then
LAUNCHER_RUNTIMES_RAW="${LAUNCHER_RUNTIMES_RAW}${rt}
"
fi
done
# Find template directory
LAUNCHER_TEMPLATE_DIR=$(find . .claude .agents -path '*/swain-init/templates/launchers' -type d -print -quit 2>/dev/null || true)
}
# --- governance check ---
check_governance() {
GOVERNANCE_INSTALLED=false
if grep -q "swain governance" AGENTS.md CLAUDE.md 2>/dev/null; then
GOVERNANCE_INSTALLED=true
fi
}
# --- readme and artifacts check ---
check_readme() {
README_EXISTS=false
README_HAS_CODE=false
README_HAS_ARTIFACTS=false
README_ACTIVE_COUNT=0
if [ -f "README.md" ]; then
README_EXISTS=true
fi
# Check for source code
if find . -maxdepth 3 \( -name '*.py' -o -name '*.js' -o -name '*.ts' -o -name '*.go' -o -name '*.rs' -o -name '*.sh' \) \
-not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.agents/*' -not -path '*/.claude/*' \
-print -quit 2>/dev/null | grep -q .; then
README_HAS_CODE=true
fi
# Check for Active artifacts
if find docs -name '*.md' -path '*/Active/*' -print -quit 2>/dev/null | grep -q .; then
README_HAS_ARTIFACTS=true
fi
# Count Active VISION/DESIGN/JOURNEY/PERSONA artifacts
README_ACTIVE_COUNT=$(find docs -path "*/Active/*" -name "*.md" 2>/dev/null | grep -cE "(VISION|DESIGN|JOURNEY|PERSONA)") || README_ACTIVE_COUNT=0
}
# --- .agents/ dir check ---
check_agents_dir() {
AGENTS_DIR_EXISTS=false
if [ -d ".agents" ]; then
AGENTS_DIR_EXISTS=true
fi
}
# --- Run all checks ---
# Each check is wrapped in a subshell-safe pattern; failures don't abort the script.
check_marker || true
check_migration || true
check_uv || true
check_tk || true
check_beads || true
check_bin_manifests || true
check_precommit || true
check_superpowers || true
check_tmux || true
check_launcher || true
check_governance || true
check_readme || true
check_agents_dir || true
# --- Emit JSON via python3 ---
python3 -c "
import json, sys
def to_bool(v):
return v.lower() == 'true'
def to_int(v):
try: return int(v)
except: return 0
def to_str_or_null(v):
return v if v else None
def to_list(raw):
return [x for x in raw.strip().split('\n') if x] if raw.strip() else []
def parse_bin_manifests(raw):
result = {}
for line in raw.strip().split('\n'):
if not line or '|' not in line:
continue
skill, cmd = line.split('|', 1)
result.setdefault(skill, []).append(cmd)
return [{'skill': k, 'commands': v} for k, v in result.items()]
data = {
'marker': {
'exists': to_bool(sys.argv[1]),
'last_version': to_str_or_null(sys.argv[2]),
'current_version': to_str_or_null(sys.argv[3]),
'action': sys.argv[4],
},
'migration': {
'state': sys.argv[5],
'claude_md': sys.argv[6],
'agents_md': sys.argv[7],
},
'uv': {
'available': to_bool(sys.argv[8]),
'path': to_str_or_null(sys.argv[9]),
},
'tk': {
'path': to_str_or_null(sys.argv[10]),
'healthy': to_bool(sys.argv[11]),
},
'beads': {
'exists': to_bool(sys.argv[12]),
'has_backup': to_bool(sys.argv[13]),
},
'bin_manifests': parse_bin_manifests(sys.argv[14]),
'precommit': {
'config_exists': to_bool(sys.argv[15]),
'framework': to_bool(sys.argv[16]),
},
'superpowers': {
'installed': to_bool(sys.argv[17]),
},
'tmux': {
'installed': to_bool(sys.argv[18]),
},
'launcher': {
'shell': sys.argv[19],
'rc_file': to_str_or_null(sys.argv[20]),
'already_installed': to_bool(sys.argv[21]),
'runtimes': to_list(sys.argv[22]),
'template_dir': to_str_or_null(sys.argv[23]),
},
'governance': {
'installed': to_bool(sys.argv[24]),
},
'readme': {
'exists': to_bool(sys.argv[25]),
'has_code': to_bool(sys.argv[26]),
'has_artifacts': to_bool(sys.argv[27]),
'active_count': to_int(sys.argv[28]),
},
'agents_dir': {
'exists': to_bool(sys.argv[29]),
},
}
json.dump(data, sys.stdout, indent=2)
print()
" \
"$MARKER_EXISTS" "$MARKER_LAST_VERSION" "$MARKER_CURRENT_VERSION" "$MARKER_ACTION" \
"$MIGRATION_STATE" "$MIGRATION_CLAUDE_MD" "$MIGRATION_AGENTS_MD" \
"$UV_AVAILABLE" "$UV_PATH" \
"$TK_PATH" "$TK_HEALTHY" \
"$BEADS_EXISTS" "$BEADS_HAS_BACKUP" \
"$BIN_MANIFESTS_RAW" \
"$PRECOMMIT_CONFIG_EXISTS" "$PRECOMMIT_FRAMEWORK" \
"$SUPERPOWERS_INSTALLED" \
"$TMUX_INSTALLED" \
"$LAUNCHER_SHELL" "$LAUNCHER_RC_FILE" "$LAUNCHER_ALREADY_INSTALLED" "$LAUNCHER_RUNTIMES_RAW" "$LAUNCHER_TEMPLATE_DIR" \
"$GOVERNANCE_INSTALLED" \
"$README_EXISTS" "$README_HAS_CODE" "$README_HAS_ARTIFACTS" "$README_ACTIVE_COUNT" \
"$AGENTS_DIR_EXISTS"
#!/usr/bin/env bash
# test-launcher-marker-check.sh — SPEC-196: Test the .swain/init.json marker check
#
# Tests the _swain_check_marker() function that shell launchers use to
# decide whether to send /swain-init or /swain-session as the initial prompt.
#
# Usage: bash test-launcher-marker-check.sh [--verbose]
set -euo pipefail
VERBOSE=0
[[ "${1:-}" == "--verbose" ]] && VERBOSE=1
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMPLATE_DIR="$SCRIPT_DIR/../templates/launchers/claude"
PASS=0
FAIL=0
TOTAL=0
assert_eq() {
local test_name="$1" expected="$2" actual="$3"
TOTAL=$((TOTAL + 1))
if [[ "$expected" == "$actual" ]]; then
PASS=$((PASS + 1))
[[ $VERBOSE -eq 1 ]] && echo " PASS: $test_name"
else
FAIL=$((FAIL + 1))
echo " FAIL: $test_name (expected: '$expected', got: '$actual')"
fi
}
# ─── Setup temp directory ───
TMPDIR_TEST=$(mktemp -d)
trap "rm -rf '$TMPDIR_TEST'" EXIT
# Create a minimal skill file for version detection
mkdir -p "$TMPDIR_TEST/skills/swain-init"
cat > "$TMPDIR_TEST/skills/swain-init/SKILL.md" << 'SKILL'
---
name: swain-init
version: 4.1.0
---
SKILL
# Source the bash launcher to get _swain_check_marker
source "$TEMPLATE_DIR/swain.bash"
echo "=== SPEC-196: Launcher marker check tests ==="
# ─── Test 1: No marker → /swain-init ───
echo "Test 1: No .swain/init.json marker"
cd "$TMPDIR_TEST"
rm -rf .swain
result=$(_swain_check_marker 2>/dev/null)
assert_eq "no marker returns /swain-init" "/swain-init" "$result"
# ─── Test 2: Current marker (same major) → /swain-session ───
echo "Test 2: Current .swain/init.json marker (same major version)"
mkdir -p "$TMPDIR_TEST/.swain"
cat > "$TMPDIR_TEST/.swain/init.json" << 'JSON'
{
"history": [
{
"version": "4.1.0",
"timestamp": "2026-03-26T18:30:00Z",
"action": "init"
}
]
}
JSON
result=$(_swain_check_marker 2>/dev/null)
assert_eq "current marker returns /swain-session" "/swain-session" "$result"
# ─── Test 3: Same major, different minor → /swain-session ───
echo "Test 3: Same major, different minor version"
mkdir -p "$TMPDIR_TEST/.swain"
cat > "$TMPDIR_TEST/.swain/init.json" << 'JSON'
{
"history": [
{
"version": "4.0.0",
"timestamp": "2026-03-20T10:00:00Z",
"action": "init"
}
]
}
JSON
result=$(_swain_check_marker 2>/dev/null)
assert_eq "same major different minor returns /swain-session" "/swain-session" "$result"
# ─── Test 4: Outdated marker (older major) → /swain-init ───
echo "Test 4: Outdated .swain/init.json marker (older major version)"
mkdir -p "$TMPDIR_TEST/.swain"
cat > "$TMPDIR_TEST/.swain/init.json" << 'JSON'
{
"history": [
{
"version": "3.2.1",
"timestamp": "2026-03-01T12:00:00Z",
"action": "init"
}
]
}
JSON
result=$(_swain_check_marker 2>/dev/null)
assert_eq "outdated major returns /swain-init" "/swain-init" "$result"
# ─── Test 5: Malformed marker (not JSON) → /swain-init ───
echo "Test 5: Malformed .swain/init.json marker"
mkdir -p "$TMPDIR_TEST/.swain"
echo "not json" > "$TMPDIR_TEST/.swain/init.json"
result=$(_swain_check_marker 2>/dev/null)
assert_eq "malformed marker returns /swain-init" "/swain-init" "$result"
# ─── Test 6: Marker with upgrade history → /swain-session ───
echo "Test 6: Marker with upgrade history (latest entry is current)"
mkdir -p "$TMPDIR_TEST/.swain"
cat > "$TMPDIR_TEST/.swain/init.json" << 'JSON'
{
"history": [
{
"version": "3.0.0",
"timestamp": "2026-02-01T12:00:00Z",
"action": "init"
},
{
"version": "4.0.0",
"timestamp": "2026-03-15T12:00:00Z",
"action": "upgrade"
}
]
}
JSON
result=$(_swain_check_marker 2>/dev/null)
assert_eq "upgrade history with current major returns /swain-session" "/swain-session" "$result"
# ─── Test 7: Performance — marker check completes in <100ms ───
echo "Test 7: Performance (<100ms)"
mkdir -p "$TMPDIR_TEST/.swain"
cat > "$TMPDIR_TEST/.swain/init.json" << 'JSON'
{
"history": [
{
"version": "4.1.0",
"timestamp": "2026-03-26T18:30:00Z",
"action": "init"
}
]
}
JSON
start_ms=$(python3 -c "import time; print(int(time.time()*1000))")
_swain_check_marker >/dev/null 2>&1
end_ms=$(python3 -c "import time; print(int(time.time()*1000))")
elapsed=$((end_ms - start_ms))
TOTAL=$((TOTAL + 1))
if [[ $elapsed -lt 100 ]]; then
PASS=$((PASS + 1))
[[ $VERBOSE -eq 1 ]] && echo " PASS: performance (${elapsed}ms)"
else
FAIL=$((FAIL + 1))
echo " FAIL: performance (${elapsed}ms, expected <100ms)"
fi
# ─── Test 8: Arguments pass through to /swain-session ───
echo "Test 8: Arguments bypass marker check (go to /swain-session with purpose)"
# When args are provided, the launcher already skips to /swain-session
# This test verifies _swain_check_marker is only for the no-args path
# ─── Summary ───
echo ""
echo "Results: $PASS/$TOTAL passed, $FAIL failed"
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
#!/usr/bin/env bash
# test-swain-init-preflight.sh — test suite for the preflight scanner
#
# Creates temp directories with various project states and verifies
# the preflight script emits correct JSON for each scenario.
#
# Usage: bash test-swain-init-preflight.sh
# Exit: 0 if all tests pass, 1 if any fail
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PREFLIGHT="$SCRIPT_DIR/swain-init-preflight.sh"
PASS=0
FAIL=0
ERRORS=""
# --- Helpers ---
setup_temp() {
local dir
dir=$(mktemp -d)
# Initialize a git repo so git rev-parse works
git init -q "$dir"
echo "$dir"
}
cleanup_temp() {
rm -rf "$1"
}
assert_json_key() {
local json="$1" key="$2" expected="$3" label="$4"
local actual
actual=$(echo "$json" | python3 -c "
import json, sys
d = json.load(sys.stdin)
keys = '$key'.split('.')
v = d
for k in keys:
if isinstance(v, dict):
v = v.get(k)
else:
v = None
break
if isinstance(v, bool):
print(str(v).lower())
elif v is None:
print('null')
else:
print(v)
" 2>/dev/null || echo "PARSE_ERROR")
if [ "$actual" = "$expected" ]; then
PASS=$((PASS + 1))
else
FAIL=$((FAIL + 1))
ERRORS="${ERRORS}FAIL: $label — expected '$expected', got '$actual'\n"
fi
}
assert_json_valid() {
local json="$1" label="$2"
if echo "$json" | python3 -c "import json, sys; json.load(sys.stdin)" 2>/dev/null; then
PASS=$((PASS + 1))
else
FAIL=$((FAIL + 1))
ERRORS="${ERRORS}FAIL: $label — invalid JSON output\n"
fi
}
# --- Test 1: Fresh project (no marker, no CLAUDE.md, no AGENTS.md) ---
test_fresh_project() {
local dir
dir=$(setup_temp)
local output
output=$(bash "$PREFLIGHT" --repo-root "$dir" 2>/dev/null)
assert_json_valid "$output" "fresh: valid JSON"
assert_json_key "$output" "marker.exists" "false" "fresh: marker.exists"
assert_json_key "$output" "marker.action" "onboard" "fresh: marker.action"
assert_json_key "$output" "migration.state" "fresh" "fresh: migration.state"
assert_json_key "$output" "migration.claude_md" "missing" "fresh: migration.claude_md"
assert_json_key "$output" "migration.agents_md" "missing" "fresh: migration.agents_md"
assert_json_key "$output" "beads.exists" "false" "fresh: beads.exists"
assert_json_key "$output" "governance.installed" "false" "fresh: governance.installed"
assert_json_key "$output" "readme.exists" "false" "fresh: readme.exists"
assert_json_key "$output" "agents_dir.exists" "false" "fresh: agents_dir.exists"
cleanup_temp "$dir"
}
# --- Test 2: Already-initialized project (same major version) ---
test_initialized_same_version() {
local dir
dir=$(setup_temp)
mkdir -p "$dir/.swain"
cat > "$dir/.swain/init.json" << 'MARKER'
{
"history": [
{
"version": "4.0.0",
"timestamp": "2026-04-01T00:00:00Z",
"action": "init"
}
]
}
MARKER
# Create a fake SKILL.md so version detection works
mkdir -p "$dir/.claude/skills/swain-init"
cat > "$dir/.claude/skills/swain-init/SKILL.md" << 'SKILL'
---
name: swain-init
metadata:
version: 4.1.0
---
SKILL
local output
output=$(bash "$PREFLIGHT" --repo-root "$dir" 2>/dev/null)
assert_json_valid "$output" "init-same: valid JSON"
assert_json_key "$output" "marker.exists" "true" "init-same: marker.exists"
assert_json_key "$output" "marker.last_version" "4.0.0" "init-same: marker.last_version"
assert_json_key "$output" "marker.current_version" "4.1.0" "init-same: marker.current_version"
assert_json_key "$output" "marker.action" "delegate" "init-same: marker.action"
cleanup_temp "$dir"
}
# --- Test 3: Initialized with older major version ---
test_initialized_older_version() {
local dir
dir=$(setup_temp)
mkdir -p "$dir/.swain"
cat > "$dir/.swain/init.json" << 'MARKER'
{
"history": [
{
"version": "3.2.0",
"timestamp": "2026-01-01T00:00:00Z",
"action": "init"
}
]
}
MARKER
mkdir -p "$dir/.claude/skills/swain-init"
cat > "$dir/.claude/skills/swain-init/SKILL.md" << 'SKILL'
---
name: swain-init
metadata:
version: 4.0.0
---
SKILL
local output
output=$(bash "$PREFLIGHT" --repo-root "$dir" 2>/dev/null)
assert_json_valid "$output" "init-upgrade: valid JSON"
assert_json_key "$output" "marker.exists" "true" "init-upgrade: marker.exists"
assert_json_key "$output" "marker.action" "upgrade" "init-upgrade: marker.action"
cleanup_temp "$dir"
}
# --- Test 4: Standard migration (CLAUDE.md has content, no AGENTS.md) ---
test_standard_migration() {
local dir
dir=$(setup_temp)
echo "# My Project Instructions" > "$dir/CLAUDE.md"
local output
output=$(bash "$PREFLIGHT" --repo-root "$dir" 2>/dev/null)
assert_json_valid "$output" "standard: valid JSON"
assert_json_key "$output" "migration.state" "standard" "standard: migration.state"
assert_json_key "$output" "migration.claude_md" "has_content" "standard: migration.claude_md"
assert_json_key "$output" "migration.agents_md" "missing" "standard: migration.agents_md"
cleanup_temp "$dir"
}
# --- Test 5: Split state (both have content) ---
test_split_state() {
local dir
dir=$(setup_temp)
echo "# CLAUDE instructions" > "$dir/CLAUDE.md"
echo "# AGENTS instructions" > "$dir/AGENTS.md"
local output
output=$(bash "$PREFLIGHT" --repo-root "$dir" 2>/dev/null)
assert_json_valid "$output" "split: valid JSON"
assert_json_key "$output" "migration.state" "split" "split: migration.state"
assert_json_key "$output" "migration.claude_md" "has_content" "split: migration.claude_md"
assert_json_key "$output" "migration.agents_md" "has_content" "split: migration.agents_md"
cleanup_temp "$dir"
}
# --- Test 6: Already migrated (CLAUDE.md is just @AGENTS.md) ---
test_already_migrated() {
local dir
dir=$(setup_temp)
echo "@AGENTS.md" > "$dir/CLAUDE.md"
echo "# AGENTS instructions" > "$dir/AGENTS.md"
local output
output=$(bash "$PREFLIGHT" --repo-root "$dir" 2>/dev/null)
assert_json_valid "$output" "migrated: valid JSON"
assert_json_key "$output" "migration.state" "migrated" "migrated: migration.state"
assert_json_key "$output" "migration.claude_md" "include_only" "migrated: migration.claude_md"
cleanup_temp "$dir"
}
# --- Test 7: Beads directory present ---
test_beads_present() {
local dir
dir=$(setup_temp)
mkdir -p "$dir/.beads/backup"
echo '{}' > "$dir/.beads/backup/issues.jsonl"
local output
output=$(bash "$PREFLIGHT" --repo-root "$dir" 2>/dev/null)
assert_json_valid "$output" "beads: valid JSON"
assert_json_key "$output" "beads.exists" "true" "beads: beads.exists"
assert_json_key "$output" "beads.has_backup" "true" "beads: beads.has_backup"
cleanup_temp "$dir"
}
# --- Test 8: README and artifacts ---
test_readme_with_artifacts() {
local dir
dir=$(setup_temp)
echo "# My Project" > "$dir/README.md"
mkdir -p "$dir/docs/vision/Active"
echo "---" > "$dir/docs/vision/Active/VISION-001-test.md"
mkdir -p "$dir/docs/design/Active"
echo "---" > "$dir/docs/design/Active/DESIGN-001-test.md"
local output
output=$(bash "$PREFLIGHT" --repo-root "$dir" 2>/dev/null)
assert_json_valid "$output" "readme: valid JSON"
assert_json_key "$output" "readme.exists" "true" "readme: readme.exists"
assert_json_key "$output" "readme.has_artifacts" "true" "readme: readme.has_artifacts"
assert_json_key "$output" "readme.active_count" "2" "readme: readme.active_count"
cleanup_temp "$dir"
}
# --- Test 9: Governance present ---
test_governance_present() {
local dir
dir=$(setup_temp)
echo "<!-- swain governance -->" > "$dir/AGENTS.md"
local output
output=$(bash "$PREFLIGHT" --repo-root "$dir" 2>/dev/null)
assert_json_valid "$output" "governance: valid JSON"
assert_json_key "$output" "governance.installed" "true" "governance: governance.installed"
cleanup_temp "$dir"
}
# --- Run all tests ---
echo "Running swain-init-preflight tests..."
echo ""
test_fresh_project
test_initialized_same_version
test_initialized_older_version
test_standard_migration
test_split_state
test_already_migrated
test_beads_present
test_readme_with_artifacts
test_governance_present
echo ""
echo "Results: $PASS passed, $FAIL failed"
if [ $FAIL -gt 0 ]; then
echo ""
printf "%b" "$ERRORS"
exit 1
fi
exit 0
# swain shell launcher — claude / bash
# Runtime: Claude Code | Shell: bash
# Version: 5.0.0
#
# Launches Claude Code interactively with swain's recommended flags.
# Handles tmux wrapping: outside tmux, starts a new tmux session;
# inside tmux, launches directly in the current pane.
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
_swain_check_marker() {
local marker=".swain/init.json"
# No marker → need init
if [ ! -f "$marker" ]; then
echo "/swain-init"
return
fi
# Extract marker major version (last history entry)
local marker_version=""
if command -v jq &>/dev/null; then
marker_version=$(jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
# Fallback: grep for version field in JSON
marker_version=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
fi
if [ -z "$marker_version" ]; then
echo "/swain-init"
return
fi
# Extract installed major version from skill file
local installed_version=""
local skill_file
skill_file=$(find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if [ -n "$skill_file" ]; then
installed_version=$(head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
fi
if [ -z "$installed_version" ]; then
# Can't determine installed version — fall through to init
echo "/swain-init"
return
fi
# Compare major versions
local marker_major="${marker_version%%.*}"
local installed_major="${installed_version%%.*}"
if [ "$marker_major" = "$installed_major" ]; then
echo "/swain-session"
else
echo "/swain-init"
fi
}
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
fi
local _prompt
if [ $# -gt 0 ]; then
export SWAIN_PURPOSE="$*"
_prompt="/swain-session Session purpose: $*"
else
_prompt=$(_swain_check_marker)
fi
if [ -z "$TMUX" ]; then
tmux new-session -s swain "claude --dangerously-skip-permissions '${_prompt}'"
else
claude --dangerously-skip-permissions "$_prompt"
fi
}
# swain shell launcher — claude / fish
# Runtime: Claude Code | Shell: fish
# Version: 5.0.0
#
# Launches Claude Code interactively with swain's recommended flags.
# Handles tmux wrapping: outside tmux, starts a new tmux session;
# inside tmux, launches directly in the current pane.
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
function _swain_check_marker
set -l marker ".swain/init.json"
# No marker → need init
if not test -f "$marker"
echo "/swain-init"
return
end
# Extract marker major version (last history entry)
set -l marker_version ""
if command -q jq
set marker_version (jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
# Fallback: grep for version field in JSON
set marker_version (grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
end
if test -z "$marker_version"
echo "/swain-init"
return
end
# Extract installed major version from skill file
set -l installed_version ""
set -l skill_file (find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if test -n "$skill_file"
set installed_version (head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
end
if test -z "$installed_version"
echo "/swain-init"
return
end
# Compare major versions
set -l marker_major (string split '.' "$marker_version")[1]
set -l installed_major (string split '.' "$installed_version")[1]
if test "$marker_major" = "$installed_major"
echo "/swain-session"
else
echo "/swain-init"
end
end
function swain
if test -x bin/swain
exec bin/swain $argv
end
set -l _prompt
if test (count $argv) -gt 0
set -gx SWAIN_PURPOSE "$argv"
set _prompt "/swain-session Session purpose: $argv"
else
set _prompt (_swain_check_marker)
end
if not set -q TMUX
tmux new-session -s swain "claude --dangerously-skip-permissions '$_prompt'"
else
claude --dangerously-skip-permissions "$_prompt"
end
end
# swain shell launcher — claude / zsh
# Runtime: Claude Code | Shell: zsh
# Version: 5.0.0
#
# Launches Claude Code interactively with swain's recommended flags.
# Handles tmux wrapping: outside tmux, starts a new tmux session;
# inside tmux, launches directly in the current pane.
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
_swain_check_marker() {
local marker=".swain/init.json"
# No marker → need init
if [ ! -f "$marker" ]; then
echo "/swain-init"
return
fi
# Extract marker major version (last history entry)
local marker_version=""
if command -v jq &>/dev/null; then
marker_version=$(jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
# Fallback: grep for version field in JSON
marker_version=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
fi
if [ -z "$marker_version" ]; then
echo "/swain-init"
return
fi
# Extract installed major version from skill file
local installed_version=""
local skill_file
skill_file=$(find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if [ -n "$skill_file" ]; then
installed_version=$(head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
fi
if [ -z "$installed_version" ]; then
echo "/swain-init"
return
fi
# Compare major versions
local marker_major="${marker_version%%.*}"
local installed_major="${installed_version%%.*}"
if [ "$marker_major" = "$installed_major" ]; then
echo "/swain-session"
else
echo "/swain-init"
fi
}
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
fi
local _prompt
if [ $# -gt 0 ]; then
export SWAIN_PURPOSE="$*"
_prompt="/swain-session Session purpose: $*"
else
_prompt=$(_swain_check_marker)
fi
if [ -z "$TMUX" ]; then
tmux new-session -s swain "claude --dangerously-skip-permissions '${_prompt}'"
else
claude --dangerously-skip-permissions "$_prompt"
fi
}
# swain shell launcher — codex / bash
# Runtime: Codex CLI (OpenAI) | Shell: bash
# Version: 5.0.0
#
# Launches Codex CLI interactively with swain's recommended flags.
# --yolo: bypass all approvals and sandboxing
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
_swain_check_marker() {
local marker=".swain/init.json"
if [ ! -f "$marker" ]; then
echo "/swain-init"
return
fi
local marker_version=""
if command -v jq &>/dev/null; then
marker_version=$(jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
marker_version=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
fi
if [ -z "$marker_version" ]; then
echo "/swain-init"
return
fi
local installed_version=""
local skill_file
skill_file=$(find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if [ -n "$skill_file" ]; then
installed_version=$(head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
fi
if [ -z "$installed_version" ]; then
echo "/swain-init"
return
fi
local marker_major="${marker_version%%.*}"
local installed_major="${installed_version%%.*}"
if [ "$marker_major" = "$installed_major" ]; then
echo "/swain-session"
else
echo "/swain-init"
fi
}
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
fi
local _prompt
if [ $# -gt 0 ]; then
export SWAIN_PURPOSE="$*"
_prompt="/swain-session Session purpose: $*"
else
_prompt=$(_swain_check_marker)
fi
if [ -z "$TMUX" ]; then
tmux new-session -s swain "codex --yolo '${_prompt}'"
else
codex --yolo "$_prompt"
fi
}
# swain shell launcher — codex / fish
# Runtime: Codex CLI (OpenAI) | Shell: fish
# Version: 5.0.0
#
# Launches Codex CLI interactively with swain's recommended flags.
# --yolo: bypass all approvals and sandboxing
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
function _swain_check_marker
set -l marker ".swain/init.json"
if not test -f "$marker"
echo "/swain-init"
return
end
set -l marker_version ""
if command -q jq
set marker_version (jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
set marker_version (grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
end
if test -z "$marker_version"
echo "/swain-init"
return
end
set -l installed_version ""
set -l skill_file (find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if test -n "$skill_file"
set installed_version (head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
end
if test -z "$installed_version"
echo "/swain-init"
return
end
set -l marker_major (string split '.' "$marker_version")[1]
set -l installed_major (string split '.' "$installed_version")[1]
if test "$marker_major" = "$installed_major"
echo "/swain-session"
else
echo "/swain-init"
end
end
function swain
if test -x bin/swain
exec bin/swain $argv
end
set -l _prompt
if test (count $argv) -gt 0
set -gx SWAIN_PURPOSE "$argv"
set _prompt "/swain-session Session purpose: $argv"
else
set _prompt (_swain_check_marker)
end
if not set -q TMUX
tmux new-session -s swain "codex --yolo '$_prompt'"
else
codex --yolo "$_prompt"
end
end
# swain shell launcher — codex / zsh
# Runtime: Codex CLI (OpenAI) | Shell: zsh
# Version: 5.0.0
#
# Launches Codex CLI interactively with swain's recommended flags.
# --yolo: bypass all approvals and sandboxing
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
_swain_check_marker() {
local marker=".swain/init.json"
if [ ! -f "$marker" ]; then
echo "/swain-init"
return
fi
local marker_version=""
if command -v jq &>/dev/null; then
marker_version=$(jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
marker_version=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
fi
if [ -z "$marker_version" ]; then
echo "/swain-init"
return
fi
local installed_version=""
local skill_file
skill_file=$(find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if [ -n "$skill_file" ]; then
installed_version=$(head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
fi
if [ -z "$installed_version" ]; then
echo "/swain-init"
return
fi
local marker_major="${marker_version%%.*}"
local installed_major="${installed_version%%.*}"
if [ "$marker_major" = "$installed_major" ]; then
echo "/swain-session"
else
echo "/swain-init"
fi
}
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
fi
local _prompt
if [ $# -gt 0 ]; then
export SWAIN_PURPOSE="$*"
_prompt="/swain-session Session purpose: $*"
else
_prompt=$(_swain_check_marker)
fi
if [ -z "$TMUX" ]; then
tmux new-session -s swain "codex --yolo '${_prompt}'"
else
codex --yolo "$_prompt"
fi
}
# swain shell launcher — copilot / bash
# Runtime: GitHub Copilot CLI | Shell: bash
# Version: 5.0.0
#
# Launches GitHub Copilot CLI interactively with swain's recommended flags.
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
_swain_check_marker() {
local marker=".swain/init.json"
if [ ! -f "$marker" ]; then
echo "/swain-init"
return
fi
local marker_version=""
if command -v jq &>/dev/null; then
marker_version=$(jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
marker_version=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
fi
if [ -z "$marker_version" ]; then
echo "/swain-init"
return
fi
local installed_version=""
local skill_file
skill_file=$(find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if [ -n "$skill_file" ]; then
installed_version=$(head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
fi
if [ -z "$installed_version" ]; then
echo "/swain-init"
return
fi
local marker_major="${marker_version%%.*}"
local installed_major="${installed_version%%.*}"
if [ "$marker_major" = "$installed_major" ]; then
echo "/swain-session"
else
echo "/swain-init"
fi
}
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
fi
local _prompt
if [ $# -gt 0 ]; then
export SWAIN_PURPOSE="$*"
_prompt="/swain-session Session purpose: $*"
else
_prompt=$(_swain_check_marker)
fi
if [ -z "$TMUX" ]; then
tmux new-session -s swain "gh copilot '${_prompt}'"
else
gh copilot "$_prompt"
fi
}
# swain shell launcher — copilot / fish
# Runtime: GitHub Copilot CLI | Shell: fish
# Version: 5.0.0
#
# Launches GitHub Copilot CLI interactively with swain's recommended flags.
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
function _swain_check_marker
set -l marker ".swain/init.json"
if not test -f "$marker"
echo "/swain-init"
return
end
set -l marker_version ""
if command -q jq
set marker_version (jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
set marker_version (grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
end
if test -z "$marker_version"
echo "/swain-init"
return
end
set -l installed_version ""
set -l skill_file (find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if test -n "$skill_file"
set installed_version (head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
end
if test -z "$installed_version"
echo "/swain-init"
return
end
set -l marker_major (string split '.' "$marker_version")[1]
set -l installed_major (string split '.' "$installed_version")[1]
if test "$marker_major" = "$installed_major"
echo "/swain-session"
else
echo "/swain-init"
end
end
function swain
if test -x bin/swain
exec bin/swain $argv
end
set -l _prompt
if test (count $argv) -gt 0
set -gx SWAIN_PURPOSE "$argv"
set _prompt "/swain-session Session purpose: $argv"
else
set _prompt (_swain_check_marker)
end
if not set -q TMUX
tmux new-session -s swain "gh copilot '$_prompt'"
else
gh copilot "$_prompt"
end
end
# swain shell launcher — copilot / zsh
# Runtime: GitHub Copilot CLI | Shell: zsh
# Version: 5.0.0
#
# Launches GitHub Copilot CLI interactively with swain's recommended flags.
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
_swain_check_marker() {
local marker=".swain/init.json"
if [ ! -f "$marker" ]; then
echo "/swain-init"
return
fi
local marker_version=""
if command -v jq &>/dev/null; then
marker_version=$(jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
marker_version=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
fi
if [ -z "$marker_version" ]; then
echo "/swain-init"
return
fi
local installed_version=""
local skill_file
skill_file=$(find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if [ -n "$skill_file" ]; then
installed_version=$(head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
fi
if [ -z "$installed_version" ]; then
echo "/swain-init"
return
fi
local marker_major="${marker_version%%.*}"
local installed_major="${installed_version%%.*}"
if [ "$marker_major" = "$installed_major" ]; then
echo "/swain-session"
else
echo "/swain-init"
fi
}
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
fi
local _prompt
if [ $# -gt 0 ]; then
export SWAIN_PURPOSE="$*"
_prompt="/swain-session Session purpose: $*"
else
_prompt=$(_swain_check_marker)
fi
if [ -z "$TMUX" ]; then
tmux new-session -s swain "gh copilot '${_prompt}'"
else
gh copilot "$_prompt"
fi
}
# swain shell launcher — crush / bash
# Runtime: Crush (formerly opencode) | Shell: bash
# Version: 4.1.0
#
# Launches Crush interactively with swain's recommended flags.
# --yolo: auto-approve all permission requests
#
# NOTE: Crush does not support an initial prompt in interactive mode
# (Partial support per ADR-017). Session initialization relies on
# AGENTS.md auto-invoke directives instead. Free-text session purpose
# is passed via the SWAIN_PURPOSE environment variable.
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
fi
if [ $# -gt 0 ]; then
export SWAIN_PURPOSE="$*"
fi
if [ -z "$TMUX" ]; then
tmux new-session -s swain "crush --yolo"
else
crush --yolo
fi
}
# swain shell launcher — crush / fish
# Runtime: Crush (formerly opencode) | Shell: fish
# Version: 4.1.0
#
# Launches Crush interactively with swain's recommended flags.
# --yolo: auto-approve all permission requests
#
# NOTE: Crush does not support an initial prompt in interactive mode
# (Partial support per ADR-017). Session initialization relies on
# AGENTS.md auto-invoke directives instead. Free-text session purpose
# is passed via the SWAIN_PURPOSE environment variable.
function swain
if test -x bin/swain
exec bin/swain $argv
end
if test (count $argv) -gt 0
set -gx SWAIN_PURPOSE "$argv"
end
if not set -q TMUX
tmux new-session -s swain "crush --yolo"
else
crush --yolo
end
end
# swain shell launcher — crush / zsh
# Runtime: Crush (formerly opencode) | Shell: zsh
# Version: 4.1.0
#
# Launches Crush interactively with swain's recommended flags.
# --yolo: auto-approve all permission requests
#
# NOTE: Crush does not support an initial prompt in interactive mode
# (Partial support per ADR-017). Session initialization relies on
# AGENTS.md auto-invoke directives instead. Free-text session purpose
# is passed via the SWAIN_PURPOSE environment variable.
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
fi
if [ $# -gt 0 ]; then
export SWAIN_PURPOSE="$*"
fi
if [ -z "$TMUX" ]; then
tmux new-session -s swain "crush --yolo"
else
crush --yolo
fi
}
# swain shell launcher — gemini / bash
# Runtime: Gemini CLI | Shell: bash
# Version: 5.0.0
#
# Launches Gemini CLI interactively with swain's recommended flags.
# -y: auto-approve all tool actions (yolo mode)
# -i: interactive mode with initial prompt
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
_swain_check_marker() {
local marker=".swain/init.json"
if [ ! -f "$marker" ]; then
echo "/swain-init"
return
fi
local marker_version=""
if command -v jq &>/dev/null; then
marker_version=$(jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
marker_version=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
fi
if [ -z "$marker_version" ]; then
echo "/swain-init"
return
fi
local installed_version=""
local skill_file
skill_file=$(find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if [ -n "$skill_file" ]; then
installed_version=$(head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
fi
if [ -z "$installed_version" ]; then
echo "/swain-init"
return
fi
local marker_major="${marker_version%%.*}"
local installed_major="${installed_version%%.*}"
if [ "$marker_major" = "$installed_major" ]; then
echo "/swain-session"
else
echo "/swain-init"
fi
}
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
fi
local _prompt
if [ $# -gt 0 ]; then
export SWAIN_PURPOSE="$*"
_prompt="/swain-session Session purpose: $*"
else
_prompt=$(_swain_check_marker)
fi
if [ -z "$TMUX" ]; then
tmux new-session -s swain "gemini -y -i '${_prompt}'"
else
gemini -y -i "$_prompt"
fi
}
# swain shell launcher — gemini / fish
# Runtime: Gemini CLI | Shell: fish
# Version: 5.0.0
#
# Launches Gemini CLI interactively with swain's recommended flags.
# -y: auto-approve all tool actions (yolo mode)
# -i: interactive mode with initial prompt
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
function _swain_check_marker
set -l marker ".swain/init.json"
if not test -f "$marker"
echo "/swain-init"
return
end
set -l marker_version ""
if command -q jq
set marker_version (jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
set marker_version (grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
end
if test -z "$marker_version"
echo "/swain-init"
return
end
set -l installed_version ""
set -l skill_file (find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if test -n "$skill_file"
set installed_version (head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
end
if test -z "$installed_version"
echo "/swain-init"
return
end
set -l marker_major (string split '.' "$marker_version")[1]
set -l installed_major (string split '.' "$installed_version")[1]
if test "$marker_major" = "$installed_major"
echo "/swain-session"
else
echo "/swain-init"
end
end
function swain
if test -x bin/swain
exec bin/swain $argv
end
set -l _prompt
if test (count $argv) -gt 0
set -gx SWAIN_PURPOSE "$argv"
set _prompt "/swain-session Session purpose: $argv"
else
set _prompt (_swain_check_marker)
end
if not set -q TMUX
tmux new-session -s swain "gemini -y -i '$_prompt'"
else
gemini -y -i "$_prompt"
end
end
# swain shell launcher — gemini / zsh
# Runtime: Gemini CLI | Shell: zsh
# Version: 5.0.0
#
# Launches Gemini CLI interactively with swain's recommended flags.
# -y: auto-approve all tool actions (yolo mode)
# -i: interactive mode with initial prompt
# When arguments are provided, they become the session purpose.
# SPEC-196: Checks .swain/init.json marker to skip the init skill on established projects.
# Check .swain/init.json marker and return the appropriate initial prompt.
# Returns /swain-session if marker is current, /swain-init otherwise.
_swain_check_marker() {
local marker=".swain/init.json"
if [ ! -f "$marker" ]; then
echo "/swain-init"
return
fi
local marker_version=""
if command -v jq &>/dev/null; then
marker_version=$(jq -r '.history[-1].version // empty' "$marker" 2>/dev/null)
else
marker_version=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$marker" 2>/dev/null | tail -1 | grep -o '"[0-9][^"]*"' | tr -d '"')
fi
if [ -z "$marker_version" ]; then
echo "/swain-init"
return
fi
local installed_version=""
local skill_file
skill_file=$(find . .claude .agents skills -path '*/swain-init/SKILL.md' -print -quit 2>/dev/null)
if [ -n "$skill_file" ]; then
installed_version=$(head -20 "$skill_file" 2>/dev/null | grep '^version:' | awk '{print $2}')
fi
if [ -z "$installed_version" ]; then
echo "/swain-init"
return
fi
local marker_major="${marker_version%%.*}"
local installed_major="${installed_version%%.*}"
if [ "$marker_major" = "$installed_major" ]; then
echo "/swain-session"
else
echo "/swain-init"
fi
}
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
fi
local _prompt
if [ $# -gt 0 ]; then
export SWAIN_PURPOSE="$*"
_prompt="/swain-session Session purpose: $*"
else
_prompt=$(_swain_check_marker)
fi
if [ -z "$TMUX" ]; then
tmux new-session -s swain "gemini -y -i '${_prompt}'"
else
gemini -y -i "$_prompt"
fi
}
# swain shell launcher — thin wrapper
# Version: 5.0.0
#
# Delegates to the project-root swain script (SPEC-180) when available.
# Falls back to direct runtime invocation for projects without it.
# See: SPEC-181 (Shell Function Refactor)
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
elif command -v claude >/dev/null 2>&1; then
local _prompt='/swain-init'
[ $# -gt 0 ] && _prompt="/swain-session Session purpose: $*"
if [ -z "$TMUX" ]; then
tmux new-session -s swain "claude --dangerously-skip-permissions '${_prompt}'"
else
claude --dangerously-skip-permissions "$_prompt"
fi
else
echo "swain: no bin/swain script and no supported runtime found" >&2
return 1
fi
}
# swain shell launcher — thin wrapper
# Version: 5.0.0
#
# Delegates to the project-root swain script (SPEC-180) when available.
# Falls back to direct runtime invocation for projects without it.
# See: SPEC-181 (Shell Function Refactor)
swain() {
if [ -x "bin/swain" ]; then
exec bin/swain "$@"
elif command -v claude >/dev/null 2>&1; then
local _prompt='/swain-init'
[ $# -gt 0 ] && _prompt="/swain-session Session purpose: $*"
if [ -z "$TMUX" ]; then
tmux new-session -s swain "claude --dangerously-skip-permissions '${_prompt}'"
else
claude --dangerously-skip-permissions "$_prompt"
fi
else
echo "swain: no bin/swain script and no supported runtime found" >&2
return 1
fi
}