feat sparv skill

This commit is contained in:
cexll
2026-01-16 14:34:03 +08:00
parent 238c7b9a13
commit 5d362852ab
25 changed files with 1464 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
#!/bin/bash
# SPARV Session Archive Script
# Archives completed session from .sparv/plan/<session_id>/ to .sparv/history/<session_id>/
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/state-lock.sh"
usage() {
cat <<'EOF'
Usage: archive-session.sh [--dry-run]
Moves current session from .sparv/plan/<session_id>/ to .sparv/history/<session_id>/
Updates .sparv/history/index.md with session info.
Options:
--dry-run Show what would be archived without doing it
EOF
}
SPARV_ROOT=".sparv"
PLAN_DIR="$SPARV_ROOT/plan"
HISTORY_DIR="$SPARV_ROOT/history"
dry_run=0
while [ $# -gt 0 ]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--dry-run) dry_run=1; shift ;;
*) usage >&2; exit 1 ;;
esac
done
# Find active session
find_active_session() {
if [ -d "$PLAN_DIR" ]; then
local session
session="$(ls -1 "$PLAN_DIR" 2>/dev/null | head -1)"
if [ -n "$session" ] && [ -f "$PLAN_DIR/$session/state.yaml" ]; then
echo "$session"
fi
fi
}
# Update history/index.md
update_history_index() {
local session_id="$1"
local index_file="$HISTORY_DIR/index.md"
local state_file="$HISTORY_DIR/$session_id/state.yaml"
[ -f "$index_file" ] || return 0
# Get feature name from state.yaml
local fname=""
if [ -f "$state_file" ]; then
fname="$(grep -E '^feature_name:' "$state_file" | sed -E 's/^feature_name:[[:space:]]*"?([^"]*)"?$/\1/' || true)"
fi
[ -z "$fname" ] && fname="unnamed"
local month="${session_id:0:6}"
local formatted_month="${month:0:4}-${month:4:2}"
# Add to monthly section if not exists
if ! grep -q "### $formatted_month" "$index_file"; then
echo -e "\n### $formatted_month\n" >> "$index_file"
fi
echo "- \`${session_id}\` - $fname" >> "$index_file"
}
SESSION_ID="$(find_active_session)"
if [ -z "$SESSION_ID" ]; then
echo "No active session to archive"
exit 0
fi
SRC_DIR="$PLAN_DIR/$SESSION_ID"
DST_DIR="$HISTORY_DIR/$SESSION_ID"
if [ "$dry_run" -eq 1 ]; then
echo "Would archive: $SRC_DIR -> $DST_DIR"
exit 0
fi
# Create history directory and move session
mkdir -p "$HISTORY_DIR"
mv "$SRC_DIR" "$DST_DIR"
# Update index
update_history_index "$SESSION_ID"
echo "✅ Session archived: $SESSION_ID"
echo "📁 Location: $DST_DIR"

View File

@@ -0,0 +1,182 @@
#!/bin/bash
# EHRB Risk Detection Script
# Heuristically detects high-risk changes/specs and writes flags to .sparv/state.yaml:ehrb_flags.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/state-lock.sh"
usage() {
cat <<'EOF'
Usage: check-ehrb.sh [options] [FILE...]
Options:
--diff Scan current git diff (staged + unstaged) and changed file names
--clear Clear ehrb_flags in .sparv/state.yaml (no scan needed)
--dry-run Do not write .sparv/state.yaml (print detected flags only)
--fail-on-flags Exit with code 2 if any flags are detected
-h, --help Show this help
Input:
- --diff
- positional FILE...
- stdin (if piped)
Examples:
check-ehrb.sh --diff --fail-on-flags
check-ehrb.sh docs/feature-prd.md
echo "touching production db" | check-ehrb.sh --fail-on-flags
EOF
}
die() {
echo "$*" >&2
exit 1
}
is_piped_stdin() {
[ ! -t 0 ]
}
git_text() {
git diff --cached 2>/dev/null || true
git diff 2>/dev/null || true
(git diff --name-only --cached 2>/dev/null; git diff --name-only 2>/dev/null) | sort -u || true
}
render_inline_list() {
if [ "$#" -eq 0 ]; then
printf "[]"
return 0
fi
printf "["
local first=1 item
for item in "$@"; do
if [ "$first" -eq 1 ]; then
first=0
else
printf ", "
fi
printf "\"%s\"" "$item"
done
printf "]"
}
write_ehrb_flags() {
local list_value="$1"
sparv_require_state_file
sparv_state_validate_or_die
sparv_yaml_set_raw ehrb_flags "$list_value"
}
scan_diff=0
dry_run=0
clear=0
fail_on_flags=0
declare -a files=()
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--diff)
scan_diff=1
shift
;;
--clear)
clear=1
shift
;;
--dry-run)
dry_run=1
shift
;;
--fail-on-flags)
fail_on_flags=1
shift
;;
--)
shift
break
;;
-*)
die "Unknown argument: $1 (use --help for usage)"
;;
*)
files+=("$1")
shift
;;
esac
done
for path in "$@"; do
files+=("$path")
done
scan_text=""
if [ "$scan_diff" -eq 1 ]; then
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
scan_text+=$'\n'"$(git_text)"
else
die "--diff requires running inside a git repository"
fi
fi
if [ "${#files[@]}" -gt 0 ]; then
for path in "${files[@]}"; do
[ -f "$path" ] || die "File not found: $path"
scan_text+=$'\n'"$(cat "$path")"
done
fi
if is_piped_stdin; then
scan_text+=$'\n'"$(cat)"
fi
declare -a flags=()
if [ "$clear" -eq 1 ]; then
flags=()
else
[ -n "$scan_text" ] || die "No scannable input (use --help to see input methods)"
if printf "%s" "$scan_text" | grep -Eiq '(^|[^a-z])(prod(uction)?|live)([^a-z]|$)|kubeconfig|kubectl|terraform|helm|eks|gke|aks'; then
flags+=("production-access")
fi
if printf "%s" "$scan_text" | grep -Eiq 'pii|phi|hipaa|ssn|password|passwd|secret|token|api[ _-]?key|private key|credit card|身份证|银行卡|医疗|患者'; then
flags+=("sensitive-data")
fi
if printf "%s" "$scan_text" | grep -Eiq 'rm[[:space:]]+-rf|drop[[:space:]]+table|delete[[:space:]]+from|truncate|terraform[[:space:]]+destroy|kubectl[[:space:]]+delete|drop[[:space:]]+database|wipe|purge'; then
flags+=("destructive-ops")
fi
if printf "%s" "$scan_text" | grep -Eiq 'stripe|paypal|billing|charge|invoice|subscription|metering|twilio|sendgrid|openai|anthropic|cost|usage'; then
flags+=("billing-external-api")
fi
if printf "%s" "$scan_text" | grep -Eiq 'auth|authentication|authorization|oauth|jwt|sso|encryption|crypto|tls|ssl|mfa|rbac|permission|权限|登录|认证'; then
flags+=("security-critical")
fi
fi
if [ "${#flags[@]}" -eq 0 ]; then
echo "EHRB: No risk flags detected"
else
echo "EHRB: Risk flags detected (require explicit user confirmation):"
for f in ${flags[@]+"${flags[@]}"}; do
echo " - $f"
done
fi
if [ "$dry_run" -eq 0 ]; then
list_value="$(render_inline_list ${flags[@]+"${flags[@]}"})"
write_ehrb_flags "$list_value"
echo "Written to: $STATE_FILE (ehrb_flags: $list_value)"
fi
if [ "$fail_on_flags" -eq 1 ] && [ "${#flags[@]}" -gt 0 ]; then
exit 2
fi
exit 0

View File

@@ -0,0 +1,135 @@
#!/bin/bash
# SPARV 3-Failure Protocol Tracker
# Maintains consecutive_failures and escalates when reaching 3.
# Notes are appended to journal.md (unified log).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/state-lock.sh"
THRESHOLD=3
usage() {
cat <<'EOF'
Usage: failure-tracker.sh <command> [options]
Commands:
status Show current consecutive_failures and protocol level
fail [--note TEXT] Increment consecutive_failures (exit 3 when reaching threshold)
reset Set consecutive_failures to 0
Auto-detects active session in .sparv/plan/<session_id>/
EOF
}
die() {
echo "$*" >&2
exit 1
}
require_state() {
# Auto-detect session (sets SPARV_DIR, STATE_FILE, JOURNAL_FILE)
sparv_require_state_file
sparv_state_validate_or_die
}
append_journal() {
local level="$1"
local note="${2:-}"
local ts
ts="$(date '+%Y-%m-%d %H:%M')"
[ -f "$JOURNAL_FILE" ] || sparv_die "Cannot find $JOURNAL_FILE; run init-session.sh first"
{
echo
echo "## Failure Protocol - $ts"
echo "- level: $level"
if [ -n "$note" ]; then
echo "- note: $note"
fi
} >>"$JOURNAL_FILE"
}
protocol_level() {
local count="$1"
if [ "$count" -le 0 ]; then
echo "0"
elif [ "$count" -eq 1 ]; then
echo "1"
elif [ "$count" -eq 2 ]; then
echo "2"
else
echo "3"
fi
}
cmd="${1:-status}"
shift || true
note=""
case "$cmd" in
-h|--help)
usage
exit 0
;;
status)
require_state
current="$(sparv_yaml_get_int consecutive_failures 0)"
level="$(protocol_level "$current")"
echo "consecutive_failures: $current"
case "$level" in
0) echo "protocol: clean (no failures)" ;;
1) echo "protocol: Attempt 1 - Diagnose and fix" ;;
2) echo "protocol: Attempt 2 - Alternative approach" ;;
3) echo "protocol: Attempt 3 - Escalate (pause, document, ask user)" ;;
esac
exit 0
;;
fail)
require_state
if [ "${1:-}" = "--note" ]; then
[ $# -ge 2 ] || die "--note requires an argument"
note="$2"
shift 2
else
note="$*"
shift $#
fi
[ "$#" -eq 0 ] || die "Unknown argument: $1 (use --help for usage)"
current="$(sparv_yaml_get_int consecutive_failures 0)"
new_count=$((current + 1))
sparv_yaml_set_int consecutive_failures "$new_count"
level="$(protocol_level "$new_count")"
case "$level" in
1)
echo "Attempt 1/3: Diagnose and fix"
[ -n "$note" ] && append_journal "1" "$note"
exit 0
;;
2)
echo "Attempt 2/3: Alternative approach"
[ -n "$note" ] && append_journal "2" "$note"
exit 0
;;
3)
echo "Attempt 3/3: Escalate"
echo "3-Failure Protocol triggered: pause, document blocker and attempted solutions, request user decision."
append_journal "3" "${note:-"(no note)"}"
exit "$THRESHOLD"
;;
esac
;;
reset)
require_state
sparv_yaml_set_int consecutive_failures 0
echo "consecutive_failures reset to 0"
exit 0
;;
*)
die "Unknown command: $cmd (use --help for usage)"
;;
esac

View File

@@ -0,0 +1,205 @@
#!/bin/bash
# SPARV Session Initialization
# Creates .sparv/plan/<session_id>/ with state.yaml and journal.md
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/state-lock.sh"
usage() {
cat <<'EOF'
Usage: init-session.sh [--force] [feature_name]
Creates .sparv/plan/<session_id>/ directory:
- state.yaml (session state)
- journal.md (unified log)
Also initializes:
- .sparv/history/index.md (if not exists)
- .sparv/CHANGELOG.md (if not exists)
Options:
--force Archive current session and start new one
feature_name Optional feature name for the session
EOF
}
SPARV_ROOT=".sparv"
PLAN_DIR="$SPARV_ROOT/plan"
HISTORY_DIR="$SPARV_ROOT/history"
force=0
feature_name=""
while [ $# -gt 0 ]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--force) force=1; shift ;;
-*) usage >&2; exit 1 ;;
*) feature_name="$1"; shift ;;
esac
done
# Find current active session
find_active_session() {
if [ -d "$PLAN_DIR" ]; then
local session
session="$(ls -1 "$PLAN_DIR" 2>/dev/null | head -1)"
if [ -n "$session" ] && [ -f "$PLAN_DIR/$session/state.yaml" ]; then
echo "$session"
fi
fi
}
# Archive a session to history
archive_session() {
local session_id="$1"
local src_dir="$PLAN_DIR/$session_id"
local dst_dir="$HISTORY_DIR/$session_id"
[ -d "$src_dir" ] || return 0
mkdir -p "$HISTORY_DIR"
mv "$src_dir" "$dst_dir"
# Update index.md
update_history_index "$session_id"
echo "📦 Archived: $dst_dir"
}
# Update history/index.md
update_history_index() {
local session_id="$1"
local index_file="$HISTORY_DIR/index.md"
local state_file="$HISTORY_DIR/$session_id/state.yaml"
# Get feature name from state.yaml
local fname=""
if [ -f "$state_file" ]; then
fname="$(grep -E '^feature_name:' "$state_file" | sed -E 's/^feature_name:[[:space:]]*"?([^"]*)"?$/\1/' || true)"
fi
[ -z "$fname" ] && fname="unnamed"
local month="${session_id:0:6}"
local formatted_month="${month:0:4}-${month:4:2}"
local timestamp="${session_id:0:12}"
# Append to index
if [ -f "$index_file" ]; then
# Add to monthly section if not exists
if ! grep -q "### $formatted_month" "$index_file"; then
echo -e "\n### $formatted_month\n" >> "$index_file"
fi
echo "- \`${session_id}\` - $fname" >> "$index_file"
fi
}
# Initialize history/index.md if not exists
init_history_index() {
local index_file="$HISTORY_DIR/index.md"
[ -f "$index_file" ] && return 0
mkdir -p "$HISTORY_DIR"
cat > "$index_file" << 'EOF'
# History Index
This file records all completed sessions for traceability.
---
## Index
| Timestamp | Feature | Type | Status | Path |
|-----------|---------|------|--------|------|
---
## Monthly Archive
EOF
}
# Initialize CHANGELOG.md if not exists
init_changelog() {
local changelog="$SPARV_ROOT/CHANGELOG.md"
[ -f "$changelog" ] && return 0
cat > "$changelog" << 'EOF'
# Changelog
All notable changes to this project will be documented in this file.
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
EOF
}
# Check for active session
active_session="$(find_active_session)"
if [ -n "$active_session" ]; then
if [ "$force" -eq 0 ]; then
echo "⚠️ Active session exists: $active_session"
echo " Use --force to archive and start new session"
echo " Or run: archive-session.sh"
exit 0
else
archive_session "$active_session"
fi
fi
# Generate new session ID
SESSION_ID=$(date +%Y%m%d%H%M%S)
SESSION_DIR="$PLAN_DIR/$SESSION_ID"
# Create directory structure
mkdir -p "$SESSION_DIR"
mkdir -p "$HISTORY_DIR"
# Initialize global files
init_history_index
init_changelog
# Create state.yaml
cat > "$SESSION_DIR/state.yaml" << EOF
session_id: "$SESSION_ID"
feature_name: "$feature_name"
current_phase: "specify"
action_count: 0
consecutive_failures: 0
max_iterations: 12
iteration_count: 0
completion_promise: ""
ehrb_flags: []
EOF
# Create journal.md
cat > "$SESSION_DIR/journal.md" << EOF
# SPARV Journal
Session: $SESSION_ID
Feature: $feature_name
Created: $(date '+%Y-%m-%d %H:%M')
## Plan
<!-- Task breakdown, sub-issues, success criteria -->
## Progress
<!-- Auto-updated every 2 actions -->
## Findings
<!-- Learnings, patterns, discoveries -->
EOF
# Verify files created
if [ ! -f "$SESSION_DIR/state.yaml" ] || [ ! -f "$SESSION_DIR/journal.md" ]; then
echo "❌ Failed to create files"
exit 1
fi
echo "✅ SPARV session: $SESSION_ID"
[ -n "$feature_name" ] && echo "📝 Feature: $feature_name"
echo "📁 $SESSION_DIR/state.yaml"
echo "📁 $SESSION_DIR/journal.md"

View File

@@ -0,0 +1,143 @@
#!/bin/bash
#
# Shared helpers for .sparv state operations.
# Supports new directory structure: .sparv/plan/<session_id>/
sparv_die() {
echo "$*" >&2
exit 1
}
# Find active session directory
sparv_find_active_session() {
local plan_dir=".sparv/plan"
if [ -d "$plan_dir" ]; then
local session
session="$(ls -1 "$plan_dir" 2>/dev/null | head -1)"
if [ -n "$session" ] && [ -f "$plan_dir/$session/state.yaml" ]; then
echo "$plan_dir/$session"
fi
fi
}
# Auto-detect SPARV_DIR and STATE_FILE
sparv_auto_detect() {
local session_dir
session_dir="$(sparv_find_active_session)"
if [ -n "$session_dir" ]; then
SPARV_DIR="$session_dir"
STATE_FILE="$session_dir/state.yaml"
JOURNAL_FILE="$session_dir/journal.md"
export SPARV_DIR STATE_FILE JOURNAL_FILE
return 0
fi
return 1
}
sparv_require_state_env() {
if [ -z "${SPARV_DIR:-}" ] || [ -z "${STATE_FILE:-}" ]; then
if ! sparv_auto_detect; then
sparv_die "No active session found; run init-session.sh first"
fi
fi
}
sparv_require_state_file() {
sparv_require_state_env
[ -f "$STATE_FILE" ] || sparv_die "File not found: $STATE_FILE; run init-session.sh first"
}
# Read a YAML value (simple key: value format)
sparv_yaml_get() {
local key="$1"
local default="${2:-}"
sparv_require_state_file
local line value
line="$(grep -E "^${key}:" "$STATE_FILE" | head -n 1 || true)"
if [ -z "$line" ]; then
printf "%s" "$default"
return 0
fi
value="${line#${key}:}"
value="$(printf "%s" "$value" | sed -E 's/^[[:space:]]+//; s/^"//; s/"$//')"
printf "%s" "$value"
}
sparv_yaml_get_int() {
local key="$1"
local default="${2:-0}"
local value
value="$(sparv_yaml_get "$key" "$default")"
if printf "%s" "$value" | grep -Eq '^[0-9]+$'; then
printf "%s" "$value"
else
printf "%s" "$default"
fi
}
# Write a YAML value (in-place update)
sparv_yaml_set_raw() {
local key="$1"
local raw_value="$2"
sparv_require_state_file
local tmp
tmp="$(mktemp)"
awk -v key="$key" -v repl="${key}: ${raw_value}" '
BEGIN { in_block = 0; replaced = 0 }
{
if (in_block) {
if ($0 ~ /^[[:space:]]*-/) next
in_block = 0
}
if ($0 ~ ("^" key ":")) {
print repl
in_block = 1
replaced = 1
next
}
print
}
END {
if (!replaced) print repl
}
' "$STATE_FILE" >"$tmp"
mv -f "$tmp" "$STATE_FILE"
}
sparv_yaml_set_int() {
local key="$1"
local value="$2"
[ "$value" -ge 0 ] 2>/dev/null || sparv_die "$key must be a non-negative integer"
sparv_yaml_set_raw "$key" "$value"
}
# Validate state.yaml has required fields (4 core fields only)
sparv_state_validate() {
sparv_require_state_file
local missing=0
local key
for key in session_id current_phase action_count consecutive_failures; do
grep -Eq "^${key}:" "$STATE_FILE" || missing=1
done
local phase
phase="$(sparv_yaml_get current_phase "")"
case "$phase" in
specify|plan|act|review|vault) ;;
*) missing=1 ;;
esac
[ "$missing" -eq 0 ]
}
sparv_state_validate_or_die() {
if ! sparv_state_validate; then
sparv_die "Corrupted state.yaml: $STATE_FILE. Run init-session.sh --force to rebuild."
fi
}

View File

@@ -0,0 +1,127 @@
#!/bin/bash
# SPARV 3-Question Reboot Test Script
# Prints (and optionally validates) the "3 questions" using the current session state.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/state-lock.sh"
usage() {
cat <<'EOF'
Usage: reboot-test.sh [options]
Options:
--strict Exit non-zero if critical answers are missing or unsafe
-h, --help Show this help
Auto-detects active session in .sparv/plan/<session_id>/
EOF
}
die() {
echo "$*" >&2
exit 1
}
tail_file() {
local path="$1"
local lines="${2:-20}"
if [ -f "$path" ]; then
tail -n "$lines" "$path"
else
echo "(missing: $path)"
fi
}
strict=0
while [ $# -gt 0 ]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--strict) strict=1; shift ;;
*) die "Unknown argument: $1 (use --help for usage)" ;;
esac
done
# Auto-detect session (sets SPARV_DIR, STATE_FILE, JOURNAL_FILE)
sparv_require_state_file
sparv_state_validate_or_die
session_id="$(sparv_yaml_get session_id "")"
feature_name="$(sparv_yaml_get feature_name "")"
current_phase="$(sparv_yaml_get current_phase "")"
completion_promise="$(sparv_yaml_get completion_promise "")"
iteration_count="$(sparv_yaml_get_int iteration_count 0)"
max_iterations="$(sparv_yaml_get_int max_iterations 0)"
consecutive_failures="$(sparv_yaml_get_int consecutive_failures 0)"
ehrb_flags="$(sparv_yaml_get ehrb_flags "")"
case "$current_phase" in
specify) next_phase="plan" ;;
plan) next_phase="act" ;;
act) next_phase="review" ;;
review) next_phase="vault" ;;
vault) next_phase="done" ;;
*) next_phase="unknown" ;;
esac
echo "== 3-Question Reboot Test =="
echo "session_id: ${session_id:-"(unknown)"}"
if [ -n "$feature_name" ]; then
echo "feature_name: $feature_name"
fi
echo
echo "1) Where am I?"
echo " current_phase: ${current_phase:-"(empty)"}"
echo
echo "2) Where am I going?"
echo " next_phase: $next_phase"
echo
echo "3) How do I prove completion?"
if [ -n "$completion_promise" ]; then
echo " completion_promise: $completion_promise"
else
echo " completion_promise: (empty)"
fi
echo
echo "journal tail (20 lines):"
tail_file "$JOURNAL_FILE" 20
echo
echo "Counters: failures=$consecutive_failures, iteration=$iteration_count/$max_iterations"
if [ -n "$ehrb_flags" ] && [ "$ehrb_flags" != "[]" ]; then
echo "EHRB: $ehrb_flags"
fi
if [ "$strict" -eq 1 ]; then
exit_code=0
case "$current_phase" in
specify|plan|act|review|vault) ;;
*) echo "❌ strict: current_phase invalid/empty: $current_phase" >&2; exit_code=1 ;;
esac
if [ -z "$completion_promise" ]; then
echo "❌ strict: completion_promise is empty; fill in a verifiable completion commitment in $STATE_FILE first." >&2
exit_code=1
fi
if [ "$max_iterations" -gt 0 ] && [ "$iteration_count" -ge "$max_iterations" ]; then
echo "❌ strict: iteration_count >= max_iterations; stop hook triggered, should pause and escalate to user." >&2
exit_code=1
fi
if [ "$consecutive_failures" -ge 3 ]; then
echo "❌ strict: consecutive_failures >= 3; 3-Failure Protocol triggered, should pause and escalate to user." >&2
exit_code=1
fi
if [ -n "$ehrb_flags" ] && [ "$ehrb_flags" != "[]" ]; then
echo "❌ strict: ehrb_flags not empty; EHRB risk exists, requires explicit user confirmation before continuing." >&2
exit_code=1
fi
exit "$exit_code"
fi
exit 0

View File

@@ -0,0 +1,55 @@
#!/bin/bash
# SPARV Progress Save Script
# Implements the 2-Action rule (called after each tool call; writes every 2 actions).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/state-lock.sh"
usage() {
cat <<'EOF'
Usage: save-progress.sh [TOOL_NAME] [RESULT]
Increments action_count and appends to journal.md every 2 actions.
Auto-detects active session in .sparv/plan/<session_id>/
EOF
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
usage
exit 0
fi
# Auto-detect session (sets SPARV_DIR, STATE_FILE, JOURNAL_FILE)
sparv_require_state_file
sparv_state_validate_or_die
[ -f "$JOURNAL_FILE" ] || sparv_die "Cannot find $JOURNAL_FILE; run init-session.sh first"
# Arguments
TOOL_NAME="${1:-unknown}"
RESULT="${2:-no result}"
ACTION_COUNT="$(sparv_yaml_get_int action_count 0)"
# Increment action count
NEW_COUNT=$((ACTION_COUNT + 1))
# Update state file
sparv_yaml_set_int action_count "$NEW_COUNT"
# Only write every 2 actions
if [ $((NEW_COUNT % 2)) -ne 0 ]; then
exit 0
fi
# Append to journal
TIMESTAMP=$(date '+%H:%M')
cat >> "$JOURNAL_FILE" << EOF
## $TIMESTAMP - Action #$NEW_COUNT
- Tool: $TOOL_NAME
- Result: $RESULT
EOF
echo "📝 journal.md saved: Action #$NEW_COUNT"