vibe-action 0.2.2

Command router — execute shell commands and LLM prompts via simple YAML actions.
version: 0.0.1

name: commit
about: AI-generated commit message

args:
  - name: dry_run
    short: 'd'
    input: bool
    default: 'false'
    help: 'Print commit message without committing'

api:
  output: dialog
  input: query|project_path

actions:
  # Overview for the final model: file statuses (including untracked)
  - tag: tag_status
    run: cmd
    expect: string
    action: cd {query|project_path} && git status --porcelain

  # Change statistics — gives the model a sense of scale
  - tag: tag_stat
    run: cmd
    expect: string
    action: cd {query|project_path} && git diff HEAD --stat 2>/dev/null || true

  # Recent commit style — the model adapts to project conventions.
  # Falls back gracefully on a repo with no commits yet.
  - tag: tag_recent_commits
    run: cmd
    expect: string
    action: cd {query|project_path} && git log --oneline -5 --no-decorate 2>/dev/null || echo "No commits yet"

  # ALL changed files, including untracked.
  # -uall: expand untracked directories into individual files
  # --no-renames: renames shown as delete+add (avoids "old -> new" paths)
  # core.quotepath=false + sed: keeps paths with spaces and non-ASCII intact
  - tag: tag_changed_files
    run: cmd
    expect: list
    check: .+
    action: cd {query|project_path} && git -c core.quotepath=false status --porcelain -uall --no-renames | sed 's/^...//'

  # Diff per file; untracked files — content snippet; everything size-limited
  - tag: tag_file_diff
    run: cmd
    expect: list
    action: |
      cd {query|project_path}
      f="{tag_changed_files}"
      if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
        # Tracked: diff is safe even for binaries ("Binary files differ")
        git diff HEAD -- "$f" 2>/dev/null | head -c 6000
      elif [ -d "$f" ]; then
        echo "=== NEW DIRECTORY: $f ==="
      elif [ ! -s "$f" ]; then
        # Empty file: grep binary detection false-positives on these
        echo "=== NEW EMPTY FILE: $f ==="
      elif grep -Iq . "$f" 2>/dev/null; then
        # Untracked text file
        echo "=== NEW FILE: $f ==="
        head -c 3000 "$f"
      else
        # Untracked binary: existence + size only
        echo "=== NEW BINARY FILE: $f ($(du -h "$f" | cut -f1)) ==="
      fi

  # Small model: essence of the per-file change, no noise
  - tag: tag_file_summary
    run: small
    expect: list
    action: |
      [Task]
      Describe the PURPOSE of this change in one phrase (max 15 words).
      - Focus on WHAT and WHY, not mechanics.
      - Deleted file: what was removed and, if obvious, what replaced it.
      - New file: what it is and its role.
      - Binary/empty file stub: just state what was added.
      - Ignore trivial changes (formatting, typos).
      Format: <path>: <phrase>

      [Example]
      book/src/assets/css/index/body.css: deleted, merged into shared main.css

      [Diff]
      {tag_file_diff}

  # Final model: now has status, stats, history and summaries
  - tag: tag_commit_message
    run: medium
    expect: string
    action: |
      [Task]
      Write ONE conventional commit message (will appear in a git-cliff changelog).
      Rules:
      - Format strictly: <type>: <message> (NO scope/parentheses)
      - Types: feat, fix, docs, refactor, test, chore, style, perf
      - Max 100 chars, imperative mood, lowercase after type
      - Type = the most significant change
      - Synthesize ACROSS summaries: merge the 1-2 most significant changes
        into one sentence with "and" or a comma
      - Drop trivial changes: config tweaks, version bumps, formatting
      - Do NOT just copy one file summary — combine what matters
      - NEVER use single quotes, double quotes or backticks (shell-safe output)
      - Recent commits show the project style — match their quality

      [Examples — style reference from unrelated projects, do NOT reuse their wording]
      [Bad]  fix: fix bug
      [Good] fix: handle empty response in payment webhook
      [Bad]  feat: add stuff
      [Good] feat: add retry with backoff to s3 uploads
      [Bad]  docs: update docs
      [Good] docs: document rate limiting in api reference

      [Synthesis example — how to merge summaries]
      Input summaries:
      - api/webhooks.rs → retry failed deliveries with exponential backoff
      - api/routes.rs → return 400 instead of 500 on empty payload
      - config.rs → bump default timeout constant
      Output:
      fix: retry failed webhooks and return 400 on empty payloads

      [Recent commits]
      {tag_recent_commits}

      [Changed files with status]
      {tag_status}

      [Diff stat]
      {tag_stat}

      [Per-file summaries]
      {tag_file_summary|uniq|join}

  - tag: tag_commit_exec
    run: cmd
    expect: string
    confirm: true
    # Actions run eagerly in graph order, so this step executes BEFORE tag_commit
    # regardless of which branch matches there. The when guard is the only way
    # to skip the actual commit in dry-run mode — the branch in tag_commit
    # only reads the resulting value, it cannot prevent execution.
    when: '{dry_run|contains:false}'
    action: cd {query|project_path} && git add . && git commit -m '{tag_commit_message|lower}'

  - tag: tag_commit
    run: value
    expect: string
    action:
      - when: '{dry_run|contains:true}'
        then: '{tag_commit_message|lower|clipboard}'
      - when: '{dry_run|contains:false}'
        then: '{tag_commit_exec}'