From 5d362852ab32dab76a482bd217a59eac63b42c5f Mon Sep 17 00:00:00 2001 From: cexll Date: Fri, 16 Jan 2026 14:34:03 +0800 Subject: [PATCH] feat sparv skill --- skills/sparv/.sparv/CHANGELOG.md | 7 + .../.sparv/history/20260114-164058/journal.md | 20 ++ .../.sparv/history/20260114-164058/state.yaml | 9 + .../.sparv/history/20260115215314/journal.md | 17 ++ .../.sparv/history/20260115215314/state.yaml | 9 + .../.sparv/history/20260115215722/journal.md | 29 +++ .../.sparv/history/20260115215722/state.yaml | 9 + .../.sparv/history/20260115220023/journal.md | 13 ++ .../.sparv/history/20260115220023/state.yaml | 9 + skills/sparv/.sparv/history/index.md | 21 ++ skills/sparv/.sparv/journal.md | 16 ++ .../.sparv/plan/20260115220038/journal.md | 13 ++ .../.sparv/plan/20260115220038/state.yaml | 9 + skills/sparv/.sparv/state.yaml | 9 + skills/sparv/README.md | 96 ++++++++ skills/sparv/SKILL.md | 103 +++++++++ skills/sparv/hooks/hooks.json | 37 ++++ skills/sparv/references/methodology.md | 96 ++++++++ skills/sparv/scripts/archive-session.sh | 95 ++++++++ skills/sparv/scripts/check-ehrb.sh | 182 ++++++++++++++++ skills/sparv/scripts/failure-tracker.sh | 135 ++++++++++++ skills/sparv/scripts/init-session.sh | 205 ++++++++++++++++++ skills/sparv/scripts/lib/state-lock.sh | 143 ++++++++++++ skills/sparv/scripts/reboot-test.sh | 127 +++++++++++ skills/sparv/scripts/save-progress.sh | 55 +++++ 25 files changed, 1464 insertions(+) create mode 100644 skills/sparv/.sparv/CHANGELOG.md create mode 100644 skills/sparv/.sparv/history/20260114-164058/journal.md create mode 100644 skills/sparv/.sparv/history/20260114-164058/state.yaml create mode 100644 skills/sparv/.sparv/history/20260115215314/journal.md create mode 100644 skills/sparv/.sparv/history/20260115215314/state.yaml create mode 100644 skills/sparv/.sparv/history/20260115215722/journal.md create mode 100644 skills/sparv/.sparv/history/20260115215722/state.yaml create mode 100644 skills/sparv/.sparv/history/20260115220023/journal.md create mode 100644 skills/sparv/.sparv/history/20260115220023/state.yaml create mode 100644 skills/sparv/.sparv/history/index.md create mode 100644 skills/sparv/.sparv/journal.md create mode 100644 skills/sparv/.sparv/plan/20260115220038/journal.md create mode 100644 skills/sparv/.sparv/plan/20260115220038/state.yaml create mode 100644 skills/sparv/.sparv/state.yaml create mode 100644 skills/sparv/README.md create mode 100644 skills/sparv/SKILL.md create mode 100644 skills/sparv/hooks/hooks.json create mode 100644 skills/sparv/references/methodology.md create mode 100755 skills/sparv/scripts/archive-session.sh create mode 100755 skills/sparv/scripts/check-ehrb.sh create mode 100755 skills/sparv/scripts/failure-tracker.sh create mode 100755 skills/sparv/scripts/init-session.sh create mode 100755 skills/sparv/scripts/lib/state-lock.sh create mode 100755 skills/sparv/scripts/reboot-test.sh create mode 100755 skills/sparv/scripts/save-progress.sh diff --git a/skills/sparv/.sparv/CHANGELOG.md b/skills/sparv/.sparv/CHANGELOG.md new file mode 100644 index 0000000..1f7a40f --- /dev/null +++ b/skills/sparv/.sparv/CHANGELOG.md @@ -0,0 +1,7 @@ +# 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] + diff --git a/skills/sparv/.sparv/history/20260114-164058/journal.md b/skills/sparv/.sparv/history/20260114-164058/journal.md new file mode 100644 index 0000000..1f7b6f8 --- /dev/null +++ b/skills/sparv/.sparv/history/20260114-164058/journal.md @@ -0,0 +1,20 @@ +# SPARV Journal +Session: 20260114-164058 +Created: 2026-01-14 16:40 + +## Plan + + +## Progress + + +## Findings + + +## 16:40 - Action #2 +- Tool: Edit +- Result: ok + +## Failure Protocol - 2026-01-14 16:40 +- level: 1 +- note: unit test flake diff --git a/skills/sparv/.sparv/history/20260114-164058/state.yaml b/skills/sparv/.sparv/history/20260114-164058/state.yaml new file mode 100644 index 0000000..be037f5 --- /dev/null +++ b/skills/sparv/.sparv/history/20260114-164058/state.yaml @@ -0,0 +1,9 @@ +session_id: "20260114-164058" +current_phase: "specify" +action_count: 2 +consecutive_failures: 0 +max_iterations: 12 +iteration_count: 0 +completion_promise: "All tests green" +feature_name: "" +ehrb_flags: [] diff --git a/skills/sparv/.sparv/history/20260115215314/journal.md b/skills/sparv/.sparv/history/20260115215314/journal.md new file mode 100644 index 0000000..a64f9d0 --- /dev/null +++ b/skills/sparv/.sparv/history/20260115215314/journal.md @@ -0,0 +1,17 @@ +# SPARV Journal +Session: 20260115215314 +Feature: test-feature +Created: 2026-01-15 21:53 + +## Plan + + +## Progress + + +## Findings + + +## 21:53 - Action #2 +- Tool: Write +- Result: test2 diff --git a/skills/sparv/.sparv/history/20260115215314/state.yaml b/skills/sparv/.sparv/history/20260115215314/state.yaml new file mode 100644 index 0000000..ec612e2 --- /dev/null +++ b/skills/sparv/.sparv/history/20260115215314/state.yaml @@ -0,0 +1,9 @@ +session_id: "20260115215314" +feature_name: "test-feature" +current_phase: "specify" +action_count: 2 +consecutive_failures: 0 +max_iterations: 12 +iteration_count: 0 +completion_promise: "" +ehrb_flags: [] diff --git a/skills/sparv/.sparv/history/20260115215722/journal.md b/skills/sparv/.sparv/history/20260115215722/journal.md new file mode 100644 index 0000000..1e3223b --- /dev/null +++ b/skills/sparv/.sparv/history/20260115215722/journal.md @@ -0,0 +1,29 @@ +# SPARV Journal +Session: 20260115215722 +Feature: detailed-test +Created: 2026-01-15 21:57 + +## Plan + + +## Progress + + +## Findings + + +## 21:57 - Action #2 +- Tool: Edit +- Result: file2 + +## Failure Protocol - 2026-01-15 21:57 +- level: 1 +- note: first error + +## Failure Protocol - 2026-01-15 21:57 +- level: 2 +- note: second error + +## Failure Protocol - 2026-01-15 21:58 +- level: 3 +- note: third error diff --git a/skills/sparv/.sparv/history/20260115215722/state.yaml b/skills/sparv/.sparv/history/20260115215722/state.yaml new file mode 100644 index 0000000..4444ddc --- /dev/null +++ b/skills/sparv/.sparv/history/20260115215722/state.yaml @@ -0,0 +1,9 @@ +session_id: "20260115215722" +feature_name: "detailed-test" +current_phase: "specify" +action_count: 2 +consecutive_failures: 0 +max_iterations: 12 +iteration_count: 0 +completion_promise: "" +ehrb_flags: [] diff --git a/skills/sparv/.sparv/history/20260115220023/journal.md b/skills/sparv/.sparv/history/20260115220023/journal.md new file mode 100644 index 0000000..2d8ebc1 --- /dev/null +++ b/skills/sparv/.sparv/history/20260115220023/journal.md @@ -0,0 +1,13 @@ +# SPARV Journal +Session: 20260115220023 +Feature: new-after-archive +Created: 2026-01-15 22:00 + +## Plan + + +## Progress + + +## Findings + diff --git a/skills/sparv/.sparv/history/20260115220023/state.yaml b/skills/sparv/.sparv/history/20260115220023/state.yaml new file mode 100644 index 0000000..ed08703 --- /dev/null +++ b/skills/sparv/.sparv/history/20260115220023/state.yaml @@ -0,0 +1,9 @@ +session_id: "20260115220023" +feature_name: "new-after-archive" +current_phase: "specify" +action_count: 0 +consecutive_failures: 0 +max_iterations: 12 +iteration_count: 0 +completion_promise: "" +ehrb_flags: [] diff --git a/skills/sparv/.sparv/history/index.md b/skills/sparv/.sparv/history/index.md new file mode 100644 index 0000000..e53c5b3 --- /dev/null +++ b/skills/sparv/.sparv/history/index.md @@ -0,0 +1,21 @@ +# History Index + +This file records all completed sessions for traceability. + +--- + +## Index + +| Timestamp | Feature | Type | Status | Path | +|-----------|---------|------|--------|------| + +--- + +## Monthly Archive + + +### 2026-01 + +- `20260115215314` - test-feature +- `20260115215722` - detailed-test +- `20260115220023` - new-after-archive diff --git a/skills/sparv/.sparv/journal.md b/skills/sparv/.sparv/journal.md new file mode 100644 index 0000000..7f2f200 --- /dev/null +++ b/skills/sparv/.sparv/journal.md @@ -0,0 +1,16 @@ +# SPARV Journal +Session: 20260114-211748 +Created: 2026-01-14 21:17 + +## Plan + + +## Progress + + +## Findings + + +## 21:17 - Action #2 +- Tool: Write +- Result: 测试写入 diff --git a/skills/sparv/.sparv/plan/20260115220038/journal.md b/skills/sparv/.sparv/plan/20260115220038/journal.md new file mode 100644 index 0000000..14e0d2f --- /dev/null +++ b/skills/sparv/.sparv/plan/20260115220038/journal.md @@ -0,0 +1,13 @@ +# SPARV Journal +Session: 20260115220038 +Feature: force-test +Created: 2026-01-15 22:00 + +## Plan + + +## Progress + + +## Findings + diff --git a/skills/sparv/.sparv/plan/20260115220038/state.yaml b/skills/sparv/.sparv/plan/20260115220038/state.yaml new file mode 100644 index 0000000..df223d9 --- /dev/null +++ b/skills/sparv/.sparv/plan/20260115220038/state.yaml @@ -0,0 +1,9 @@ +session_id: "20260115220038" +feature_name: "force-test" +current_phase: "specify" +action_count: 0 +consecutive_failures: 0 +max_iterations: 12 +iteration_count: 0 +completion_promise: "" +ehrb_flags: [] diff --git a/skills/sparv/.sparv/state.yaml b/skills/sparv/.sparv/state.yaml new file mode 100644 index 0000000..dbd9a39 --- /dev/null +++ b/skills/sparv/.sparv/state.yaml @@ -0,0 +1,9 @@ +session_id: "20260114-211748" +current_phase: "specify" +action_count: 2 +consecutive_failures: 0 +max_iterations: 12 +iteration_count: 0 +completion_promise: "" +feature_name: "" +ehrb_flags: [] diff --git a/skills/sparv/README.md b/skills/sparv/README.md new file mode 100644 index 0000000..c5d65b5 --- /dev/null +++ b/skills/sparv/README.md @@ -0,0 +1,96 @@ +# SPARV - Unified Development Workflow (Simplified) + +[![Skill Version](https://img.shields.io/badge/version-1.0.0-blue.svg)]() +[![Claude Code](https://img.shields.io/badge/Claude%20Code-Compatible-green.svg)]() + +**SPARV** is an end-to-end development workflow: maximize delivery quality with minimal rules while avoiding "infinite iteration + self-rationalization." + +``` +S-Specify → P-Plan → A-Act → R-Review → V-Vault + Clarify Plan Execute Review Archive +``` + +## Key Changes (Over-engineering Removed) + +- External memory merged from 3 files into 1 `.sparv/journal.md` +- Specify scoring simplified from 100-point to 10-point scale (threshold `>=9`) +- Reboot Test reduced from 5 questions to 3 questions +- Removed concurrency locks (Claude is single-threaded; locks only cause failures) + +## Installation + +SPARV is installed at `~/.claude/skills/sparv/`. + +Install from ZIP: + +```bash +unzip sparv.zip -d ~/.claude/skills/ +``` + +## Quick Start + +Run in project root: + +```bash +~/.claude/skills/sparv/scripts/init-session.sh --force +``` + +Creates: + +``` +.sparv/ +├── state.yaml +├── journal.md +└── history/ +``` + +## External Memory System (Two Files) + +- `state.yaml`: State (minimum fields: `session_id/current_phase/action_count/consecutive_failures`) +- `journal.md`: Unified log (Plan/Progress/Findings all go here) + +After archiving: + +``` +.sparv/history// +├── state.yaml +└── journal.md +``` + +## Key Numbers + +| Number | Meaning | +|--------|---------| +| **9/10** | Specify score passing threshold | +| **2** | Write to journal every 2 tool calls | +| **3** | Failure retry limit / Review fix limit | +| **3** | Reboot Test question count | +| **12** | Default max iterations (optional safety valve) | + +## Script Tools + +```bash +~/.claude/skills/sparv/scripts/init-session.sh --force +~/.claude/skills/sparv/scripts/save-progress.sh "Edit" "done" +~/.claude/skills/sparv/scripts/check-ehrb.sh --diff --fail-on-flags +~/.claude/skills/sparv/scripts/failure-tracker.sh fail --note "tests are flaky" +~/.claude/skills/sparv/scripts/reboot-test.sh --strict +~/.claude/skills/sparv/scripts/archive-session.sh +``` + +## Hooks + +Hooks defined in `hooks/hooks.json`: + +- PostToolUse: 2-Action auto-write to `journal.md` +- PreToolUse: EHRB risk prompt (default dry-run) +- Stop: 3-question reboot test (strict) + +## References + +- `SKILL.md`: Skill definition (for agent use) +- `references/methodology.md`: Methodology quick reference + +--- + +*Quality over speed—iterate until truly complete.* diff --git a/skills/sparv/SKILL.md b/skills/sparv/SKILL.md new file mode 100644 index 0000000..81719e9 --- /dev/null +++ b/skills/sparv/SKILL.md @@ -0,0 +1,103 @@ +--- +name: sparv +description: Minimal SPARV workflow (Specify→Plan→Act→Review→Vault) with 10-point spec gate, unified journal, 2-action saves, 3-failure protocol, and EHRB risk detection. +--- + +# SPARV + +Five-phase workflow: **S**pecify → **P**lan → **A**ct → **R**eview → **V**ault. + +Goal: Complete "requirements → verifiable delivery" in one pass, recording key decisions in external memory instead of relying on assumptions. + +## Core Rules (Mandatory) + +- **10-Point Specify Gate**: Spec score `0-10`; must be `>=9` to enter Plan. +- **2-Action Save**: Append an entry to `.sparv/journal.md` every 2 tool calls. +- **3-Failure Protocol**: Stop and escalate to user after 3 consecutive failures. +- **EHRB**: Require explicit user confirmation when high-risk detected (production/sensitive data/destructive/billing API/security-critical). +- **Fixed Phase Names**: `specify|plan|act|review|vault` (stored in `.sparv/state.yaml:current_phase`). + +## External Memory (Two Files) + +Initialize (run in project root): + +```bash +~/.claude/skills/sparv/scripts/init-session.sh --force +``` + +File conventions: + +- `.sparv/state.yaml`: State machine (minimum fields: `session_id/current_phase/action_count/consecutive_failures`) +- `.sparv/journal.md`: Unified log (Plan/Progress/Findings all go here) +- `.sparv/history//`: Archive directory + +## Phase 1: Specify (10-Point Scale) + +Each item scores 0/1/2, total 0-10: + +1) **Value**: Why do it, are benefits/metrics verifiable +2) **Scope**: MVP + what's out of scope +3) **Acceptance**: Testable acceptance criteria +4) **Boundaries**: Error/performance/compatibility/security critical boundaries +5) **Risk**: EHRB/dependencies/unknowns + handling approach + +`score < 9`: Keep asking questions; do not enter Plan. +`score >= 9`: Write a clear `completion_promise` (verifiable completion commitment), then enter Plan. + +## Phase 2: Plan + +- Break into atomic tasks (2-5 minute granularity), each with a verifiable output/test point. +- Write the plan to `.sparv/journal.md` (Plan section or append directly). + +## Phase 3: Act + +- **TDD Rule**: No failing test → no production code. +- Auto-write journal every 2 actions (PostToolUse hook). +- Failure counting (3-Failure Protocol): + +```bash +~/.claude/skills/sparv/scripts/failure-tracker.sh fail --note "short blocker" +~/.claude/skills/sparv/scripts/failure-tracker.sh reset +``` + +## Phase 4: Review + +- Two stages: Spec conformance → Code quality (correctness/performance/security/tests). +- Maximum 3 fix rounds; escalate to user if exceeded. + +Run 3-question reboot test before session ends: + +```bash +~/.claude/skills/sparv/scripts/reboot-test.sh --strict +``` + +## Phase 5: Vault + +Archive current session: + +```bash +~/.claude/skills/sparv/scripts/archive-session.sh +``` + +## Script Tools + +| Script | Purpose | +|--------|---------| +| `scripts/init-session.sh` | Initialize `.sparv/`, generate `state.yaml` + `journal.md` | +| `scripts/save-progress.sh` | Maintain `action_count`, append to `journal.md` every 2 actions | +| `scripts/check-ehrb.sh` | Scan diff/text, output (optionally write) `ehrb_flags` | +| `scripts/failure-tracker.sh` | Maintain `consecutive_failures`, exit code 3 when reaching 3 | +| `scripts/reboot-test.sh` | 3-question self-check (optional strict mode) | +| `scripts/archive-session.sh` | Archive `journal.md` + `state.yaml` to `history/` | + +## Auto Hooks + +`hooks/hooks.json`: + +- PostToolUse: `save-progress.sh` (2-Action save) +- PreToolUse: `check-ehrb.sh --diff --dry-run` (prompt only, no state write) +- Stop: `reboot-test.sh --strict` (3-question self-check) + +--- + +*Quality over speed—iterate until truly complete.* diff --git a/skills/sparv/hooks/hooks.json b/skills/sparv/hooks/hooks.json new file mode 100644 index 0000000..0bffe88 --- /dev/null +++ b/skills/sparv/hooks/hooks.json @@ -0,0 +1,37 @@ +{ + "description": "SPARV auto-hooks for 2-Action save, EHRB detection, and 3-Question reboot test", + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write|Bash|Read|Glob|Grep", + "hooks": [ + { + "type": "command", + "command": "[ -f .sparv/state.yaml ] && ${SKILL_PATH}/scripts/save-progress.sh \"${TOOL_NAME:-unknown}\" \"completed\" 2>/dev/null || true" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "[ -f .sparv/state.yaml ] && ${SKILL_PATH}/scripts/check-ehrb.sh --diff --dry-run 2>/dev/null || true" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "[ -f .sparv/state.yaml ] && ${SKILL_PATH}/scripts/reboot-test.sh --strict 2>/dev/null || true" + } + ] + } + ] + } +} diff --git a/skills/sparv/references/methodology.md b/skills/sparv/references/methodology.md new file mode 100644 index 0000000..4654bab --- /dev/null +++ b/skills/sparv/references/methodology.md @@ -0,0 +1,96 @@ +# SPARV Methodology (Short) + +This document is a quick reference; the canonical spec is in `SKILL.md`. + +## Five Phases + +- **Specify**: Write requirements as verifiable specs (10-point gate) +- **Plan**: Break into atomic tasks (2-5 minute granularity) +- **Act**: TDD-driven implementation; write to journal every 2 actions +- **Review**: Spec conformance → Code quality; maximum 3 fix rounds +- **Vault**: Archive session (state + journal) + +## Specify (10-Point Scale) + +Each item scores 0/1/2, total 0-10; `>=9` required to enter Plan: + +1) Value: Why do it, are benefits/metrics verifiable +2) Scope: MVP + what's out of scope +3) Acceptance: Testable acceptance criteria +4) Boundaries: Error/performance/compatibility/security critical boundaries +5) Risk: EHRB/dependencies/unknowns + handling approach + +If below threshold, keep asking—don't "just start coding." + +## Journal Convention (Unified Log) + +All Plan/Progress/Findings go into `.sparv/journal.md`. + +Recommended format (just append, no need to "insert into specific sections"): + +```markdown +## 14:32 - Action #12 +- Tool: Edit +- Result: Updated auth flow +- Next: Add test for invalid token +``` + +## 2-Action Save + +Hook triggers `save-progress.sh` after each tool call; script only writes to journal when `action_count` is even. + +## 3-Failure Protocol + +When you fail consecutively, escalate by level: + +1. Diagnose and fix (read errors, verify assumptions, minimal fix) +2. Alternative approach (change strategy/entry point) +3. Escalate (stop: document blocker + attempted solutions + request user decision) + +Tools: + +```bash +~/.claude/skills/sparv/scripts/failure-tracker.sh fail --note "short reason" +~/.claude/skills/sparv/scripts/failure-tracker.sh reset +``` + +## 3-Question Reboot Test + +Self-check before session ends (or when lost): + +1) Where am I? (current_phase) +2) Where am I going? (next_phase) +3) How do I prove completion? (completion_promise + evidence at journal end) + +```bash +~/.claude/skills/sparv/scripts/reboot-test.sh --strict +``` + +## EHRB (High-Risk Changes) + +Detection items (any match requires explicit user confirmation): + +- Production access +- Sensitive data +- Destructive operations +- Billing external API +- Security-critical changes + +```bash +~/.claude/skills/sparv/scripts/check-ehrb.sh --diff --fail-on-flags +``` + +## state.yaml (Minimal Schema) + +Scripts only enforce 4 core fields; other fields are optional: + +```yaml +session_id: "20260114-143022" +current_phase: "act" +action_count: 14 +consecutive_failures: 0 +max_iterations: 12 +iteration_count: 0 +completion_promise: "All acceptance criteria have tests and are green." +ehrb_flags: [] +``` diff --git a/skills/sparv/scripts/archive-session.sh b/skills/sparv/scripts/archive-session.sh new file mode 100755 index 0000000..e53e27a --- /dev/null +++ b/skills/sparv/scripts/archive-session.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# SPARV Session Archive Script +# Archives completed session from .sparv/plan// to .sparv/history// + +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// to .sparv/history// +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" diff --git a/skills/sparv/scripts/check-ehrb.sh b/skills/sparv/scripts/check-ehrb.sh new file mode 100755 index 0000000..3a3d310 --- /dev/null +++ b/skills/sparv/scripts/check-ehrb.sh @@ -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 diff --git a/skills/sparv/scripts/failure-tracker.sh b/skills/sparv/scripts/failure-tracker.sh new file mode 100755 index 0000000..487843e --- /dev/null +++ b/skills/sparv/scripts/failure-tracker.sh @@ -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 [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// +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 diff --git a/skills/sparv/scripts/init-session.sh b/skills/sparv/scripts/init-session.sh new file mode 100755 index 0000000..39eaf07 --- /dev/null +++ b/skills/sparv/scripts/init-session.sh @@ -0,0 +1,205 @@ +#!/bin/bash +# SPARV Session Initialization +# Creates .sparv/plan// 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// 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 + + +## Progress + + +## Findings + +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" diff --git a/skills/sparv/scripts/lib/state-lock.sh b/skills/sparv/scripts/lib/state-lock.sh new file mode 100755 index 0000000..e93368c --- /dev/null +++ b/skills/sparv/scripts/lib/state-lock.sh @@ -0,0 +1,143 @@ +#!/bin/bash +# +# Shared helpers for .sparv state operations. +# Supports new directory structure: .sparv/plan// + +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 +} diff --git a/skills/sparv/scripts/reboot-test.sh b/skills/sparv/scripts/reboot-test.sh new file mode 100755 index 0000000..28a9c28 --- /dev/null +++ b/skills/sparv/scripts/reboot-test.sh @@ -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// +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 diff --git a/skills/sparv/scripts/save-progress.sh b/skills/sparv/scripts/save-progress.sh new file mode 100755 index 0000000..56c72b7 --- /dev/null +++ b/skills/sparv/scripts/save-progress.sh @@ -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// +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"