# llms-full (private-aware)
> Built from GitHub files and website pages. Large files may be truncated.

--- .config/scripts/build_install_oraclepack.md ---
```bash
# path: scripts/build_install_oraclepack.sh
#!/usr/bin/env bash
set -euo pipefail

# Builds oraclepack for:
# - WSL/Linux (oraclepack)
# - Windows amd64 (oraclepack.exe)
# Then installs:
# - WSL binary -> ~/.local/bin/
# - Windows exe -> /mnt/c/Users/<winuser>/.local/bin/
# And writes a Git Bash wrapper:
# - /mnt/c/Users/<winuser>/bin/oraclepack  (calls WSL binary via wsl.exe)

usage() {
  cat <<'USAGE'
Usage:
  scripts/build_install_oraclepack.sh [options]

Options:
  --repo-root <path>      Repo root (default: git toplevel, else current dir)
  --win-user <name>       Windows username (default: auto-detect, else "user")
  --wsl-distro <name>     WSL distro name for wsl.exe -d (default: $WSL_DISTRO_NAME or "Ubuntu-24.04")
  --wsl-user <name>       WSL username for wsl.exe -u (default: current user)
  --no-linux              Skip building/installing Linux binary
  --no-windows            Skip building/installing Windows .exe
  --no-wrapper            Skip writing the Git Bash wrapper script
  --cgo <0|1>             Set CGO_ENABLED (default: leave unchanged)

Examples:
  scripts/build_install_oraclepack.sh
  scripts/build_install_oraclepack.sh --win-user Alice --wsl-distro Ubuntu-24.04
USAGE
}

die() { echo "error: $*" >&2; exit 1; }

have() { command -v "$1" >/dev/null 2>&1; }

detect_repo_root() {
  if have git && git rev-parse --show-toplevel >/dev/null 2>&1; then
    git rev-parse --show-toplevel
  else
    pwd
  fi
}

detect_win_user() {
  # Best-effort: ask Windows for %USERNAME% via cmd.exe (works in most WSL setups).
  if have cmd.exe; then
    local u
    u="$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r' | tail -n 1 || true)"
    if [[ -n "${u:-}" && "${u:-}" != "%USERNAME%" ]]; then
      echo "$u"
      return
    fi
  fi
  echo "user"
}

REPO_ROOT=""
WIN_USER=""
WSL_DISTRO="${WSL_DISTRO_NAME:-Ubuntu-24.04}"
WSL_USER="$(whoami)"
DO_LINUX=1
DO_WINDOWS=1
DO_WRAPPER=1
CGO=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --repo-root)  REPO_ROOT="${2:-}"; shift 2;;
    --win-user)   WIN_USER="${2:-}"; shift 2;;
    --wsl-distro) WSL_DISTRO="${2:-}"; shift 2;;
    --wsl-user)   WSL_USER="${2:-}"; shift 2;;
    --no-linux)   DO_LINUX=0; shift;;
    --no-windows) DO_WINDOWS=0; shift;;
    --no-wrapper) DO_WRAPPER=0; shift;;
    --cgo)        CGO="${2:-}"; shift 2;;
    -h|--help)    usage; exit 0;;
    *)            die "unknown option: $1 (use --help)";;
  esac
done

[[ -n "$REPO_ROOT" ]] || REPO_ROOT="$(detect_repo_root)"
[[ -d "$REPO_ROOT" ]] || die "repo root not found: $REPO_ROOT"

[[ -n "$WIN_USER" ]] || WIN_USER="$(detect_win_user)"

have go || die "go not found in PATH"

# Paths
LINUX_OUT="$REPO_ROOT/oraclepack"
WIN_OUT="$REPO_ROOT/oraclepack.exe"

LINUX_INSTALL_DIR="$HOME/.local/bin"
LINUX_INSTALL_PATH="$LINUX_INSTALL_DIR/oraclepack"

WIN_HOME="/mnt/c/Users/$WIN_USER"
WIN_LOCAL_BIN_DIR="$WIN_HOME/.local/bin"
WIN_LOCAL_BIN_PATH="$WIN_LOCAL_BIN_DIR/oraclepack.exe"

WIN_GITBASH_BIN_DIR="$WIN_HOME/bin"
WIN_GITBASH_WRAPPER_PATH="$WIN_GITBASH_BIN_DIR/oraclepack"

# Optional CGO toggle
if [[ -n "$CGO" ]]; then
  [[ "$CGO" == "0" || "$CGO" == "1" ]] || die "--cgo must be 0 or 1"
  export CGO_ENABLED="$CGO"
fi

cd "$REPO_ROOT"

# Build binaries
if [[ "$DO_LINUX" -eq 1 ]]; then
  echo "==> Building Linux (WSL) binary: $LINUX_OUT"
  go build -o "$LINUX_OUT" ./cmd/oraclepack
fi

if [[ "$DO_WINDOWS" -eq 1 ]]; then
  echo "==> Building Windows amd64 exe: $WIN_OUT"
  GOOS=windows GOARCH=amd64 go build -o "$WIN_OUT" ./cmd/oraclepack
fi

# Install binaries
if [[ "$DO_LINUX" -eq 1 ]]; then
  echo "==> Installing Linux binary -> $LINUX_INSTALL_PATH"
  mkdir -p "$LINUX_INSTALL_DIR"
  cp -f "$LINUX_OUT" "$LINUX_INSTALL_PATH"
fi

if [[ "$DO_WINDOWS" -eq 1 ]]; then
  echo "==> Installing Windows exe -> $WIN_LOCAL_BIN_PATH"
  mkdir -p "$WIN_LOCAL_BIN_DIR"
  cp -f "$WIN_OUT" "$WIN_LOCAL_BIN_PATH"
fi

# Write Git Bash wrapper (stored on Windows filesystem)
if [[ "$DO_WRAPPER" -eq 1 ]]; then
  echo "==> Writing Git Bash wrapper -> $WIN_GITBASH_WRAPPER_PATH"
  mkdir -p "$WIN_GITBASH_BIN_DIR"

  cat > "$WIN_GITBASH_WRAPPER_PATH" <<EOF
#!/usr/bin/env bash
set -euo pipefail

# Git for Windows (Git Bash) path-conversion off for this exec call.
# Required so /home/... is not rewritten into C:/Program Files/Git/...
MSYS_NO_PATHCONV=1 exec wsl.exe -d ${WSL_DISTRO@Q} -u ${WSL_USER@Q} -- ${LINUX_INSTALL_PATH@Q} "\$@"
EOF

  # Ensure LF line endings (in case editor/tool wrote CRLF) and best-effort executable bit.
  sed -i 's/\r$//' "$WIN_GITBASH_WRAPPER_PATH" || true
  chmod +x "$WIN_GITBASH_WRAPPER_PATH" 2>/dev/null || true

  echo "==> Note: In Git Bash, ensure ~/bin is in PATH (so 'oraclepack' resolves to this wrapper)."
fi

echo "==> Done."
echo "    WSL binary:     $LINUX_INSTALL_PATH"
echo "    Windows exe:    $WIN_LOCAL_BIN_PATH"
echo "    Git Bash wrap:  $WIN_GITBASH_WRAPPER_PATH"
```

Run (from repo root in WSL):

```bash
chmod +x scripts/build_install_oraclepack.sh
scripts/build_install_oraclepack.sh
```

Optional example:

```bash
scripts/build_install_oraclepack.sh --win-user user --wsl-distro Ubuntu-24.04 --wsl-user user
```


--- .cursor/commands/tm/install-taskmaster.md ---
Check if Task Master is installed and install it if needed.

This command helps you get Task Master set up globally on your system.

## Detection and Installation Process

1. **Check Current Installation**
   ```bash
   # Check if task-master command exists
   which task-master || echo "Task Master not found"

   # Check npm global packages
   npm list -g task-master-ai
   ```

2. **System Requirements Check**
   ```bash
   # Verify Node.js is installed
   node --version

   # Verify npm is installed
   npm --version

   # Check Node version (need 16+)
   ```

3. **Install Task Master Globally**
   If not installed, run:
   ```bash
   npm install -g task-master-ai
   ```

4. **Verify Installation**
   ```bash
   # Check version
   task-master --version

   # Verify command is available
   which task-master
   ```

5. **Initial Setup**
   ```bash
   # Initialize in current directory
   task-master init
   ```

6. **Configure AI Provider**
   Ensure you have at least one AI provider API key set:
   ```bash
   # Check current configuration
   task-master models --status

   # If no API keys found, guide setup
   echo "You'll need at least one API key:"
   echo "- ANTHROPIC_API_KEY for Claude"
   echo "- OPENAI_API_KEY for GPT models"
   echo "- PERPLEXITY_API_KEY for research"
   echo ""
   echo "Set them in your shell profile or .env file"
   ```

7. **Quick Test**
   ```bash
   # Create a test PRD
   echo "Build a simple hello world API" > test-prd.txt

   # Try parsing it
   task-master parse-prd test-prd.txt -n 3
   ```

## Troubleshooting

If installation fails:

**Permission Errors:**
```bash
# Try with sudo (macOS/Linux)
sudo npm install -g task-master-ai

# Or fix npm permissions
npm config set prefix ~/.npm-global
export PATH=~/.npm-global/bin:$PATH
```

**Network Issues:**
```bash
# Use different registry
npm install -g task-master-ai --registry https://registry.npmjs.org/
```

**Node Version Issues:**
```bash
# Install Node 20+ via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 20
nvm use 20
```

## Success Confirmation

Once installed, you should see:
```
✅ Task Master installed
✅ Command 'task-master' available globally
✅ AI provider configured
✅ Ready to use slash commands!

Try: /taskmaster:init your-prd.md
```

## Next Steps

After installation:
1. Run `/taskmaster:status` to verify setup
2. Configure AI providers with `/taskmaster:setup-models`
3. Start using Task Master commands!

--- .cursor/commands/tm/quick-install-taskmaster.md ---
Quick install Task Master globally if not already installed.

Execute this streamlined installation:

```bash
# Check and install in one command
task-master --version 2>/dev/null || npm install -g task-master-ai

# Verify installation
task-master --version

# Quick setup check
task-master models --status || echo "Note: You'll need to set up an AI provider API key"
```

If you see "command not found" after installation, you may need to:
1. Restart your terminal
2. Or add npm global bin to PATH: `export PATH=$(npm bin -g):$PATH`

Once installed, you can use all the Task Master commands!

Quick test: Run `/taskmaster:help` to see all available commands.

--- .config/mcp/oraclepack-taskify/references/workflow-overview.md ---
# Stage 3 (oraclepack-taskify) — Workflow overview

## What this stage solves

Stage 1 produces a 20-question oracle pack.
Stage 2 runs oraclepack and produces 20 answer files.

Stage 2 outputs are answers, not work. Stage 3 creates the deterministic bridge from answers to executable planning artifacts and (by default) starts a guarded autopilot to begin implementation.

## Inputs

- A completed oraclepack output directory containing exactly:
  - `01-*.md` … `20-*.md` (one file per prefix)
- Optional additional files to improve synthesis fidelity (extra attachments)

## Primary output (this skill generates)

- An “Action Pack” markdown file at:
  - default: `docs/oracle-actions-pack-YYYY-MM-DD.md`
  - override: `pack_path=...`

The Action Pack is designed to be executed as a deterministic pipeline.

## Artifacts the Action Pack produces when executed

- Canonical actions:
  - `<out_dir>/_actions.json` (machine-consumable)
  - `<out_dir>/_actions.md` (human summary)
- PRD/spec suitable for Task Master:
  - `.taskmaster/docs/oracle-actions-prd.md`
- Task Master outputs:
  - tasks created/expanded by `task-master`
  - complexity report: `<out_dir>/tm-complexity.json`
- Optional:
  - pipelines doc: `docs/oracle-actions-pipelines.md` (pipelines mode)
  - autopilot entrypoint + state file (autopilot mode, default)

## Execution modes

- backlog: actions → PRD → tasks
- pipelines: backlog + pipelines generation
- autopilot (default): backlog + guarded autopilot entrypoint


--- docs/strategist-questions-oracle-pack-2026-01-02.md ---
Output file: docs/strategist-questions-oracle-pack-2026-01-02.md
<!-- generated_at: 2026-01-02T00:00:00Z -->

# oracle strategist question pack
<!-- generated_at: 2026-01-02T00:00:00Z -->

---

## parsed args

- codebase_name: Unknown
- constraints: None
- non_goals: None
- team_size: Unknown
- deadline: Unknown
- out_dir: docs/oracle/strategist-questions/2026-01-02
- oracle_cmd: oracle
- oracle_flags: --files-report
- extra_files: 

---

## commands (exactly 20; sorted by ROI desc; ties by lower effort)

```bash
out_dir="docs/oracle/strategist-questions/2026-01-02"
mkdir -p "$out_dir"

# 01) ROI=4.5 impact=0.9 confidence=0.9 effort=0.2 horizon=Strategic category=permissions reference=internal/exec/sanitize.go:Sanitize
oracle --files-report --write-output "$out_dir/01-permissions-sanitize.md" -p "Strategist question #01
Reference: internal/exec/sanitize.go:Sanitize
Category: permissions
Horizon: Strategic
ROI: 4.5 (impact=0.9, confidence=0.9, effort=0.2)
Question: Are there risks of prompt injection or data exfiltration in the sanitization logic?
Rationale: Sanitization is the primary defense against malicious inputs in CLI tools that process external content; weaknesses here compromise the entire execution model.
Smallest experiment today: Review the regex patterns or replacement logic in sanitize.go for known bypass vectors.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/exec/sanitize.go" -f "internal/exec/inject.go" 

# 02) ROI=4.0 impact=0.8 confidence=0.8 effort=0.2 horizon=Immediate category=invariants reference=internal/exec/oracle_validate.go:Validate
oracle --files-report --write-output "$out_dir/02-invariants-validate.md" -p "Strategist question #02
Reference: internal/exec/oracle_validate.go:Validate
Category: invariants
Horizon: Immediate
ROI: 4.0 (impact=0.8, confidence=0.8, effort=0.2)
Question: What validation rules are strictly enforced before execution, and can they be bypassed?
Rationale: Ensuring that input parameters meet strict invariants prevents runtime failures and undefined behavior during the execution phase.
Smallest experiment today: Trace the validation calls in oracle_validate.go to see if they block execution or just warn.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/exec/oracle_validate.go" -f "internal/exec/runner.go" 

# 03) ROI=3.5 impact=0.7 confidence=0.8 effort=0.2 horizon=Immediate category=failure modes reference=internal/exec/runner.go:Run
oracle --files-report --write-output "$out_dir/03-failure-modes-runner.md" -p "Strategist question #03
Reference: internal/exec/runner.go:Run
Category: failure modes
Horizon: Immediate
ROI: 3.5 (impact=0.7, confidence=0.8, effort=0.2)
Question: How does the runner handle hanging processes or execution timeouts?
Rationale: CLI tools must fail gracefully; indefinite hangs are a poor user experience and can lock up resources.
Smallest experiment today: Check runner.go for context.WithTimeout usage or signal handling.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/exec/runner.go" -f "internal/cli/run.go" 

# 04) ROI=3.5 impact=0.7 confidence=0.8 effort=0.2 horizon=Strategic category=caching/state reference=internal/state/persist.go:Save
oracle --files-report --write-output "$out_dir/04-caching-state-persist.md" -p "Strategist question #04
Reference: internal/state/persist.go:Save
Category: caching/state
Horizon: Strategic
ROI: 3.5 (impact=0.7, confidence=0.8, effort=0.2)
Question: How robust is state persistence against crashes or write interruptions?
Rationale: Corrupted state files can render the tool unusable across sessions; atomic writes or backups are standard mitigation.
Smallest experiment today: Review persist.go to see if temp files and atomic renames are used during save.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/state/persist.go" -f "internal/state/types.go" 

# 05) ROI=3.2 impact=0.8 confidence=0.8 effort=0.2 horizon=Immediate category=contracts/interfaces reference=internal/pack/parser.go:Parse
oracle --files-report --write-output "$out_dir/05-contracts-parser.md" -p "Strategist question #05
Reference: internal/pack/parser.go:Parse
Category: contracts/interfaces
Horizon: Immediate
ROI: 3.2 (impact=0.8, confidence=0.8, effort=0.2)
Question: How does the parser handle malformed or partial inputs?
Rationale: The parser is the entry point for data; its robustness defines the tool's tolerance for user error.
Smallest experiment today: Inspect parser.go error handling logic for invalid syntax.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/pack/parser.go" -f "internal/pack/types.go" 

# 06) ROI=3.0 impact=0.6 confidence=0.8 effort=0.2 horizon=Immediate category=observability reference=internal/report/generate.go:Generate
oracle --files-report --write-output "$out_dir/06-observability-report.md" -p "Strategist question #06
Reference: internal/report/generate.go:Generate
Category: observability
Horizon: Immediate
ROI: 3.0 (impact=0.6, confidence=0.8, effort=0.2)
Question: Does the generated report capture stderr and stdout separately for debugging?
Rationale: When external commands fail, users need precise stream separation to diagnose the issue.
Smallest experiment today: Check generate.go structs to see if output streams are stored in distinct fields.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/report/generate.go" -f "internal/report/types.go" 

# 07) ROI=2.8 impact=0.7 confidence=0.8 effort=0.2 horizon=Strategic category=migrations reference=internal/state/types.go:State
oracle --files-report --write-output "$out_dir/07-migrations-state-schema.md" -p "Strategist question #07
Reference: internal/state/types.go:State
Category: migrations
Horizon: Strategic
ROI: 2.8 (impact=0.7, confidence=0.8, effort=0.2)
Question: How do we handle schema changes in the persisted state file?
Rationale: As the tool evolves, the state format will change; backward compatibility or migration logic is essential to prevent breaking user workflows.
Smallest experiment today: Check types.go or persist.go for versioning fields or migration functions.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/state/types.go" -f "internal/state/persist.go" 

# 08) ROI=2.5 impact=0.5 confidence=0.8 effort=0.2 horizon=Immediate category=UX flows reference=internal/tui/tui.go:Run
oracle --files-report --write-output "$out_dir/08-ux-flows-tui-perf.md" -p "Strategist question #08
Reference: internal/tui/tui.go:Run
Category: UX flows
Horizon: Immediate
ROI: 2.5 (impact=0.5, confidence=0.8, effort=0.2)
Question: How is TUI performance maintained when processing large outputs?
Rationale: TUI tools often freeze if the main thread is blocked by heavy rendering or data processing.
Smallest experiment today: Look for separate goroutines or buffering in tui.go.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/tui/tui.go" -f "internal/tui/preview_test.go" 

# 09) ROI=2.5 impact=0.5 confidence=0.8 effort=0.2 horizon=Immediate category=feature flags reference=internal/exec/flags.go:Parse
oracle --files-report --write-output "$out_dir/09-feature-flags-override.md" -p "Strategist question #09
Reference: internal/exec/flags.go:Parse
Category: feature flags
Horizon: Immediate
ROI: 2.5 (impact=0.5, confidence=0.8, effort=0.2)
Question: How do command-line flags override configuration file settings?
Rationale: Clear precedence rules are critical for debugging and for users who need to temporarily override defaults.
Smallest experiment today: Check the merge logic in flags.go or overrides/merge.go.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/exec/flags.go" -f "internal/overrides/merge.go" 

# 10) ROI=2.4 impact=0.6 confidence=0.8 effort=0.2 horizon=Strategic category=failure modes reference=internal/errors/errors.go:Error
oracle --files-report --write-output "$out_dir/10-failure-modes-errors.md" -p "Strategist question #10
Reference: internal/errors/errors.go:Error
Category: failure modes
Horizon: Strategic
ROI: 2.4 (impact=0.6, confidence=0.8, effort=0.2)
Question: Are errors structured for machine parsing or primarily for human reading?
Rationale: Structured errors allow for better automation and integration with other tools.
Smallest experiment today: Review error definitions in errors.go for JSON tags or error codes.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/errors/errors.go" 

# 11) ROI=2.4 impact=0.6 confidence=0.8 effort=0.2 horizon=Immediate category=contracts/interfaces reference=internal/cli/root.go:Execute
oracle --files-report --write-output "$out_dir/11-contracts-cli-flags.md" -p "Strategist question #11
Reference: internal/cli/root.go:Execute
Category: contracts/interfaces
Horizon: Immediate
ROI: 2.4 (impact=0.6, confidence=0.8, effort=0.2)
Question: How are global flags propagated to subcommands?
Rationale: Inconsistent flag propagation causes confusion where global settings (like verbosity) apply to some commands but not others.
Smallest experiment today: Check the PersistentFlags definition in root.go.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/cli/root.go" -f "internal/cli/cmds.go" 

# 12) ROI=2.1 impact=0.5 confidence=0.8 effort=0.2 horizon=Immediate category=UX flows reference=internal/tui/url_picker.go:Pick
oracle --files-report --write-output "$out_dir/12-ux-flows-url-picker.md" -p "Strategist question #12
Reference: internal/tui/url_picker.go:Pick
Category: UX flows
Horizon: Immediate
ROI: 2.1 (impact=0.5, confidence=0.8, effort=0.2)
Question: How does the URL picker handle non-HTTP protocols or invalid URLs?
Rationale: Input components should filter or validate data before passing it to the business logic to prevent runtime errors.
Smallest experiment today: Review the input validation logic inside url_picker.go.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/tui/url_picker.go" 

# 13) ROI=2.1 impact=0.5 confidence=0.8 effort=0.2 horizon=Strategic category=caching/state reference=internal/tui/url_store.go:Store
oracle --files-report --write-output "$out_dir/13-caching-state-url-store.md" -p "Strategist question #13
Reference: internal/tui/url_store.go:Store
Category: caching/state
Horizon: Strategic
ROI: 2.1 (impact=0.5, confidence=0.8, effort=0.2)
Question: Is the URL store persisted across sessions, and how is it scoped?
Rationale: Persisting user choices improves ergonomics; scoping (e.g., per project vs. global) prevents context leakage.
Smallest experiment today: Check if url_store.go interacts with the state persistence layer.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/tui/url_store.go" -f "internal/state/persist.go" 

# 14) ROI=2.0 impact=0.5 confidence=0.8 effort=0.2 horizon=Strategic category=observability reference=internal/cli/run.go:Run
oracle --files-report --write-output "$out_dir/14-observability-telemetry.md" -p "Strategist question #14
Reference: internal/cli/run.go:Run
Category: observability
Horizon: Strategic
ROI: 2.0 (impact=0.5, confidence=0.8, effort=0.2)
Question: Is there telemetry or debug logging available for users to diagnose issues?
Rationale: Without visible logs (e.g., --verbose), users are blind when the tool behaves unexpectedly.
Smallest experiment today: Check run.go for log initialization or verbosity flag handling.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/cli/run.go" -f "internal/exec/flags.go" 

# 15) ROI=2.0 impact=0.5 confidence=0.8 effort=0.2 horizon=Strategic category=invariants reference=internal/exec/inject.go:Inject
oracle --files-report --write-output "$out_dir/15-invariants-context-limit.md" -p "Strategist question #15
Reference: internal/exec/inject.go:Inject
Category: invariants
Horizon: Strategic
ROI: 2.0 (impact=0.5, confidence=0.8, effort=0.2)
Question: How do we ensure injected context doesn't exceed downstream token or size limits?
Rationale: Large contexts can cause API failures or truncation in LLM-based tools; boundaries must be enforced at injection time.
Smallest experiment today: Look for size checks or truncation logic in inject.go.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/exec/inject.go" 

# 16) ROI=2.0 impact=0.5 confidence=0.8 effort=0.2 horizon=Strategic category=permissions reference=internal/exec/oracle_scan.go:Scan
oracle --files-report --write-output "$out_dir/16-permissions-scan-scope.md" -p "Strategist question #16
Reference: internal/exec/oracle_scan.go:Scan
Category: permissions
Horizon: Strategic
ROI: 2.0 (impact=0.5, confidence=0.8, effort=0.2)
Question: Does the scan step access file system locations outside the project root?
Rationale: Scanners should be scoped to the project directory to prevent accidental leakage of system files or unauthorized access.
Smallest experiment today: Check if path traversal logic in oracle_scan.go enforces root directory constraints.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/exec/oracle_scan.go" 

# 17) ROI=1.8 impact=0.4 confidence=0.8 effort=0.2 horizon=Immediate category=failure modes reference=internal/exec/stream.go:Stream
oracle --files-report --write-output "$out_dir/17-failure-modes-stream.md" -p "Strategist question #17
Reference: internal/exec/stream.go:Stream
Category: failure modes
Horizon: Immediate
ROI: 1.8 (impact=0.4, confidence=0.8, effort=0.2)
Question: How are stream interruptions or network failures handled during execution?
Rationale: Streaming responses are fragile; clients must handle broken pipes or timeouts without crashing.
Smallest experiment today: Inspect stream.go for error handling loops or retry mechanisms.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/exec/stream.go" 

# 18) ROI=1.8 impact=0.4 confidence=0.8 effort=0.2 horizon=Immediate category=UX flows reference=internal/tui/overrides_confirm.go:Confirm
oracle --files-report --write-output "$out_dir/18-ux-flows-confirm-bypass.md" -p "Strategist question #18
Reference: internal/tui/overrides_confirm.go:Confirm
Category: UX flows
Horizon: Immediate
ROI: 1.8 (impact=0.4, confidence=0.8, effort=0.2)
Question: Is the confirmation flow bypassable via flags for CI/CD environments?
Rationale: Interactive confirmations break automation; a --yes or --force flag is standard practice for CLI tools.
Smallest experiment today: Check if overrides_confirm.go checks a "skip confirm" flag.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/tui/overrides_confirm.go" -f "internal/exec/flags.go" 

# 19) ROI=1.8 impact=0.4 confidence=0.8 effort=0.2 horizon=Strategic category=feature flags reference=internal/tui/overrides_steps.go:Steps
oracle --files-report --write-output "$out_dir/19-feature-flags-steps.md" -p "Strategist question #19
Reference: internal/tui/overrides_steps.go:Steps
Category: feature flags
Horizon: Strategic
ROI: 1.8 (impact=0.4, confidence=0.8, effort=0.2)
Question: Can specific execution steps be disabled dynamically via configuration or flags?
Rationale: Modular execution allows users to skip expensive or irrelevant steps during development loops.
Smallest experiment today: Review overrides_steps.go to see if steps are conditional.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/tui/overrides_steps.go" 

# 20) ROI=1.6 impact=0.4 confidence=0.8 effort=0.2 horizon=Strategic category=contracts/interfaces reference=internal/render/render.go:Render
oracle --files-report --write-output "$out_dir/20-contracts-render-formats.md" -p "Strategist question #20
Reference: internal/render/render.go:Render
Category: contracts/interfaces
Horizon: Strategic
ROI: 1.6 (impact=0.4, confidence=0.8, effort=0.2)
Question: Does the renderer support different output formats (e.g., JSON vs Markdown) for downstream consumption?
Rationale: Flexible output formats enable the tool to be used as part of larger pipelines (Unix philosophy).
Smallest experiment today: Check render.go for format switching logic.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." -f "internal/render/render.go" 
```

---

## coverage check (must be satisfied)

* contracts/interfaces: OK

* invariants: OK

* caching/state: OK

* background jobs: Missing (no clear background job system found in file list; likely synchronous CLI)

* observability: OK

* permissions: OK

* migrations: OK

* UX flows: OK

* failure modes: OK

* feature flags: OK

--- docs/oracle/oracle-llms.txt ---
# Oracle

> Oracle enables developers and users to bundle prompts with files and submit them to multiple AI models for context-aware responses, reducing the need to manually copy-paste or manage isolated prompts. It supports both API-based (stable) and browser-based (experimental) execution modes, with features like multi-model runs, session replay, file safety checks, and remote browser services. The project aims to streamline AI-assisted development workflows by providing a unified interface for interacting with diverse LLMs while preserving context from local files.

**Remember:**
- Prompt bundling
- Multi-model execution
- Browser automation (via Chrome)
- Session management
- API endpoints (OpenAI, Gemini, Claude, OpenRouter)
- Token estimation and usage reporting

## Docs
- [Anthropic](https://github.com/steipete/oracle/blob/main/docs/anthropic.md): docs page.
- [Browser Mode](https://github.com/steipete/oracle/blob/main/docs/browser-mode.md): docs page.
- [Chromium Forks](https://github.com/steipete/oracle/blob/main/docs/chromium-forks.md): docs page.
- [Configuration](https://github.com/steipete/oracle/blob/main/docs/configuration.md): docs page.
- [Docs List](https://github.com/steipete/oracle/blob/main/scripts/docs-list.ts): docs page.
- [Gemini](https://github.com/steipete/oracle/blob/main/docs/gemini.md): docs page.
- [Grok](https://github.com/steipete/oracle/blob/main/docs/grok.md): docs page.
- [Linux](https://github.com/steipete/oracle/blob/main/docs/linux.md): docs page.
- [Manual Tests](https://github.com/steipete/oracle/blob/main/docs/manual-tests.md): docs page.
- [Mcp](https://github.com/steipete/oracle/blob/main/docs/mcp.md): docs page.

## API
- [Win Dpapi.D](https://github.com/steipete/oracle/blob/main/types/win-dpapi.d.ts): docs page.

## Optional
- [Changelog](https://github.com/steipete/oracle/blob/main/CHANGELOG.md): version history.
- [Agents](https://github.com/steipete/oracle/blob/main/AGENTS.md): docs page.
- [README](https://github.com/steipete/oracle/blob/main/README.md): docs page.
- [Skill](https://github.com/steipete/oracle/blob/main/skills/oracle/SKILL.md): docs page.
- [README](https://github.com/steipete/oracle/blob/main/vendor/oracle-notifier/README.md): install & quickstart.
- [Vitest.Config](https://github.com/steipete/oracle/blob/main/vitest.config.ts): docs page.

## Bin
- [Oracle Cli](https://github.com/steipete/oracle/blob/main/bin/oracle-cli.ts): docs page.
- [Oracle Mcp](https://github.com/steipete/oracle/blob/main/bin/oracle-mcp.ts): docs page.

## Scripts
- [Agent Send](https://github.com/steipete/oracle/blob/main/scripts/agent-send.ts): docs page.
- [Browser Tools](https://github.com/steipete/oracle/blob/main/scripts/browser-tools.ts): docs page.
- [Build Vendor](https://github.com/steipete/oracle/blob/main/scripts/build-vendor.js): docs page.
- [Check](https://github.com/steipete/oracle/blob/main/scripts/check.ts): docs page.
- [Git Policy](https://github.com/steipete/oracle/blob/main/scripts/git-policy.ts): docs page.
- [Run Cli](https://github.com/steipete/oracle/blob/main/scripts/run-cli.ts): docs page.
- [Runner](https://github.com/steipete/oracle/blob/main/scripts/runner.ts): docs page.
- [Test Browser](https://github.com/steipete/oracle/blob/main/scripts/test-browser.ts): docs page.
- [Test Remote Chrome](https://github.com/steipete/oracle/blob/main/scripts/test-remote-chrome.ts): docs page.

## Src
- [Browsermode](https://github.com/steipete/oracle/blob/main/src/browserMode.ts): docs page.
- [Config](https://github.com/steipete/oracle/blob/main/src/config.ts): docs page.
- [Heartbeat](https://github.com/steipete/oracle/blob/main/src/heartbeat.ts): docs page.
- [Oracle](https://github.com/steipete/oracle/blob/main/src/oracle.ts): docs page.
- [Oraclehome](https://github.com/steipete/oracle/blob/main/src/oracleHome.ts): docs page.
- [Sessionmanager](https://github.com/steipete/oracle/blob/main/src/sessionManager.ts): docs page.
- [Sessionstore](https://github.com/steipete/oracle/blob/main/src/sessionStore.ts): docs page.
- [Version](https://github.com/steipete/oracle/blob/main/src/version.ts): docs page.
- [Background](https://github.com/steipete/oracle/blob/main/src/oracle/background.ts): docs page.
- [Browserconfig](https://github.com/steipete/oracle/blob/main/src/cli/browserConfig.ts): docs page.

## Tests
- [Config.Test](https://github.com/steipete/oracle/blob/main/tests/config.test.ts): docs page.
- [Engine.Test](https://github.com/steipete/oracle/blob/main/tests/engine.test.ts): docs page.
- [Gemini.Test](https://github.com/steipete/oracle/blob/main/tests/gemini.test.ts): docs page.
- [Logging.Test](https://github.com/steipete/oracle/blob/main/tests/logging.test.ts): docs page.
- [Mcp.Integration.Test](https://github.com/steipete/oracle/blob/main/tests/mcp.integration.test.ts): docs page.
- [Mcp.Resources.Test](https://github.com/steipete/oracle/blob/main/tests/mcp.resources.test.ts): docs page.
- [Mcp.Schema.Test](https://github.com/steipete/oracle/blob/main/tests/mcp.schema.test.ts): docs page.
- [Mcp.Stdout.Test](https://github.com/steipete/oracle/blob/main/tests/mcp.stdout.test.ts): docs page.
- [Mcp.Test](https://github.com/steipete/oracle/blob/main/tests/mcp.test.ts): docs page.
- [Notifier.Test](https://github.com/steipete/oracle/blob/main/tests/notifier.test.ts): docs page.

## Types
- [Oracle.D](https://github.com/steipete/oracle/blob/main/types/oracle.d.ts): docs page.
- [Pty.D](https://github.com/steipete/oracle/blob/main/types/pty.d.ts): docs page.
- [Toasted Notifier.D](https://github.com/steipete/oracle/blob/main/types/toasted-notifier.d.ts): docs page.


## Links discovered
- [Anthropic](https://github.com/steipete/oracle/blob/main/docs/anthropic.md)
- [Browser Mode](https://github.com/steipete/oracle/blob/main/docs/browser-mode.md)
- [Chromium Forks](https://github.com/steipete/oracle/blob/main/docs/chromium-forks.md)
- [Configuration](https://github.com/steipete/oracle/blob/main/docs/configuration.md)
- [Docs List](https://github.com/steipete/oracle/blob/main/scripts/docs-list.ts)
- [Gemini](https://github.com/steipete/oracle/blob/main/docs/gemini.md)
- [Grok](https://github.com/steipete/oracle/blob/main/docs/grok.md)
- [Linux](https://github.com/steipete/oracle/blob/main/docs/linux.md)
- [Manual Tests](https://github.com/steipete/oracle/blob/main/docs/manual-tests.md)
- [Mcp](https://github.com/steipete/oracle/blob/main/docs/mcp.md)
- [Win Dpapi.D](https://github.com/steipete/oracle/blob/main/types/win-dpapi.d.ts)
- [Changelog](https://github.com/steipete/oracle/blob/main/CHANGELOG.md)
- [Agents](https://github.com/steipete/oracle/blob/main/AGENTS.md)
- [README](https://github.com/steipete/oracle/blob/main/README.md)
- [Skill](https://github.com/steipete/oracle/blob/main/skills/oracle/SKILL.md)
- [README](https://github.com/steipete/oracle/blob/main/vendor/oracle-notifier/README.md)
- [Vitest.Config](https://github.com/steipete/oracle/blob/main/vitest.config.ts)
- [Oracle Cli](https://github.com/steipete/oracle/blob/main/bin/oracle-cli.ts)
- [Oracle Mcp](https://github.com/steipete/oracle/blob/main/bin/oracle-mcp.ts)
- [Agent Send](https://github.com/steipete/oracle/blob/main/scripts/agent-send.ts)
- [Browser Tools](https://github.com/steipete/oracle/blob/main/scripts/browser-tools.ts)
- [Build Vendor](https://github.com/steipete/oracle/blob/main/scripts/build-vendor.js)
- [Check](https://github.com/steipete/oracle/blob/main/scripts/check.ts)
- [Git Policy](https://github.com/steipete/oracle/blob/main/scripts/git-policy.ts)
- [Run Cli](https://github.com/steipete/oracle/blob/main/scripts/run-cli.ts)
- [Runner](https://github.com/steipete/oracle/blob/main/scripts/runner.ts)
- [Test Browser](https://github.com/steipete/oracle/blob/main/scripts/test-browser.ts)
- [Test Remote Chrome](https://github.com/steipete/oracle/blob/main/scripts/test-remote-chrome.ts)
- [Browsermode](https://github.com/steipete/oracle/blob/main/src/browserMode.ts)
- [Config](https://github.com/steipete/oracle/blob/main/src/config.ts)
- [Heartbeat](https://github.com/steipete/oracle/blob/main/src/heartbeat.ts)
- [Oracle](https://github.com/steipete/oracle/blob/main/src/oracle.ts)
- [Oraclehome](https://github.com/steipete/oracle/blob/main/src/oracleHome.ts)
- [Sessionmanager](https://github.com/steipete/oracle/blob/main/src/sessionManager.ts)
- [Sessionstore](https://github.com/steipete/oracle/blob/main/src/sessionStore.ts)
- [Version](https://github.com/steipete/oracle/blob/main/src/version.ts)
- [Background](https://github.com/steipete/oracle/blob/main/src/oracle/background.ts)
- [Browserconfig](https://github.com/steipete/oracle/blob/main/src/cli/browserConfig.ts)
- [Config.Test](https://github.com/steipete/oracle/blob/main/tests/config.test.ts)
- [Engine.Test](https://github.com/steipete/oracle/blob/main/tests/engine.test.ts)
- [Gemini.Test](https://github.com/steipete/oracle/blob/main/tests/gemini.test.ts)
- [Logging.Test](https://github.com/steipete/oracle/blob/main/tests/logging.test.ts)
- [Mcp.Integration.Test](https://github.com/steipete/oracle/blob/main/tests/mcp.integration.test.ts)
- [Mcp.Resources.Test](https://github.com/steipete/oracle/blob/main/tests/mcp.resources.test.ts)
- [Mcp.Schema.Test](https://github.com/steipete/oracle/blob/main/tests/mcp.schema.test.ts)
- [Mcp.Stdout.Test](https://github.com/steipete/oracle/blob/main/tests/mcp.stdout.test.ts)
- [Mcp.Test](https://github.com/steipete/oracle/blob/main/tests/mcp.test.ts)
- [Notifier.Test](https://github.com/steipete/oracle/blob/main/tests/notifier.test.ts)
- [Oracle.D](https://github.com/steipete/oracle/blob/main/types/oracle.d.ts)
- [Pty.D](https://github.com/steipete/oracle/blob/main/types/pty.d.ts)
- [Toasted Notifier.D](https://github.com/steipete/oracle/blob/main/types/toasted-notifier.d.ts)

--- docs/oracle/oracle-llms-full.txt ---
# llms-full (private-aware)
> Built from GitHub files and website pages. Large files may be truncated.

--- docs/anthropic.md ---
# Anthropic (Claude) Integration Plan

Status: **shipped** (November 20, 2025)  
Scope: API support for Claude 4.5 Sonnet and Claude 4.1 Opus in Oracle CLI.

## Models & Pricing (public list prices)
- **claude-sonnet-4-5** (CLI alias: `claude-4.5-sonnet`) — 200k context, ~$3 / 1M input tokens, ~$15 / 1M output tokens.
- **claude-opus-4-1** (CLI alias: `claude-4.1-opus`) — 200k context, ~$15 / 1M input tokens, ~$75 / 1M output tokens.
- Prompt-caching premium (not modeled in CLI costs): cached input portion >200k is billed higher (Sonnet ~$6 / 1M; Opus ~$18.75 / 1M).

## Requirements
- Environment: `ANTHROPIC_API_KEY` (required), `ANTHROPIC_BASE_URL` (optional; defaults to `https://api.anthropic.com`).
- Engine: **API only**. Browser mode is blocked for Claude.
- Tokenizer: `@anthropic-ai/tokenizer` (wrapped to accept Oracle’s array inputs). Estimates are approximate; rely on API `usage` for actual billing.

## Planned CLI Behavior
- Add models to `--model/--models`: `claude-4.5-sonnet`, `claude-4.1-opus`. Aliases: “sonnet”, “opus” map to those IDs.
- Background runs: **disabled** for Claude (`supportsBackground=false`). Even if `--background` is set, the run streams normally and logs a note.
- Search / tools: `web_search_preview` is ignored for Claude; `--search` is effectively off with a warning.
- Base URL: `--base-url` / `apiBaseUrl` applies per provider; falls back to `ANTHROPIC_BASE_URL` for Claude, `OPENAI_BASE_URL` for GPT.
- Cost display: uses the prices above; prompt-caching billing is not modeled (estimates are upper/lower bounds only).

## Usage Examples
- Single model (Sonnet):  
  ```bash
  oracle --engine api --model claude-4.5-sonnet --prompt "Summarize the design doc" --file docs/design.md
  ```
- High-reasoning (Opus) with files report:  
  ```bash
  oracle -m claude-4.1-opus --files-report --prompt "Analyze risk register" --file docs/risk.md
  ```
- Multi-model compare (GPT + Claude):  
  ```bash
  oracle --models gpt-5.1-pro,claude-4.5-sonnet --prompt "Propose mitigation steps" --file docs/plan.md
  ```
  Background stays off for Claude; GPT may still use background.

## Implementation Notes (for maintainers)
- Types/config: Claude entries use `apiModel` mapping to Anthropic IDs (`claude-sonnet-4-5`, `claude-opus-4-1`); Opus stays in `ProModelName`; pricing + 200k inputLimit; Anthropic tokenizer wrapper; `supportsBackground=false`. Opus gets `reasoning: high`.
- Client factory: branch on `claude*` to the Anthropic adapter (messages.stream/create); pass provider-specific `baseUrl`.
- Env selection: `ANTHROPIC_API_KEY` and `ANTHROPIC_BASE_URL`; log masked key per provider.
- Token estimates: wrapper flattens Oracle message arrays into text before calling `countTokens`.
- Multi-model: shared `runOptions.background` is gated by per-model `supportsBackground`; Claude never enters the background polling path.
- Docs to update alongside code: this file, `README.md` model list, `docs/configuration.md`, `docs/multimodel.md`.

## Limitations / Caveats
- Token estimates for Claude are approximate; API may still reject >200k inputs even if estimate passes.
- Prompt-caching cost deltas are not reflected in CLI estimates.
- No tool use/search for Claude in v1.

## Troubleshooting
- Missing key: “Missing ANTHROPIC_API_KEY…” — set the env var or pass `--api-key`.
- Background ignored: expected; Claude does not support the Responses-style job API.
- Search ignored: expected; Claude adapter currently drops `web_search_preview`.

## Next Steps (post-v1)
- Optional: add tool/use support with a provider-agnostic tool layer.
- Add dated model-id mapping if Anthropic starts versioned IDs (similar to Gemini resolver) — currently aliases map to the undated IDs above.
- Improve cost estimation if API exposes cached-token counters.


--- docs/browser-mode.md ---
# Browser Mode

Oracle’s `--engine browser` supports two different execution paths:

- **ChatGPT automation** (GPT-* models): drives the ChatGPT web UI with Chrome automation.
- **Gemini web mode** (Gemini models): talks directly to `gemini.google.com` using your signed-in Chrome cookies (no ChatGPT automation).

If you’re running Gemini, also see `docs/gemini.md`.

`oracle --engine browser` routes the assembled prompt bundle through the ChatGPT web UI instead of the Responses API. (Legacy `--browser` still maps to `--engine browser`, but it will be removed.) If you omit `--engine`, Oracle first honors any `engine` value in `~/.oracle/config.json`, then auto-picks API when `OPENAI_API_KEY` is available and falls back to browser otherwise. The CLI writes the same session metadata/logs as API runs, and by default pastes the payload into ChatGPT via a temporary Chrome profile (manual-login mode can reuse a persistent automation profile).

`--preview` now works with `--engine browser`: it renders the composed prompt, lists which files would be uploaded vs inlined, and shows the bundle location when bundling is enabled, without launching Chrome.

## Quick example: browser mode with custom cookies

```bash
# Minimal inline-cookies flow: keep ChatGPT logged in without Keychain
jq '.' ~/.oracle/cookies.json  # file must contain CookieParam[]
oracle --engine browser \
  --browser-inline-cookies-file ~/.oracle/cookies.json \
  --model "GPT-5.2 Pro" \
  -p "Run the UI smoke" \
  --file "src/**/*.ts" --file "!src/**/*.test.ts"
```

`~/.oracle/cookies.json` should be a JSON array shaped like:

```json
[
  { "name": "__Secure-next-auth.session-token", "value": "<token>", "domain": "chatgpt.com", "path": "/", "secure": true, "httpOnly": true },
  { "name": "_account", "value": "personal", "domain": "chatgpt.com", "path": "/", "secure": true }
]
```

You can pass the same payload inline (`--browser-inline-cookies '<json or base64>'`) or via env (`ORACLE_BROWSER_COOKIES_JSON`, `ORACLE_BROWSER_COOKIES_FILE`). Cloudflare cookies (`cf_clearance`, `__cf_bm`, etc.) are only needed when you hit a challenge.

## Current Pipeline

1. **Prompt assembly** – we reuse the normal prompt builder (`buildPrompt`) and the markdown renderer. Browser mode pastes the system + user text (no special markers) into the ChatGPT composer and, by default, pastes resolved file contents inline until the total pasted content reaches ~60k characters (then switches to uploads).
2. **Automation stack** – code lives in `src/browserMode.ts` and is a lightly refactored version of the `oraclecheap` utility:
   - Launches Chrome via `chrome-launcher` and connects with `chrome-remote-interface`.
   - (Optional) copies cookies from the requested browser profile via Oracle’s built-in cookie reader (Keychain/DPAPI aware) so you stay signed in.
   - Navigates to `chatgpt.com`, switches the model to the requested **GPT-5.2** variant (Auto/Thinking/Instant/Pro), pastes the prompt, waits for completion, and copies the markdown via the built-in “copy turn” button.
   - Immediately probes `/backend-api/me` in the ChatGPT tab to verify the session is authenticated; if the endpoint returns 401/403 we abort early with a login-specific error instead of timing out waiting for the composer.
   - When `--file` inputs would push the pasted composer content over ~60k characters, we switch to uploading attachments (optionally bundled) and wait for ChatGPT to re-enable the send button before submitting the combined system+user prompt.
   - Cleans up the temporary profile unless `--browser-keep-browser` is passed.
3. **Session integration** – browser sessions use the normal log writer, add `mode: "browser"` plus `browser.config/runtime` metadata, and log the Chrome PID/port so `oracle session <id>` (or `oracle status <id>`) shows a marker for the background Chrome process.
4. **Usage accounting** – we estimate input tokens with the same tokenizer used for API runs and estimate output tokens via `estimateTokenCount`. `oracle status` therefore shows comparable cost/timing info even though the call ran through the browser.

### CLI Options

- `--engine browser`: enables browser mode (legacy `--browser` remains as an alias for now). Without `--engine`, Oracle chooses API when `OPENAI_API_KEY` exists, otherwise browser.
- `--browser-chrome-profile`, `--browser-chrome-path`: cookie source + binary override (defaults to the standard `"Default"` Chrome profile so existing ChatGPT logins carry over).
- `--browser-cookie-path`: explicit path to the Chrome/Chromium/Edge `Cookies` SQLite DB. Handy when you launch a fork via `--browser-chrome-path` and want to copy its session cookies; see [docs/chromium-forks.md](chromium-forks.md) for examples.
- `--chatgpt-url`: override the ChatGPT base URL. Works with the root homepage (`https://chatgpt.com/`) **or** a specific workspace/folder link such as `https://chatgpt.com/g/.../project`. `--browser-url` stays as a hidden alias.
- `--browser-timeout`, `--browser-input-timeout`: `1200s (20m)`/`30s` defaults. Durations accept `ms`, `s`, `m`, or `h` and can be chained (`1h2m10s`).
- `--browser-model-strategy <select|current|ignore>`: control ChatGPT model selection. `select` (default) switches to the requested model; `current` keeps the active model and logs its label; `ignore` skips the picker entirely. (Ignored for Gemini web runs.)
- `--browser-thinking-time <light|standard|extended|heavy>`: set the ChatGPT thinking-time intensity (Thinking/Pro models only). You can also set a default in `~/.oracle/config.json` via `browser.thinkingTime`.
- `--browser-port <port>` (alias: `--browser-debug-port`; env: `ORACLE_BROWSER_PORT`/`ORACLE_BROWSER_DEBUG_PORT`): pin the DevTools port (handy on WSL/Windows firewalls). When omitted, a random open port is chosen.
- `--browser-no-cookie-sync`, `--browser-manual-login` (persistent automation profile + user-driven login), `--browser-headless`, `--browser-hide-window`, `--browser-keep-browser`, and the global `-v/--verbose` flag for detailed automation logs.
- `--browser-url`: override ChatGPT base URL if needed.
- `--browser-attachments <auto|never|always>`: control how `--file` inputs are delivered in browser mode. Default `auto` pastes file contents inline up to ~60k characters and switches to uploads above that.
- `--browser-inline-files`: alias for `--browser-attachments never` (forces inline paste; never uploads attachments).
- `--browser-bundle-files`: bundle all resolved attachments into a single temp file before uploading (only used when uploads are enabled/selected).
- sqlite bindings: automatic rebuilds now require `ORACLE_ALLOW_SQLITE_REBUILD=1`. Without it, the CLI logs instructions instead of running `pnpm rebuild` on your behalf.
- `--model`: the same flag used for API runs is accepted, but the ChatGPT automation path only supports **GPT-5.2** variants (Auto/Thinking/Instant/Pro). Use `gpt-5.2`, `gpt-5.2-thinking`, `gpt-5.2-instant`, or `gpt-5.2-pro`. Other GPT families still require API mode.
- Cookie sync is mandatory—if we can’t copy cookies from Chrome, the run exits early. Use the hidden `--browser-allow-cookie-errors` flag only when you’re intentionally running logged out (it skips the early exit but still warns).
- Experimental cookie controls (hidden flags/env):
  - `--browser-cookie-names <comma-list>` or `ORACLE_BROWSER_COOKIE_NAMES`: allowlist which cookies to sync. Useful for “only NextAuth/Cloudflare, drop the rest.”
  - `--browser-cookie-wait <ms|s|m>`: if cookie sync fails or returns no cookies, wait once and retry (helps when macOS Keychain prompts are slow).
  - `--browser-inline-cookies <jsonOrBase64>` or `ORACLE_BROWSER_COOKIES_JSON`: skip Chrome/keychain and set cookies directly. Payload is a JSON array of DevTools `CookieParam` objects (or the same, base64-encoded). At minimum you need `name`, `value`, and either `url` or `domain`; we infer `path=/`, `secure=true`, `httpOnly=false`.
  - `--browser-inline-cookies-file <path>` or `ORACLE_BROWSER_COOKIES_FILE`: load the same payload from disk (JSON or base64 JSON). If no args/env are provided, Oracle also auto-loads `~/.oracle/cookies.json` or `~/.oracle/cookies.base64` when present.
  - Practical minimal set that keeps ChatGPT logged in and avoids the workspace picker: `__Secure-next-auth.session-token` (include `.0`/`.1` variants) and `_account` (active workspace/account). Cloudflare proofs (`cf_clearance`, `__cf_bm`/`_cfuvid`/`CF_Authorization`/`__cflb`) are only needed when a challenge is active. In practice our allowlist pulls just two cookies (session token + `_account`) and works; add the Cloudflare names if you hit a challenge.
  - Inline payload shape example (we ignore extra fields like `expirationDate`, `sameSite`, `hostOnly`):  
    ```json
    [
      { "name": "__Secure-next-auth.session-token", "value": "<token>", "domain": "chatgpt.com", "path": "/", "secure": true, "httpOnly": true, "expires": 1771295753 },
      { "name": "_account", "value": "personal", "domain": "chatgpt.com", "path": "/", "secure": true, "httpOnly": false, "expires": 1770702447 }
    ]
    ```

All options are persisted with the session so reruns (`oracle exec <id>`) reuse the same automation settings.

### Manual login mode (persistent profile, no cookie copy)

Use `--browser-manual-login` when cookie decrypt is blocked (e.g., Windows app-bound cookies) or you prefer to sign in explicitly. You can also make it the default via `browser.manualLogin` in `~/.oracle/config.json`.

```bash
oracle --engine browser \
  --browser-manual-login \
  --browser-keep-browser \
  --model "GPT-5.2 Pro" \
  -p "Say hi"
```

- Oracle launches Chrome headful with a persistent automation profile at `~/.oracle/browser-profile` (override with `ORACLE_BROWSER_PROFILE_DIR` or `browser.manualLoginProfileDir` in `~/.oracle/config.json`).
- Log into chatgpt.com in that window the first time; Oracle polls until the session is active, then proceeds.
- Reuse the same profile on subsequent runs (no re-login unless the session expires).
- Add `--browser-keep-browser` (or config `browser.keepBrowser=true`) when doing the initial login/setup or debugging so the Chrome window stays open after the run. When omitted, Oracle closes Chrome but preserves the profile on disk.
- Cookie copy is skipped by default in this mode. To automate manual-login runs, set `browser.manualLoginCookieSync=true` in `~/.oracle/config.json` to seed the persistent profile from your existing Chrome cookies; inline cookies apply when cookie sync is enabled.
- If Chrome is already running with that profile and DevTools remote debugging enabled (see `DevToolsActivePort` in the profile dir), you can reuse it instead of relaunching by pointing Oracle at it with `--remote-chrome <host:port>`.

## Remote Chrome Sessions (headless/server workflows)

Oracle can reuse an already-running Chrome/Edge instance on another machine by tunneling over the Chrome DevTools Protocol. This is handy when:

- Your CLI runs on a headless server (Linux/macOS CI, remote mac minis, etc.) but you want the browser UI to live on a desktop where you can see uploads or respond to Captcha challenges.
- You want to keep a single signed-in profile open (e.g., Windows VM with company SSO) while sending prompts from other hosts.

### 1. Start Chrome with remote debugging enabled

On the machine that should host the browser window:

```bash
google-chrome \
  --remote-debugging-port=9222 \
  --remote-debugging-address=0.0.0.0 \
  --user-data-dir=/path/to/profile \
  --profile-directory='Default'
```

Notes:

- Any Chromium flavor works (Chrome, Edge, Vivaldi, etc.)—just ensure CDP is exposed on a reachable host:port. Linux distributions often call the binary `google-chrome-stable`. On macOS you can run `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`.
- `--remote-debugging-address=0.0.0.0` is required if the CLI connects from another machine. Lock it down behind a VPN or SSH tunnel if the network is untrusted.
- Keep this browser window open and signed into ChatGPT; Oracle will reuse that session and **will not** copy cookies over the wire.

### 2. Point Oracle at the remote browser

From the machine running `oracle`:

```bash
oracle --engine browser \
  --remote-chrome 192.168.1.10:9222 \
  --prompt "Summarize the latest incident doc" \
  --file docs/incidents/latest.md
```

Key behavior:

- Use IPv6 by wrapping the host in brackets, e.g. `--remote-chrome "[2001:db8::1]:9222"`.
- Local-only flags like `--browser-headless`, `--browser-hide-window`, `--browser-keep-browser`, and `--browser-chrome-path` are ignored because Oracle no longer launches Chrome. You still get verbose logging, model switching, attachment uploads, and markdown capture.
- Cookie sync is skipped automatically (the remote browser already has cookies). If you need inline cookies, use them on the machine that’s actually running Chrome.
- Oracle opens a dedicated CDP target (new tab) for each run and closes it afterward so your existing tabs stay untouched.
- Attachments are transferred via CDP: Oracle reads each file locally, base64-encodes it, and uses `DataTransfer` inside the remote browser to populate the upload field. Files larger than 20 MB are rejected to keep CDP messages reasonable.
- When the remote WebSocket disconnects, Oracle errors with “Remote Chrome connection lost…” so you can re-run after restarting the browser.

### 3. Troubleshooting

- Run `scripts/test-remote-chrome.ts <host> [port]` to sanity-check connectivity (`npx tsx scripts/test-remote-chrome.ts my-host 9222`).
- If you target IPv6 without brackets (e.g., `2001:db8::1:9222`), the CLI rejects it—wrap the address like `[2001:db8::1]:9222`.
- Ensure firewalls allow inbound TCP to the debugging port and that you’re not behind a captive proxy stripping WebSocket upgrades.
- Because we do not control the remote lifecycle, Chrome stays running after the session. Shut it down manually when you’re done or remove `--remote-debugging-port` to stop exposing CDP.

### Remote Service Mode (`oracle serve`)

Prefer to keep Chrome entirely on the remote Mac (no DevTools tunneling, no manual cookie shuffling)? Use the built-in service:

1. **Start the host**
   ```bash
   oracle serve
   ```
   Oracle picks a free port, launches Chrome, starts an HTTP/SSE API, and prints:
   ```
   Listening at 0.0.0.0:9473
   Access token: c4e5f9...
   ```
   Use `--host`, `--port`, or `--token` to override the defaults if needed.
   If the host Chrome profile is not signed into ChatGPT, the service opens chatgpt.com for login and exits—sign in, then restart `oracle serve`.

2. **Run from your laptop**
   ```bash
   oracle --engine browser \
     --remote-host 192.168.64.2:9473 \
     --remote-token c4e5f9... \
   --prompt "Summarize the incident doc" \
    --file docs/incidents/latest.md
   ```

   - `--remote-host` points the CLI at the VM.
   - `--remote-token` matches the token printed by `oracle serve` (set `ORACLE_REMOTE_TOKEN` to avoid repeating it).
   - You can also set defaults in `~/.oracle/config.json` (`remote.host`, `remote.token`) so you don’t need the flags; env vars still override those when present.
   - Cookies are **not** transferred from your laptop. The service requires the host Chrome profile to be signed in; if not, it opens chatgpt.com and exits so you can log in, then restart `oracle serve`.

3. **What happens**
   - The CLI assembles the composed prompt + file bundle locally, sends them to the VM, and streams log lines/answer text back through the same HTTP connection.
   - The remote host runs Chrome locally, pulls ChatGPT cookies from its own Chrome profile, and reuses them across runs while the service is up. If cookies are missing, the service exits after opening chatgpt.com so you can sign in before restarting.
   - Background/detached sessions (`--no-wait`) are disabled in remote mode so the CLI can keep streaming output.
   - `oracle serve` logs the DevTools port of the manual-login Chrome (e.g., `Manual-login Chrome DevTools port: 54371`). Runs automatically attach to that logged-in Chrome; you can use the printed port/JSON URL for debugging if needed.

4. **Stop the host**
   - `Ctrl+C` on the VM shuts down the HTTP server and Chrome. Restart `oracle serve` whenever you need a new session; omit `--token` to let it rotate automatically.

This mode is ideal when you have a macOS VM (or spare Mac mini) logged into ChatGPT and you just want to run the CLI from another machine without ever copying profiles or keeping Chrome visible locally.

## Limitations / Follow-Up Plan

- **Attachment lifecycle** – in `auto` mode we prefer inlining files into the composer (fewer moving parts). When we do upload, each `--file` path is uploaded separately (or bundled) so ChatGPT can ingest filenames/content. The automation waits for uploads to finish (send button enabled, upload chips visible) before submitting. When inline paste is rejected by ChatGPT (too large), Oracle retries automatically with uploads.
- **Model picker drift** – we rely on heuristics to pick GPT-5.2 variants. If OpenAI changes the DOM we need to refresh the selectors quickly. Consider snapshot tests or a small “self check” command.
- **Non-mac platforms** – window hiding uses AppleScript today; Linux/Windows just ignore the flag. We should detect platforms explicitly and document the behavior.
- **Streaming UX** – browser runs cannot stream tokens, so we log a warning before launching Chrome. Investigate whether we can stream clipboard deltas via mutation observers for a closer UX.

## Testing Notes

- ChatGPT automation smoke: `pnpm test:browser`
- Gemini web (cookie) smoke: `ORACLE_LIVE_TEST=1 pnpm vitest run tests/live/gemini-web-live.test.ts` (requires a signed-in Chrome profile at `gemini.google.com`)
- `pnpm test --filter browser` does not exist yet; manual runs with `--engine browser -v` are the current validation path.
- Most of the heavy lifting lives in `src/browserMode.ts`. If you change selectors or the mutation observer logic, run a local `oracle --engine browser --browser-keep-browser` session so you can inspect DevTools before cleanup.


## Links discovered
- [docs/chromium-forks.md](https://github.com/steipete/oracle/blob/main/docs/chromium-forks.md)

--- docs/chromium-forks.md ---
# Chromium-based browsers (Chromium, Edge, Brave variants)

Oracle’s browser engine assumes Google Chrome by default: it launches Chrome via `chrome-launcher` and copies cookies from Chrome’s profile/keychain so you stay signed in to ChatGPT. Chromium, Microsoft Edge, and other forks ship the same DevTools protocol, but they keep the executable and cookie store in different locations. Use the knobs below to point Oracle at those assets explicitly.

## 1. Point Oracle at the right executable

Either pass the CLI flag or set it once in `~/.oracle/config.json`:

- CLI: `oracle --engine browser --browser-chrome-path "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" …`
- Config:
  ```json5
  {
    browser: {
      chromePath: "/Applications/Chromium.app/Contents/MacOS/Chromium"
    }
  }
  ```

`--browser-chrome-path` (also exposed in `oracle --debug-help`) controls which binary `chrome-launcher` starts. You can still keep `chromeProfile: "Default"` if you want to copy cookies from Chrome proper while launching Edge/Chromium.

## 2. Tell cookie sync where your session lives

Set the new `--browser-cookie-path` flag (or `browser.chromeCookiePath` in config) to the absolute path of the fork’s `Cookies` SQLite database. When present, Oracle feeds this path straight into the internal cookie reader, skipping Chrome-only heuristics and profile-name guesses.

```bash
oracle --engine browser \
  --browser-chrome-path "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" \
  --browser-cookie-path "$HOME/Library/Application Support/Microsoft Edge/Profile 1/Cookies" \
  --prompt "Summarize the release notes"
```

Config example (JSON5):

```json5
{
  browser: {
    chromePath: "/usr/bin/chromium",
    chromeCookiePath: "/home/you/.config/chromium/Default/Cookies",
    chromeProfile: null
  }
}
```

If you omit `chromeCookiePath`, Oracle falls back to `chromeProfile` (name or explicit path). Providing both keeps things unambiguous.

## Common cookie DB paths

| Browser | macOS | Linux | Windows |
| --- | --- | --- | --- |
| Chrome (default) | `~/Library/Application Support/Google/Chrome/Default/Cookies` | `~/.config/google-chrome/Default/Cookies` | `%LOCALAPPDATA%/Google/Chrome/User Data/Default/Network/Cookies` |
| Chromium | `~/Library/Application Support/Chromium/Default/Cookies` | `~/.config/chromium/Default/Cookies` | `%LOCALAPPDATA%/Chromium/User Data/Default/Network/Cookies` |
| Microsoft Edge | `~/Library/Application Support/Microsoft Edge/Default/Cookies` (profiles are `Profile 1`, `Profile 2`, …) | `~/.config/microsoft-edge/Default/Cookies` | `%LOCALAPPDATA%/Microsoft/Edge/User Data/Default/Network/Cookies` |

Brave and other forks work the same way—inspect `%APPDATA%`/`~/Library/Application Support`/`~/.config` for their `Cookies` file and pass its full path to `--browser-cookie-path`.

### macOS / Windows encryption caveat

Oracle now detects the right Keychain/DPAPI label based on the cookie path (`Chrome Safe Storage`, `Chromium Safe Storage`, `Microsoft Edge Safe Storage`, etc.) and pulls the key automatically. If macOS asks for Keychain access, approve it. When the system doesn’t expose that secret (e.g., the browser hasn’t stored any cookies yet), fall back to `--browser-inline-cookies[(-file)]` until you can sign in once via the target browser.

## Troubleshooting checklist

- `oracle --debug-help` lists both `--browser-chrome-path` and `--browser-cookie-path`.
- Run with `-v` to verify which cookie source Oracle is using (Chrome profile, inline payload, or explicit path).
- If cookie sync fails with “Chrome Safe Storage” prompts while using another fork, fall back to inline cookies until the fork’s password store is supported.
- `CHROME_PATH` still works as a last-resort override for the executable; config + CLI flags are preferred because they’re persisted per workspace.


--- docs/configuration.md ---
# Local configuration (JSON5)

Oracle reads an optional per-user config from `~/.oracle/config.json`. The file uses JSON5 parsing, so trailing commas and comments are allowed.

## Example (`~/.oracle/config.json`)

```json5
{
  // Default engine when neither CLI flag nor env decide
  engine: "api",           // or "browser"
  model: "gpt-5.1-pro",    // API alias → gpt-5.2-pro
  search: "on",            // "on" | "off"

  notify: {
    enabled: true,          // default notifications (still auto-mutes in CI/SSH unless forced on)
    sound: false,           // play a sound on completion
    muteIn: ["CI", "SSH"], // auto-disable when these env vars are set
  },

  browser: {
    chromeProfile: "Default",
    chromePath: null,
    chromeCookiePath: null,
    chatgptUrl: "https://chatgpt.com/", // root is fine; folder URLs also work
    url: null, // alias for chatgptUrl (kept for back-compat)
    debugPort: null,          // fixed DevTools port (env: ORACLE_BROWSER_PORT / ORACLE_BROWSER_DEBUG_PORT)
    timeoutMs: 1200000,
    inputTimeoutMs: 30000,
    cookieSyncWaitMs: 0,      // wait (ms) before retrying cookie sync when Chrome cookies are empty/locked
    modelStrategy: "select", // select | current | ignore (ChatGPT only; ignored for Gemini web)
    thinkingTime: "extended", // light | standard | extended | heavy (ChatGPT Thinking/Pro models)
    manualLogin: false,        // set true to reuse a persistent automation profile and sign in once (Windows defaults to true when unset)
    manualLoginProfileDir: null, // override profile dir (or set ORACLE_BROWSER_PROFILE_DIR)
    headless: false,
    hideWindow: false,
    keepBrowser: false,
    manualLoginCookieSync: false, // allow cookie sync even in manual-login mode
  },

  // Default target for `oracle serve` remote browser runs
  remote: {
    host: "192.168.64.2:9473",
    token: "c4e5f9...", // printed by `oracle serve`
  },

  // Azure OpenAI defaults (only used when endpoint is set)
  azure: {
    endpoint: "https://your-resource-name.openai.azure.com/",
    deployment: "gpt-5-1-pro",
    apiVersion: "2024-02-15-preview"
  },

  heartbeatSeconds: 30,     // default heartbeat interval
  filesReport: false,       // default per-file token report
  background: true,         // default background mode for API runs
  sessionRetentionHours: 72, // prune cached sessions older than 72h before each run (0 disables)
  promptSuffix: "// signed-off by me", // appended to every prompt
  apiBaseUrl: "https://api.openai.com/v1" // override for LiteLLM / custom gateways
}
```

## Precedence

CLI flags → `config.json` → environment → built-in defaults.

- `engine`, `model`, `search`, `filesReport`, `heartbeatSeconds`, and `apiBaseUrl` in `config.json` override the auto-detected values unless explicitly set on the CLI.
- If `azure.endpoint` (or `--azure-endpoint`) is set, Oracle reads `AZURE_OPENAI_API_KEY` first and falls back to `OPENAI_API_KEY` for GPT models.
- Remote browser defaults follow the same order: `--remote-host/--remote-token` win, then `remote.host` / `remote.token` (or `remoteHost` / `remoteToken`) in the config, then `ORACLE_REMOTE_HOST` / `ORACLE_REMOTE_TOKEN` if still unset.
- `OPENAI_API_KEY` only influences engine selection when neither the CLI nor `config.json` specify an engine (API when present, otherwise browser).
- `ORACLE_NOTIFY*` env vars still layer on top of the config’s `notify` block.
- `sessionRetentionHours` controls the default value for `--retain-hours`. When unset, `ORACLE_RETAIN_HOURS` (if present) becomes the fallback, and the CLI flag still wins over both.
- `browser.chatgptUrl` accepts either the root ChatGPT URL (`https://chatgpt.com/`) or a folder/workspace URL (e.g., `https://chatgpt.com/g/.../project`); `browser.url` remains as a legacy alias.
- Browser automation defaults can be set under `browser.*`, including `browser.manualLogin`, `browser.manualLoginProfileDir`, and `browser.thinkingTime` (CLI override: `--browser-thinking-time`). On Windows, `browser.manualLogin` defaults to `true` when omitted.

If the config is missing or invalid, Oracle falls back to defaults and prints a warning for parse errors.

Chromium-based browsers usually need both `chromePath` (binary) and `chromeCookiePath` (cookie DB) set so automation can launch the right executable and reuse your login. See [docs/chromium-forks.md](chromium-forks.md) for detailed paths per browser/OS.

## Session retention

Each invocation can optionally prune cached sessions before starting new work:

- `--retain-hours <n>` deletes sessions older than `<n>` hours right before the run begins. Use `0` (or omit the flag) to skip pruning.
- In `config.json`, set `sessionRetentionHours` to apply pruning automatically for every CLI/TUI/MCP invocation.
- Set `ORACLE_RETAIN_HOURS` in the environment to override the config on shared machines without editing the JSON file.

Under the hood, pruning removes entire session directories (metadata + logs). The command-line cleanup command (`oracle session --clear`) still exists when you need to wipe everything manually.

## API timeouts

- `--timeout <seconds|auto>` controls the overall API deadline for a run.
- Defaults: `auto` = 60 m for `gpt-5.1-pro`; non-pro API models use `120s` if you don’t set a value.
- Heartbeat messages print the live remaining time so you can see when the client-side deadline will fire.


## Links discovered
- [docs/chromium-forks.md](https://github.com/steipete/oracle/blob/main/docs/chromium-forks.md)

--- scripts/docs-list.ts ---
#!/usr/bin/env tsx

import { readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { compact } from 'es-toolkit';

const docsListFile = fileURLToPath(import.meta.url);
const docsListDir = dirname(docsListFile);
const DOCS_DIR = join(docsListDir, '..', 'docs');

const EXCLUDED_DIRS = new Set(['archive', 'research']);

function walkMarkdownFiles(dir: string, base: string = dir): string[] {
  const entries = readdirSync(dir, { withFileTypes: true });
  const files: string[] = [];
  for (const entry of entries) {
    if (entry.name.startsWith('.')) {
      continue;
    }
    const fullPath = join(dir, entry.name);
    if (entry.isDirectory()) {
      if (EXCLUDED_DIRS.has(entry.name)) {
        continue;
      }
      files.push(...walkMarkdownFiles(fullPath, base));
    } else if (entry.isFile() && entry.name.endsWith('.md')) {
      files.push(relative(base, fullPath));
    }
  }
  return files.sort((a, b) => a.localeCompare(b));
}

function extractMetadata(fullPath: string): {
  summary: string | null;
  readWhen: string[];
  error?: string;
} {
  const content = readFileSync(fullPath, 'utf8');

  if (!content.startsWith('---')) {
    return { summary: null, readWhen: [], error: 'missing front matter' };
  }

  const endIndex = content.indexOf('\n---', 3);
  if (endIndex === -1) {
    return { summary: null, readWhen: [], error: 'unterminated front matter' };
  }

  const frontMatter = content.slice(3, endIndex).trim();
  const lines = frontMatter.split('\n');

  let summaryLine: string | null = null;
  const readWhen: string[] = [];
  let collectingField: 'read_when' | null = null;

  for (const rawLine of lines) {
    const line = rawLine.trim();

    if (line.startsWith('summary:')) {
      summaryLine = line;
      collectingField = null;
      continue;
    }

    if (line.startsWith('read_when:')) {
      collectingField = 'read_when';
      const inline = line.slice('read_when:'.length).trim();
      if (inline.startsWith('[') && inline.endsWith(']')) {
        try {
          const parsed = JSON.parse(inline.replace(/'/g, '"')) as unknown;
          if (Array.isArray(parsed)) {
            readWhen.push(...compact(parsed.map((item) => String(item).trim())));
          }
        } catch {
          // ignore malformed inline arrays
        }
      }
      continue;
    }

    if (collectingField === 'read_when') {
      if (line.startsWith('- ')) {
        const hint = line.slice(2).trim();
        if (hint) {
          readWhen.push(hint);
        }
      } else if (line === '') {
      } else {
        collectingField = null;
      }
    }
  }

  if (!summaryLine) {
    return { summary: null, readWhen, error: 'summary key missing' };
  }

  const summaryValue = summaryLine.slice('summary:'.length).trim();
  const normalized = summaryValue
    .replace(/^['"]|['"]$/g, '')
    .replace(/\s+/g, ' ')
    .trim();

  if (!normalized) {
    return { summary: null, readWhen, error: 'summary is empty' };
  }

  return { summary: normalized, readWhen };
}

console.log('Listing all markdown files in docs folder:');

const markdownFiles = walkMarkdownFiles(DOCS_DIR);

for (const relativePath of markdownFiles) {
  const fullPath = join(DOCS_DIR, relativePath);
  const { summary, readWhen, error } = extractMetadata(fullPath);
  if (summary) {
    console.log(`${relativePath} - ${summary}`);
    if (readWhen.length > 0) {
      console.log(`  Read when: ${readWhen.join('; ')}`);
    }
  } else {
    const reason = error ? ` - [${error}]` : '';
    console.log(`${relativePath}${reason}`);
  }
}

console.log(
  '\nReminder: keep docs up to date as behavior changes. When your task matches any "Read when" hint above (React hooks, cache directives, database work, tests, etc.), read that doc before coding, and suggest new coverage when it is missing.'
);


--- docs/gemini.md ---
# Gemini Integration

Oracle supports Gemini in two distinct ways:

1. **Gemini API mode** (`--engine api`) via `GEMINI_API_KEY`
2. **Gemini web (cookie) mode** (`--engine browser`) via your signed-in Chrome cookies at `gemini.google.com` (no API key required)

## Usage (API)

1. **Get an API Key:** Obtain a key from [Google AI Studio](https://aistudio.google.com/).
2. **Set Environment Variable:** Export the key as `GEMINI_API_KEY`.
   ```bash
   export GEMINI_API_KEY="your-google-api-key"
   ```
3. **Run Oracle:** Use the `--model` (or `-m`) flag to select Gemini.
   ```bash
   oracle --engine api --model gemini --prompt "Explain quantum entanglement"
   ```
   You can also use the explicit model ID:
   ```bash
   oracle --engine api --model gemini-3-pro --prompt "..."
   ```

## Usage (Gemini web / cookies)

Gemini web mode is a cookie-based client for `gemini.google.com`. It does **not** use `GEMINI_API_KEY` and does **not** drive ChatGPT.

Prereqs:
- Chrome installed.
- Signed into `gemini.google.com` in the Chrome profile Oracle uses (default: `Default` profile).

Examples:
```bash
# Text run
oracle --engine browser --model gemini-3-pro --prompt "Say OK."

# Generate an image (writes an output file)
oracle --engine browser --model gemini-3-pro \
  --prompt "a cute robot holding a banana" \
  --generate-image out.jpg --aspect 1:1

# Edit an image (input via --edit-image, output via --output)
oracle --engine browser --model gemini-3-pro \
  --prompt "add sunglasses" \
  --edit-image in.png --output out.jpg
```

Notes:
- If your logged-in Gemini account can’t access “Pro”, Oracle will auto-fallback to a supported model for web runs (and logs the fallback in verbose mode).
- This path runs fully in Node/TypeScript (no Python/venv dependency).
- `--browser-model-strategy` only affects ChatGPT automation; Gemini web always uses the explicit Gemini model ID.

## Implementation details

### Gemini API adapter

- `src/oracle/gemini.ts` — adapter using `@google/genai` that returns a `ClientLike`.
  - Model IDs: `gemini-3-pro` maps to the provider ID (currently `gemini-3-pro-preview`).
  - Request mapping: `OracleRequestBody` → Gemini request; `web_search_preview` maps to Gemini search tooling.
  - Response mapping: Gemini responses → `OracleResponse`.
  - Streaming: wraps Gemini’s async iterator as `ResponseStreamLike`.
- `src/oracle/run.ts` — selects `GEMINI_API_KEY` vs `OPENAI_API_KEY` based on model prefix.
- `src/oracle/config.ts` / `src/oracle/types.ts` — model config + `ModelName`.

### Gemini web client (cookie-based)

- `src/gemini-web/client.ts` — talks to `gemini.google.com` and downloads generated images via authenticated `gg-dl` redirects.
- `src/gemini-web/executor.ts` — browser-engine executor for Gemini (loads Chrome cookies and runs the web client).

## Testing

- Unit/regression: `pnpm vitest run tests/gemini.test.ts tests/gemini-web`
- Live (API): `ORACLE_LIVE_TEST=1 pnpm vitest run tests/live/gemini-live.test.ts`
- Live (Gemini web/cookies): `ORACLE_LIVE_TEST=1 pnpm vitest run tests/live/gemini-web-live.test.ts`


## Links discovered
- [Google AI Studio](https://aistudio.google.com/)

--- docs/grok.md ---
# Grok 4.1 (xAI) Support

Status: **experimental** (November 21, 2025)  
Owner: Oracle CLI

- Model key: `grok-4.1` (mapped to API id `grok-4-1-fast-reasoning`). Alias: `grok`.
- Endpoint: defaults to `https://api.x.ai/v1` or `XAI_BASE_URL`. Uses the OpenAI **Responses API** surface.
- Auth: `XAI_API_KEY`.
- Background runs: **not supported** by the Grok API (requests with `background: true` are rejected). Oracle forces foreground streaming even if `--background` is set.
- Search tools: Grok expects `web_search`; OpenAI’s `web_search_preview` is not accepted.
- Pricing (preview): $0.20 / 1M input tokens, $0.50 / 1M output tokens; 2M token context.

Notes:
- If you supply `--base-url`, it overrides the default xAI endpoint.
- Browser engine is not supported for Grok; Oracle coerces `--engine browser` to `api`.


--- docs/linux.md ---
# Linux Notes

- Browser engine now works on Linux (Chrome/Chromium/Edge) without the old `DISPLAY` guard. Oracle will launch whatever `chrome-launcher` finds or what you pass via `CHROME_PATH`.
- Cookie sync supports snap-installed Chromium automatically. Common cookie DB for the Default profile:
  - `~/snap/chromium/common/chromium/Default/Cookies`
- If you use a non-default profile or a custom install, point Oracle at the correct paths:
  - `--browser-chrome-path /path/to/chrome`
  - `--browser-cookie-path /path/to/profile/Default/Cookies`
- Browser runs are headful (Cloudflare blocks headless). Keep a compositor/virtual display running if you don’t have a desktop session.
- If cookie sync still can’t find your DB, rerun with `--browser-allow-cookie-errors --browser-no-cookie-sync` and sign in manually, or dump the session cookies with `--browser-inline-cookies-file`.


--- docs/manual-tests.md ---
# Manual Test Suite (Browser Mode + Live API)

These checks validate the real Chrome automation path and the optional live
Responses API smoke suite. Run the browser steps whenever you touch Chrome
automation (lifecycle, cookie sync, prompt injection, Markdown capture, etc.),
and run the live API suite before shipping major transport changes.

## Prerequisites

- macOS with Chrome installed (default profile signed in to ChatGPT Pro).
- Node 22+ and `pnpm install` already completed.
- Headful display access (no `--browser-headless`).
- When debugging, add `--browser-keep-browser` so Chrome stays open after Oracle exits, then connect with `pnpm exec tsx scripts/browser-tools.ts ...` (screenshot, eval, DOM picker, etc.).
- Ensure no Chrome instances are force-terminated mid-run; let Oracle clean up once you’re done capturing state.
- Clipboard checks (`browser-tools.ts eval "navigator.clipboard.readText()"`) trigger a permission dialog in Chrome—approve it for debugging, but remember that we can’t rely on readText in unattended runs.

## Test Cases

### Quick browser port smoke

- `pnpm test:browser` — launches headful Chrome and checks the DevTools endpoint is reachable. Set `ORACLE_BROWSER_PORT` (or `ORACLE_BROWSER_DEBUG_PORT`) to reuse a fixed port when you’ve already opened a firewall rule.

### Gemini browser mode (Gemini web / cookies)

Run this whenever you touch the Gemini web client or the `--generate-image` / `--edit-image` plumbing.

Prereqs:
- Chrome profile is signed into `gemini.google.com`.

1. Generate an image:
   `pnpm run oracle -- --engine browser --model gemini-3-pro --prompt "a cute robot holding a banana" --generate-image /tmp/gemini-gen.jpg --aspect 1:1 --wait --verbose`
   - Confirm the output file exists and is a real image (`file /tmp/gemini-gen.jpg`).
2. Edit an image:
   `pnpm run oracle -- --engine browser --model gemini-3-pro --prompt "add sunglasses" --edit-image /tmp/gemini-gen.jpg --output /tmp/gemini-edit.jpg --wait --verbose`
   - Confirm `/tmp/gemini-edit.jpg` exists.

### Multi-Model CLI fan-out

Run this whenever you touch the session store, CLI session views, or TUI wiring for multi-model runs.

1. Kick off an API multi-run:  
   `pnpm run oracle -- --models "gpt-5.1-pro,gemini-3-pro" --prompt "Compare the moon & sun."`
   - Expect stdout to print sequential sections, one per model (`[gpt-5.1-pro] …` followed by `[gemini-3-pro] …`). No interleaved tokens.
2. Capture the session ID from the summary line. Run `oracle session --status --model gpt-5.1-pro`.  
   - Table should collapse to sessions that include GPT-5.1 Pro and show status icons (✓/⌛/✖) per model.
3. Inspect detailed logs: `oracle session <id>`
   - The metadata header now includes a `Models:` block with one line per model plus token counts.
   - When prompted, pick `View gemini-3-pro log` and confirm only that model’s stream renders. Refresh should keep completed models intact even if others still run.
4. Model filter path: `oracle session <id> --model gemini-3-pro`  
   - Attach mode should error if that model is missing (double-check by filtering for a bogus model), otherwise it should render the prompt + single-model log only.

### Write-output export (API)

Run this when touching session serialization, file IO helpers, or CLI flag plumbing.

1. `ORACLE_LIVE_TEST=1 OPENAI_API_KEY=<real key> pnpm vitest run tests/live/write-output-live.test.ts --runInBand`
   - Expect the test to create a temp `write-output-live.md` file containing `write-output e2e`.
2. Manual spot-check: `oracle --prompt "answer file smoke" --write-output /tmp/out.md --wait`
   - Confirm `/tmp/out.md` exists with the answer text and a trailing newline.
3. Multi-model spot-check: `oracle --models "gpt-5.1-pro,gemini-3-pro" --prompt "two files" --write-output /tmp/out.md --wait`
   - Confirm `/tmp/out.gpt-5.1-pro.md` and `/tmp/out.gemini-3-pro.md` exist with distinct content.

### Lightweight Browser CLI (manual exploration)

Before running any agent-driven debugging, you can rely on the TypeScript CLI in `scripts/browser-tools.ts`:

```bash
# Show help / available commands
pnpm tsx scripts/browser-tools.ts --help

# Launch Chrome with your normal profile so you stay logged in
pnpm tsx scripts/browser-tools.ts start --profile

# Drive the active tab
pnpm tsx scripts/browser-tools.ts nav https://example.com
pnpm tsx scripts/browser-tools.ts eval 'document.title'
pnpm tsx scripts/browser-tools.ts screenshot
pnpm tsx scripts/browser-tools.ts pick "Select checkout button"
pnpm tsx scripts/browser-tools.ts cookies
pnpm tsx scripts/browser-tools.ts inspect   # show DevTools-enabled Chrome PIDs/ports/tabs
pnpm tsx scripts/browser-tools.ts kill --all --force   # tear down straggler DevTools sessions
```

This mirrors Mario Zechner’s “What if you don’t need MCP?” technique and is handy when you just need a few quick interactions without spinning up additional tooling.

1. **Prompt Submission & Model Switching**
   - With Chrome signed in and cookie sync enabled, run  
     ```bash
     pnpm run oracle -- --engine browser --model "GPT-5.2" \
       --prompt "Line 1\nLine 2\nLine 3"
     ```
   - Observe logs for:
     - `Prompt textarea ready (xxx chars queued)` (twice: initial + after model switch).
     - `Model picker: ... 5.2 ...`.
     - `Clicked send button` (or Enter fallback).
   - In the attached Chrome window, verify the multi-line prompt appears exactly as sent.

2. **Markdown Capture**
   - Prompt:
     ```bash
     pnpm run oracle -- --engine browser --model "GPT-5.2" \
       --prompt "Produce a short bullet list with code fencing."
     ```
   - Expected CLI output:
     - `Answer:` section containing bullet list with Markdown preserved (e.g., `- item`, fenced code).
     - Session log (`oracle session <id>`) should show the assistant markdown (confirm via `grep -n '```' ~/.oracle/sessions/<id>/output.log`).

3. **Stop Button Handling**
  - Start a long prompt (`"Write a detailed essay about browsers"`) and once ChatGPT responds, manually click “Stop generating” inside Chrome.
  - Oracle should detect the assistant message (partial) and still store the markdown.

4. **Override Flag**
  - Run with `--browser-allow-cookie-errors` while intentionally breaking bindings.
  - Confirm log shows `Cookie sync failed (continuing with override)` and the run proceeds headless/logged-out.
- Remember: the browser composer now pastes only the user prompt (plus any inline file blocks). If you see the default “You are Oracle…” text or other system-prefixed content in the ChatGPT composer, something regressed in `assembleBrowserPrompt` and you should stop and file a bug.
- Heartbeats: Browser runs do **not** emit `--heartbeat` logs today. Heartbeat settings apply to streaming API runs only; ignore heartbeat toggles when validating browser mode.

## Post-Run Validation

- `oracle session <id>` should replay the transcript with markdown.
- `~/.oracle/sessions/<id>/meta.json` must include `browser.config` metadata (model label, cookie settings) and `browser.runtime` (PID/port).

Document results (pass/fail, session IDs) in PR descriptions so reviewers can audit real-world behavior.

## Recent Smoke Runs

- 2025-11-18 — API gpt-5.1 (`api-smoke-give-two-words`): returned “blue sky” in 2.5s.
- 2025-11-18 — API gpt-5.1-pro (`api-smoke-pro-three-words`): completed in 3m08s with “Fast API verification”.
- 2025-11-18 — Browser gpt-5.1 Instant (`browser-smoke-instant-two-words`): completed in ~10s; replied with a clarification prompt.
- 2025-11-18 — Browser gpt-5.1-pro (`browser-smoke-pro-three-words`): completed in ~1m33s; response noted “Search tool used.”.
- 2025-11-18 (rerun) — API gpt-5.1 (`api-smoke-give-two-words`): reconfirmed OK; same answer + cost bracket.
- 2025-11-18 (rerun) — Browser gpt-5.1-pro (`browser-smoke-pro-three-words`): reconfirmed OK; included heartbeat progress and search tool note.
- 2025-11-20 — Browser gpt-5.1 via `oracle serve` (remote host on same Mac): fetched https://example.com; title “Example Domain”; first sentence “This domain is for use in documentation examples without needing permission.” (ran via tmux sessions `oracle-serve` and `oracle-client`).

## Browser Regression Checklist (manual)

Run these four smoke tests whenever we touch browser automation:

1. **GPT-5.2 simple prompt**  
   `pnpm run oracle -- --engine browser --model "GPT-5.2" --prompt "Give me two short markdown bullet points about tables"`  
   Expect two markdown bullets, no files/search referenced. Note the session ID (e.g., `give-me-two-short-markdown`).

2. **GPT-5.2 simple prompt**  
   `pnpm run oracle -- --engine browser --model gpt-5.2 --prompt "List two reasons Markdown is handy"`  
   Confirm the answer arrives (and only once) even if it takes ~2–3 minutes.

3. **GPT-5.2 + attachment**  
   Prepare `/tmp/browser-md.txt` with a short note, then run  
   `pnpm run oracle -- --engine browser --model "GPT-5.2" --prompt "Summarize the key idea from the attached note" --file /tmp/browser-md.txt`  
   Ensure upload logs show “Attachment queued” and the answer references the file contents explicitly.

4. **GPT-5.2 + attachment (verbose)**  
   Prepare `/tmp/browser-report.txt` with faux metrics, then run  
   `pnpm run oracle -- --engine browser --model gpt-5.2 --prompt "Use the attachment to report current CPU and memory figures" --file /tmp/browser-report.txt --verbose`  
   Verify verbose logs show attachment upload and the final answer matches the file data.

Record session IDs and outcomes in the PR description (pass/fail, notable delays). This ensures reviewers can audit real runs.

### Remote Chrome smoke test (CDP)

Run this whenever you touch CDP connection logic (remote chrome lifecycle, attachment transfer) or before executing remote sessions in CI.

1. Launch a throwaway Chrome instance with remote debugging enabled (adjust the path per OS):
   ```bash
   REMOTE_PROFILE=/tmp/oracle-remote-test-profile
   rm -rf "$REMOTE_PROFILE"
   "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
     --headless=new \
     --disable-gpu \
     --remote-debugging-port=9333 \
     --remote-allow-origins=* \
     --user-data-dir="$REMOTE_PROFILE" \
     >/tmp/oracle-remote-chrome.log 2>&1 &
   export REMOTE_CHROME_PID=$!
   sleep 3
   ```
2. Run the helper to verify CDP connectivity:
   ```bash
   pnpm tsx scripts/test-remote-chrome.ts localhost 9333
   ```
   Expect ✓ logs for connection, protocol info, navigation to https://chatgpt.com/, and the final “POC successful!” line.
3. Tear down the temporary browser:
   ```bash
   kill "$REMOTE_CHROME_PID"
   rm -rf "$REMOTE_PROFILE"
   ```
   Use `pkill -f oracle-remote-test-profile` if Chrome refuses to exit cleanly.

Capture the pass/fail result (include the helper’s log snippet) in your PR description alongside other manual browser tests.

## Chrome DevTools / MCP Debugging

Use this when you need to inspect the live ChatGPT composer (DOM state, markdown text, screenshots, etc.). For smaller ad‑hoc pokes, you can often rely on `pnpm tsx scripts/browser-tools.ts …` instead.

1. **Launch within tmux**
   ```bash
   tmux new -d -s oracle-browser \\
     "pnpm run oracle -- --engine browser --browser-keep-browser \\
       --model 'GPT-5.2 Pro' --prompt 'Debug via DevTools.'"
   ```
   Keeping the run in tmux prevents your shell from blocking and ensures Chrome stays open afterward.

2. **Grab the DevTools port**
   - `tmux capture-pane -pt oracle-browser` to read the logs (`Launched Chrome … on port 56663`).
   - Verify the endpoint:
     ```bash
     curl http://127.0.0.1:<PORT>/json/version
     ```
     Note the `webSocketDebuggerUrl` for reference.

3. **Attach Chrome DevTools MCP**
   - One-off: `CHROME_DEVTOOLS_URL=http://127.0.0.1:<PORT> npx -y chrome-devtools-mcp@latest`
   - `mcporter` config snippet:
     ```json
     {
       "chrome-devtools": {
         "command": "npx",
         "args": [
           "-y",
           "chrome-devtools-mcp@latest",
           "--browserUrl",
           "http://127.0.0.1:<PORT>"
         ]
       }
     }
     ```
   - Once the server prints `chrome-devtools-mcp exposes…`, you can list/call tools via `mcporter`.

4. **Interact & capture**
   - Use MCP tools (`click`, `evaluate_js`, `screenshot`, etc.) to debug the composer contents.
   - Record any manual actions you take (e.g., “fired evaluate_js to dump #prompt-textarea.innerText”).

5. **Cleanup**
   - `tmux kill-session -t oracle-browser`
   - `pkill -f oracle-browser-<slug>` if Chrome is still running.

> **Tip:** Running `npx chrome-devtools-mcp@latest --help` lists additional switches (custom Chrome binary, headless, viewport, etc.).

## Responses API Live Smoke Tests

These Vitest cases hit the real OpenAI API to exercise both transports:

1. Export a real key and explicitly opt in (default runs stay fast):
   ```bash
   export OPENAI_API_KEY=sk-...
   export ORACLE_LIVE_TEST=1
   pnpm vitest run tests/live/openai-live.test.ts
   ```
2. The first test sends a `gpt-5.1-pro` prompt (API: `gpt-5.2-pro`) and expects the CLI to stay
   in background mode until OpenAI finishes (up to 30 minutes). The second test
   targets the standard GPT-5 (`gpt-5.1`) path with foreground streaming.
3. Watch the console for `Reconnected to OpenAI background response...` if
   you're debugging transport flakiness; the test will fail if the response
   status isn't `completed` or if the text doesn't contain the hard-coded
   smoke strings.

Skip these unless you're intentionally validating the production API; they are
fully gated behind `ORACLE_LIVE_TEST=1` to avoid accidental CI runs.


--- docs/mcp.md ---
# MCP Server

`oracle-mcp` is a minimal MCP stdio server that mirrors the Oracle CLI. It shares session storage with the CLI (`~/.oracle/sessions` or `ORACLE_HOME_DIR`) so you can mix and match: run with the CLI, inspect or re-run via MCP, or vice versa.

## Tools

### `consult`
- Inputs: `prompt` (required), `files?: string[]` (globs), `model?: string` (defaults to CLI), `engine?: "api" | "browser"` (CLI auto-defaults), `slug?: string`.
- Behavior: starts a session, runs it with the chosen engine, returns final output + metadata. Background/foreground follows the CLI (e.g., GPT‑5 Pro detaches by default).
- Logging: emits MCP logs (`info` per line, `debug` for streamed chunks with byte sizes). If browser prerequisites are missing, returns an error payload instead of running.

### `sessions`
- Inputs: `{id?, hours?, limit?, includeAll?, detail?}` mirroring `oracle status` / `oracle session`.
- Behavior: without `id`, returns a bounded list of recent sessions. With `id`/slug, returns a summary row; set `detail: true` to fetch full metadata, log, and stored request body.

## Resources
- `oracle-session://{id}/{metadata|log|request}` — read-only resources that surface stored session artifacts via MCP resource reads.

## Background / detach behavior
- Same as the CLI: heavy models (e.g., GPT‑5 Pro) detach by default; reattach via `oracle session <id>` / `oracle status`. MCP does not expose extra background flags.

## Launching & usage
- Installed from npm:
  - One-off: `npx @steipete/oracle oracle-mcp`
  - Global: `oracle-mcp`
- From the repo (contributors):
  - `pnpm build`
  - `pnpm mcp` (or `oracle-mcp` in the repo root)
- mcporter example (stdio):
  ```json
  {
    "name": "oracle",
    "type": "stdio",
    "command": "npx",
    "args": ["@steipete/oracle", "oracle-mcp"]
  }
  ```
- Project-scoped Claude (.mcp.json) example:
  ```json
  {
    "mcpServers": {
      "oracle": { "type": "stdio", "command": "npx", "args": ["@steipete/oracle", "oracle-mcp"] }
    }
  }
  ```
- Tools and resources operate on the same session store as `oracle status|session`.
- Defaults (model/engine/etc.) come from your Oracle CLI config; see `docs/configuration.md` or `~/.oracle/config.json`.


--- types/win-dpapi.d.ts ---
declare module 'win-dpapi' {
  export type Scope = 'CurrentUser' | 'LocalMachine';

  export interface DpapiModule {
    unprotectData(encrypted: Buffer, optionalEntropy: Buffer | null, scope: Scope): Buffer;
  }

  const dpapi: DpapiModule;
  export = dpapi;
}


--- CHANGELOG.md ---
# Changelog

## 0.8.4 — Unreleased

### Changed
- Deps: update zod to `4.3.3`.
- Deps: add `qs` as a direct dependency (avoids Dependabot pnpm transitive-update failures).

## 0.8.3 — 2025-12-31

### Added
- Config: allow `browser.forceEnglishLocale` to opt into `--lang/--accept-lang` for browser runs.
- Browser: add `--browser-cookie-wait` / `browser.cookieSyncWaitMs` to wait once and retry cookie sync. Original PR #55 by bheemreddy-samsara — thank you!

### Fixed
- Browser: avoid stray attachment removal clicks while still detecting stale chips, and allow completed uploads even if send stays disabled. Original PR #56 by Alex Naidis (@TheCrazyLex) — thank you!
- Browser: dismiss blocking modals when a custom ChatGPT project URL is missing, and harden attachment uploads (force input/change events; retry via DataTransfer; treat “file selected” as insufficient unless the composer shows attachment UI).
- Browser: prefer a trusted (CDP) click on the composer “+” button so attachment uploads work even when ChatGPT ignores synthetic clicks.

## 0.8.2 — 2025-12-30

### Changed
- Release: disable npm progress output in Codex runs via `scripts/release.sh`.

### Docs
- Release checklist now requires GitHub release notes to match the full changelog section.

### Tests
- Live: tolerate truncated prompt echo in browser model selection checks.
- Live: skip mixed OpenRouter assertions when a provider returns empty output.
- Live: wait for browser runtime hint before reattaching in the reattach smoke.

## 0.8.1 — 2025-12-30

### Added
- Config: allow `browser.thinkingTime`, `browser.manualLogin`, and `browser.manualLoginProfileDir` defaults in `~/.oracle/config.json`.

### Fixed
- Browser: thinking-time chip selection now recognizes "Pro" labeled composer pills. Original PR #54 by Alex Naidis (@TheCrazyLex) — thank you!
- Browser: when a custom ChatGPT project URL is missing, retry on the base URL with a longer prompt timeout.
- Browser: increase attachment wait budget and proceed with sending the prompt if completion times out (skip attachment gating/verification).
- CLI: disable OSC progress output when running under Codex (`CODEX_MANAGED_BY_NPM=1`) to avoid spinner noise.

### Tests
- Stabilize OSC progress detection tests when `CODEX_MANAGED_BY_NPM=1` is set.
- Add fast live browser runs for missing-project fallback + attachment uploads (`test:live:fast`).

## 0.8.0 — 2025-12-28

### Highlights
- Browser reliability push: stronger reattach, response capture, and attachment uploads (fewer prompt-echoes, truncations, and duplicate uploads).
- Cookie stack revamp via Sweet Cookie (no native addons) with better inline-cookie handling; Gemini web now works on Windows and honors `--browser-cookie-path`.
- New `--browser-model-strategy` flag to control ChatGPT model selection (`select`/`current`/`ignore`) in browser mode. Original PR #49 by @djangonavarro220 — thank you!

### Improvements
- Browser reattach now preserves `/c/` conversation URLs and project URL prefixes, validates conversation ids, and recovers from mid-run disconnects or capture failures.
- Response capture is more stable: wider selectors, assistant-only copy-turn capture, prompt-echo avoidance, and stop-button/clipboard stability checks.
- Attachment uploads are idempotent and count-aware (composer + chips + file inputs), with explicit completion waits and stale-input cleanup.
- Login flow adds richer diagnostics, auto-accepts the “Welcome back” picker, and always logs the active ChatGPT URL.
- Cookie handling prefers live Chrome over legacy `~/.oracle/cookies.json`; Gemini web can use inline cookies when sync is disabled.

### Fixes
- CLI: stream Markdown via Markdansi’s block renderer and guard the live renderer for non‑TTY edge cases.
- Tests: stabilize browser live tests (serialization + project URL fallback) and add response-observer assertions; browser smoke runs are faster.

## 0.7.6 — 2025-12-25

### Changed
- CLI: compact finish line summary across API, browser, and session views.
- CLI: token counts now render as `↑in ↓out ↻reasoning Δtotal`.

### Fixed
- CLI/Browser: ignore duplicate `--file` inputs (log once) and improve attachment presence detection so re-runs don’t spam “already attached” upload errors.
- Browser: harden session reattach (better conversation targeting, longer prompt-commit wait, avoid closing shared DevTools targets).
- Live tests: add coverage + retries for browser reattach/model selection; tolerate transient OpenRouter free-tier failures.

## 0.7.5 — 2025-12-23

### Fixed
- Packaging: switch tokentally to npm release so Homebrew installs don't trigger git prepare builds.

## 0.7.4 — 2025-12-23

### Changed
- Browser: add `--browser-thinking-time <light|standard|extended|heavy>` to select thinking-time intensity in ChatGPT.

### Fixed
- Browser: throttle attachment upload pokes and pace multi-file uploads to avoid duplicate “already attached” warnings.
- Browser: correct GPT-5.2 variant selection (Auto/Thinking/Instant/Pro) with stricter matching and improved testid scoring; thinking-time selection now supports multiple levels. Original PR #45 by Manish Malhotra (@manmal) — thank you!
- Browser: only reload stalled conversations after an assistant-response failure (and only once), instead of always refreshing after submit.

## 0.7.3 — 2025-12-23

### Changed
- API: streaming answers in a rich TTY now use Markdansi’s live renderer (`createLiveRenderer`) so we can stream *and* render Markdown in-place.

### Fixed
- Browser: prevent `chrome-launcher` from auto-killing Chrome on SIGINT so reattach sessions survive Ctrl+C.
- Sessions: running browser sessions now mark as errored when the Chrome PID/port are no longer reachable.
- Browser: reattach now recovers even if Chrome was closed by reopening, locating the conversation in the sidebar, and resuming the response.

## 0.7.2 — 2025-12-17

### Fixed
- Browser: stop auto-clicking the “Answer now” gate; wait for the full Pro-thinking response instead of skipping it.
- Browser: reject `?temporary-chat=true` URLs when targeting Pro models (Pro picker entries are not available in Temporary Chat); error message now calls this out explicitly.
- Browser: attachment uploads re-trigger the file-input change event until ChatGPT renders the attachment card (avoids hydration races); verify attachments are present on the sent user message before waiting for the assistant.
- Live tests: make the `gpt-5.2-instant` OpenAI smoke test resilient to transient API stalls/errors.

## 0.7.1 — 2025-12-17

### Changed
- API: default model is now `gpt-5.2-pro` (and “Pro” label inference prefers GPT‑5.2 Pro).
- Tests: updated fixtures/defaults to use `gpt-5.2-pro` instead of `gpt-5.1-pro`.
- API: clarify `gpt-5.1-pro` as a stable alias that targets `gpt-5.2-pro`.
- Browser: browser engine GPT selection now supports ChatGPT 5.2 (`gpt-5.2`) and ChatGPT 5.2 Pro (`gpt-5.2-pro`); legacy labels like `gpt-5.1` normalize to 5.2, and “Pro” always resolves to 5.2 Pro (ignores Legacy GPT‑5.1 Pro submenu) with a top-bar label confirmation.

### Fixed
- Browser: prompt commit verification handles markdown code fences better; prompt-echo recovery is more robust (including remote browser mode); multi-file uploads are less flaky (dynamic timeouts + better filename matching). Original PR #41 by Muly Oved (@mulyoved) — thank you!
- Browser: adapt to ChatGPT DOM changes (`data-turn=assistant|user`) and “Answer now” gating in Pro thinking so we don’t capture placeholders/truncate answers.
- Gemini web: add abortable timeouts + retries for cookie-based runs so live tests are less likely to hang on transient Gemini web responses.

## 0.7.0 — 2025-12-14

### Added
- Browser: Gemini browser mode via direct Gemini web client (uses Chrome cookies; no API key required; runs fully in Node/TypeScript — no Python/venv). Includes `--youtube`, `--generate-image`, `--edit-image`, `--output`, `--aspect`, and `--gemini-show-thoughts`. Original PR #39 by Nico Bailon (@nicobailon) — thank you!
- Browser: media files passed via `--file` (images/video/audio/PDF) are treated as upload attachments instead of being inlined into the prompt (enables Gemini file analysis).
- Browser: Gemini image ops follow `gg-dl` redirects while preserving cookies, so `--generate-image`/`--edit-image` actually create output files.
- Browser: Gemini web runs support “Pro” auto-fallback when unavailable and include compatibility init for Gemini web token changes.
- Live tests: add opt-in Gemini web smoke coverage for image generation/editing (cookie-based browser mode).

### Changed
- Browser guard now allows Gemini models (browser engine supports GPT + Gemini; other models require `--engine api`).

## 0.6.1 — 2025-12-13

### Changed
- Browser: default model target now prefers ChatGPT 5.2. Original PR #40 by Muly Oved (@mulyoved) — thank you!
- Browser: remove the “browser fallback” API retry suggestion to avoid accidental billable reruns. Idea from PR #38 by Nico Bailon (@nicobailon) — thank you!

### Fixed
- Browser: manual-login runs now reuse an already-running Chrome more reliably (persist DevTools port in the profile; probe with retries; clean up stale port state). Original PR #40 by Muly Oved (@mulyoved) — thank you!
- Browser: response capture is less likely to truncate by mistaking earlier turns as complete; completion detection is scoped to the last assistant turn and requires brief stability before capture. Original PR #40 by Muly Oved (@mulyoved) — thank you!
- Browser: stale profile cleanup avoids deleting lock files when an active Chrome process is using the profile.

## 0.6.0 — 2025-12-12

### Added
- GPT-5.2 model support (`gpt-5.2` Thinking, `gpt-5.2-instant`, `gpt-5.2-pro`) plus browser thinking-time automation. Original PR #37 by Nico Bailon (@nicobailon) — thank you!

### Changed
- API: `gpt-5.1-pro` now targets `gpt-5.2-pro` instead of older Pro fallbacks.
- Browser: “Thinking time → Extended” selection now reuses centralized menu selectors, normalizes text matching, and ships a best-effort helper for future “auto” mode. Original PR #36 by Victor Vannara (@voctory) — thank you!
- Browser: new `--browser-attachments <auto|never|always>` (default `auto`) pastes file contents inline up to ~60k characters, then switches to uploads; if ChatGPT rejects an inline paste as too large, Oracle retries automatically with uploads.
  - Note: the ~60k threshold is based on pasted **characters** in the ChatGPT composer (not token estimates); on rejection we log the retry and switch to uploads automatically.

## 0.5.6 — 2025-12-09 (re-release of 0.5.5)

### Changed
- Browser uploads: after `setFileInputFiles` we now log the chips + file-input contents and only mark success when the real file input contains the uploaded filename; the generic “Files” pill is no longer treated as proof of attachment.
- Inline prompt commit: verification now matches on a normalized prefix and logs the last user turn + counts when commit fails, reducing false negatives for inline/file-paste runs.

### Fixed
- Inline fallback (pasting file contents) now reliably submits and captures the user turn; headful smoke confirms the marker text is echoed back.

## 0.5.4 — 2025-12-08

### Changed
- Docs: README now explicitly warns against `pnpx @steipete/oracle` (pnpx cache breaks sqlite bindings); use `npx -y @steipete/oracle` instead. Thanks Xuanwo for flagging this.
- Browser uploads: stick to the single reliable file-input path (no drag/drop fallbacks), wait for the composer to render the new “N files” pill/remove-card UI before sending, and prefer non-image inputs. Thanks Peter for the repros and screenshots that caught the regressions.

### Fixed
- API fallback: gpt-5.1-pro API runs now automatically downgrade to gpt-5.0-pro with a one-line notice (5.1 Pro is not yet available via API).
- Browser uploads: detect ChatGPT’s composer attachment chip (not echoed in the last user turn) to avoid false “Attachment did not appear” failures. Thanks Mariano Belinky (@mbelinky) for the fix.
- Browser interruption: if the user/agent sends SIGINT/SIGTERM/SIGQUIT while the assistant response is still pending, Oracle leaves Chrome running, writes runtime hints, and logs how to reattach with `oracle session <slug>` instead of killing the browser mid-run.
- Browser uploads (ChatGPT UI 2025-12): wait for DOM ready, avoid duplicate uploads, and block Send until the attachment chip/file name (or “N files” pill) is visible so files aren’t sent empty or multiple times.
- Browser i18n: stop-button detection now uses data-testid instead of English `aria-label`; send/input/+ selectors favor data-testid/structural cues to work across localized UIs.

## 0.5.3 — 2025-12-06

### Changed
- `oracle` with no arguments now prints the help/usage banner; launch the interactive UI explicitly via `oracle tui` (keeps `ORACLE_FORCE_TUI` for automation/tests). README updated to match.
- TUI exits gracefully when the terminal drops raw mode (e.g., `setRawMode EIO` after pager issues) instead of looping the paging error; prints a hint to run `stty sane`.
- Ctrl+C in the TUI menu now exits cleanly without printing the paging error loop.
- Exit banner is printed once when leaving the TUI (prevents duplicate “Closing the book” messages after SIGINT or exit actions).

## 0.5.2 — 2025-12-06

### Changed
- Updated Inquirer to 13.x and aligned TUI prompts with `select` to stay compatible with the latest API.
- Browser click automation now uses a shared pointer/mouse event sequence for send/model/copy/stop buttons, improving reliability with React/ProseMirror UIs. Original fix by community contributor Mike Demarais in PR #30—thank you!

### Fixed
- Browser config defaults from `~/.oracle/config.json` now apply when CLI flags are untouched (chromePath/profile/cookiePath), fixing “No Chrome installations found” when a custom browser path is configured.
- Browser engine now verifies each attachment shows up in the composer before sending (including remote/serve uploads), fixing cases where file selection succeeded but ChatGPT never received the files (e.g., WKWebView blank runs).

## 0.5.1 — 2025-12-03

### Added
- Browser runs now auto-click the ChatGPT “Answer now” gate after sending, so workspace prompts continue without manual intervention.

### Changed
- `oracle status` uses the same session table formatting as the TUI (status/model/mode/timestamp/chars/cost/slug) for consistent layout.
- Browser mode inserts a 500 ms settle before submitting prompts and after clicking gates to avoid subscription/widget races.
- OpenRouter paths route through the chat/completions API (Responses API avoided); live smokes use `z-ai/glm-4.6`, and the mixed run covers Grok fast path without skips.
- Docs/guardrails: AGENTS explains sqlite/keytar rebuilds for Node 25 browser runs; changelog notes the browser cookie-sync guard.

### Fixed
- Browser mode fails fast when cookie sync copies zero cookies (e.g., keytar not built); the error names the Chrome profile and rebuild command instead of silently hanging.

## 0.5.0 — 2025-11-25

### Added
- Browser sessions now persist Chrome reattach hints (port/host/target/url) and log them inline; `oracle session <id>` can reconnect to a live tab, harvest the assistant turn, and mark the run completed even if the original controller died. Includes a reconnection helper and regression tests for runtime hint capture and reattach.
- OpenRouter support: `OPENROUTER_API_KEY` auto-routes API runs (when provider keys are missing or the base URL points at OpenRouter), accepts arbitrary model ids (`minimax/minimax-m2`, `z-ai/glm-4.6`, etc.), mixes with built-in models in `--models`, passes attribution headers (`OPENROUTER_REFERER`/`OPENROUTER_TITLE`), and stores per-model logs with safe slugs.
- `pnpm test:browser` runs a Chrome DevTools connectivity check plus headless browser smokes across GPT-5.1 / GPT-5.1-Pro / 5.1 Instant.

### Changed
- All API errors now surface as transport reason `api-error` with the raw message and are shown in status/render/TUI; verbose mode still prints transport details. Multi-model callback order test stabilized.
- Default system prompt no longer asks models to announce when the search tool was used.
- API now surfaces a clear error when `gpt-5.1-pro` isn’t available yet (suggests using `gpt-5-pro`); remove once OpenAI enables the model.
- Dependency refresh: openai 6.9.1, clipboardy 5, Vitest 4.0.13 (+ coverage), Biome 2.3.7, puppeteer-core 24.31.0, devtools-protocol 0.0.1548823; pinned zod-to-json-schema to 3.24.1 to stay compatible with zod 3.x.

### Fixed
- CLI/TUI now print the intro banner only once; forced TUI launches (`ORACLE_FORCE_TUI` or no args in a TTY) no longer show duplicate 🧿 header lines.
- TUI session list cleans up separators, removing the `__disabled__ (Disabled)` placeholder and `(Disabled)` tag on the header row.
- `oracle session --render` no longer drops answers when the model filter is empty or per-model logs are missing (common for browser runs); stored session output is rendered again.
- Browser uploads no longer time out in ChatGPT project workspaces: file input/send-button selectors are broader, upload completion falls back to attached files when buttons are missing, and we added tests to guard the new selectors.
- Live tests now call out that `gpt-5.1` must be reached via api.openai.com; OpenRouter’s Responses API endpoint doesn’t expose `openai/gpt-5.1`, so runs will fail there with `model_not_found` until they add it.
- Browser reattach flow survives controller loss: the controller PID is persisted with the Chrome port/URL so `oracle session <id>` can reconnect, harvest the assistant turn, and mark the run completed even if the original process died.
- Live multi-model smokes force first-party API bases and soft-skip HTML/transport errors (e.g., proxy 404 pages) so missing provider access doesn’t fail the suite.
- Gemini live coverage confirmed with `gemini-2.5-flash-lite` after refreshing `GEMINI_API_KEY`; multi-model live now passes end-to-end when first-party keys are present.
- Token usage formatter again emits two-decimal abbreviations for thousands (e.g., 4.25k) to match CLI output and tests.

### Added
- `--browser-manual-login` skips cookie copy, reuses a persistent automation profile (`~/.oracle/browser-profile` by default), and waits for manual ChatGPT login—handy on Windows where app-bound cookies can’t be decrypted; works as an opt-in on macOS/Linux too.
- Manual-login browser sessions can reuse an already-running automation Chrome when remote debugging is enabled; point Oracle at it via `--remote-chrome <host:port>` to avoid relaunching/locks.
- `--browser-port` (alias `--browser-debug-port`, env `ORACLE_BROWSER_PORT`) pins the DevTools port so WSL/Windows users can open a single firewall rule; includes a lightweight `pnpm test:browser` DevTools reachability check.

### Changed
- Windows cookie reader now accepts any `v**` AES-GCM prefix (v10/v11/v20) to stay forward compatible.
- On Windows, cookie sync is disabled by default and manual-login is forced; use inline cookies or `--browser-manual-login` (default) instead of profile-based cookie copy.

## 0.4.5 — 2025-11-22

### Fixed
- MCP/API responses now report 404/405 from `/v1/responses` as “unsupported-endpoint” with guidance to fix base URLs/gateways or use browser engine; avoids silent failures when proxies lack the Responses API.

## 0.4.4 — 2025-11-22

### Fixed
- MCP/API runs now surface 404/405 Responses API failures as “unsupported-endpoint” with actionable guidance (check OPENAI_BASE_URL/Azure setup or use the browser engine) instead of a generic transport error.
- Publish metadata now declares Node >=20 (engines/devEngines) and drops the implicit bun runtime so `npx @steipete/oracle` no longer fails with EBADDEVENGINES on newer Node versions.

## 0.4.3 — 2025-11-22

### Added
- xAI Grok 4.1 API support (`--model grok-4.1` / alias `grok`): defaults to `https://api.x.ai/v1`, uses `XAI_API_KEY`, maps search to `web_search`, and includes docs + live smoke.
- Per-model search tool selection so Grok can use `web_search` while OpenAI models keep `web_search_preview`.
- Multi-model coverage now includes Grok in orchestrator tests.
- Grok “thinking”/non-fast variant is not available via API yet; Oracle aliases `grok` to the fast reasoning model to match what xAI ships today.
- PTY-driven CLI/TUI harness landed for e2e coverage (browser guard, TUI exit path); PTY suites are opt-in via `ORACLE_ENABLE_PTY_TESTS=1` and stub tokenizers to stay lightweight.

### Fixed
- MCP (global installs): keep the stdio transport alive until the client closes it so `oracle-mcp` doesn’t exit right after `connect()`; npm -g / host-spawned MCP clients now handshake successfully (tarball regression in 0.4.2).

## 0.4.2 — 2025-11-21

### Fixed
- MCP: `npx @steipete/oracle oracle-mcp` now routes directly to the MCP server (even when npx defaults to the CLI binary) and keeps stdout JSON-only for Cursor/other MCP hosts.
- Added the missing `@anthropic-ai/tokenizer` runtime dependency so `npx @steipete/oracle oracle-mcp` starts cleanly.

## 0.4.1 — 2025-11-21

### Fixed
- Removed duplicate MCP release note entry; no code changes (meta cleanup only).

## 0.4.0 — 2025-11-21

### Added
- Remote Chrome + remote browser service: `oracle serve` launches Chrome with host/token defaults for cross-machine runs, requires the host profile to be signed in, and supports reusing an existing Chrome via `--remote-chrome <host:port>` (IPv6 with `[host]:port`), including remote attachment uploads and clearer validation errors.
- Linux browser support: Chrome/Chromium/Edge runs now work on Linux (including snap-installed Chromium) with cookie sync picking up the snap profile paths. See [docs/linux.md](docs/linux.md) for paths and display guidance.
- Browser engine can target Chromium/Edge by pairing `--browser-chrome-path` with the new `--browser-cookie-path` (also configurable via `browser.chromePath` / `browser.chromeCookiePath`). See [docs/chromium-forks.md](docs/chromium-forks.md) for OS-specific paths and setup steps.
- Markdown bundles render better in the CLI and ChatGPT: each attached file now appears as `### File: <path>` followed by a fenced code block (language inferred), across API bundles, browser bundles (including inline mode), and render/dry-run output; ANSI highlighting still applies on rich TTYs.
- `--render-plain` forces plain markdown output (no ANSI/highlighting) even in a rich TTY; takes precedence when combined with `--render` / `--render-markdown`.
- `--write-output <path>` saves just the final assistant message to disk (adds `.<model>` per file for multi-model runs), with safe path guards and non-fatal write failures.
- Browser engine: `--chatgpt-url` (alias `--browser-url`) and `browser.chatgptUrl` config let you target specific ChatGPT workspace/folder URLs while keeping API `--base-url` separate.
- Multi-model API runner orchestrates multiple API models in one command and aggregates usage/cost; browser engine stays single-model.
- GPT-5.1 Pro API support (new default) and `gpt-5-pro` alias for earlier Pro rollout; GPT-5.1 Codex (API-only) now works end-to-end with high reasoning and auto-forces the API engine. GPT-5.1 Codex Max isn’t available via API yet; the CLI rejects that model until OpenAI ships it.
- Duplicate prompt guard remains active: Oracle blocks a second run when the exact prompt is already running.

### Changed
- Cookie sync covers Chrome, Chromium, Edge, Brave, and Vivaldi profiles; targets chatgpt.com, chat.openai.com, and atlas.openai.com. Windows browser automation is still partial—prefer API or clipboard fallback there.
- Reject prompts shorter than 10 characters with a friendly hint for pro-tier models (`gpt-5.1-pro`) only (prevents accidental costly runs while leaving cheaper models unblocked). Override via ORACLE_MIN_PROMPT_CHARS for automated environments.
- Browser engine default timeout bumped from 15m (900s) to 20m (1200s) so long GPT-5.x Pro responses don’t get cut off; CLI docs/help text now reflect the new ceiling.
- Duration flags such as `--browser-timeout`/`--browser-input-timeout` now accept chained units (`1h2m10s`, `3m10s`, etc.) plus `h`, `m`, `s`, or `ms` suffixes, matching the formats we already log.
- GPT-5.1 Pro and GPT-5 Pro API runs now default to a 60-minute timeout (was 20m) and the “zombie” detector waits the same hour before marking sessions as `error`; CLI messaging/docs updated accordingly so a single “auto” limit covers both behaviors.
- Browser-to-API coercion now happens automatically for GPT-5.1 Codex and Gemini (with a console hint) instead of failing when `--engine browser` is set.
- Browser engine now fails fast (with guidance) when explicitly requested alongside non-GPT models such as Grok, Claude, or Gemini; pick `--engine api` for those.
- Multi-model output is easier to scan: aggregate header/summary, deduped per-model headings, and on-demand OSC progress when replaying combined logs.
- `--write-output` adds stricter path safety, rejecting unsafe destinations while keeping writes non-fatal to avoid breaking runs.
- Session slugs now trim individual words to 10 characters to keep auto-generated IDs readable when prompts include very long tokens.
- CLI: `--mode` is now a silent alias for `--engine` for backward compatibility with older docs/scripts; prefer `--engine`.
- CLI guardrail: if a session with the same prompt is already running, new runs abort with guidance to reattach unless `--force` is provided (prevents unintended duplicate API/browser runs).

### Fixed
- Browser assistant capture is more resilient: markdown cleanup no longer drops real answers and prompt-echo recovery keeps the assistant text intact.
- Browser cookie sync on Windows now copies the profile DB into a named temp directory with the expected `Cookies` filename so `chrome-cookies-secure` can read it reliably during browser fallbacks.
- Streaming runs in `--render-plain` mode now send chunks directly to stdout and keep the log sink newline-aligned, preventing missing or double-printed output in TTY and background runs.
- CLI output is consistent again: final answers always print to stdout (even when a log sink is active) and inline runs once more echo the assistant text to stdout.
- MCP: stdout is now muted during MCP runs, preventing non-JSON logs from breaking hosts like Cursor.

## 0.3.0 — 2025-11-19

### Added
- Native Azure OpenAI support! Set `AZURE_OPENAI_ENDPOINT` (plus `AZURE_OPENAI_API_KEY` and optionally `AZURE_OPENAI_DEPLOYMENT`/`AZURE_OPENAI_API_VERSION`) or use the new CLI flags (`--azure-endpoint`, `--azure-deployment`, etc.) to switch automatically to the Azure client.
- **Gemini 3 Pro Support**: Use Google's latest model via `oracle --model gemini`. Requires `GEMINI_API_KEY`.
- Configurable API timeout: `--timeout <seconds|auto>` (auto = 20m for most models, 60m for pro models such as gpt-5.1-pro as of 0.4.0). Enforced for streaming and background runs.
- OpenAI-compatible base URL override: `--base-url` (or `apiBaseUrl` in config / `OPENAI_BASE_URL`) lets you target LiteLLM proxies, Azure gateways, and other compatible hosts.
- Help text tip: best results come from 6–30 sentences plus key source files; very short prompts tend to be generic.
- Browser inline cookies: `--browser-inline-cookies[(-file)]` (or env) accepts JSON/base64 payloads, auto-loads `~/.oracle/cookies.{json,base64}`, adds a cookie allowlist (`--browser-cookie-names`), and dry-run now reports whether cookies come from Chrome or inline sources.
- Inline runs now print a single completion line (removed duplicate “Finished” summary), keeping output concise.
- Gemini runs stay on API (no browser detours), and the CLI logs the resolved model id alongside masked keys when it differs.
- `--dry-run [summary|json|full]` is now the single preview flag; `--preview` remains as a hidden alias for compatibility.

### Changed
 - Browser engine is now macOS-only; Windows and Linux runs fail fast with guidance to re-run via `--engine api`. Cross-platform browser support is in progress.
 - Browser fallback tips focus on `--browser-bundle-files`, making it clear users can drag the single bundled file into ChatGPT when automation fails.
 - Sessions TUI separates recent vs older runs, adds an Older/Newer action, keeps headers aligned with rows, and avoids separator crashes while preserving an always-selectable “ask oracle” entry.
- CLI output is tidier and more resilient: graceful Ctrl+C, shorter headers/footers, clearer verbose token labels, and reduced trailing spacing.
- File discovery is more reliable on Windows thanks to normalized paths, native-fs glob handling, and `.gitignore` respect across platforms.

## 0.2.0 — 2025-11-18

### Added
- `oracle-mcp` stdio server (bin) with `consult` and `sessions` tools plus read-only session resources at `oracle-session://{id}/{metadata|log|request}`.
- MCP logging notifications for consult streaming (info/debug with byte sizes); browser engine guardrails now check Chrome availability before a browser run starts.
- Hidden root-level aliases `--message` (prompt) and `--include` (files) to mirror common agent calling conventions.
- `--preview` now works with `--engine browser`, emitting the composed browser payload (token estimate, attachment list, optional JSON/full dumps) without launching Chrome or requiring an API key.
- New `--browser-bundle-files` flag to opt into bundling all attachments into a single upload; bundling is still auto-applied when more than 10 files are provided.
- Desktop session notifications (default on unless CI/SSH) with `--[no-]notify` and optional `--notify-sound`; completed runs announce session name, API cost, and character count via OS-native toasts.
- Per-user JSON5 config at `~/.oracle/config.json` to set default engine/model, notification prefs (including sound/mute rules), browser defaults, heartbeat, file-reporting, background mode, and prompt suffixes. CLI/env still override config.
- Session lists now show headers plus a cost column for quick scanning.

### Changed
- Browser model picker is now more robust: longer menu-open window, richer tokens/testids for GPT-5.1 and GPT-5 Pro, fallback snapshot logging, and best-effort selection to reduce “model not found” errors.
- MCP consult honors notification settings so the macOS Swift notifier fires for MCP-triggered runs.
- `sessions` tool now returns a summary row for `id` lookups by default; pass `detail: true` to fetch full metadata/log/request to avoid large accidental payloads.
- Directory/glob expansions now honor `.gitignore` files and skip dotfiles by default; explicitly matching patterns (e.g., `--file "src/**/.keep"`) still opt in.
- Default ignores when crawling project roots now drop common build/cache folders (`node_modules`, `dist`, `coverage`, `.git`, `.turbo`, `.next`, `build`, `tmp`) unless the path is passed explicitly. Oracle logs each skipped path for transparency.
- Browser engine now logs a one-line warning before cookie sync, noting macOS may prompt for a Keychain password and how to bypass via `--browser-no-cookie-sync` or `--browser-allow-cookie-errors`.
- gpt-5.1-pro API runs default to non-blocking; add `--wait` to block. `gpt-5.1` and browser runs still block by default. CLI now polls once for `in_progress` responses before failing.
- macOS notifier helper now ships signed/notarized with the Oracle icon and auto-repairs execute bits for the fallback terminal-notifier.
- Session summaries and cost displays are clearer, with zombie-session detection to avoid stale runs.
- Token estimation now uses the full request body (instructions + input text + tools/reasoning/background/store) and compares estimated vs actual tokens in the finished stats to reduce 400/413 surprises.
- Help banner and first tip now require “prompt + --file” (dirs/globs fine) and remind you Oracle can’t see your project without attachments.
- Help tips/examples now call out project/platform/version requirements and show how to label cross-repo attachments so the model has the right context.

#### MCP configuration (quick reference)
- Local stdio (mcporter): add to `config/mcporter.json`
  ```json
  {
    "name": "oracle",
    "type": "stdio",
    "command": "npx",
    "args": ["-y", "@steipete/oracle", "oracle-mcp"]
  }
  ```
- Claude Code (global/user scope):  
  `claude mcp add --scope user --transport stdio oracle -- oracle-mcp`
- Project-scoped Claude: drop `.mcp.json` next to the repo root with
  ```json
  {
    "mcpServers": {
      "oracle": { "type": "stdio", "command": "npx", "args": ["-y", "@steipete/oracle", "oracle-mcp"] }
    }
  }
  ```
- The MCP `consult` tool honors `~/.oracle/config.json` defaults (engine/model/search/prompt suffix/heartbeat/background/filesReport) unless the caller overrides them.

## 0.1.1 — 2025-11-20

### Added
- Hidden `--files`, `--path`, and `--paths` aliases for `--file`, so all path inputs (including `--include`) merge cleanly; commas still split within a single flag.
- CLI path-merging helper now has unit coverage for alias ordering and comma splitting.
- New `--copy-markdown` flag (alias `--copy`) assembles the markdown bundle and copies it to the clipboard, printing a one-line summary; combine with `--render-markdown` to both print and copy. Clipboard handling now uses `clipboardy` for macOS/Windows/Linux/Wayland/Termux/WSL with graceful failure messaging.

## 0.1.0 — 2025-11-17

Highlights
- Markdown rendering for completed sessions (`oracle session|status <id> --render` / `--render-markdown`) with ANSI formatting in rich TTYs; falls back to raw when logs are huge or stdout isn’t a TTY.
- New `--path` flag on `oracle session <id>` prints the stored session directory plus metadata/request/log files, erroring if anything is missing. Uses soft color in rich terminals for quick scanning.

Details
### Added
- `oracle session <id> --path` now prints the on-disk session directory plus metadata/request/log files, exiting with an error when any expected file is missing instead of attaching.
- When run in a rich TTY, `--path` labels and paths are colorized for easier scanning.

### Improved
- `oracle session|status <id> --render` (alias `--render-markdown`) pretty-prints completed session markdown to ANSI in rich TTYs, falls back to raw when non-TTY or oversized logs.
## 0.0.10 — 2025-11-17

### Added
- Rich terminals that support OSC 9;4 (Ghostty 1.2+, WezTerm, Windows Terminal) now show an inline progress bar while Oracle waits for the OpenAI response; disable with `ORACLE_NO_OSC_PROGRESS=1`, force with `ORACLE_FORCE_OSC_PROGRESS=1`.

## 0.0.9 — 2025-11-16

### Added
- `oracle session|status <id> --render` (alias `--render-markdown`) pretty-prints completed session markdown to ANSI in rich TTYs, falls back to raw when non-TTY or oversized logs.
- Hidden root-level `--session <id>` alias attaches directly to a stored session (for agents/automation).
- README now recommends preferring API engine for reliability and longer uninterrupted runs when an API key is available.
- Session rendering now uses Markdansi (micromark/mdast-based), removing markdown-it-terminal and eliminating HTML leakage/crashes during replays.
- Added a local Markdansi type shim for now; switch to official types once the npm package ships them.
- Markdansi renderer now enables color/hyperlinks when TTY by default and auto-renders sessions unless the user explicitly disables it.

## 0.0.8 — 2025-11-16

### Changed
- Help tips call out that Oracle is one-shot and does not remember prior runs, so every query should include full context.
- `oracle session <id>` now logs a brief notice when extra root-only flags are present (e.g., `--render-markdown`) to make it clear those options are ignored during reattach.

## 0.0.7 — 2025-11-16

### Changed
- Browser-mode thinking monitor now emits a text-only progress bar instead of the "Pro thinking" string.
- `oracle session <id>` trims preamble/log noise and prints from the first `Answer:` line once a session is finished.
- Help tips now stress sending whole directories and richer project briefings for better answers.

## 0.0.6 — 2025-11-15

### Changed
- Colorized live run header (model/tokens/files) when a rich TTY is available.
- Added a blank line before the `Answer:` prefix for readability.
- Masked API key logging now shows first/last 4 characters (e.g., `OPENAI_API_KEY=sk-p****qfAA`).
- Suppressed duplicate session header on reattach and removed repeated background response IDs in heartbeats.

### Browser mode
- When more than 10 files are provided, automatically bundles all files into a single `attachments-bundle.txt` to stay under ChatGPT’s upload cap and logs a verbose warning when bundling occurs.

## 0.0.5 — 2025-11-15

### Added
- Logs the masked OpenAI key in use (`Using OPENAI_API_KEY=xxxx****yyyy`) so runs are traceable without leaking secrets.
- Logs a helpful tip when you run without attachments, reminding you to pass context via `--file`.

## 0.0.3 — 2025-11-15

## 0.0.2 — 2025-11-15

### Added
- Positional prompt shorthand: `oracle "prompt here"` (and `npx -y @steipete/oracle "..."`) now maps the positional argument to `--prompt` automatically.

### Fixed
- `oracle status/session` missing-prompt guard now coexists with the positional prompt path and still shows the cleanup tip when no sessions exist.

## 0.0.1 — 2025-11-15

### Fixed
- Corrected npm binary mapping so `oracle` is installed as an executable. Published with `--tag beta`.

## 0.0.0 — 2025-11-15

### Added
- Dual-engine support (API and browser) with automatic selection: defaults to API when `OPENAI_API_KEY` is set, otherwise falls back to browser mode.
- Session-friendly prompt guard that allows `status`/`session` commands to run without a prompt while still enforcing prompts for normal runs, previews, and dry runs.
- Browser mode uploads each `--file` individually and logs Chrome PID/port for detachable runs.
- Background GPT-5 Pro runs with heartbeat logging and reconnect support for long responses.
- File token accounting (`--files-report`) and dry-run summaries for both engines.
- Comprehensive CLI and browser automation test suites, including engine selection and prompt requirement coverage.

### Changed
- Help text, README, and browser-mode docs now describe the auto engine fallback and the deprecated `--browser` alias.
- CLI engine resolution is centralized to keep legacy flags, model inference, and environment defaults consistent.

### Fixed
- `oracle status` and `oracle session` no longer demand `--prompt` when used directly.


## Links discovered
- [docs/linux.md](https://github.com/steipete/oracle/blob/main/docs/linux.md)
- [docs/chromium-forks.md](https://github.com/steipete/oracle/blob/main/docs/chromium-forks.md)

--- AGENTS.md ---
# AGENTS.MD

Oracle-specific notes:
- ChatGPT project URLs: steipete@gmail.com -> https://chatgpt.com/g/g-p-691edc9fec088191b553a35093da1ea8-oracle/project; studpete@gmail.com -> https://chatgpt.com/g/g-p-69505ed97e3081918a275477a647a682/project. Prefer studpete URL if steipete project not found.
- Pro browser runs: allow up to 10 minutes; never click "Answer now"; keep at least 1–2 Pro live tests (reattach must stay Pro); move other tests to faster models where safe.
- Live smoke tests: OpenAI live tests are opt-in. Run `ORACLE_LIVE_TEST=1 pnpm vitest run tests/live/openai-live.test.ts` with a real `OPENAI_API_KEY` when you need the background path; gpt-5-pro can take ~10 minutes.
- Wait defaults: gpt-5-pro API runs detach by default; use `--wait` to stay attached. gpt-5.1 and browser runs block by default; every run prints `oracle session <id>` for reattach.
- Session storage: Oracle stores session data under `~/.oracle`; delete it if you need a clean slate.
- CLI output: the first line of any top-level CLI start banner should use the oracle emoji, e.g. `🧿 oracle (<version>) ...`; keep it only for the initial command headline. Exception: the TUI exit message also keeps the emoji.
- Model access note (2025-11-23): gpt-5.1-pro and grok-4.1 are not yet available on Peter’s keys; live tests that require them will fail until access is granted.
- Oracle CLI on Node 25: if `pnpm dlx @steipete/oracle --help` fails with a missing `node_sqlite3.node`, rebuild sqlite3 in the pnpm dlx cache using system Python: `PYTHON=/usr/bin/python3 /Users/steipete/Projects/oracle/runner npx node-gyp rebuild` from the sqlite3 package dir printed in the error, then rerun the command.
- Before a release, skim manual smokes in `docs/manual-tests.md` and rerun any that cover your change surface (especially browser/serve paths).
- If browser smokes echo the prompt (Instant), rerun with `--browser-keep-browser --verbose` in tmux, then inspect DOM with `pnpm tsx scripts/browser-tools.ts eval ...` to confirm assistant turns exist; we fixed a case by refreshing assistant snapshots post-send.
- Browser “Pro thinking” gate: never click/auto-click ChatGPT’s “Answer now” button. Treat it as a placeholder and wait 10m–1h for the real assistant response (auto-clicking skips long thinking and changes behavior).
- Browser smokes should preserve Markdown (lists, fences); if output looks flattened or echoed, inspect the captured assistant turn via `browser-tools.ts eval` before shipping.
- Working on Windows? Read and update `docs/windows-work.md` before you start.
- Sparkle signing key lives at `/Users/steipete/Library/CloudStorage/Dropbox/Backup/Sparkle`; set `SPARKLE_PRIVATE_KEY_FILE` to that path when notarizing the notifier.
- Browser cookie sync + Node 25: if browser runs fail with “Failed to load keytar… Cannot find module '../build/Release/keytar.node'” and no cookies are applied, rebuild keytar in the pnpm dlx cache: run `PYTHON=/usr/bin/python3 /Users/steipete/Projects/oracle/runner npx node-gyp rebuild` inside the keytar directory printed in the error, then rerun the oracle command.

Browser-mode debug notes (ChatGPT URL override)
- When a ChatGPT folder/workspace URL is set, Cloudflare can block automation even after cookie sync. Use `--browser-keep-browser` to leave Chrome open, solve the interstitial manually, then rerun.
- If a run stalls/looks finished but CLI didn’t stream output, check the latest session (`oracle status`) and open it (`oracle session <id> --render`) to confirm completion.
- Active Chrome port/pid live in session metadata (`~/.oracle/sessions/<id>/meta.json`). Connect with `npx tsx scripts/browser-tools.ts eval --port <port> "({ href: window.location.href, ready: document.readyState })"` to inspect the page.
- To debug with agent-tools, launch Chrome via an Oracle browser run (cookies copied) and keep it open (`--browser-keep-browser`). Then use `~/Projects/agent-scripts/bin/browser-tools ... --port <port>` with the port from `~/.oracle/sessions/<id>/meta.json`. Avoid starting a fresh browser-tools Chrome when you need the synced cookies.
- Double-hop nav is implemented (root then target URL), but Cloudflare may still need manual clearance or inline cookies.
- After finishing a feature, ask whether it matters to end users; if yes, update the changelog. Read the top ~100 lines first and group related edits into one entry instead of scattering multiple bullets.
- Beta publishing: when asked to ship a beta to npm, bump the version with a beta suffix (e.g., `0.4.4-beta.1`) before publishing; npm will not let you overwrite an existing beta tag without a new version.


--- README.md ---
# oracle 🧿 — Whispering your tokens to the silicon sage

<p align="center">
  <img src="./README-header.png" alt="Oracle CLI header banner" width="1100">
</p>

<p align="center">
  <a href="https://www.npmjs.com/package/@steipete/oracle"><img src="https://img.shields.io/npm/v/@steipete/oracle?style=for-the-badge&logo=npm&logoColor=white" alt="npm version"></a>
  <a href="https://github.com/steipete/oracle/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/steipete/oracle/ci.yml?branch=main&style=for-the-badge&label=tests" alt="CI Status"></a>
  <a href="https://github.com/steipete/oracle"><img src="https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20Windows-blue?style=for-the-badge" alt="Platforms"></a>
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License"></a>
</p>

Oracle bundles your prompt and files so another AI can answer with real context. It speaks GPT-5.1 Pro (default alias to GPT-5.2 Pro on the API), GPT-5.1 Codex (API-only), GPT-5.1, GPT-5.2, Gemini 3 Pro, Claude Sonnet 4.5, Claude Opus 4.1, and more—and it can ask one or multiple models in a single run. Browser automation is available; use `--browser-model-strategy current` to keep the active ChatGPT model (or `ignore` to skip the picker). API remains the most reliable path, and `--copy` is an easy manual fallback.

## Quick start

Install globally: `npm install -g @steipete/oracle`
Homebrew: `brew install steipete/tap/oracle`

Requires Node 22+. Or use `npx -y @steipete/oracle …` (or pnpx).

```bash
# Copy the bundle and paste into ChatGPT
npx -y @steipete/oracle --render --copy -p "Review the TS data layer for schema drift" --file "src/**/*.ts,*/*.test.ts"

# Minimal API run (expects OPENAI_API_KEY in your env)
npx -y @steipete/oracle -p "Write a concise architecture note for the storage adapters" --file src/storage/README.md

# Multi-model API run
npx -y @steipete/oracle -p "Cross-check the data layer assumptions" --models gpt-5.1-pro,gemini-3-pro --file "src/**/*.ts"

# Preview without spending tokens
npx -y @steipete/oracle --dry-run summary -p "Check release notes" --file docs/release-notes.md

# Browser run (no API key, will open ChatGPT)
npx -y @steipete/oracle --engine browser -p "Walk through the UI smoke test" --file "src/**/*.ts"

# Gemini browser mode (no API key; uses Chrome cookies from gemini.google.com)
npx -y @steipete/oracle --engine browser --model gemini-3-pro --prompt "a cute robot holding a banana" --generate-image out.jpg --aspect 1:1

# Sessions (list and replay)
npx -y @steipete/oracle status --hours 72
npx -y @steipete/oracle session <id> --render

# TUI (interactive, only for humans)
npx -y @steipete/oracle tui
```

Engine auto-picks API when `OPENAI_API_KEY` is set, otherwise browser; browser is stable on macOS and works on Linux and Windows. On Linux pass `--browser-chrome-path/--browser-cookie-path` if detection fails; on Windows prefer `--browser-manual-login` or inline cookies if decryption is blocked.

## Integration

**CLI**
- API mode expects API keys in your environment: `OPENAI_API_KEY` (GPT-5.x), `GEMINI_API_KEY` (Gemini 3 Pro), `ANTHROPIC_API_KEY` (Claude Sonnet 4.5 / Opus 4.1).
- Gemini browser mode uses Chrome cookies instead of an API key—just be logged into `gemini.google.com` in Chrome (no Python/venv required).
- If your Gemini account can’t access “Pro”, Oracle auto-falls back to a supported model for web runs (and logs the fallback in verbose mode).
- Prefer API mode or `--copy` + manual paste; browser automation is experimental.
- Browser support: stable on macOS; works on Linux (add `--browser-chrome-path/--browser-cookie-path` when needed) and Windows (manual-login or inline cookies recommended when app-bound cookies block decryption).
- Remote browser service: `oracle serve` on a signed-in host; clients use `--remote-host/--remote-token`.
- AGENTS.md/CLAUDE.md:
  ```
  - Oracle bundles a prompt plus the right files so another AI (GPT 5 Pro + more) can answer. Use when stuck/bugs/reviewing.
  - Run `npx -y @steipete/oracle --help` once per session before first use.
  ```
- Tip: set `browser.chatgptUrl` in config (or `--chatgpt-url`) to a dedicated ChatGPT project folder so browser runs don’t clutter your main history.

**Codex skill**
- Copy the bundled skill from this repo to your Codex skills folder:
  - `mkdir -p ~/.codex/skills`
  - `cp -R skills/oracle ~/.codex/skills/oracle`
- Then reference it in your `AGENTS.md`/`CLAUDE.md` so Codex loads it.

**MCP**
- Run the stdio server via `oracle-mcp`.
- Configure clients via [steipete/mcporter](https://github.com/steipete/mcporter) or `.mcp.json`; see [docs/mcp.md](docs/mcp.md) for connection examples.
```bash
npx -y @steipete/oracle oracle-mcp
```
- Cursor setup (MCP): drop a `.cursor/mcp.json` like below, then pick “oracle” in Cursor’s MCP sources. See https://cursor.com/docs/context/mcp for UI steps.
[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=oracle&config=eyJjb21tYW5kIjoibnB4IC15IEBzdGVpcGV0ZS9vcmFjbGUgb3JhY2xlLW1jcCJ9)

```json
{
  "oracle": {
    "command": "oracle-mcp",
    "args": []
  }
}
```

## Highlights

- Bundle once, reuse anywhere (API or experimental browser).
- Multi-model API runs with aggregated cost/usage, including OpenRouter IDs alongside first-party models.
- Render/copy bundles for manual paste into ChatGPT when automation is blocked.
- GPT‑5 Pro API runs detach by default; reattach via `oracle session <id>` / `oracle status` or block with `--wait`.
- Azure endpoints supported via `--azure-endpoint/--azure-deployment/--azure-api-version` or `AZURE_OPENAI_*` envs.
- File safety: globs/excludes, size guards, `--files-report`.
- Sessions you can replay (`oracle status`, `oracle session <id> --render`).
- Session logs and bundles live in `~/.oracle/sessions` (override with `ORACLE_HOME_DIR`).

## Flags you’ll actually use

| Flag | Purpose |
| --- | --- |
| `-p, --prompt <text>` | Required prompt. |
| `-f, --file <paths...>` | Attach files/dirs (globs + `!` excludes). |
| `-e, --engine <api\|browser>` | Choose API or browser (browser is experimental). |
| `-m, --model <name>` | Built-ins (`gpt-5.1-pro` default, `gpt-5-pro`, `gpt-5.1`, `gpt-5.1-codex`, `gpt-5.2`, `gpt-5.2-instant`, `gpt-5.2-pro`, `gemini-3-pro`, `claude-4.5-sonnet`, `claude-4.1-opus`) plus any OpenRouter id (e.g., `minimax/minimax-m2`, `openai/gpt-4o-mini`). |
| `--models <list>` | Comma-separated API models (mix built-ins and OpenRouter ids) for multi-model runs. |
| `--base-url <url>` | Point API runs at LiteLLM/Azure/OpenRouter/etc. |
| `--chatgpt-url <url>` | Target a ChatGPT workspace/folder (browser). |
| `--browser-model-strategy <select\|current\|ignore>` | Control ChatGPT model selection in browser mode (current keeps the active model; ignore skips the picker). |
| `--browser-manual-login` | Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login. |
| `--browser-thinking-time <light\|standard\|extended\|heavy>` | Set ChatGPT thinking-time intensity (browser; Thinking/Pro models only). |
| `--browser-port <port>` | Pin the Chrome DevTools port (WSL/Windows firewall helper). |
| `--browser-inline-cookies[(-file)] <payload|path>` | Supply cookies without Chrome/Keychain (browser). |
| `--browser-timeout`, `--browser-input-timeout` | Control overall/browser input timeouts (supports h/m/s/ms). |
| `--render`, `--copy` | Print and/or copy the assembled markdown bundle. |
| `--wait` | Block for background API runs (e.g., GPT‑5.1 Pro) instead of detaching. |
| `--write-output <path>` | Save only the final answer (multi-model adds `.<model>`). |
| `--files-report` | Print per-file token usage. |
| `--dry-run [summary\|json\|full]` | Preview without sending. |
| `--remote-host`, `--remote-token` | Use a remote `oracle serve` host (browser). |
| `--remote-chrome <host:port>` | Attach to an existing remote Chrome session (browser). |
| `--youtube <url>` | YouTube video URL to analyze (Gemini browser mode). |
| `--generate-image <file>` | Generate image and save to file (Gemini browser mode). |
| `--edit-image <file>` | Edit existing image with `--output` (Gemini browser mode). |
| `--azure-endpoint`, `--azure-deployment`, `--azure-api-version` | Target Azure OpenAI endpoints (picks Azure client automatically). |

## Configuration

Put defaults in `~/.oracle/config.json` (JSON5). Example:
```json5
{
  model: "gpt-5.1-pro",
  engine: "api",
  filesReport: true,
  browser: {
    chatgptUrl: "https://chatgpt.com/g/g-p-691edc9fec088191b553a35093da1ea8-oracle/project"
  }
}
```
Use `browser.chatgptUrl` (or the legacy alias `browser.url`) to target a specific ChatGPT workspace/folder for browser automation.
See [docs/configuration.md](docs/configuration.md) for precedence and full schema.

Advanced flags

| Area | Flags |
| --- | --- |
| Browser | `--browser-manual-login`, `--browser-thinking-time`, `--browser-timeout`, `--browser-input-timeout`, `--browser-cookie-wait`, `--browser-inline-cookies[(-file)]`, `--browser-attachments`, `--browser-inline-files`, `--browser-bundle-files`, `--browser-keep-browser`, `--browser-headless`, `--browser-hide-window`, `--browser-no-cookie-sync`, `--browser-allow-cookie-errors`, `--browser-chrome-path`, `--browser-cookie-path`, `--chatgpt-url` |
| Azure/OpenAI | `--azure-endpoint`, `--azure-deployment`, `--azure-api-version`, `--base-url` |

Remote browser example
```bash
# Host (signed-in Chrome): launch serve
oracle serve --host 0.0.0.0:9473 --token secret123

# Client: target that host
oracle --engine browser --remote-host 192.168.1.10:9473 --remote-token secret123 -p "Run the UI smoke" --file "src/**/*.ts"

# If cookies can’t sync, pass them inline (JSON/base64)
oracle --engine browser --browser-inline-cookies-file ~/.oracle/cookies.json -p "Run the UI smoke" --file "src/**/*.ts"
```

Session management
```bash
# Prune stored sessions (default path ~/.oracle/sessions; override ORACLE_HOME_DIR)
oracle status --clear --hours 168
```

## More docs
- Browser mode & forks: [docs/browser-mode.md](docs/browser-mode.md) (includes `oracle serve` remote service), [docs/chromium-forks.md](docs/chromium-forks.md), [docs/linux.md](docs/linux.md)
- MCP: [docs/mcp.md](docs/mcp.md)
- OpenAI/Azure/OpenRouter endpoints: [docs/openai-endpoints.md](docs/openai-endpoints.md), [docs/openrouter.md](docs/openrouter.md)
- Manual smokes: [docs/manual-tests.md](docs/manual-tests.md)
- Testing: [docs/testing.md](docs/testing.md)

If you’re looking for an even more powerful context-management tool, check out https://repoprompt.com  
Name inspired by: https://ampcode.com/news/oracle

## More free stuff from steipete
- ✂️ [Trimmy](https://trimmy.app) — “Paste once, run once.” Flatten multi-line shell snippets so they paste and run.
- 🟦🟩 [CodexBar](https://codexbar.app) — Keep Codex token windows visible in your macOS menu bar.
- 🧳 [MCPorter](https://mcporter.dev) — TypeScript toolkit + CLI for Model Context Protocol servers.


## Links discovered
- [steipete/mcporter](https://github.com/steipete/mcporter)
- [docs/mcp.md](https://github.com/steipete/oracle/blob/main/docs/mcp.md)
- [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)
- [docs/configuration.md](https://github.com/steipete/oracle/blob/main/docs/configuration.md)
- [docs/browser-mode.md](https://github.com/steipete/oracle/blob/main/docs/browser-mode.md)
- [docs/chromium-forks.md](https://github.com/steipete/oracle/blob/main/docs/chromium-forks.md)
- [docs/linux.md](https://github.com/steipete/oracle/blob/main/docs/linux.md)
- [docs/openai-endpoints.md](https://github.com/steipete/oracle/blob/main/docs/openai-endpoints.md)
- [docs/openrouter.md](https://github.com/steipete/oracle/blob/main/docs/openrouter.md)
- [docs/manual-tests.md](https://github.com/steipete/oracle/blob/main/docs/manual-tests.md)
- [docs/testing.md](https://github.com/steipete/oracle/blob/main/docs/testing.md)
- [Trimmy](https://trimmy.app)
- [CodexBar](https://codexbar.app)
- [MCPorter](https://mcporter.dev)
- [<img src="https://img.shields.io/npm/v/@steipete/oracle?style=for-the-badge&logo=npm&logoColor=white" alt="npm version">](https://www.npmjs.com/package/@steipete/oracle)
- [<img src="https://img.shields.io/github/actions/workflow/status/steipete/oracle/ci.yml?branch=main&style=for-the-badge&label=tests" alt="CI Status">](https://github.com/steipete/oracle/actions/workflows/ci.yml)
- [<img src="https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20Windows-blue?style=for-the-badge" alt="Platforms">](https://github.com/steipete/oracle)
- [<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License">](https://github.com/steipete/oracle/blob/main/LICENSE.md)

--- skills/oracle/SKILL.md ---
---
name: oracle
description: Use the @steipete/oracle CLI to bundle a prompt plus the right files and get a second-model review (API or browser) for debugging, refactors, design checks, or cross-validation.
---

# Oracle (CLI) — best use

Oracle bundles your prompt + selected files into one “one-shot” request so another model can answer with real repo context (API or browser automation). Treat outputs as advisory: verify against the codebase + tests.

## Main use case (browser, GPT‑5.2 Pro)

Default workflow here: `--engine browser` with GPT‑5.2 Pro in ChatGPT. This is the “human in the loop” path: it can take ~10 minutes to ~1 hour; expect a stored session you can reattach to.

Recommended defaults:
- Engine: browser (`--engine browser`)
- Model: GPT‑5.2 Pro (either `--model gpt-5.2-pro` or a ChatGPT picker label like `--model "5.2 Pro"`)
- Attachments: directories/globs + excludes; avoid secrets.

## Golden path (fast + reliable)

1. Pick a tight file set (fewest files that still contain the truth).
2. Preview what you’re about to send (`--dry-run` + `--files-report` when needed).
3. Run in browser mode for the usual GPT‑5.2 Pro ChatGPT workflow; use API only when you explicitly want it.
4. If the run detaches/timeouts: reattach to the stored session (don’t re-run).

## Commands (preferred)

- Show help (once/session):
  - `npx -y @steipete/oracle --help`

- Preview (no tokens):
  - `npx -y @steipete/oracle --dry-run summary -p "<task>" --file "src/**" --file "!**/*.test.*"`
  - `npx -y @steipete/oracle --dry-run full -p "<task>" --file "src/**"`

- Token/cost sanity:
  - `npx -y @steipete/oracle --dry-run summary --files-report -p "<task>" --file "src/**"`

- Browser run (main path; long-running is normal):
  - `npx -y @steipete/oracle --engine browser --model gpt-5.2-pro -p "<task>" --file "src/**"`

- Manual paste fallback (assemble bundle, copy to clipboard):
  - `npx -y @steipete/oracle --render --copy -p "<task>" --file "src/**"`
  - Note: `--copy` is a hidden alias for `--copy-markdown`.

## Attaching files (`--file`)

`--file` accepts files, directories, and globs. You can pass it multiple times; entries can be comma-separated.

- Include:
  - `--file "src/**"` (directory glob)
  - `--file src/index.ts` (literal file)
  - `--file docs --file README.md` (literal directory + file)

- Exclude (prefix with `!`):
  - `--file "src/**" --file "!src/**/*.test.ts" --file "!**/*.snap"`

- Defaults (important behavior from the implementation):
  - Default-ignored dirs: `node_modules`, `dist`, `coverage`, `.git`, `.turbo`, `.next`, `build`, `tmp` (skipped unless you explicitly pass them as literal dirs/files).
  - Honors `.gitignore` when expanding globs.
  - Does not follow symlinks (glob expansion uses `followSymbolicLinks: false`).
  - Dotfiles are filtered unless you explicitly opt in with a pattern that includes a dot-segment (e.g. `--file ".github/**"`).
  - Hard cap: files > 1 MB are rejected (split files or narrow the match).

## Budget + observability

- Target: keep total input under ~196k tokens.
- Use `--files-report` (and/or `--dry-run json`) to spot the token hogs before spending.
- If you need hidden/advanced knobs: `npx -y @steipete/oracle --help --verbose`.

## Engines (API vs browser)

- Auto-pick: uses `api` when `OPENAI_API_KEY` is set, otherwise `browser`.
- Browser engine supports GPT + Gemini only; use `--engine api` for Claude/Grok/Codex or multi-model runs.
- **API runs require explicit user consent** before starting because they incur usage costs.
- Browser attachments:
  - `--browser-attachments auto|never|always` (auto pastes inline up to ~60k chars then uploads).
- Remote browser host (signed-in machine runs automation):
  - Host: `oracle serve --host 0.0.0.0 --port 9473 --token <secret>`
  - Client: `oracle --engine browser --remote-host <host:port> --remote-token <secret> -p "<task>" --file "src/**"`

## Sessions + slugs (don’t lose work)

- Stored under `~/.oracle/sessions` (override with `ORACLE_HOME_DIR`).
- Runs may detach or take a long time (browser + GPT‑5.2 Pro often does). If the CLI times out: don’t re-run; reattach.
  - List: `oracle status --hours 72`
  - Attach: `oracle session <id> --render`
- Use `--slug "<3-5 words>"` to keep session IDs readable.
- Duplicate prompt guard exists; use `--force` only when you truly want a fresh run.

## Prompt template (high signal)

Oracle starts with **zero** project knowledge. Assume the model cannot infer your stack, build tooling, conventions, or “obvious” paths. Include:
- Project briefing (stack + build/test commands + platform constraints).
- “Where things live” (key directories, entrypoints, config files, dependency boundaries).
- Exact question + what you tried + the error text (verbatim).
- Constraints (“don’t change X”, “must keep public API”, “perf budget”, etc).
- Desired output (“return patch plan + tests”, “list risky assumptions”, “give 3 options with tradeoffs”).

### “Exhaustive prompt” pattern (for later restoration)

When you know this will be a long investigation, write a prompt that can stand alone later:
- Top: 6–30 sentence project briefing + current goal.
- Middle: concrete repro steps + exact errors + what you already tried.
- Bottom: attach *all* context files needed so a fresh model can fully understand (entrypoints, configs, key modules, docs).

If you need to reproduce the same context later, re-run with the same prompt + `--file …` set (Oracle runs are one-shot; the model doesn’t remember prior runs).

## Safety

- Don’t attach secrets by default (`.env`, key files, auth tokens). Redact aggressively; share only what’s required.
- Prefer “just enough context”: fewer files + better prompt beats whole-repo dumps.


--- vendor/oracle-notifier/README.md ---
# Oracle Notifier helper (macOS, arm64)

Builds a tiny signed helper app for macOS notifications with the Oracle icon.

## Build

```bash
cd vendor/oracle-notifier
# Optional: notarize by setting App Store Connect key credentials
export APP_STORE_CONNECT_API_KEY_P8="$(cat AuthKey_XXXXXX.p8)" # with literal newlines or \n escaped
export APP_STORE_CONNECT_KEY_ID=XXXXXX
export APP_STORE_CONNECT_ISSUER_ID=YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY
./build-notifier.sh
```

- Requires Xcode command line tools (swiftc) and a macOS Developer ID certificate. Without a valid cert, the build fails (no ad-hoc fallback).
- If `APP_STORE_CONNECT_*` vars are set, the script notarizes and staples the ticket.
- Output: `OracleNotifier.app` (arm64 only), bundled with `OracleIcon.icns`.

## Usage
The CLI prefers this helper on macOS; if it fails or is missing, it falls back to toasted-notifier/terminal-notifier.

## Permissions
After first run, allow notifications for “Oracle Notifier” in System Settings → Notifications.


--- vitest.config.ts ---
import { defineConfig } from 'vitest/config';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

export default defineConfig({
  test: {
    setupFiles: ['tests/setup-env.ts', 'tests/cli/runOracle/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
      all: true,
      // Measure the real TypeScript sources (the repo doesn’t ship .js in src).
      include: ['src/**/*.ts'],
      // Exclude interactive/IPC entrypoints that aren’t practical to unit test.
      exclude: [
        'src/cli/tui/**',
        'src/remote/**',
        'src/mcp/**',
        'src/browser/actions/**',
        'src/browser/index.ts',
        'src/browser/pageActions.ts',
        'src/browser/chromeLifecycle.ts',
        'src/browserMode.ts',
        'src/oracle.ts',
        'src/oracle/modelRunner.ts',
        'src/oracle/stringifier.ts',
        'src/oracle/types.ts',
        'src/types/**',
      ],
    },
  },
  resolve: {
    alias: {
      '@src': fileURLToPath(new URL('./src', import.meta.url)),
      '@tests': fileURLToPath(new URL('./tests', import.meta.url)),
    },
  },
});


--- bin/oracle-cli.ts ---
#!/usr/bin/env node
import 'dotenv/config';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { once } from 'node:events';
import { Command, Option } from 'commander';
import type { OptionValues } from 'commander';
// Allow `npx @steipete/oracle oracle-mcp` to resolve the MCP server even though npx runs the default binary.
if (process.argv[2] === 'oracle-mcp') {
  const { startMcpServer } = await import('../src/mcp/server.js');
  await startMcpServer();
  process.exit(0);
}
import { resolveEngine, type EngineMode, defaultWaitPreference } from '../src/cli/engine.js';
import { shouldRequirePrompt } from '../src/cli/promptRequirement.js';
import chalk from 'chalk';
import type { SessionMetadata, SessionMode, BrowserSessionConfig } from '../src/sessionStore.js';
import { sessionStore, pruneOldSessions } from '../src/sessionStore.js';
import { DEFAULT_MODEL, MODEL_CONFIGS, runOracle, readFiles, estimateRequestTokens, buildRequestBody } from '../src/oracle.js';
import { isKnownModel } from '../src/oracle/modelResolver.js';
import type { ModelName, PreviewMode, RunOracleOptions } from '../src/oracle.js';
import { CHATGPT_URL, normalizeChatgptUrl } from '../src/browserMode.js';
import { createRemoteBrowserExecutor } from '../src/remote/client.js';
import { createGeminiWebExecutor } from '../src/gemini-web/index.js';
import { applyHelpStyling } from '../src/cli/help.js';
import {
  collectPaths,
  collectModelList,
  parseFloatOption,
  parseIntOption,
  parseSearchOption,
  usesDefaultStatusFilters,
  resolvePreviewMode,
  normalizeModelOption,
  normalizeBaseUrl,
  resolveApiModel,
  inferModelFromLabel,
  parseHeartbeatOption,
  parseTimeoutOption,
  mergePathLikeOptions,
  dedupePathInputs,
} from '../src/cli/options.js';
import { copyToClipboard } from '../src/cli/clipboard.js';
import { buildMarkdownBundle } from '../src/cli/markdownBundle.js';
import { shouldDetachSession } from '../src/cli/detach.js';
import { applyHiddenAliases } from '../src/cli/hiddenAliases.js';
import { buildBrowserConfig, resolveBrowserModelLabel } from '../src/cli/browserConfig.js';
import { performSessionRun } from '../src/cli/sessionRunner.js';
import type { BrowserSessionRunnerDeps } from '../src/browser/sessionRunner.js';
import { isMediaFile } from '../src/browser/prompt.js';
import { attachSession, showStatus, formatCompletionSummary } from '../src/cli/sessionDisplay.js';
import type { ShowStatusOptions } from '../src/cli/sessionDisplay.js';
import { formatCompactNumber } from '../src/cli/format.js';
import { formatIntroLine } from '../src/cli/tagline.js';
import { warnIfOversizeBundle } from '../src/cli/bundleWarnings.js';
import { formatRenderedMarkdown } from '../src/cli/renderOutput.js';
import { resolveRenderFlag, resolveRenderPlain } from '../src/cli/renderFlags.js';
import { resolveGeminiModelId } from '../src/oracle/gemini.js';
import { handleSessionCommand, type StatusOptions, formatSessionCleanupMessage } from '../src/cli/sessionCommand.js';
import { isErrorLogged } from '../src/cli/errorUtils.js';
import { handleSessionAlias, handleStatusFlag } from '../src/cli/rootAlias.js';
import { resolveOutputPath } from '../src/cli/writeOutputPath.js';
import { getCliVersion } from '../src/version.js';
import { runDryRunSummary, runBrowserPreview } from '../src/cli/dryRun.js';
import { launchTui } from '../src/cli/tui/index.js';
import {
  resolveNotificationSettings,
  deriveNotificationSettingsFromMetadata,
  type NotificationSettings,
} from '../src/cli/notifier.js';
import { loadUserConfig, type UserConfig } from '../src/config.js';
import { applyBrowserDefaultsFromConfig } from '../src/cli/browserDefaults.js';
import { shouldBlockDuplicatePrompt } from '../src/cli/duplicatePromptGuard.js';

interface CliOptions extends OptionValues {
  prompt?: string;
  message?: string;
  file?: string[];
  include?: string[];
  files?: string[];
  path?: string[];
  paths?: string[];
  render?: boolean;
  model: string;
  models?: string[];
  force?: boolean;
  slug?: string;
  filesReport?: boolean;
  maxInput?: number;
  maxOutput?: number;
  system?: string;
  silent?: boolean;
  search?: boolean;
  preview?: boolean | string;
  previewMode?: PreviewMode;
  apiKey?: string;
  session?: string;
  execSession?: string;
  notify?: boolean;
  notifySound?: boolean;
  renderMarkdown?: boolean;
  sessionId?: string;
  engine?: EngineMode;
  browser?: boolean;
  timeout?: number | 'auto';
  browserChromeProfile?: string;
  browserChromePath?: string;
  browserCookiePath?: string;
  chatgptUrl?: string;
  browserUrl?: string;
  browserTimeout?: string;
  browserInputTimeout?: string;
  browserCookieWait?: string;
  browserNoCookieSync?: boolean;
  browserInlineCookiesFile?: string;
  browserCookieNames?: string;
  browserInlineCookies?: string;
  browserHeadless?: boolean;
  browserHideWindow?: boolean;
  browserKeepBrowser?: boolean;
  browserModelStrategy?: 'select' | 'current' | 'ignore';
  browserManualLogin?: boolean;
  browserManualLoginProfileDir?: string;
  browserThinkingTime?: 'light' | 'standard' | 'extended' | 'heavy';
  browserAllowCookieErrors?: boolean;
  browserAttachments?: string;
  browserInlineFiles?: boolean;
  browserBundleFiles?: boolean;
  remoteChrome?: string;
  browserPort?: number;
  browserDebugPort?: number;
  remoteHost?: string;
  remoteToken?: string;
  copyMarkdown?: boolean;
  copy?: boolean;
  verbose?: boolean;
  debugHelp?: boolean;
  heartbeat?: number;
  status?: boolean;
  dryRun?: boolean;
  wait?: boolean;
  noWait?: boolean;
  baseUrl?: string;
  azureEndpoint?: string;
  azureDeployment?: string;
  azureApiVersion?: string;
  showModelId?: boolean;
  retainHours?: number;
  writeOutput?: string;
  writeOutputPath?: string;
}

type ResolvedCliOptions = Omit<CliOptions, 'model'> & {
  model: ModelName;
  models?: ModelName[];
  effectiveModelId?: string;
  writeOutputPath?: string;
};

const VERSION = getCliVersion();
const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
const rawCliArgs = process.argv.slice(2);
const userCliArgs = rawCliArgs[0] === CLI_ENTRYPOINT ? rawCliArgs.slice(1) : rawCliArgs;
const isTty = process.stdout.isTTY;

const program = new Command();
let introPrinted = false;
program.hook('preAction', () => {
  if (introPrinted) return;
  console.log(formatIntroLine(VERSION, { env: process.env, richTty: isTty }));
  introPrinted = true;
});
applyHelpStyling(program, VERSION, isTty);
program.hook('preAction', (thisCommand) => {
  if (thisCommand !== program) {
    return;
  }
  if (userCliArgs.some((arg) => arg === '--help' || arg === '-h')) {
    return;
  }
  if (userCliArgs.length === 0) {
    // Let the root action handle zero-arg entry (help + hint to `oracle tui`).
    return;
  }
  const opts = thisCommand.optsWithGlobals() as CliOptions;
  applyHiddenAliases(opts, (key, value) => thisCommand.setOptionValue(key, value));
  const positional = thisCommand.args?.[0] as string | undefined;
  if (!opts.prompt && positional) {
    opts.prompt = positional;
    thisCommand.setOptionValue('prompt', positional);
  }
  if (shouldRequirePrompt(userCliArgs, opts)) {
    console.log(chalk.yellow('Prompt is required. Provide it via --prompt "<text>" or positional [prompt].'));
    thisCommand.help({ error: false });
    process.exitCode = 1;
    return;
  }
});
program
  .name('oracle')
  .description('One-shot GPT-5.2 Pro / GPT-5.2 / GPT-5.1 Codex tool for hard questions that benefit from large file context and server-side search.')
  .version(VERSION)
  .argument('[prompt]', 'Prompt text (shorthand for --prompt).')
  .option('-p, --prompt <text>', 'User prompt to send to the model.')
  .addOption(new Option('--message <text>', 'Alias for --prompt.').hideHelp())
  .option(
    '-f, --file <paths...>',
    'Files/directories or glob patterns to attach (prefix with !pattern to exclude). Files larger than 1 MB are rejected automatically.',
    collectPaths,
    [],
  )
  .addOption(
    new Option('--include <paths...>', 'Alias for --file.')
      .argParser(collectPaths)
      .default([])
      .hideHelp(),
  )
  .addOption(
    new Option('--files <paths...>', 'Alias for --file.')
      .argParser(collectPaths)
      .default([])
      .hideHelp(),
  )
  .addOption(
    new Option('--path <paths...>', 'Alias for --file.')
      .argParser(collectPaths)
      .default([])
      .hideHelp(),
  )
  .addOption(
    new Option('--paths <paths...>', 'Alias for --file.')
      .argParser(collectPaths)
      .default([])
      .hideHelp(),
  )
  .addOption(
    new Option(
      '--copy-markdown',
      'Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.',
    ).default(false),
  )
  .addOption(new Option('--copy').hideHelp().default(false))
  .option('-s, --slug <words>', 'Custom session slug (3-5 words).')
  .option(
    '-m, --model <model>',
    'Model to target (gpt-5.2-pro default; also supports gpt-5.1-pro alias). Also gpt-5-pro, gpt-5.1, gpt-5.1-codex API-only, gpt-5.2, gpt-5.2-instant, gpt-5.2-pro, gemini-3-pro, claude-4.5-sonnet, claude-4.1-opus, or ChatGPT labels like "5.2 Thinking" for browser runs).',
    normalizeModelOption,
  )
  .addOption(
    new Option(
      '--models <models>',
      'Comma-separated API model list to query in parallel (e.g., "gpt-5.2-pro,gemini-3-pro").',
    )
      .argParser(collectModelList)
      .default([]),
  )
  .addOption(
    new Option(
      '-e, --engine <mode>',
      'Execution engine (api | browser). Browser engine: GPT models automate ChatGPT; Gemini models use a cookie-based client for gemini.google.com. If omitted, oracle picks api when OPENAI_API_KEY is set, otherwise browser.',
    ).choices(['api', 'browser'])
  )
  .addOption(
    new Option('--mode <mode>', 'Alias for --engine (api | browser).').choices(['api', 'browser']).hideHelp(),
  )
  .option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
  .option('-v, --verbose', 'Enable verbose logging for all operations.', false)
  .addOption(
    new Option('--[no-]notify', 'Desktop notification when a session finishes (default on unless CI/SSH).')
      .default(undefined),
  )
  .addOption(
    new Option('--[no-]notify-sound', 'Play a notification sound on completion (default off).').default(undefined),
  )
  .addOption(
    new Option(
      '--timeout <seconds|auto>',
      'Overall timeout before aborting the API call (auto = 60m for gpt-5.2-pro, 120s otherwise).',
    )
      .argParser(parseTimeoutOption)
      .default('auto'),
  )
  .addOption(
    new Option(
      '--preview [mode]',
      '(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.',
    )
      .hideHelp()
      .choices(['summary', 'json', 'full'])
      .preset('summary'),
  )
  .addOption(
    new Option('--dry-run [mode]', 'Preview without calling the model (summary | json | full).')
      .choices(['summary', 'json', 'full'])
      .preset('summary')
      .default(false),
  )
  .addOption(new Option('--exec-session <id>').hideHelp())
  .addOption(new Option('--session <id>').hideHelp())
  .addOption(new Option('--status', 'Show stored sessions (alias for `oracle status`).').default(false).hideHelp())
  .option(
    '--render-markdown',
    'Print the assembled markdown bundle for prompt + files and exit; pair with --copy to put it on the clipboard.',
    false,
  )
  .option('--render', 'Alias for --render-markdown.', false)
  .option('--render-plain', 'Render markdown without ANSI/highlighting (use plain text even in a TTY).', false)
  .option(
    '--write-output <path>',
    'Write only the final assistant message to this file (overwrites; multi-model appends .<model> before the extension).',
  )
  .option('--verbose-render', 'Show render/TTY diagnostics when replaying sessions.', false)
  .addOption(
    new Option('--search <mode>', 'Set server-side search behavior (on/off).')
      .argParser(parseSearchOption)
      .hideHelp(),
  )
  .addOption(
    new Option('--max-input <tokens>', 'Override the input token budget for the selected model.')
      .argParser(parseIntOption)
      .hideHelp(),
  )
  .addOption(
    new Option('--max-output <tokens>', 'Override the max output tokens for the selected model.')
      .argParser(parseIntOption)
      .hideHelp(),
  )
  .option(
    '--base-url <url>',
    'Override the OpenAI-compatible base URL for API runs (e.g. LiteLLM proxy endpoint).',
  )
  .option('--azure-endpoint <url>', 'Azure OpenAI Endpoint (e.g. https://resource.openai.azure.com/).')
  .option('--azure-deployment <name>', 'Azure OpenAI Deployment Name.')
  .option('--azure-api-version <version>', 'Azure OpenAI API Version.')
  .addOption(new Option('--browser', '(deprecated) Use --engine browser instead.').default(false).hideHelp())
  .addOption(new Option('--browser-chrome-profile <name>', 'Chrome profile name/path for cookie reuse.').hideHelp())
  .addOption(new Option('--browser-chrome-path <path>', 'Explicit Chrome or Chromium executable path.').hideHelp())
  .addOption(
    new Option('--browser-cookie-path <path>', 'Explicit Chrome/Chromium cookie DB path for session reuse.'),
  )
  .addOption(
    new Option(
      '--chatgpt-url <url>',
      `Override the ChatGPT web URL (e.g., workspace/folder like https://chatgpt.com/g/.../project; default ${CHATGPT_URL}).`,
    ),
  )
  .addOption(new Option('--browser-url <url>', `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
  .addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 1200s / 20m).').hideHelp())
  .addOption(
    new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 30s).').hideHelp(),
  )
  .addOption(
    new Option(
      '--browser-cookie-wait <ms|s|m>',
      'Wait before retrying cookie sync when Chrome cookies are empty or locked.',
    ).hideHelp(),
  )
  .addOption(
    new Option('--browser-port <port>', 'Use a fixed Chrome DevTools port (helpful on WSL firewalls).')
      .argParser(parseIntOption),
  )
  .addOption(
    new Option('--browser-debug-port <port>', '(alias) Use a fixed Chrome DevTools port.').argParser(parseIntOption).hideHelp(),
  )
  .addOption(new Option('--browser-cookie-names <names>', 'Comma-separated cookie allowlist for sync.').hideHelp())
  .addOption(
    new Option('--browser-inline-cookies <jsonOrBase64>', 'Inline cookies payload (JSON array or base64-encoded JSON).').hideHelp(),
  )
  .addOption(
    new Option('--browser-inline-cookies-file <path>', 'Load inline cookies from file (JSON or base64 JSON).').hideHelp(),
  )
  .addOption(new Option('--browser-no-cookie-sync', 'Skip copying cookies from Chrome.').hideHelp())
  .addOption(
    new Option(
      '--browser-manual-login',
      'Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.',
    ).hideHelp(),
  )
  .addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
  .addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
  .addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
  .addOption(
    new Option(
      '--browser-model-strategy <mode>',
      'ChatGPT model picker strategy: select (default) switches to the requested model, current keeps the active model, ignore skips the picker entirely.',
    ).choices(['select', 'current', 'ignore']),
  )
  .addOption(
    new Option('--browser-thinking-time <level>', 'Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.')
      .choices(['light', 'standard', 'extended', 'heavy'])
      .hideHelp(),
  )
  .addOption(
    new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp(),
  )
  .addOption(
    new Option(
      '--browser-attachments <mode>',
      'How to deliver --file inputs in browser mode: auto (default) pastes inline up to ~60k chars then uploads; never always paste inline; always always upload.',
    )
      .choices(['auto', 'never', 'always'])
      .default('auto'),
  )
  .addOption(
    new Option(
      '--remote-chrome <host:port>',
      'Connect to remote Chrome DevTools Protocol (e.g., 192.168.1.10:9222 or [2001:db8::1]:9222 for IPv6).',
    ),
  )
  .addOption(new Option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.'))
  .addOption(new Option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.'))
  .addOption(
    new Option('--browser-inline-files', 'Alias for --browser-attachments never (force pasting file contents inline).').default(false),
  )
  .addOption(new Option('--browser-bundle-files', 'Bundle all attachments into a single archive before uploading.').default(false))
  .addOption(
    new Option(
      '--youtube <url>',
      'YouTube video URL to analyze (Gemini web/cookie mode only; uses your signed-in Chrome cookies for gemini.google.com).',
    ),
  )
  .addOption(
    new Option(
      '--generate-image <file>',
      'Generate image and save to file (Gemini web/cookie mode only; requires gemini.google.com Chrome cookies).',
    ),
  )
  .addOption(new Option('--edit-image <file>', 'Edit existing image (use with --output, Gemini web/cookie mode only).'))
  .addOption(new Option('--output <file>', 'Output file path for image operations (Gemini web/cookie mode only).'))
  .addOption(
    new Option(
      '--aspect <ratio>',
      'Aspect ratio for image generation: 16:9, 1:1, 4:3, 3:4 (Gemini web/cookie mode only).',
    ),
  )
  .addOption(new Option('--gemini-show-thoughts', 'Display Gemini thinking process (Gemini web/cookie mode only).').default(false))
  .option(
    '--retain-hours <hours>',
    'Prune stored sessions older than this many hours before running (set 0 to disable).',
    parseFloatOption,
  )
  .option('--force', 'Force start a new session even if an identical prompt is already running.', false)
  .option('--debug-help', 'Show the advanced/debug option set and exit.', false)
  .option('--heartbeat <seconds>', 'Emit periodic in-progress updates (0 to disable).', parseHeartbeatOption, 30)
  .addOption(new Option('--wait').default(undefined))
  .addOption(new Option('--no-wait').default(undefined).hideHelp())
  .showHelpAfterError('(use --help for usage)');

program.addHelpText(
  'after',
  `
Examples:
  # Quick API run with two files
  oracle --prompt "Summarize the risk register" --file docs/risk-register.md docs/risk-matrix.md

  # Browser run (no API key) + globbed TypeScript sources, excluding tests
  oracle --engine browser --prompt "Review the TS data layer" \\
    --file "src/**/*.ts" --file "!src/**/*.test.ts"

  # Build, print, and copy a markdown bundle (semi-manual)
  oracle --render --copy -p "Review the TS data layer" --file "src/**/*.ts" --file "!src/**/*.test.ts"
`,
);

program
  .command('serve')
  .description('Run Oracle browser automation as a remote service for other machines.')
  .option('--host <address>', 'Interface to bind (default 0.0.0.0).')
  .option('--port <number>', 'Port to listen on (default random).', parseIntOption)
  .option('--token <value>', 'Access token clients must provide (random if omitted).')
  .action(async (commandOptions) => {
    const { serveRemote } = await import('../src/remote/server.js');
    await serveRemote({
      host: commandOptions.host,
      port: commandOptions.port,
      token: commandOptions.token,
    });
  });

program
  .command('tui')
  .description('Launch the interactive terminal UI for humans (no automation).')
  .action(async () => {
    await sessionStore.ensureStorage();
    await launchTui({ version: VERSION, printIntro: false });
  });

const sessionCommand = program
  .command('session [id]')
  .description('Attach to a stored session or list recent sessions when no ID is provided.')
  .option('--hours <hours>', 'Look back this many hours when listing sessions (default 24).', parseFloatOption, 24)
  .option('--limit <count>', 'Maximum sessions to show when listing (max 1000).', parseIntOption, 100)
  .option('--all', 'Include all stored sessions regardless of age.', false)
  .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
  .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
  .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
  .option('--render-markdown', 'Alias for --render.', false)
  .option('--model <name>', 'Filter sessions/output for a specific model.', '')
  .option('--path', 'Print the stored session paths instead of attaching.', false)
  .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
  .action(async (sessionId, _options: StatusOptions, cmd: Command) => {
    await handleSessionCommand(sessionId, cmd);
  });

const statusCommand = program
  .command('status [id]')
  .description('List recent sessions (24h window by default) or attach to a session when an ID is provided.')
  .option('--hours <hours>', 'Look back this many hours (default 24).', parseFloatOption, 24)
  .option('--limit <count>', 'Maximum sessions to show (max 1000).', parseIntOption, 100)
  .option('--all', 'Include all stored sessions regardless of age.', false)
  .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
  .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
  .option('--render-markdown', 'Alias for --render.', false)
  .option('--model <name>', 'Filter sessions/output for a specific model.', '')
  .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
  .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
  .action(async (sessionId: string | undefined, _options: StatusOptions, command: Command) => {
    const statusOptions = command.opts<StatusOptions>();
    const clearRequested = Boolean(statusOptions.clear || statusOptions.clean);
    if (clearRequested) {
      if (sessionId) {
        console.error('Cannot combine a session ID with --clear. Remove the ID to delete cached sessions.');
        process.exitCode = 1;
        return;
      }
      const hours = statusOptions.hours;
      const includeAll = statusOptions.all;
      const result = await sessionStore.deleteOlderThan({ hours, includeAll });
      const scope = includeAll ? 'all stored sessions' : `sessions older than ${hours}h`;
      console.log(formatSessionCleanupMessage(result, scope));
      return;
    }
    if (sessionId === 'clear' || sessionId === 'clean') {
      console.error('Session cleanup now uses --clear. Run "oracle status --clear --hours <n>" instead.');
      process.exitCode = 1;
      return;
    }
    if (sessionId) {
      const autoRender = !command.getOptionValueSource?.('render') && !command.getOptionValueSource?.('renderMarkdown')
        ? process.stdout.isTTY
        : false;
      const renderMarkdown = Boolean(statusOptions.render || statusOptions.renderMarkdown || autoRender);
      await attachSession(sessionId, { renderMarkdown, renderPrompt: !statusOptions.hidePrompt });
      return;
    }
    const showExamples = usesDefaultStatusFilters(command);
    await showStatus({
      hours: statusOptions.all ? Infinity : statusOptions.hours,
      includeAll: statusOptions.all,
      limit: statusOptions.limit,
      showExamples,
    });
  });

function buildRunOptions(options: ResolvedCliOptions, overrides: Partial<RunOracleOptions> = {}): RunOracleOptions {
  if (!options.prompt) {
    throw new Error('Prompt is required.');
  }
  const normalizedBaseUrl = normalizeBaseUrl(overrides.baseUrl ?? options.baseUrl);
  const azure =
    options.azureEndpoint || overrides.azure?.endpoint
      ? {
          endpoint: overrides.azure?.endpoint ?? options.azureEndpoint,
          deployment: overrides.azure?.deployment ?? options.azureDeployment,
          apiVersion: overrides.azure?.apiVersion ?? options.azureApiVersion,
        }
      : undefined;

  return {
    prompt: options.prompt,
    model: options.model,
    models: overrides.models ?? options.models,
    effectiveModelId: overrides.effectiveModelId ?? options.effectiveModelId ?? options.model,
    file: overrides.file ?? options.file ?? [],
    slug: overrides.slug ?? options.slug,
    filesReport: overrides.filesReport ?? options.filesReport,
    maxInput: overrides.maxInput ?? options.maxInput,
    maxOutput: overrides.maxOutput ?? options.maxOutput,
    system: overrides.system ?? options.system,
    timeoutSeconds: overrides.timeoutSeconds ?? (options.timeout as number | 'auto' | undefined),
    silent: overrides.silent ?? options.silent,
    search: overrides.search ?? options.search,
    preview: overrides.preview ?? undefined,
    previewMode: overrides.previewMode ?? options.previewMode,
    apiKey: overrides.apiKey ?? options.apiKey,
    baseUrl: normalizedBaseUrl,
    azure,
    sessionId: overrides.sessionId ?? options.sessionId,
    verbose: overrides.verbose ?? options.verbose,
    heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
    browserAttachments: overrides.browserAttachments ?? (options.browserAttachments as 'auto' | 'never' | 'always' | undefined) ?? 'auto',
    browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
    browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
    background: overrides.background ?? undefined,
    renderPlain: overrides.renderPlain ?? options.renderPlain ?? false,
    writeOutputPath: overrides.writeOutputPath ?? options.writeOutputPath,
  };
}

export function enforceBrowserSearchFlag(
  runOptions: RunOracleOptions,
  sessionMode: SessionMode,
  logFn: (message: string) => void = console.log,
): void {
  if (sessionMode === 'browser' && runOptions.search === false) {
    logFn(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
    runOptions.search = undefined;
  }
}

function resolveHeartbeatIntervalMs(seconds: number | undefined): number | undefined {
  if (typeof seconds !== 'number' || seconds <= 0) {
    return undefined;
  }
  return Math.round(seconds * 1000);
}

function buildRunOptionsFromMetadata(metadata: SessionMetadata): RunOracleOptions {
  const stored = metadata.options ?? {};
  return {
    prompt: stored.prompt ?? '',
    model: (stored.model as ModelName) ?? DEFAULT_MODEL,
    models: stored.models as ModelName[] | undefined,
    effectiveModelId: stored.effectiveModelId ?? stored.model,
    file: stored.file ?? [],
    slug: stored.slug,
    filesReport: stored.filesReport,
    maxInput: stored.maxInput,
    maxOutput: stored.maxOutput,
    system: stored.system,
    silent: stored.silent,
    search: stored.search,
    preview: false,
    previewMode: undefined,
    apiKey: undefined,
    baseUrl: normalizeBaseUrl(stored.baseUrl),
    azure: stored.azure,
    sessionId: metadata.id,
    verbose: stored.verbose,
    heartbeatIntervalMs: stored.heartbeatIntervalMs,
    browserAttachments: stored.browserAttachments,
    browserInlineFiles: stored.browserInlineFiles,
    browserBundleFiles: stored.browserBundleFiles,
    background: stored.background,
    renderPlain: stored.renderPlain,
    writeOutputPath: stored.writeOutputPath,
  };
}

function getSessionMode(metadata: SessionMetadata): SessionMode {
  return metadata.mode ?? metadata.options?.mode ?? 'api';
}

function getBrowserConfigFromMetadata(metadata: SessionMetadata): BrowserSessionConfig | undefined {
  return metadata.options?.browserConfig ?? metadata.browser?.config;
}

async function runRootCommand(options: CliOptions): Promise<void> {
  if (process.env.ORACLE_FORCE_TUI === '1') {
    await sessionStore.ensureStorage();
    await launchTui({ version: VERSION, printIntro: false });
    return;
  }
  const userConfig = (await loadUserConfig()).config;
  const helpRequested = rawCliArgs.some((arg: string) => arg === '--help' || arg === '-h');
  const multiModelProvided = Array.isArray(options.models) && options.models.length > 0;
  if (multiModelProvided) {
    const modelFromConfigOrCli = normalizeModelOption(options.model ?? userConfig.model ?? '');
    if (modelFromConfigOrCli) {
      throw new Error('--models cannot be combined with --model.');
    }
  }
  const optionUsesDefault = (name: string): boolean => {
    // Commander reports undefined for untouched options, so treat undefined/default the same
    const source = program.getOptionValueSource?.(name);
    return source == null || source === 'default';
  };
  if (helpRequested) {
    if (options.verbose) {
      console.log('');
      printDebugHelp(program.name());
      console.log('');
    }
    program.help({ error: false });
    return;
  }
  const previewMode = resolvePreviewMode(options.dryRun || options.preview);
  const mergedFileInputs = mergePathLikeOptions(
    options.file,
    options.include,
    options.files,
    options.path,
    options.paths,
  );
  if (mergedFileInputs.length > 0) {
    const { deduped, duplicates } = dedupePathInputs(mergedFileInputs, { cwd: process.cwd() });
    if (duplicates.length > 0) {
      const preview = duplicates.slice(0, 8).join(', ');
      const suffix = duplicates.length > 8 ? ` (+${duplicates.length - 8} more)` : '';
      console.log(chalk.dim(`Ignoring duplicate --file inputs: ${preview}${suffix}`));
    }
    options.file = deduped;
  }
  const copyMarkdown = options.copyMarkdown || options.copy;
  const renderMarkdown = resolveRenderFlag(options.render, options.renderMarkdown);
  const renderPlain = resolveRenderPlain(options.renderPlain, options.render, options.renderMarkdown);

  const applyRetentionOption = (): void => {
    if (optionUsesDefault('retainHours') && typeof userConfig.sessionRetentionHours === 'number') {
      options.retainHours = userConfig.sessionRetentionHours;
    }
    const envRetention = process.env.ORACLE_RETAIN_HOURS;
    if (optionUsesDefault('retainHours') && envRetention) {
      const parsed = Number.parseFloat(envRetention);
      if (!Number.isNaN(parsed)) {
        options.retainHours = parsed;
      }
    }
  };
  applyRetentionOption();

  const remoteHost =
    options.remoteHost ?? userConfig.remoteHost ?? userConfig.remote?.host ?? process.env.ORACLE_REMOTE_HOST;
  const remoteToken =
    options.remoteToken ?? userConfig.remoteToken ?? userConfig.remote?.token ?? process.env.ORACLE_REMOTE_TOKEN;
  if (remoteHost) {
    console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
  }

  if (userCliArgs.length === 0) {
    console.log(chalk.yellow('No prompt or subcommand supplied. Run `oracle --help` or `oracle tui` for the TUI.'));
    program.outputHelp();
    return;
  }
  const retentionHours = typeof options.retainHours === 'number' ? options.retainHours : undefined;
  await sessionStore.ensureStorage();
  await pruneOldSessions(retentionHours, (message) => console.log(chalk.dim(message)));

  if (options.debugHelp) {
    printDebugHelp(program.name());
    return;
  }
  if (options.dryRun && options.renderMarkdown) {
    throw new Error('--dry-run cannot be combined with --render-markdown.');
  }

  const preferredEngine = options.engine ?? userConfig.engine;
  let engine: EngineMode = resolveEngine({ engine: preferredEngine, browserFlag: options.browser, env: process.env });
  if (options.browser) {
    console.log(chalk.yellow('`--browser` is deprecated; use `--engine browser` instead.'));
  }
  if (optionUsesDefault('model') && userConfig.model) {
    options.model = userConfig.model;
  }
  if (optionUsesDefault('search') && userConfig.search) {
    options.search = userConfig.search === 'on';
  }
  if (optionUsesDefault('filesReport') && userConfig.filesReport != null) {
    options.filesReport = Boolean(userConfig.filesReport);
  }
  if (optionUsesDefault('heartbeat') && typeof userConfig.heartbeatSeconds === 'number') {
    options.heartbeat = userConfig.heartbeatSeconds;
  }
  if (optionUsesDefault('baseUrl') && userConfig.apiBaseUrl) {
    options.baseUrl = userConfig.apiBaseUrl;
  }

  if (remoteHost && engine !== 'browser') {
    throw new Error('--remote-host requires --engine browser.');
  }
  if (remoteHost && options.remoteChrome) {
    throw new Error('--remote-host cannot be combined with --remote-chrome.');
  }

  if (optionUsesDefault('azureEndpoint')) {
    if (process.env.AZURE_OPENAI_ENDPOINT) {
      options.azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
    } else if (userConfig.azure?.endpoint) {
      options.azureEndpoint = userConfig.azure.endpoint;
    }
  }
  if (optionUsesDefault('azureDeployment')) {
    if (process.env.AZURE_OPENAI_DEPLOYMENT) {
      options.azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT;
    } else if (userConfig.azure?.deployment) {
      options.azureDeployment = userConfig.azure.deployment;
    }
  }
  if (optionUsesDefault('azureApiVersion')) {
    if (process.env.AZURE_OPENAI_API_VERSION) {
      options.azureApiVersion = process.env.AZURE_OPENAI_API_VERSION;
    } else if (userConfig.azure?.apiVersion) {
      options.azureApiVersion = userConfig.azure.apiVersion;
    }
  }

  const normalizedMultiModels: ModelName[] = multiModelProvided
    ? Array.from(new Set(options.models!.map((entry) => resolveApiModel(entry))))
    : [];
  const cliModelArg = normalizeModelOption(options.model) || (multiModelProvided ? '' : DEFAULT_MODEL);
  const resolvedModelCandidate: ModelName = multiModelProvided
    ? normalizedMultiModels[0]
    : engine === 'browser'
      ? inferModelFromLabel(cliModelArg || DEFAULT_MODEL)
      : resolveApiModel(cliModelArg || DEFAULT_MODEL);
  const primaryModelCandidate = normalizedMultiModels[0] ?? resolvedModelCandidate;
  const isGemini = primaryModelCandidate.startsWith('gemini');
  const isCodex = primaryModelCandidate.startsWith('gpt-5.1-codex');
  const isClaude = primaryModelCandidate.startsWith('claude');
  const userForcedBrowser = options.browser || options.engine === 'browser';
  const isBrowserCompatible = (model: string) => model.startsWith('gpt-') || model.startsWith('gemini');
  const hasNonBrowserCompatibleTarget =
    (engine === 'browser' || userForcedBrowser) &&
    (normalizedMultiModels.length > 0
      ? normalizedMultiModels.some((model) => !isBrowserCompatible(model))
      : !isBrowserCompatible(resolvedModelCandidate));
  if (hasNonBrowserCompatibleTarget) {
    throw new Error(
      'Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.'
    );
  }
  if (isClaude && engine === 'browser') {
    console.log(chalk.dim('Browser engine is not supported for Claude models; switching to API.'));
    engine = 'api';
  }
  if (isCodex && engine === 'browser') {
    console.log(chalk.dim('Browser engine is not supported for gpt-5.1-codex; switching to API.'));
    engine = 'api';
  }
  if (normalizedMultiModels.length > 0) {
    engine = 'api';
  }
  if (remoteHost && normalizedMultiModels.length > 0) {
    throw new Error('--remote-host does not support --models yet. Use API engine locally instead.');
  }
  const resolvedModel: ModelName =
    normalizedMultiModels[0] ?? (isGemini ? resolveApiModel(cliModelArg) : resolvedModelCandidate);
  const effectiveModelId = resolvedModel.startsWith('gemini')
    ? resolveGeminiModelId(resolvedModel)
    : isKnownModel(resolvedModel)
      ? MODEL_CONFIGS[resolvedModel].apiModel ?? resolvedModel
      : resolvedModel;
  const resolvedBaseUrl = normalizeBaseUrl(
    options.baseUrl ?? (isClaude ? process.env.ANTHROPIC_BASE_URL : process.env.OPENAI_BASE_URL),
  );
  const { models: _rawModels, ...optionsWithoutModels } = options;
  const resolvedOptions: ResolvedCliOptions = { ...optionsWithoutModels, model: resolvedModel };
  if (normalizedMultiModels.length > 0) {
    resolvedOptions.models = normalizedMultiModels;
  }
  resolvedOptions.baseUrl = resolvedBaseUrl;
  resolvedOptions.effectiveModelId = effectiveModelId;
  resolvedOptions.writeOutputPath = resolveOutputPath(options.writeOutput, process.cwd());

  // Decide whether to block until completion:
  // - explicit --wait / --no-wait wins
  // - otherwise block for fast models (gpt-5.1, browser) and detach by default for pro API runs
  let waitPreference = resolveWaitFlag({
    waitFlag: options.wait,
    noWaitFlag: options.noWait,
    model: resolvedModel,
    engine,
  });
  if (remoteHost && !waitPreference) {
    console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
    waitPreference = true;
  }

  if (await handleStatusFlag(options, { attachSession, showStatus })) {
    return;
  }

  if (await handleSessionAlias(options, { attachSession })) {
    return;
  }

  if (options.execSession) {
    await executeSession(options.execSession);
    return;
  }

  if (renderMarkdown || copyMarkdown) {
    if (!options.prompt) {
      throw new Error('Prompt is required when using --render-markdown or --copy-markdown.');
    }
    const bundle = await buildMarkdownBundle(
      { prompt: options.prompt, file: options.file, system: options.system },
      { cwd: process.cwd() },
    );
    const modelConfig = isKnownModel(resolvedModel) ? MODEL_CONFIGS[resolvedModel] : MODEL_CONFIGS['gpt-5.1'];
    const requestBody = buildRequestBody({
      modelConfig,
      systemPrompt: bundle.systemPrompt,
      userPrompt: bundle.promptWithFiles,
      searchEnabled: options.search !== false,
      background: false,
      storeResponse: false,
    });
    const estimatedTokens = estimateRequestTokens(requestBody, modelConfig);
    const warnThreshold = Math.min(196_000, modelConfig.inputLimit ?? 196_000);
    warnIfOversizeBundle(estimatedTokens, warnThreshold, console.log);
    if (renderMarkdown) {
      const output = renderPlain
        ? bundle.markdown
        : await formatRenderedMarkdown(bundle.markdown, { richTty: isTty });
      // Trim trailing newlines from the rendered bundle so we print exactly one blank before the summary line.
      console.log(output.replace(/\n+$/u, ''));
    }
    if (copyMarkdown) {
      const result = await copyToClipboard(bundle.markdown);
      if (result.success) {
        const filesPart = bundle.files.length > 0 ? `; ${bundle.files.length} files` : '';
        const summary = `Copied markdown to clipboard (~${formatCompactNumber(estimatedTokens)} tokens${filesPart}).`;
        console.log(chalk.green(summary));
      } else {
        const reason = result.error instanceof Error ? result.error.message : String(result.error ?? 'unknown error');
        console.log(
          chalk.dim(
            `Copy failed (${reason}); markdown not printed. Re-run with --render-markdown if you need the content.`,
          ),
        );
      }
    }
    return;
  }

  if (previewMode) {
    if (!options.prompt) {
      throw new Error('Prompt is required when using --dry-run/preview.');
    }
    if (userConfig.promptSuffix) {
      options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
    }
    resolvedOptions.prompt = options.prompt;
    const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode, baseUrl: resolvedBaseUrl });
    if (engine === 'browser') {
      await runBrowserPreview(
        {
          runOptions,
          cwd: process.cwd(),
          version: VERSION,
          previewMode,
          log: console.log,
        },
        {},
      );
      return;
    }
    // API dry-run/preview path
    if (previewMode === 'summary') {
      await runDryRunSummary(
        {
          engine,
          runOptions,
          cwd: process.cwd(),
          version: VERSION,
          log: console.log,
        },
        {},
      );
      return;
    }
    await runDryRunSummary(
      {
        engine,
        runOptions,
        cwd: process.cwd(),
        version: VERSION,
        log: console.log,
      },
      {},
    );
    return;
  }

  if (!options.prompt) {
    throw new Error('Prompt is required when starting a new session.');
  }

  if (userConfig.promptSuffix) {
    options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
  }
  resolvedOptions.prompt = options.prompt;

  const duplicateBlocked = await shouldBlockDuplicatePrompt({
    prompt: resolvedOptions.prompt,
    force: options.force,
    sessionStore,
    log: console.log,
  });
  if (duplicateBlocked) {
    process.exitCode = 1;
    return;
  }

  if (options.file && options.file.length > 0) {
    const isBrowserMode = engine === 'browser' || userForcedBrowser;
    const filesToValidate = isBrowserMode ? options.file.filter((f: string) => !isMediaFile(f)) : options.file;
    if (filesToValidate.length > 0) {
      await readFiles(filesToValidate, { cwd: process.cwd() });
    }
  }

  const getSource = (key: keyof CliOptions) => program.getOptionValueSource?.(key as string) ?? undefined;
  applyBrowserDefaultsFromConfig(options, userConfig, getSource);

  const notifications = resolveNotificationSettings({
    cliNotify: options.notify,
    cliNotifySound: options.notifySound,
    env: process.env,
    config: userConfig.notify,
  });

  const sessionMode: SessionMode = engine === 'browser' ? 'browser' : 'api';
  const browserModelLabelOverride =
    sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
  const browserConfig =
    sessionMode === 'browser'
      ? await buildBrowserConfig({
          ...options,
          model: resolvedModel,
          browserModelLabel: browserModelLabelOverride,
        })
      : undefined;

  let browserDeps: BrowserSessionRunnerDeps | undefined;
  if (browserConfig && remoteHost) {
    browserDeps = {
      executeBrowser: createRemoteBrowserExecutor({ host: remoteHost, token: remoteToken }),
    };
    console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
  } else if (browserConfig && resolvedModel.startsWith('gemini')) {
    browserDeps = {
      executeBrowser: createGeminiWebExecutor({
        youtube: options.youtube,
        generateImage: options.generateImage,
        editImage: options.editImage,
        outputPath: options.output,
        aspectRatio: options.aspect,
        showThoughts: options.geminiShowThoughts,
      }),
    };
    console.log(chalk.dim('Using Gemini web client for browser automation'));
    if (browserConfig.modelStrategy && browserConfig.modelStrategy !== 'select') {
      console.log(chalk.dim('Browser model strategy is ignored for Gemini web runs.'));
    }
  }
  const remoteExecutionActive = Boolean(browserDeps);

  if (options.dryRun) {
    const baseRunOptions = buildRunOptions(resolvedOptions, {
      preview: false,
      previewMode: undefined,
      baseUrl: resolvedBaseUrl,
    });
    await runDryRunSummary(
      {
        engine,
        runOptions: baseRunOptions,
        cwd: process.cwd(),
        version: VERSION,
        log: console.log,
        browserConfig,
      },
      {},
    );
    return;
  }

  await sessionStore.ensureStorage();
  const baseRunOptions = buildRunOptions(resolvedOptions, {
    preview: false,
    previewMode: undefined,
    background: userConfig.background ?? resolvedOptions.background,
    baseUrl: resolvedBaseUrl,
  });
  enforceBrowserSearchFlag(baseRunOptions, sessionMode, console.log);
  if (sessionMode === 'browser' && baseRunOptions.search === false) {
    console.log(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
    baseRunOptions.search = undefined;
  }
  const sessionMeta = await sessionStore.createSession(
    {
      ...baseRunOptions,
      mode: sessionMode,
      browserConfig,
    },
    process.cwd(),
    notifications,
  );
  const liveRunOptions: RunOracleOptions = {
    ...baseRunOptions,
    sessionId: sessionMeta.id,
    effectiveModelId,
  };
  const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
  const detachAllowed = remoteExecutionActive
    ? false
    : shouldDetachSession({
        engine,
        model: resolvedModel,
        waitPreference,
        disableDetachEnv,
      });
  const detached = !detachAllowed
    ? false
    : await launchDetachedSession(sessionMeta.id).catch((error) => {
      const message = error instanceof Error ? error.message : String(error);
      console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
      return false;
    });

  if (!waitPreference) {
    if (!detached) {
      console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
      process.exitCode = 1;
      return;
    }
    console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
    console.log(
      chalk.dim('Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached.'),
    );
    return;
  }

  if (detached === false) {
    await runInteractiveSession(
      sessionMeta,
      liveRunOptions,
      sessionMode,
      browserConfig,
      false,
      notifications,
      userConfig,
      true,
      browserDeps,
    );
    return;
  }
  if (detached) {
    console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
    await attachSession(sessionMeta.id, { suppressMetadata: true });
  }
}

async function runInteractiveSession(
  sessionMeta: SessionMetadata,
  runOptions: RunOracleOptions,
  mode: SessionMode,
  browserConfig?: BrowserSessionConfig,
  showReattachHint = true,
  notifications?: NotificationSettings,
  userConfig?: UserConfig,
  suppressSummary = false,
  browserDeps?: BrowserSessionRunnerDeps,
): Promise<void> {
  const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionMeta.id);
  let headerAugmented = false;
  const combinedLog = (message = ''): void => {
    if (!headerAugmented && message.startsWith('oracle (')) {
      headerAugmented = true;
      if (showReattachHint) {
        console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
      } else {
        console.log(message);
      }
      logLine(message);
      return;
    }
    console.log(message);
    logLine(message);
  };
  const combinedWrite = (chunk: string): boolean => {
    // runOracle handles stdout; keep this write hook for session logs only to avoid double-printing
    writeChunk(chunk);
    return true;
  };
  try {
    await performSessionRun({
      sessionMeta,
      runOptions,
      mode,
      browserConfig,
      cwd: process.cwd(),
      log: combinedLog,
      write: combinedWrite,
      version: VERSION,
      notifications:
        notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
      browserDeps,
    });
    const latest = await sessionStore.readSession(sessionMeta.id);
    if (!suppressSummary) {
      const summary = latest ? formatCompletionSummary(latest, { includeSlug: true }) : null;
      if (summary) {
        console.log('\n' + chalk.green.bold(summary));
        logLine(summary); // plain text in log, colored on stdout
      }
    }
  } catch (error) {
    throw error;
  } finally {
    stream.end();
  }
}

async function launchDetachedSession(sessionId: string): Promise<boolean> {
  return new Promise((resolve, reject) => {
    try {
      const args = ['--', CLI_ENTRYPOINT, '--exec-session', sessionId];
      const child = spawn(process.execPath, args, {
        detached: true,
        stdio: 'ignore',
        env: process.env,
      });
      child.once('error', reject);
      child.once('spawn', () => {
        child.unref();
        resolve(true);
      });
    } catch (error) {
      reject(error);
    }
  });
}

async function executeSession(sessionId: string) {
  const metadata = await sessionStore.readSession(sessionId);
  if (!metadata) {
    console.error(chalk.red(`No session found with ID ${sessionId}`));
    process.exitCode = 1;
    return;
  }
  const runOptions = buildRunOptionsFromMetadata(metadata);
  const sessionMode = getSessionMode(metadata);
  const browserConfig = getBrowserConfigFromMetadata(metadata);
  const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionId);
  const userConfig = (await loadUserConfig()).config;
  const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
  try {
    await performSessionRun({
      sessionMeta: metadata,
      runOptions,
      mode: sessionMode,
      browserConfig,
      cwd: metadata.cwd ?? process.cwd(),
      log: logLine,
      write: writeChunk,
      version: VERSION,
      notifications,
    });
  } catch {
    // Errors are already logged to the session log; keep quiet to mirror stored-session behavior.
  } finally {
    stream.end();
  }
}

function printDebugHelp(cliName: string): void {
  console.log(chalk.bold('Advanced Options'));
  printDebugOptionGroup([
    ['--search <on|off>', 'Enable or disable the server-side search tool (default on).'],
    ['--max-input <tokens>', 'Override the input token budget.'],
    ['--max-output <tokens>', 'Override the max output tokens (model default otherwise).'],
  ]);
  console.log('');
  console.log(chalk.bold('Browser Options'));
  printDebugOptionGroup([
    ['--chatgpt-url <url>', 'Override the ChatGPT web URL (workspace/folder targets).'],
    ['--browser-chrome-profile <name>', 'Reuse cookies from a specific Chrome profile.'],
    ['--browser-chrome-path <path>', 'Point to a custom Chrome/Chromium binary.'],
    ['--browser-cookie-path <path>', 'Use a specific Chrome/Chromium cookie store file.'],
    ['--browser-url <url>', 'Alias for --chatgpt-url.'],
    ['--browser-timeout <ms|s|m>', 'Cap total wait time for the assistant response.'],
    ['--browser-input-timeout <ms|s|m>', 'Cap how long we wait for the composer textarea.'],
    ['--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.'],
    ['--browser-no-cookie-sync', 'Skip copying cookies from your main profile.'],
    ['--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and log in manually.'],
    ['--browser-headless', 'Launch Chrome in headless mode.'],
    ['--browser-hide-window', 'Hide the Chrome window (macOS headful only).'],
    ['--browser-keep-browser', 'Leave Chrome running after completion.'],
  ]);
  console.log('');
  console.log(chalk.dim(`Tip: run \`${cliName} --help\` to see the primary option set.`));
}

function printDebugOptionGroup(entries: Array<[string, string]>): void {
  const flagWidth = Math.max(...entries.map(([flag]) => flag.length));
  entries.forEach(([flag, description]) => {
    const label = chalk.cyan(flag.padEnd(flagWidth + 2));
    console.log(`  ${label}${description}`);
  });
}

function resolveWaitFlag({
  waitFlag,
  noWaitFlag,
  model,
  engine,
}: {
  waitFlag?: boolean;
  noWaitFlag?: boolean;
  model: ModelName;
  engine: EngineMode;
}): boolean {
  if (waitFlag === true) return true;
  if (noWaitFlag === true) return false;
  return defaultWaitPreference(model, engine);
}

program.action(async function (this: Command) {
  const options = this.optsWithGlobals() as CliOptions;
  await runRootCommand(options);
});

async function main(): Promise<void> {
  const parsePromise = program.parseAsync(process.argv);
  const sigintPromise = once(process, 'SIGINT').then(() => 'sigint' as const);
  const result = await Promise.race([parsePromise.then(() => 'parsed' as const), sigintPromise]);
  if (result === 'sigint') {
    console.log(chalk.yellow('\nCancelled.'));
    process.exitCode = 130;
  }
}

void main().catch((error: unknown) => {
  if (error instanceof Error) {
    if (!isErrorLogged(error)) {
      console.error(chalk.red('✖'), error.message);
    }
  } else {
    console.error(chalk.red('✖'), error);
  }
  process.exitCode = 1;
});


--- bin/oracle-mcp.ts ---
#!/usr/bin/env node
import { startMcpServer } from '../src/mcp/server.js';

startMcpServer().catch((error) => {
  console.error('oracle-mcp exited with an error:', error);
  process.exitCode = 1;
});


--- scripts/agent-send.ts ---
#!/usr/bin/env bun
// @ts-nocheck

/**
 * Lightweight helper to send a one-off message to a tmux-based agent session.
 *
 * Usage:
 *   bun scripts/agent-send.ts --session claude-haiku -- "/model"
 *
 * Options:
 *   --session NAME             Target tmux session (or session:window.pane)
 *   --entry single|double|none How many Enter keys to send (default single)
 *   --escape                   Send ESC before typing (to interrupt/resume)
 *   --wait-ms N                Extra wait (ms) after typing before Enter
 */

import { spawnSync } from 'node:child_process';
import { sleepSync } from 'bun';

type EntryMode = 'single' | 'double' | 'none';

interface CliOptions {
  session: string;
  entry: EntryMode;
  escape: boolean;
  waitMs: number;
  message: string;
}

function usage(message?: string): never {
  if (message) {
    console.error(`Error: ${message}`);
  }
  console.error(`\
Usage: bun scripts/agent-send.ts --session <name[:window[.pane]]> [--entry single|double|none] [--escape] [--wait-ms N] -- "<message>"

Examples:
  bun scripts/agent-send.ts --session claude-haiku -- "/model"
  bun scripts/agent-send.ts --session ma-worker-1 --escape --entry double -- "Continue and focus on API routes"
`);
  process.exit(1);
}

function parseArgs(argv: string[]): CliOptions {
  let session: string | undefined;
  let entry: EntryMode = 'single';
  let shouldEscape = false;
  let waitMs = 400;
  const literalSeparator = argv.indexOf('--');
  const optionPart = literalSeparator === -1 ? argv : argv.slice(0, literalSeparator);
  const literalPart = literalSeparator === -1 ? [] : argv.slice(literalSeparator + 1);

  for (let i = 0; i < optionPart.length; i += 1) {
    const token = optionPart[i];
    if (!token.startsWith('--')) {
      usage(`Unexpected argument: ${token}`);
    }
    const key = token.slice(2);
    switch (key) {
      case 'session': {
        const value = optionPart[i + 1];
        if (!value) usage('--session requires a value');
        session = value;
        i += 1;
        break;
      }
      case 'entry': {
        const value = optionPart[i + 1];
        if (value !== 'single' && value !== 'double' && value !== 'none') {
          usage(`Unknown entry mode: ${value}`);
        }
        entry = value;
        i += 1;
        break;
      }
      case 'escape': {
        shouldEscape = true;
        break;
      }
      case 'wait-ms': {
        const value = optionPart[i + 1];
        if (!value || Number.isNaN(Number.parseInt(value, 10))) {
          usage('--wait-ms requires an integer value');
        }
        waitMs = Number.parseInt(value, 10);
        i += 1;
        break;
      }
      default:
        usage(`Unknown option: --${key}`);
    }
  }

  const message = literalPart.join(' ').trim();
  if (!session) usage('Missing --session');
  if (!message) usage('Missing message (provide text after -- separator)');

  return { session, entry, escape: shouldEscape, waitMs, message };
}

function runTmux(args: string[], allowFailure = false): string {
  const result = spawnSync('tmux', args, { encoding: 'utf8' });
  if (result.error) {
    if (allowFailure) return '';
    throw result.error;
  }
  if (result.status !== 0) {
    if (allowFailure) return result.stderr?.trim() ?? '';
    throw new Error(`tmux ${args.join(' ')} failed: ${result.stderr?.trim()}`);
  }
  return result.stdout?.trimEnd() ?? '';
}

function ensureSession(target: string): void {
  const session = target.split(':')[0] ?? target;
  const result = spawnSync('tmux', ['has-session', '-t', session]);
  if (result.status !== 0) {
    usage(`tmux session '${session}' not found. Start it first (e.g., tmux new-session -s ${session} ...)`);
  }
}

function sendMessage(options: CliOptions): void {
  ensureSession(options.session);

  if (options.escape) {
    runTmux(['send-keys', '-t', options.session, 'Escape'], true);
    sleepSync(200);
  }

  // Clear existing prompt
  runTmux(['send-keys', '-t', options.session, 'Escape'], true);
  sleepSync(120);
  runTmux(['send-keys', '-t', options.session, 'C-u'], true);
  sleepSync(120);

  // Type the message
  runTmux(['send-keys', '-t', options.session, '-l', options.message], true);
  sleepSync(Math.max(120, options.waitMs));

  // Send Enter(s)
  const pressEnter = () => runTmux(['send-keys', '-t', options.session, 'C-m'], true);
  switch (options.entry) {
    case 'single':
      pressEnter();
      break;
    case 'double':
      pressEnter();
      sleepSync(200);
      pressEnter();
      break;
    case 'none':
      break;
    default:
      usage(`Unsupported entry mode: ${options.entry}`);
  }

  sleepSync(600);
  const tail = runTmux(['capture-pane', '-pt', options.session, '-S', '-6'], true);
  console.log(tail);
}

try {
  const options = parseArgs(process.argv.slice(2));
  sendMessage(options);
} catch (error) {
  usage(error instanceof Error ? error.message : String(error));
}


--- scripts/browser-tools.ts ---
#!/usr/bin/env ts-node

/**
 * Minimal Chrome DevTools helpers inspired by Mario Zechner's
 * "What if you don't need MCP?" article.
 *
 * Keeps everything in one TypeScript CLI so agents (or humans) can drive Chrome
 * directly via the DevTools protocol without pulling in a large MCP server.
 */
import { Command } from 'commander';
import { execSync, spawn } from 'node:child_process';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
import puppeteer from 'puppeteer-core';

/** Utility type so TypeScript knows the async function constructor */
type AsyncFunctionCtor = new (...args: string[]) => (...fnArgs: unknown[]) => Promise<unknown>;

const DEFAULT_PORT = 9222;
const DEFAULT_PROFILE_DIR = path.join(os.homedir(), '.cache', 'scraping');
const DEFAULT_CHROME_BIN = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';

function browserURL(port: number): string {
  return `http://localhost:${port}`;
}

async function connectBrowser(port: number) {
  return puppeteer.connect({ browserURL: browserURL(port), defaultViewport: null });
}

async function getActivePage(port: number) {
  const browser = await connectBrowser(port);
  const pages = await browser.pages();
  const page = pages.at(-1);
  if (!page) {
    await browser.disconnect();
    throw new Error('No active tab found');
  }
  return { browser, page };
}

const program = new Command();
program
  .name('browser-tools')
  .description('Lightweight Chrome DevTools helpers (no MCP required).')
  .configureHelp({ sortSubcommands: true })
  .showSuggestionAfterError();

program
  .command('start')
  .description('Launch Chrome with remote debugging enabled.')
  .option('-p, --port <number>', 'Remote debugging port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
  .option('--profile', 'Copy your default Chrome profile before launch.', false)
  .option('--profile-dir <path>', 'Directory for the temporary Chrome profile.', DEFAULT_PROFILE_DIR)
  .option('--chrome-path <path>', 'Path to the Chrome binary.', DEFAULT_CHROME_BIN)
  .action(async (options) => {
    const { port, profile, profileDir, chromePath } = options as {
      port: number;
      profile: boolean;
      profileDir: string;
      chromePath: string;
    };

    try {
      execSync("killall 'Google Chrome'", { stdio: 'ignore' });
    } catch {
      // ignore missing processes
    }
    await new Promise((resolve) => setTimeout(resolve, 1000));
    execSync(`mkdir -p "${profileDir}"`);
    if (profile) {
      const source = `${path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome')}/`;
      execSync(`rsync -a --delete "${source}" "${profileDir}/"`, { stdio: 'ignore' });
    }

    spawn(chromePath, [`--remote-debugging-port=${port}`, `--user-data-dir=${profileDir}`, '--no-first-run', '--disable-popup-blocking'], {
      detached: true,
      stdio: 'ignore',
    }).unref();

    let connected = false;
    for (let attempt = 0; attempt < 30; attempt++) {
      try {
        const browser = await connectBrowser(port);
        await browser.disconnect();
        connected = true;
        break;
      } catch {
        await new Promise((resolve) => setTimeout(resolve, 500));
      }
    }

    if (!connected) {
      console.error(`✗ Failed to start Chrome on port ${port}`);
      process.exit(1);
    }
    console.log(`✓ Chrome listening on http://localhost:${port}${profile ? ' (profile copied)' : ''}`);
  });

program
  .command('nav <url>')
  .description('Navigate the current tab or open a new tab.')
  .option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
  .option('--new', 'Open in a new tab.', false)
  .action(async (url: string, options) => {
    const port = options.port as number;
    const browser = await connectBrowser(port);
    try {
      if (options.new) {
        const page = await browser.newPage();
        await page.goto(url, { waitUntil: 'domcontentloaded' });
        console.log('✓ Opened in new tab:', url);
      } else {
        const pages = await browser.pages();
        const page = pages.at(-1);
        if (!page) {
          throw new Error('No active tab found');
        }
        await page.goto(url, { waitUntil: 'domcontentloaded' });
        console.log('✓ Navigated current tab to:', url);
      }
    } finally {
      await browser.disconnect();
    }
  });

program
  .command('eval <code...>')
  .description('Evaluate JavaScript in the active page context.')
  .option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
  .action(async (code: string[], options) => {
    const snippet = code.join(' ');
    const port = options.port as number;
    const { browser, page } = await getActivePage(port);
    try {
      const result = await page.evaluate((body) => {
        const ASYNC_FN = Object.getPrototypeOf(async () => {}).constructor as AsyncFunctionCtor;
        return new ASYNC_FN(`return (${body})`)();
      }, snippet);

      if (Array.isArray(result)) {
        result.forEach((entry, index) => {
          if (index > 0) {
            console.log('');
          }
          Object.entries(entry).forEach(([key, value]) => {
            console.log(`${key}: ${value}`);
          });
        });
      } else if (typeof result === 'object' && result !== null) {
        Object.entries(result).forEach(([key, value]) => {
          console.log(`${key}: ${value}`);
        });
      } else {
        console.log(result);
      }
    } finally {
      await browser.disconnect();
    }
  });

program
  .command('screenshot')
  .description('Capture the current viewport and print the temp PNG path.')
  .option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
  .action(async (options) => {
    const port = options.port as number;
    const { browser, page } = await getActivePage(port);
    try {
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
      const filePath = path.join(
        os.tmpdir(),
        `screenshot-${timestamp}.png`,
      ) as `${string}.png`;
      await page.screenshot({ path: filePath });
      console.log(filePath);
    } finally {
      await browser.disconnect();
    }
  });

program
  .command('pick <message...>')
  .description('Interactive DOM picker that prints metadata for clicked elements.')
  .option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
  .action(async (messageParts: string[], options) => {
    const message = messageParts.join(' ');
    const port = options.port as number;
    const { browser, page } = await getActivePage(port);
    try {
      await page.evaluate(() => {
        const scope = globalThis as typeof globalThis & {
          pickOverlayInjected?: boolean;
          pick?: (prompt: string) => Promise<unknown>;
        };
        if (scope.pickOverlayInjected) {
          return;
        }
        scope.pickOverlayInjected = true;
        scope.pick = async (prompt: string) =>
          new Promise((resolve) => {
            const selections: unknown[] = [];
            const selectedElements = new Set<HTMLElement>();

            const overlay = document.createElement('div');
            overlay.style.cssText =
              'position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none';

            const highlight = document.createElement('div');
            highlight.style.cssText =
              'position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);transition:all 0.05s ease';
            overlay.appendChild(highlight);

            const banner = document.createElement('div');
            banner.style.cssText =
              'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:#fff;padding:12px 24px;border-radius:8px;font:14px system-ui;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647';

            const updateBanner = () => {
              banner.textContent = `${prompt} (${selections.length} selected, Cmd/Ctrl+click to add, Enter to finish, ESC to cancel)`;
            };

            const cleanup = () => {
              document.removeEventListener('mousemove', onMove, true);
              document.removeEventListener('click', onClick, true);
              document.removeEventListener('keydown', onKey, true);
              overlay.remove();
              banner.remove();
              selectedElements.forEach((el) => {
                el.style.outline = '';
              });
            };

            const serialize = (el: HTMLElement) => {
              const parents: string[] = [];
              let current = el.parentElement;
              while (current && current !== document.body) {
                const id = current.id ? `#${current.id}` : '';
                const cls = current.className ? `.${current.className.trim().split(/\s+/).join('.')}` : '';
                parents.push(`${current.tagName.toLowerCase()}${id}${cls}`);
                current = current.parentElement;
              }
              return {
                tag: el.tagName.toLowerCase(),
                id: el.id || null,
                class: el.className || null,
                text: el.textContent?.trim()?.slice(0, 200) || null,
                html: el.outerHTML.slice(0, 500),
                parents: parents.join(' > '),
              };
            };

            const onMove = (event: MouseEvent) => {
              const node = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement | null;
              if (!node || overlay.contains(node) || banner.contains(node)) return;
              const rect = node.getBoundingClientRect();
              highlight.style.cssText = `position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);top:${rect.top}px;left:${rect.left}px;width:${rect.width}px;height:${rect.height}px`;
            };
            const onClick = (event: MouseEvent) => {
              if (banner.contains(event.target as Node)) return;
              event.preventDefault();
              event.stopPropagation();
              const node = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement | null;
              if (!node || overlay.contains(node) || banner.contains(node)) return;

              if (event.metaKey || event.ctrlKey) {
                if (!selectedElements.has(node)) {
                  selectedElements.add(node);
                  node.style.outline = '3px solid #10b981';
                  selections.push(serialize(node));
                  updateBanner();
                }
              } else {
                cleanup();
                const info = serialize(node);
                resolve(selections.length > 0 ? selections : info);
              }
            };

            const onKey = (event: KeyboardEvent) => {
              if (event.key === 'Escape') {
                cleanup();
                resolve(null);
              } else if (event.key === 'Enter' && selections.length > 0) {
                cleanup();
                resolve(selections);
              }
            };

            document.addEventListener('mousemove', onMove, true);
            document.addEventListener('click', onClick, true);
            document.addEventListener('keydown', onKey, true);

            document.body.append(overlay, banner);
            updateBanner();
          });
      });

      const result = await page.evaluate((msg) => {
        const pickFn = (window as Window & { pick?: (message: string) => Promise<unknown> }).pick;
        if (!pickFn) {
          return null;
        }
        return pickFn(msg);
      }, message);

      if (Array.isArray(result)) {
        result.forEach((entry, index) => {
          if (index > 0) {
            console.log('');
          }
          Object.entries(entry).forEach(([key, value]) => {
            console.log(`${key}: ${value}`);
          });
        });
      } else if (result && typeof result === 'object') {
        Object.entries(result).forEach(([key, value]) => {
          console.log(`${key}: ${value}`);
        });
      } else {
        console.log(result);
      }
    } finally {
      await browser.disconnect();
    }
  });

program
  .command('cookies')
  .description('Dump cookies from the active tab as JSON.')
  .option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
  .action(async (options) => {
    const port = options.port as number;
    const { browser, page } = await getActivePage(port);
    try {
      const cookies = await page.cookies();
      console.log(JSON.stringify(cookies, null, 2));
    } finally {
      await browser.disconnect();
    }
  });

program
  .command('inspect')
  .description('List Chrome processes launched with --remote-debugging-port and show their open tabs.')
  .option('--ports <list>', 'Comma-separated list of ports to include.', parseNumberListArg)
  .option('--pids <list>', 'Comma-separated list of PIDs to include.', parseNumberListArg)
  .option('--json', 'Emit machine-readable JSON output.', false)
  .action(async (options) => {
    const ports = (options.ports as number[] | undefined)?.filter((entry) => Number.isFinite(entry) && entry > 0);
    const pids = (options.pids as number[] | undefined)?.filter((entry) => Number.isFinite(entry) && entry > 0);
    const sessions = await describeChromeSessions({
      ports,
      pids,
      includeAll: !ports?.length && !pids?.length,
    });
    if (options.json) {
      console.log(JSON.stringify(sessions, null, 2));
      return;
    }
    if (sessions.length === 0) {
      console.log('No Chrome instances with DevTools ports found.');
      return;
    }
    sessions.forEach((session, index) => {
      if (index > 0) {
        console.log('');
      }
      const header = [`Chrome PID ${session.pid}`, `(port ${session.port})`];
      if (session.version?.Browser) {
        header.push(`- ${session.version.Browser}`);
      }
      console.log(header.join(' '));
      if (session.tabs.length === 0) {
        console.log('  (no tabs reported)');
        return;
      }
      session.tabs.forEach((tab, idx) => {
        const title = tab.title || '(untitled)';
        const url = tab.url || '(no url)';
        console.log(`  Tab ${idx + 1}: ${title}`);
        console.log(`           ${url}`);
      });
    });
  });

program
  .command('kill')
  .description('Terminate Chrome instances that have DevTools ports open.')
  .option('--ports <list>', 'Comma-separated list of ports to target.', parseNumberListArg)
  .option('--pids <list>', 'Comma-separated list of PIDs to target.', parseNumberListArg)
  .option('--all', 'Kill every matching Chrome instance.', false)
  .option('--force', 'Skip the confirmation prompt.', false)
  .action(async (options) => {
    const ports = (options.ports as number[] | undefined)?.filter((entry) => Number.isFinite(entry) && entry > 0);
    const pids = (options.pids as number[] | undefined)?.filter((entry) => Number.isFinite(entry) && entry > 0);
    const killAll = Boolean(options.all);
    if (!killAll && (!ports?.length && !pids?.length)) {
      console.error('Specify --all, --ports <list>, or --pids <list> to select targets.');
      process.exit(1);
    }
    const sessions = await describeChromeSessions({ ports, pids, includeAll: killAll });
    if (sessions.length === 0) {
      console.log('No matching Chrome instances found.');
      return;
    }
    if (!options.force) {
      console.log('About to terminate the following Chrome sessions:');
      sessions.forEach((session) => {
        console.log(`  PID ${session.pid} (port ${session.port})`);
      });
      const rl = readline.createInterface({ input, output });
      const answer = (await rl.question('Proceed? [y/N] ')).trim().toLowerCase();
      rl.close();
      if (answer !== 'y' && answer !== 'yes') {
        console.log('Aborted.');
        return;
      }
    }
    const failures: { pid: number; error: string }[] = [];
    sessions.forEach((session) => {
      try {
        process.kill(session.pid);
        console.log(`✓ Killed Chrome PID ${session.pid} (port ${session.port})`);
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        console.error(`✗ Failed to kill PID ${session.pid}: ${message}`);
        failures.push({ pid: session.pid, error: message });
      }
    });
    if (failures.length > 0) {
      process.exitCode = 1;
    }
  });

interface ChromeProcessInfo {
  pid: number;
  port: number;
  command: string;
}

interface ChromeTabInfo {
  id?: string;
  title?: string;
  url?: string;
  type?: string;
}

interface ChromeSessionDescription extends ChromeProcessInfo {
  version?: Record<string, string>;
  tabs: ChromeTabInfo[];
}

function parseNumberListArg(value: string): number[] {
  return parseNumberList(value) ?? [];
}

function parseNumberList(inputValue: string | undefined): number[] | undefined {
  if (!inputValue) {
    return undefined;
  }
  const parsed = inputValue
    .split(',')
    .map((entry) => Number.parseInt(entry.trim(), 10))
    .filter((value) => Number.isFinite(value));
  return parsed.length > 0 ? parsed : undefined;
}

async function describeChromeSessions(options: {
  ports?: number[];
  pids?: number[];
  includeAll?: boolean;
}): Promise<ChromeSessionDescription[]> {
  const { ports, pids, includeAll } = options;
  const processes = await listDevtoolsChromes();
  const portSet = new Set(ports ?? []);
  const pidSet = new Set(pids ?? []);
  const candidates = processes.filter((proc) => {
    if (includeAll) {
      return true;
    }
    if (portSet.size > 0 && portSet.has(proc.port)) {
      return true;
    }
    if (pidSet.size > 0 && pidSet.has(proc.pid)) {
      return true;
    }
    return false;
  });
  const results: ChromeSessionDescription[] = [];
  for (const proc of candidates) {
    const [version, tabs] = await Promise.all([
      fetchJson(`http://localhost:${proc.port}/json/version`).catch(() => undefined),
      fetchJson(`http://localhost:${proc.port}/json/list`).catch(() => []),
    ]);
    const filteredTabs = Array.isArray(tabs)
      ? (tabs as ChromeTabInfo[]).filter((tab) => {
          const type = tab.type?.toLowerCase() ?? '';
          if (type && type !== 'page' && type !== 'app') {
            if (!tab.url || tab.url.startsWith('devtools://') || tab.url.startsWith('chrome-extension://')) {
              return false;
            }
          }
          if (!tab.url || tab.url.trim().length === 0) {
            return false;
          }
          return true;
        })
      : [];
    results.push({
      ...proc,
      version: (version as Record<string, string>) ?? undefined,
      tabs: filteredTabs,
    });
  }
  return results;
}

async function listDevtoolsChromes(): Promise<ChromeProcessInfo[]> {
  if (process.platform !== 'darwin' && process.platform !== 'linux') {
    console.warn('Chrome inspection is only supported on macOS and Linux for now.');
    return [];
  }
  let output = '';
  try {
    output = execSync('ps -ax -o pid=,command=', { encoding: 'utf8' });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    throw new Error(`Failed to enumerate processes: ${message}`);
  }
  const processes: ChromeProcessInfo[] = [];
  output
    .split('\n')
    .map((line) => line.trim())
    .filter(Boolean)
    .forEach((line) => {
      const match = line.match(/^(\d+)\s+(.+)$/);
      if (!match) {
        return;
      }
      const pid = Number.parseInt(match[1], 10);
      const command = match[2];
      if (!Number.isFinite(pid) || pid <= 0) {
        return;
      }
      if (!/chrome/i.test(command) || !/--remote-debugging-port/.test(command)) {
        return;
      }
      const portMatch = command.match(/--remote-debugging-port(?:=|\s+)(\d+)/);
      if (!portMatch) {
        return;
      }
      const port = Number.parseInt(portMatch[1], 10);
      if (!Number.isFinite(port)) {
        return;
      }
      processes.push({ pid, port, command });
    });
  return processes;
}

function fetchJson(url: string, timeoutMs = 2000): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const request = http.get(url, { timeout: timeoutMs }, (response) => {
      const chunks: Buffer[] = [];
      response.on('data', (chunk) => chunks.push(chunk));
      response.on('end', () => {
        const body = Buffer.concat(chunks).toString('utf8');
        if ((response.statusCode ?? 500) >= 400) {
          reject(new Error(`HTTP ${response.statusCode} for ${url}`));
          return;
        }
        try {
          resolve(JSON.parse(body));
        } catch {
          resolve(undefined);
        }
      });
    });
    request.on('timeout', () => {
      request.destroy(new Error(`Request to ${url} timed out`));
    });
    request.on('error', (error) => {
      reject(error);
    });
  });
}

program.parseAsync(process.argv);


--- scripts/build-vendor.js ---
import { cp, mkdir, stat } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

async function main() {
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
  const source = path.join(root, 'vendor', 'oracle-notifier');
  const targetDir = path.join(root, 'dist', 'vendor');
  const target = path.join(targetDir, 'oracle-notifier');

  try {
    await stat(source);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.warn(`Skipping vendor copy: ${message}`);
    return;
  }

  await mkdir(targetDir, { recursive: true });
  await cp(source, target, { recursive: true });
  console.log(`Copied ${source} -> ${target}`);
}

main().catch((error) => {
  console.warn(`Skipping vendor copy: ${error instanceof Error ? error.message : String(error)}`);
});


--- scripts/check.ts ---
#!/usr/bin/env bun
import process from 'node:process';

type BunBuildConfig = {
  entrypoints: string[];
  outdir?: string;
  target?: string;
  minify?: boolean;
  write?: boolean;
  sourcemap?: 'inline' | 'external' | 'none';
};

const buildConfig: BunBuildConfig = {
  entrypoints: ['./bin/oracle-cli.js'],
  outdir: './.bun-check',
  target: 'bun',
  minify: false,
  write: false,
};

const result = await Bun.build(buildConfig);

if (!result.success) {
  console.error('Build failed while checking syntax:');
  for (const log of result.logs) {
    console.error(log.message);
    if (log.position) {
      console.error(`\tat ${log.position.file}:${log.position.line}:${log.position.column}`);
    }
  }
  process.exit(1);
}

console.log('Syntax OK');


--- scripts/git-policy.ts ---
import { resolve } from 'node:path';

export type GitInvocation = {
  index: number;
  argv: string[];
};

export type GitCommandInfo = {
  name: string;
  index: number;
};

export type GitExecutionContext = {
  invocation: GitInvocation | null;
  command: GitCommandInfo | null;
  subcommand: string | null;
  workDir: string;
};

export type GitPolicyEvaluation = {
  requiresCommitHelper: boolean;
  requiresExplicitConsent: boolean;
  isDestructive: boolean;
};

const COMMIT_HELPER_SUBCOMMANDS = new Set(['add', 'commit']);
const GUARDED_SUBCOMMANDS = new Set(['push', 'pull', 'merge', 'rebase', 'cherry-pick']);
const DESTRUCTIVE_SUBCOMMANDS = new Set([
  'reset',
  'checkout',
  'clean',
  'restore',
  'switch',
  'stash',
  'branch',
  'filter-branch',
  'fast-import',
]);

export function extractGitInvocation(commandArgs: string[]): GitInvocation | null {
  for (const [index, token] of commandArgs.entries()) {
    if (token === 'git' || token.endsWith('/git')) {
      return { index, argv: commandArgs.slice(index) };
    }
  }
  return null;
}

export function findGitSubcommand(commandArgs: string[]): GitCommandInfo | null {
  if (commandArgs.length <= 1) {
    return null;
  }

  const optionsWithValue = new Set(['-C', '--git-dir', '--work-tree', '-c']);
  let index = 1;

  while (index < commandArgs.length) {
    const token = commandArgs[index];
    if (token === undefined) {
      break;
    }
    if (token === '--') {
      const next = commandArgs[index + 1];
      return next ? { name: next, index: index + 1 } : null;
    }
    if (!token.startsWith('-')) {
      return { name: token, index };
    }
    if (token.includes('=')) {
      index += 1;
      continue;
    }
    if (optionsWithValue.has(token)) {
      index += 2;
      continue;
    }
    index += 1;
  }
  return null;
}

export function determineGitWorkdir(baseDir: string, gitArgs: string[], command: GitCommandInfo | null): string {
  let workDir = baseDir;
  const limit = command ? command.index : gitArgs.length;
  let index = 1;

  while (index < limit) {
    const token = gitArgs[index];
    if (token === undefined) {
      break;
    }
    if (token === '-C') {
      const next = gitArgs[index + 1];
      if (next) {
        workDir = resolve(workDir, next);
      }
      index += 2;
      continue;
    }
    if (token.startsWith('-C')) {
      const pathSegment = token.slice(2);
      if (pathSegment.length > 0) {
        workDir = resolve(workDir, pathSegment);
      }
    }
    index += 1;
  }

  return workDir;
}

export function analyzeGitExecution(commandArgs: string[], workspaceDir: string): GitExecutionContext {
  const invocation = extractGitInvocation(commandArgs);
  const command = invocation ? findGitSubcommand(invocation.argv) : null;
  const workDir = invocation ? determineGitWorkdir(workspaceDir, invocation.argv, command) : workspaceDir;

  return {
    invocation,
    command,
    subcommand: command?.name ?? null,
    workDir,
  };
}

export function requiresCommitHelper(subcommand: string | null): boolean {
  if (!subcommand) {
    return false;
  }
  return COMMIT_HELPER_SUBCOMMANDS.has(subcommand);
}

export function requiresExplicitGitConsent(subcommand: string | null): boolean {
  if (!subcommand) {
    return false;
  }
  return GUARDED_SUBCOMMANDS.has(subcommand);
}

export function isDestructiveGitSubcommand(command: GitCommandInfo | null, gitArgv: string[]): boolean {
  if (!command) {
    return false;
  }

  const subcommand = command.name;
  if (DESTRUCTIVE_SUBCOMMANDS.has(subcommand)) {
    return true;
  }

  if (subcommand === 'bisect') {
    const action = gitArgv[command.index + 1] ?? '';
    return action === 'reset';
  }

  return false;
}

export function evaluateGitPolicies(context: GitExecutionContext): GitPolicyEvaluation {
  const invocationArgv = context.invocation?.argv;
  const normalizedArgv = Array.isArray(invocationArgv) ? invocationArgv : [];
  return {
    requiresCommitHelper: requiresCommitHelper(context.subcommand),
    requiresExplicitConsent: requiresExplicitGitConsent(context.subcommand),
    isDestructive: isDestructiveGitSubcommand(context.command, normalizedArgv),
  };
}


--- scripts/run-cli.ts ---
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const rawArgs = process.argv.slice(2);
const args: string[] = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs;

const here = path.dirname(fileURLToPath(import.meta.url));
const cliEntry = path.join(here, '../bin/oracle-cli.js');

const child = spawn(process.execPath, ['--', cliEntry, ...args], {
  stdio: 'inherit',
});
child.on('exit', (code) => {
  process.exit(code ?? 0);
});


--- scripts/runner.ts ---
#!/usr/bin/env bun
/**
 * Sweetistics runner wrapper: enforces timeouts, git policy, and trash-safe deletes before dispatching any repo command.
 * When you tweak its behavior, add a short note to AGENTS.md via `./scripts/committer "docs: update AGENTS for runner" "AGENTS.md"` so other agents know the new expectations.
 */

import { type ChildProcess, spawn } from 'node:child_process';
import { cpSync, existsSync, renameSync, rmSync } from 'node:fs';
import { constants as osConstants } from 'node:os';
import { basename, isAbsolute, join, normalize, resolve } from 'node:path';
import process from 'node:process';

import {
  analyzeGitExecution,
  evaluateGitPolicies,
  type GitCommandInfo,
  type GitExecutionContext,
  type GitInvocation,
} from './git-policy';

const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
const EXTENDED_TIMEOUT_MS = 20 * 60 * 1000;
const LONG_TIMEOUT_MS = 25 * 60 * 1000; // Build + full-suite commands (Next.js build, test:all) routinely spike past 20 minutes—give them explicit headroom before tmux escalation.
const LINT_TIMEOUT_MS = 30 * 60 * 1000;
const LONG_RUN_REPORT_THRESHOLD_MS = 60 * 1000;
const ENABLE_DEBUG_LOGS = process.env.RUNNER_DEBUG === '1';
const MAX_SLEEP_SECONDS = 30;

const WRAPPER_COMMANDS = new Set([
  'sudo',
  '/usr/bin/sudo',
  'env',
  '/usr/bin/env',
  'command',
  '/bin/command',
  'nohup',
  '/usr/bin/nohup',
]);

type SummaryStyle = 'compact' | 'minimal' | 'verbose';
const SUMMARY_STYLE = resolveSummaryStyle(process.env.RUNNER_SUMMARY_STYLE);

// biome-ignore format: keep each keyword on its own line for grep-friendly diffs.
const LONG_SCRIPT_KEYWORDS = [
  'build',
  'test:all',
  'test:browser',
  'test:e2e',
  'test:e2e:headed',
  'vitest.browser',
  'vitest.browser.config.ts',
];
const EXTENDED_SCRIPT_KEYWORDS = ['lint', 'test', 'playwright', 'check', 'docker'];
const SINGLE_TEST_SCRIPTS = new Set(['test:file']);
const SINGLE_TEST_FLAGS = new Set(['--run', '--filter']);
const TEST_BINARIES = new Set(['vitest', 'playwright', 'jest']);
const LINT_BINARIES = new Set(['eslint', 'biome', 'oxlint', 'knip']);

type RunnerExecutionContext = {
  commandArgs: string[];
  workspaceDir: string;
  timeoutMs: number;
};

type CommandInterceptionResult = { handled: true } | { handled: false; gitContext: GitExecutionContext };

type GitRmPlan = {
  paths: string[];
  stagingOptions: string[];
  allowMissing: boolean;
  shouldIntercept: boolean;
};

type MoveResult = {
  missing: string[];
  errors: string[];
};

let cachedTrashCliCommand: string | null | undefined;

(async () => {
  const commandArgs = parseArgs(process.argv.slice(2));

  if (commandArgs.length === 0) {
    printUsage('Missing command to execute.');
    process.exit(1);
  }

  const workspaceDir = process.cwd();
  const timeoutMs = determineEffectiveTimeoutMs(commandArgs);
  const context: RunnerExecutionContext = {
    commandArgs,
    workspaceDir,
    timeoutMs,
  };

  const interception = await resolveCommandInterception(context);
  if (interception.handled) {
    return;
  }

  enforceGitPolicies(interception.gitContext);

  await runCommand(context);
})().catch((error) => {
  console.error('[runner] Unexpected failure:', error instanceof Error ? error.message : String(error));
  process.exit(1);
});

// Parses the runner CLI args and rejects unsupported flags early.
function parseArgs(argv: string[]): string[] {
  const commandArgs: string[] = [];
  let parsingOptions = true;

  for (const token of argv) {
    if (!parsingOptions) {
      commandArgs.push(token);
      continue;
    }

    if (token === '--') {
      parsingOptions = false;
      continue;
    }

    if (token === '--help' || token === '-h') {
      printUsage();
      process.exit(0);
    }

    if (token === '--timeout' || token.startsWith('--timeout=')) {
      console.error('[runner] --timeout is no longer supported; rely on the automatic timeouts.');
      process.exit(1);
    }

    parsingOptions = false;
    commandArgs.push(token);
  }

  return commandArgs;
}

// Computes the timeout tier for the provided command tokens.
function determineEffectiveTimeoutMs(commandArgs: string[]): number {
  const strippedTokens = stripWrappersAndAssignments(commandArgs);
  if (isTestRunnerSuiteInvocation(strippedTokens, 'integration')) {
    return EXTENDED_TIMEOUT_MS;
  }
  if (referencesIntegrationSpec(strippedTokens)) {
    return EXTENDED_TIMEOUT_MS;
  }
  if (shouldUseLintTimeout(commandArgs)) {
    return LINT_TIMEOUT_MS;
  }
  if (shouldUseLongTimeout(commandArgs)) {
    return LONG_TIMEOUT_MS;
  }
  if (shouldExtendTimeout(commandArgs) && !isSingleTestInvocation(commandArgs)) {
    return EXTENDED_TIMEOUT_MS;
  }
  return DEFAULT_TIMEOUT_MS;
}

// Determines whether the command matches any keyword requiring extra time.
function shouldExtendTimeout(commandArgs: string[]): boolean {
  const tokens = stripWrappersAndAssignments(commandArgs);
  if (tokens.length === 0) {
    return false;
  }

  const [first, ...rest] = tokens;
  if (!first) {
    return false;
  }

  if (first === 'pnpm') {
    return shouldExtendViaPnpm(rest);
  }
  if (first === 'bun') {
    return shouldExtendViaBun(rest);
  }

  if (shouldExtendForScript(first) || TEST_BINARIES.has(first.toLowerCase())) {
    return true;
  }

  return rest.some((token) => shouldExtendForScript(token) || TEST_BINARIES.has(token.toLowerCase()));
}

function shouldExtendViaPnpm(rest: string[]): boolean {
  if (rest.length === 0) {
    return false;
  }
  const subcommand = rest[0];
  if (!subcommand) {
    return false;
  }
  if (subcommand === 'run') {
    const script = rest[1];
    return typeof script === 'string' && shouldExtendForScript(script);
  }
  if (subcommand === 'exec') {
    const execTarget = rest[1];
    if (execTarget && (shouldExtendForScript(execTarget) || TEST_BINARIES.has(execTarget.toLowerCase()))) {
      return true;
    }
    return rest.slice(1).some((token) => shouldExtendForScript(token) || TEST_BINARIES.has(token.toLowerCase()));
  }
  return shouldExtendForScript(subcommand);
}

function shouldExtendViaBun(rest: string[]): boolean {
  if (rest.length === 0) {
    return false;
  }
  const subcommand = rest[0];
  if (!subcommand) {
    return false;
  }
  if (subcommand === 'run') {
    const script = rest[1];
    return typeof script === 'string' && shouldExtendForScript(script);
  }
  if (subcommand === 'test') {
    return true;
  }
  if (subcommand === 'x' || subcommand === 'bunx') {
    const execTarget = rest[1];
    if (execTarget && TEST_BINARIES.has(execTarget.toLowerCase())) {
      return true;
    }
  }
  return shouldExtendForScript(subcommand);
}

// Checks script names for long-running markers (lint/test/build/etc.).
function shouldExtendForScript(script: string): boolean {
  if (SINGLE_TEST_SCRIPTS.has(script)) {
    return false;
  }
  return matchesScriptKeyword(script, EXTENDED_SCRIPT_KEYWORDS);
}

// Gives lint invocations the dedicated timeout bucket.
function shouldUseLintTimeout(commandArgs: string[]): boolean {
  const tokens = stripWrappersAndAssignments(commandArgs);
  if (tokens.length === 0) {
    return false;
  }

  const [first, ...rest] = tokens;
  if (!first) {
    return false;
  }

  if (first === 'pnpm') {
    return shouldUseLintTimeoutViaPnpm(rest);
  }
  if (first === 'bun') {
    return shouldUseLintTimeoutViaBun(rest);
  }

  return LINT_BINARIES.has(first.toLowerCase());
}

function shouldUseLintTimeoutViaPnpm(rest: string[]): boolean {
  if (rest.length === 0) {
    return false;
  }
  const subcommand = rest[0];
  if (!subcommand) {
    return false;
  }
  if (subcommand === 'run') {
    const script = rest[1];
    return typeof script === 'string' && script.startsWith('lint');
  }
  if (subcommand === 'exec') {
    const execTarget = rest[1];
    if (execTarget && LINT_BINARIES.has(execTarget.toLowerCase())) {
      return true;
    }
    return rest.slice(1).some((token) => LINT_BINARIES.has(token.toLowerCase()));
  }
  return LINT_BINARIES.has(subcommand.toLowerCase());
}

function shouldUseLintTimeoutViaBun(rest: string[]): boolean {
  if (rest.length === 0) {
    return false;
  }
  const subcommand = rest[0];
  if (!subcommand) {
    return false;
  }
  if (subcommand === 'run') {
    const script = rest[1];
    return typeof script === 'string' && script.startsWith('lint');
  }
  if (subcommand === 'x' || subcommand === 'bunx') {
    return rest.slice(1).some((token) => LINT_BINARIES.has(token.toLowerCase()));
  }
  return LINT_BINARIES.has(subcommand.toLowerCase());
}

// Detects when a user is running a single spec so we can keep the shorter timeout.
function isSingleTestInvocation(commandArgs: string[]): boolean {
  const tokens = stripWrappersAndAssignments(commandArgs);
  if (tokens.length === 0) {
    return false;
  }

  if (tokens.some((token) => SINGLE_TEST_FLAGS.has(token))) {
    return true;
  }

  const [first, ...rest] = tokens;
  if (!first) {
    return false;
  }

  if (first === 'pnpm') {
    return isSingleTestViaPnpm(rest);
  }
  if (first === 'bun') {
    return isSingleTestViaBun(rest);
  }
  if (first === 'vitest') {
    return rest.some((token) => SINGLE_TEST_FLAGS.has(token));
  }

  return SINGLE_TEST_SCRIPTS.has(first);
}

function isSingleTestViaPnpm(rest: string[]): boolean {
  if (rest.length === 0) {
    return false;
  }
  const subcommand = rest[0];
  if (!subcommand) {
    return false;
  }
  if (subcommand === 'run') {
    const script = rest[1];
    return typeof script === 'string' && SINGLE_TEST_SCRIPTS.has(script);
  }
  if (subcommand === 'exec') {
    return rest.slice(1).some((token) => SINGLE_TEST_FLAGS.has(token));
  }
  return SINGLE_TEST_SCRIPTS.has(subcommand);
}

function isSingleTestViaBun(rest: string[]): boolean {
  if (rest.length === 0) {
    return false;
  }
  const subcommand = rest[0];
  if (!subcommand) {
    return false;
  }
  if (subcommand === 'run') {
    const script = rest[1];
    return typeof script === 'string' && SINGLE_TEST_SCRIPTS.has(script);
  }
  if (subcommand === 'test') {
    return true;
  }
  if (subcommand === 'x' || subcommand === 'bunx') {
    return rest.slice(1).some((token) => SINGLE_TEST_FLAGS.has(token));
  }
  return false;
}

// Normalizes potential file paths/flags to aid comparison across shells.
function normalizeForPathComparison(token: string): string {
  return token.replaceAll('\\', '/');
}

// Heuristically checks if a CLI token references an integration spec.
function tokenReferencesIntegrationTest(token: string): boolean {
  const normalized = normalizeForPathComparison(token);
  if (normalized.includes('tests/integration/')) {
    return true;
  }
  if (normalized.startsWith('--run=') || normalized.startsWith('--include=')) {
    const value = normalized.split('=', 2)[1] ?? '';
    return value.includes('tests/integration/');
  }
  return false;
}

// Scans the entire command for integration spec references.
function referencesIntegrationSpec(tokens: string[]): boolean {
  for (let index = 0; index < tokens.length; index += 1) {
    const token = tokens[index];
    if (!token) {
      continue;
    }
    if (token === '--run' || token === '--include') {
      const next = tokens[index + 1];
      if (next && tokenReferencesIntegrationTest(next)) {
        return true;
      }
    }
    if (tokenReferencesIntegrationTest(token)) {
      return true;
    }
  }
  return false;
}

// Helper that matches a script token against a keyword allowlist.
function matchesScriptKeyword(script: string, keywords: readonly string[]): boolean {
  const lowered = script.toLowerCase();
  return keywords.some((keyword) => lowered === keyword || lowered.startsWith(`${keyword}:`));
}

// Removes wrapper binaries/env assignments so heuristics see the real command.
function stripWrappersAndAssignments(args: string[]): string[] {
  const tokens = [...args];

  while (tokens.length > 0) {
    const candidate = tokens[0];
    if (!candidate) {
      break;
    }
    if (!isEnvAssignment(candidate)) {
      break;
    }
    tokens.shift();
  }

  while (tokens.length > 0) {
    const wrapper = tokens[0];
    if (!wrapper) {
      break;
    }
    if (!WRAPPER_COMMANDS.has(wrapper)) {
      break;
    }
    tokens.shift();
    while (tokens.length > 0) {
      const assignment = tokens[0];
      if (!assignment) {
        break;
      }
      if (!isEnvAssignment(assignment)) {
        break;
      }
      tokens.shift();
    }
  }

  return tokens;
}

// Checks whether a token is an inline environment variable assignment.
function isEnvAssignment(token: string): boolean {
  return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);
}

// Detects `pnpm test:<suite>` style calls regardless of wrappers.
function isTestRunnerSuiteInvocation(tokens: string[], suite: string): boolean {
  if (tokens.length === 0) {
    return false;
  }

  const normalizedSuite = suite.toLowerCase();
  for (let index = 0; index < tokens.length; index += 1) {
    const token = tokens[index];
    if (!token) {
      continue;
    }
    const normalizedToken = token.replace(/^[./\\]+/, '');
    if (normalizedToken === 'scripts/test-runner.ts' || normalizedToken.endsWith('/scripts/test-runner.ts')) {
      const suiteToken = tokens[index + 1]?.toLowerCase();
      if (suiteToken === normalizedSuite) {
        return true;
      }
    }
  }

  return false;
}

// Grants the longest timeout to explicitly tagged long-running scripts.
function shouldUseLongTimeout(commandArgs: string[]): boolean {
  const tokens = stripWrappersAndAssignments(commandArgs);
  if (tokens.length === 0) {
    return false;
  }

  const first = tokens[0];
  if (!first) {
    return false;
  }
  const rest = tokens.slice(1);
  const matches = (token: string): boolean => matchesScriptKeyword(token, LONG_SCRIPT_KEYWORDS);

  if (first === 'pnpm') {
    if (rest.length === 0) {
      return false;
    }
    const subcommand = rest[0];
    if (!subcommand) {
      return false;
    }
    if (subcommand === 'run') {
      const script = rest[1];
      if (script && matches(script)) {
        return true;
      }
    } else if (matches(subcommand)) {
      return true;
    }
    for (const token of rest.slice(1)) {
      if (matches(token)) {
        return true;
      }
    }
    return false;
  }

  if (matches(first)) {
    return true;
  }

  for (const token of rest) {
    if (matches(token)) {
      return true;
    }
  }

  return false;
}

// Kicks off the requested command with logging, timeouts, and monitoring.
async function runCommand(context: RunnerExecutionContext): Promise<void> {
  const { command, args, env } = buildExecutionParams(context.commandArgs, context.workspaceDir);
  const commandLabel = formatDisplayCommand(context.commandArgs);

  const startTime = Date.now();

  const child = spawn(command, args, {
    cwd: context.workspaceDir,
    env,
    stdio: ['inherit', 'pipe', 'pipe'],
  });

  if (isRunnerTmuxSession()) {
    const childPidInfo = typeof child.pid === 'number' ? ` (pid ${child.pid})` : '';
    console.error(`[runner] Watching ${commandLabel}${childPidInfo}. Wait for the closing sentinel before moving on.`);
  }

  const removeSignalHandlers = registerSignalForwarding(child);

  if (child.stdout) {
    child.stdout.on('data', (chunk: Buffer) => {
      process.stdout.write(chunk);
    });
  }

  if (child.stderr) {
    child.stderr.on('data', (chunk: Buffer) => {
      process.stderr.write(chunk);
    });
  }

  let killTimer: NodeJS.Timeout | null = null;
  try {
    const result = await new Promise<{ exitCode: number; timedOut: boolean }>((resolve, reject) => {
      let timedOut = false;
      const timeout = setTimeout(() => {
        timedOut = true;
        if (ENABLE_DEBUG_LOGS) {
          console.error(`[runner] Command exceeded ${formatDuration(context.timeoutMs)}; sending SIGTERM.`);
        }
        if (!child.killed) {
          child.kill('SIGTERM');
          killTimer = setTimeout(() => {
            if (!child.killed) {
              child.kill('SIGKILL');
            }
          }, 5_000);
        }
      }, context.timeoutMs);

      child.once('error', (error) => {
        clearTimeout(timeout);
        if (killTimer) {
          clearTimeout(killTimer);
        }
        removeSignalHandlers();
        reject(error);
      });

      child.once('exit', (code, signal) => {
        clearTimeout(timeout);
        if (killTimer) {
          clearTimeout(killTimer);
        }
        removeSignalHandlers();
        resolve({ exitCode: code ?? exitCodeFromSignal(signal), timedOut });
      });
    });
    const { exitCode, timedOut } = result;

    const elapsedMs = Date.now() - startTime;
    if (timedOut) {
      console.error(
        `[runner] Command terminated after ${formatDuration(context.timeoutMs)}. Re-run inside tmux for long-lived work.`
      );
      console.error(
        formatCompletionSummary({ exitCode, elapsedMs, timedOut: true, commandLabel })
      );
      process.exit(124);
    }

    if (elapsedMs >= LONG_RUN_REPORT_THRESHOLD_MS) {
      console.error(
        `[runner] Completed in ${formatDuration(elapsedMs)}. For long-running tasks, prefer tmux directly.`
      );
    }

    console.error(formatCompletionSummary({ exitCode, elapsedMs, commandLabel }));
    process.exit(exitCode);
  } catch (error) {
    console.error('[runner] Failed to launch command:', error instanceof Error ? error.message : String(error));
    process.exit(1);
    return;
  }
}

async function runCommandWithoutTimeout(context: RunnerExecutionContext): Promise<void> {
  const { command, args, env } = buildExecutionParams(context.commandArgs, context.workspaceDir);
  const commandLabel = formatDisplayCommand(context.commandArgs);
  const startTime = Date.now();

  const child = spawn(command, args, {
    cwd: context.workspaceDir,
    env,
    stdio: 'inherit',
  });

  const removeSignalHandlers = registerSignalForwarding(child);

  try {
    const exitCode = await new Promise<number>((resolve, reject) => {
      child.once('error', (error) => {
        removeSignalHandlers();
        reject(error);
      });
      child.once('exit', (code, signal) => {
        removeSignalHandlers();
        resolve(code ?? exitCodeFromSignal(signal));
      });
    });
    const elapsedMs = Date.now() - startTime;
    console.error(formatCompletionSummary({ exitCode, elapsedMs, commandLabel }));
    process.exit(exitCode);
  } catch (error) {
    console.error('[runner] Failed to launch command:', error instanceof Error ? error.message : String(error));
    process.exit(1);
  }
}

// Prepares the executable, args, and sanitized env for the child process.
function buildExecutionParams(
  commandArgs: string[],
  workspaceDir: string,
): { command: string; args: string[]; env: NodeJS.ProcessEnv } {
  const env = { ...process.env };
  injectWorkspaceBinDirs(env, workspaceDir);
  const args: string[] = [];
  let commandStarted = false;

  for (const token of commandArgs) {
    if (!commandStarted && isEnvAssignment(token)) {
      const [key, ...rest] = token.split('=');
      if (key) {
        env[key] = rest.join('=');
      }
      continue;
    }
    commandStarted = true;
    args.push(token);
  }

  if (args.length === 0 || !args[0]) {
    printUsage('Missing command to execute.');
    process.exit(1);
  }

  const [command, ...restArgs] = args;
  return { command, args: restArgs, env };
}

function injectWorkspaceBinDirs(env: NodeJS.ProcessEnv, workspaceDir: string): void {
  if (ENABLE_DEBUG_LOGS) {
    console.error(`[runner] Checking workspace bin dirs under ${workspaceDir}`);
  }
  const binCandidates = [
    join(workspaceDir, 'node_modules', '.bin'),
    join(workspaceDir, 'bin'),
  ];
  const existingPath = env.PATH ?? process.env.PATH ?? '';
  const existingSegments = existingPath
    .split(':')
    .map((segment) => segment.trim())
    .filter((segment) => segment.length > 0);

  const additions: string[] = [];
  for (const dir of binCandidates) {
    if (!dir || !existsSync(dir)) {
      continue;
    }
    if (existingSegments.includes(dir) || additions.includes(dir)) {
      continue;
    }
    additions.push(dir);
  }

  if (additions.length === 0) {
    return;
  }

  if (ENABLE_DEBUG_LOGS) {
    console.error(`[runner] Prepending workspace PATH entries: ${additions.join(', ')}`);
  }

  const merged = [...additions, ...existingSegments];
  env.PATH = merged.join(':');
}

// Forwards termination signals to the child and returns an unregister hook.
function registerSignalForwarding(child: ChildProcess): () => void {
  const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'];
  const handlers = new Map<NodeJS.Signals, () => void>();

  for (const signal of signals) {
    const handler = () => {
      if (!child.killed) {
        child.kill(signal);
      }
    };
    handlers.set(signal, handler);
    process.on(signal, handler);
  }

  return () => {
    for (const [signal, handler] of handlers) {
      process.off(signal, handler);
    }
  };
}

// Maps a terminating signal to the exit code conventions bash expects.
function exitCodeFromSignal(signal: NodeJS.Signals | null): number {
  if (!signal) {
    return 0;
  }
  const code = (osConstants.signals as Record<string, number | undefined>)[signal];
  if (typeof code === 'number') {
    return 128 + code;
  }
  return 1;
}

// Gives policy interceptors a chance to fully handle a command before exec.
async function resolveCommandInterception(context: RunnerExecutionContext): Promise<CommandInterceptionResult> {
  const interceptors: Array<(ctx: RunnerExecutionContext) => Promise<boolean>> = [
    maybeHandleTmuxInvocation,
    maybeHandleFindInvocation,
    maybeHandleRmInvocation,
    maybeHandleSleepInvocation,
  ];

  for (const interceptor of interceptors) {
    if (await interceptor(context)) {
      return { handled: true };
    }
  }

  const gitContext = analyzeGitExecution(context.commandArgs, context.workspaceDir);

  if (await maybeHandleGitRm(gitContext)) {
    return { handled: true };
  }

  return { handled: false, gitContext };
}

// Runs the shared git policy analyzers before dispatching the command.
function enforceGitPolicies(gitContext: GitExecutionContext) {
  const evaluation = evaluateGitPolicies(gitContext);
  const hasConsentOverride = process.env.RUNNER_THE_USER_GAVE_ME_CONSENT === '1';

  if (gitContext.subcommand === 'rebase' && !hasConsentOverride) {
    console.error(
      'git rebase requires the user to explicitly type "rebase" in chat. Once they do, rerun with RUNNER_THE_USER_GAVE_ME_CONSENT=1 in the same command (e.g. RUNNER_THE_USER_GAVE_ME_CONSENT=1 ./runner git rebase --continue).'
    );
    process.exit(1);
  }

  if (evaluation.requiresCommitHelper) {
    console.error(
      'Direct git add/commit is disabled. Use ./scripts/committer "chore(runner): describe change" "scripts/runner.ts" instead—see AGENTS.md and ./scripts/committer for details. The helper auto-stashes unrelated files before committing.'
    );
    process.exit(1);
  }

  if (evaluation.requiresExplicitConsent || evaluation.isDestructive) {
    if (hasConsentOverride) {
      if (ENABLE_DEBUG_LOGS) {
        const reason = evaluation.isDestructive ? 'destructive git command' : 'guarded git command';
        console.error(`[runner] Proceeding with ${reason} because RUNNER_THE_USER_GAVE_ME_CONSENT=1.`);
      }
    } else {
      if (evaluation.isDestructive) {
        console.error(
          `git ${gitContext.subcommand ?? ''} can overwrite or discard work. Confirm with the user first, then re-run with RUNNER_THE_USER_GAVE_ME_CONSENT=1 if they approve.`
        );
      } else {
        console.error(
          `Using git ${gitContext.subcommand ?? ''} requires consent. Set RUNNER_THE_USER_GAVE_ME_CONSENT=1 after verifying with the user, or ask them explicitly before proceeding.`
        );
      }
      process.exit(1);
    }
  }
}

// Handles guarded `find` invocations that may delete files outright.
async function maybeHandleFindInvocation(context: RunnerExecutionContext): Promise<boolean> {
  const findInvocation = extractFindInvocation(context.commandArgs);
  if (!findInvocation) {
    return false;
  }

  const findPlan = await buildFindDeletePlan(findInvocation.argv, context.workspaceDir);
  if (!findPlan) {
    return false;
  }

  const moveResult = await movePathsToTrash(findPlan.paths, context.workspaceDir, { allowMissing: false });
  if (moveResult.missing.length > 0) {
    for (const path of moveResult.missing) {
      console.error(`find: ${path}: No such file or directory`);
    }
    process.exit(1);
  }
  if (moveResult.errors.length > 0) {
    for (const error of moveResult.errors) {
      console.error(error);
    }
    process.exit(1);
  }
  process.exit(0);
  return true;
}

// Intercepts plain `rm` commands to route them through trash safeguards.
async function maybeHandleRmInvocation(context: RunnerExecutionContext): Promise<boolean> {
  const rmInvocation = extractRmInvocation(context.commandArgs);
  if (!rmInvocation) {
    return false;
  }

  const rmPlan = parseRmArguments(rmInvocation.argv);
  if (!rmPlan?.shouldIntercept) {
    return false;
  }

  try {
    const moveResult = await movePathsToTrash(rmPlan.targets, context.workspaceDir, { allowMissing: rmPlan.force });
    reportMissingForRm(moveResult.missing, rmPlan.force);
    if (moveResult.errors.length > 0) {
      for (const error of moveResult.errors) {
        console.error(error);
      }
      process.exit(1);
    }
    process.exit(0);
  } catch (error) {
    console.error(formatTrashError(error));
    process.exit(1);
  }
  return true;
}

// Applies git-specific rm protections before the command executes.
async function maybeHandleGitRm(gitContext: GitExecutionContext): Promise<boolean> {
  if (gitContext.command?.name !== 'rm' || !gitContext.invocation) {
    return false;
  }

  const gitRmPlan = parseGitRmArguments(gitContext.invocation.argv, gitContext.command);
  if (!gitRmPlan?.shouldIntercept) {
    return false;
  }

  try {
    const moveResult = await movePathsToTrash(gitRmPlan.paths, gitContext.workDir, {
      allowMissing: gitRmPlan.allowMissing,
    });
    if (!gitRmPlan.allowMissing && moveResult.missing.length > 0) {
      for (const path of moveResult.missing) {
        console.error(`git rm: ${path}: No such file or directory`);
      }
      process.exit(1);
    }
    if (moveResult.errors.length > 0) {
      for (const error of moveResult.errors) {
        console.error(error);
      }
      process.exit(1);
    }
    await stageGitRm(gitContext.workDir, gitRmPlan);
    process.exit(0);
  } catch (error) {
    console.error(formatTrashError(error));
    process.exit(1);
  }
  return true;
}

// Blocks `sleep` calls longer than the AGENTS.md ceiling so scripts cannot stall the runner.
async function maybeHandleSleepInvocation(context: RunnerExecutionContext): Promise<boolean> {
  const tokens = stripWrappersAndAssignments(context.commandArgs);
  if (tokens.length === 0) {
    return false;
  }
  const [first, ...rest] = tokens;
  if (!first || !isSleepBinary(first) || rest.length === 0) {
    return false;
  }

  const commandIndex = context.commandArgs.length - tokens.length;
  if (commandIndex < 0) {
    return false;
  }

  const adjustedArgs = [...context.commandArgs];
  const adjustments: string[] = [];

  for (let offset = 0; offset < rest.length; offset += 1) {
    const token = rest[offset];
    const durationSeconds = parseSleepDurationSeconds(token);
    if (durationSeconds == null || durationSeconds <= MAX_SLEEP_SECONDS) {
      continue;
    }
    adjustments.push(`${token}→${formatSleepDuration(MAX_SLEEP_SECONDS)}`);
    adjustedArgs[commandIndex + 1 + offset] = formatSleepArgument(MAX_SLEEP_SECONDS);
  }

  if (adjustments.length === 0) {
    return false;
  }

  console.error(
    `[runner] sleep arguments exceed ${MAX_SLEEP_SECONDS}s; clamping (${adjustments.join(', ')}).`
  );
  context.commandArgs = adjustedArgs;
  return false;
}

async function maybeHandleTmuxInvocation(context: RunnerExecutionContext): Promise<boolean> {
  const tokens = stripWrappersAndAssignments(context.commandArgs);
  if (tokens.length === 0) {
    return false;
  }
  const candidate = tokens[0];
  if (!candidate) {
    return false;
  }
  if (basename(candidate) !== 'tmux') {
    return false;
  }
  console.error('[runner] Detected tmux invocation; executing command without runner timeout guardrails.');
  await runCommandWithoutTimeout(context);
  return true;
}

function parseSleepDurationSeconds(token: string): number | null {
  const match = /^(\d+(?:\.\d+)?)([smhdSMHD]?)$/.exec(token);
  if (!match) {
    return null;
  }
  const value = Number(match[1]);
  if (!Number.isFinite(value)) {
    return null;
  }
  const unit = match[2]?.toLowerCase() ?? '';
  const multiplier = unit === 'm' ? 60 : unit === 'h' ? 60 * 60 : unit === 'd' ? 60 * 60 * 24 : 1;
  return value * multiplier;
}

function formatSleepArgument(seconds: number): string {
  return Number.isInteger(seconds) ? `${seconds}` : seconds.toString();
}

function formatSleepDuration(seconds: number): string {
  if (Number.isInteger(seconds)) {
    return `${seconds}s`;
  }
  return `${seconds.toFixed(2)}s`;
}

function isSleepBinary(token: string): boolean {
  return token === 'sleep' || token.endsWith('/sleep');
}

// Detects `git find` invocations that need policy enforcement.
function extractFindInvocation(commandArgs: string[]): GitInvocation | null {
  for (const [index, token] of commandArgs.entries()) {
    if (token === 'find' || token.endsWith('/find')) {
      return { index, argv: commandArgs.slice(index) };
    }
  }
  return null;
}

// Detects `git rm` variants so we can intercept destructive operations.
function extractRmInvocation(commandArgs: string[]): GitInvocation | null {
  if (commandArgs.length === 0) {
    return null;
  }

  const wrappers = new Set([
    'sudo',
    '/usr/bin/sudo',
    'env',
    '/usr/bin/env',
    'command',
    '/bin/command',
    'nohup',
    '/usr/bin/nohup',
  ]);

  let index = 0;
  while (index < commandArgs.length) {
    const token = commandArgs[index];
    if (!token) {
      break;
    }
    if (token.includes('=') && !token.startsWith('-')) {
      index += 1;
      continue;
    }
    if (wrappers.has(token)) {
      index += 1;
      continue;
    }
    break;
  }

  const commandToken = commandArgs[index];
  if (!commandToken) {
    return null;
  }

  const isRmCommand =
    commandToken === 'rm' ||
    commandToken.endsWith('/rm') ||
    commandToken === 'rm.exe' ||
    commandToken.endsWith('\\rm.exe');

  if (!isRmCommand) {
    return null;
  }

  return { index, argv: commandArgs.slice(index) };
}

// Expands guarded find expressions into an explicit delete plan for review.
async function buildFindDeletePlan(findArgs: string[], workspaceDir: string): Promise<{ paths: string[] } | null> {
  if (!findArgs.some((token) => token === '-delete')) {
    return null;
  }

  if (findArgs.some((token) => token === '-exec' || token === '-execdir' || token === '-ok' || token === '-okdir')) {
    console.error(
      'Runner cannot safely translate find invocations that combine -delete with -exec/-ok. Run the command manually after reviewing the paths.'
    );
    process.exit(1);
  }

  const printableArgs: string[] = [];
  for (const token of findArgs) {
    if (token === '-delete') {
      continue;
    }
    printableArgs.push(token);
  }
  printableArgs.push('-print0');

  const proc = Bun.spawn(printableArgs, {
    cwd: workspaceDir,
    stdout: 'pipe',
    stderr: 'pipe',
  });

  const [exitCode, stdoutBuf, stderrBuf] = await Promise.all([
    proc.exited,
    readProcessStream(proc.stdout),
    readProcessStream(proc.stderr),
  ]);

  if (exitCode !== 0) {
    const stderrText = stderrBuf.trim();
    const stdoutText = stdoutBuf.trim();
    if (stderrText.length > 0) {
      console.error(stderrText);
    } else if (stdoutText.length > 0) {
      console.error(stdoutText);
    }
    process.exit(exitCode);
  }

  const matches = stdoutBuf.split('\0').filter((entry: string) => entry.length > 0);
  if (matches.length === 0) {
    return { paths: [] };
  }

  const uniquePaths = new Map<string, string>();
  const workspaceCanonical = normalize(workspaceDir);

  for (const match of matches) {
    const absolute = isAbsolute(match) ? match : resolve(workspaceDir, match);
    const canonical = normalize(absolute);
    if (canonical === workspaceCanonical) {
      console.error('Refusing to trash the current workspace via find -delete. Narrow your find predicate.');
      process.exit(1);
    }
    if (!uniquePaths.has(canonical)) {
      uniquePaths.set(canonical, match);
    }
  }

  return { paths: Array.from(uniquePaths.values()) };
}

// Parses rm flags/targets to decide whether the runner should intervene.
function parseRmArguments(argv: string[]): { targets: string[]; force: boolean; shouldIntercept: boolean } | null {
  if (argv.length <= 1) {
    return null;
  }
  const targets: string[] = [];
  let force = false;
  let treatAsTarget = false;

  let index = 1;
  while (index < argv.length) {
    const token = argv[index];
    if (token === undefined) {
      break;
    }
    if (!treatAsTarget && token === '--') {
      treatAsTarget = true;
      index += 1;
      continue;
    }
    if (!treatAsTarget && token.startsWith('-') && token.length > 1) {
      if (token.includes('f')) {
        force = true;
      }
      if (token.includes('i') || token === '--interactive') {
        return null;
      }
      if (token === '--help' || token === '--version') {
        return null;
      }
      index += 1;
      continue;
    }
    targets.push(token);
    index += 1;
  }

  const firstTarget = targets[0];
  if (firstTarget === undefined) {
    return null;
  }

  return { targets, force, shouldIntercept: true };
}

// Generates a safe plan for git rm invocations, honoring guarded paths.
function parseGitRmArguments(argv: string[], command: GitCommandInfo): GitRmPlan | null {
  const stagingOptions: string[] = [];
  const paths: string[] = [];
  const optionsExpectingValue = new Set(['--pathspec-from-file']);
  let allowMissing = false;
  let treatAsPath = false;

  let index = command.index + 1;
  while (index < argv.length) {
    const token = argv[index];
    if (token === undefined) {
      break;
    }
    if (!treatAsPath && token === '--') {
      treatAsPath = true;
      index += 1;
      continue;
    }
    if (!treatAsPath && token.startsWith('-') && token.length > 1) {
      if (token === '--cached' || token === '--dry-run' || token === '-n') {
        return null;
      }
      if (token === '--ignore-unmatch' || token === '--force' || token === '-f') {
        allowMissing = true;
        stagingOptions.push(token);
        index += 1;
        continue;
      }
      if (optionsExpectingValue.has(token)) {
        const value = argv[index + 1];
        if (value) {
          stagingOptions.push(token, value);
          index += 2;
        } else {
          index += 1;
        }
        continue;
      }
      if (!token.startsWith('--')) {
        const flags = token.slice(1).split('');
        const retainedFlags: string[] = [];
        for (const flag of flags) {
          if (flag === 'n') {
            return null;
          }
          if (flag === 'f') {
            allowMissing = true;
            continue;
          }
          retainedFlags.push(flag);
        }
        if (retainedFlags.length > 0) {
          stagingOptions.push(`-${retainedFlags.join('')}`);
        }
        index += 1;
        continue;
      }
      stagingOptions.push(token);
      index += 1;
      continue;
    }
    if (token.length > 0) {
      paths.push(token);
    }
    index += 1;
  }

  if (paths.length === 0) {
    return null;
  }
  return {
    paths,
    stagingOptions,
    allowMissing,
    shouldIntercept: true,
  };
}

// Emits actionable messaging when git rm targets are already gone.
function reportMissingForRm(missing: string[], forced: boolean) {
  if (missing.length === 0 || forced) {
    return;
  }
  for (const path of missing) {
    console.error(`rm: ${path}: No such file or directory`);
  }
  process.exit(1);
}

// Attempts to move the provided paths into trash instead of deleting in place.
async function movePathsToTrash(
  paths: string[],
  baseDir: string,
  options: { allowMissing: boolean }
): Promise<MoveResult> {
  const missing: string[] = [];
  const existing: { raw: string; absolute: string }[] = [];

  for (const rawPath of paths) {
    const absolute = resolvePath(baseDir, rawPath);
    if (!existsSync(absolute)) {
      if (!options.allowMissing) {
        missing.push(rawPath);
      }
      continue;
    }
    existing.push({ raw: rawPath, absolute });
  }

  if (existing.length === 0) {
    return { missing, errors: [] };
  }

  const trashCliCommand = await findTrashCliCommand();
  if (trashCliCommand) {
    try {
      const cliArgs = [trashCliCommand, ...existing.map((item) => item.absolute)];
      const proc = Bun.spawn(cliArgs, {
        stdout: 'ignore',
        stderr: 'pipe',
      });
      const [exitCode, stderrText] = await Promise.all([proc.exited, readProcessStream(proc.stderr)]);
      if (exitCode === 0) {
        return { missing, errors: [] };
      }
      if (ENABLE_DEBUG_LOGS && stderrText.trim().length > 0) {
        console.error(`[runner] trash-cli error (${trashCliCommand}): ${stderrText.trim()}`);
      }
    } catch (error) {
      if (ENABLE_DEBUG_LOGS) {
        console.error(`[runner] trash-cli invocation failed: ${formatTrashError(error)}`);
      }
    }
  }

  const trashDir = getTrashDirectory();
  if (!trashDir) {
    return {
      missing,
      errors: ['Unable to locate macOS Trash directory (HOME/.Trash).'],
    };
  }

  const errors: string[] = [];

  for (const item of existing) {
    try {
      const target = buildTrashTarget(trashDir, item.absolute);
      try {
        renameSync(item.absolute, target);
      } catch (error) {
        if (isCrossDeviceError(error)) {
          cpSync(item.absolute, target, { recursive: true });
          rmSync(item.absolute, { recursive: true, force: true });
        } else {
          throw error;
        }
      }
    } catch (error) {
      errors.push(`Failed to move ${item.raw} to Trash: ${formatTrashError(error)}`);
    }
  }

  return { missing, errors };
}

// Resolves a potentially relative path against the workspace root.
function resolvePath(baseDir: string, input: string): string {
  if (input.startsWith('/')) {
    return input;
  }
  return resolve(baseDir, input);
}

// Returns the trash CLI directory if available so deletes can be safe.
function getTrashDirectory(): string | null {
  const home = process.env.HOME;
  if (!home) {
    return null;
  }
  const trash = join(home, '.Trash');
  if (!existsSync(trash)) {
    return null;
  }
  return trash;
}

// Builds the destination path inside the trash directory for a file.
function buildTrashTarget(trashDir: string, absolutePath: string): string {
  const baseName = basename(absolutePath);
  const timestamp = Date.now();
  let attempt = 0;
  let candidate = join(trashDir, baseName);
  while (existsSync(candidate)) {
    candidate = join(trashDir, `${baseName}-${timestamp}${attempt > 0 ? `-${attempt}` : ''}`);
    attempt += 1;
  }
  return candidate;
}

// Determines whether a rename failed because the devices differ.
function isCrossDeviceError(error: unknown): boolean {
  return error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'EXDEV';
}

// Normalizes trash/rename errors into a readable string.
function formatTrashError(error: unknown): string {
  if (error instanceof Error) {
    return error.message;
  }
  return String(error);
}

// Replays a git rm plan via spawn so we can surface errors consistently.
async function stageGitRm(workDir: string, plan: GitRmPlan) {
  if (plan.paths.length === 0) {
    return;
  }
  const args = ['git', 'rm', '--cached', '--quiet', ...plan.stagingOptions, '--', ...plan.paths];
  const proc = Bun.spawn(args, {
    cwd: workDir,
    stdout: 'inherit',
    stderr: 'inherit',
  });
  const exitCode = await proc.exited;
  if (exitCode !== 0) {
    throw new Error(`git rm --cached exited with status ${exitCode}.`);
  }
}

// Locates a usable trash CLI binary, caching the lookup per runner process.
async function findTrashCliCommand(): Promise<string | null> {
  if (cachedTrashCliCommand !== undefined) {
    return cachedTrashCliCommand;
  }

  const candidateNames = ['trash-put', 'trash'];
  const searchDirs = new Set<string>();

  if (process.env.PATH) {
    for (const segment of process.env.PATH.split(':')) {
      if (segment && segment.length > 0) {
        searchDirs.add(segment);
      }
    }
  }

  const homebrewPrefix = process.env.HOMEBREW_PREFIX ?? '/opt/homebrew';
  searchDirs.add(join(homebrewPrefix, 'opt', 'trash', 'bin'));
  searchDirs.add('/usr/local/opt/trash/bin');

  const candidatePaths = new Set<string>();
  for (const name of candidateNames) {
    candidatePaths.add(name);
    for (const dir of searchDirs) {
      candidatePaths.add(join(dir, name));
    }
  }

  for (const candidate of candidatePaths) {
    try {
      const proc = Bun.spawn([candidate, '--help'], {
        stdout: 'ignore',
        stderr: 'ignore',
      });
      const exitCode = await proc.exited;
      if (exitCode === 0 || exitCode === 1) {
        cachedTrashCliCommand = candidate;
        return candidate;
      }
    } catch (error) {
      if (ENABLE_DEBUG_LOGS) {
        console.error(`[runner] trash-cli probe failed for ${candidate}: ${formatTrashError(error)}`);
      }
    }
  }

  cachedTrashCliCommand = null;
  return null;
}

// Consumes a child process stream to completion for logging/error output.
async function readProcessStream(stream: unknown): Promise<string> {
  if (!stream) {
    return '';
  }
  try {
    const candidate = stream as { text?: () => Promise<string> };
    if (candidate.text) {
      return (await candidate.text()) ?? '';
    }
  } catch {
    // ignore
  }
  try {
    if (stream instanceof ReadableStream) {
      return await new Response(stream).text();
    }
    if (typeof stream === 'object' && stream !== null) {
      return await new Response(stream as BodyInit).text();
    }
  } catch {
    // ignore errors and return empty string
  }
  return '';
}

// Shows CLI usage plus optional error messaging.
function printUsage(message?: string) {
  if (message) {
    console.error(`[runner] ${message}`);
  }
  console.error('Usage: runner [--] <command...>');
  console.error('');
  console.error(
    `Defaults: ${formatDuration(DEFAULT_TIMEOUT_MS)} timeout for most commands, ${formatDuration(
      EXTENDED_TIMEOUT_MS
    )} when lint/test suites are detected.`
  );
}

// Pretty-prints a millisecond duration for logs.
function formatDuration(durationMs: number): string {
  if (durationMs < 1000) {
    return `${durationMs}ms`;
  }
  const seconds = durationMs / 1000;
  if (seconds < 60) {
    return `${seconds.toFixed(1)}s`;
  }
  const minutes = Math.floor(seconds / 60);
  const remainingSeconds = Math.round(seconds % 60);
  if (minutes < 60) {
    if (remainingSeconds === 0) {
      return `${minutes}m`;
    }
    return `${minutes}m ${remainingSeconds}s`;
  }
  const hours = Math.floor(minutes / 60);
  const remainingMinutes = minutes % 60;
  if (remainingMinutes === 0) {
    return `${hours}h`;
  }
  return `${hours}h ${remainingMinutes}m`;
}

function resolveSummaryStyle(rawValue: string | undefined | null): SummaryStyle {
  if (!rawValue) {
    return 'compact';
  }
  const normalized = rawValue.trim().toLowerCase();
  switch (normalized) {
    case 'minimal':
      return 'minimal';
    case 'verbose':
      return 'verbose';
    case 'short':
      return 'compact';
    default:
      return 'compact';
  }
}

function formatCompletionSummary(options: {
  exitCode: number;
  elapsedMs?: number;
  timedOut?: boolean;
  commandLabel: string;
}): string {
  const { exitCode, elapsedMs, timedOut, commandLabel } = options;
  const durationText = typeof elapsedMs === 'number' ? formatDuration(elapsedMs) : null;
  // biome-ignore lint/nursery/noUnnecessaryConditions: switch makes the formatter easier to scan.
  switch (SUMMARY_STYLE) {
    case 'minimal': {
      const parts = [`${exitCode}`];
      if (durationText) {
        parts.push(durationText);
      }
      if (timedOut) {
        parts.push('timeout');
      }
      return `[runner] ${parts.join(' · ')}`;
    }
    case 'verbose': {
      const elapsedPart = durationText ? `, elapsed ${durationText}` : '';
      const timeoutPart = timedOut ? '; timed out' : '';
      return `[runner] Finished ${commandLabel} (exit ${exitCode}${elapsedPart}${timeoutPart}).`;
    }
    default: {
      const elapsedPart = durationText ? ` in ${durationText}` : '';
      const timeoutPart = timedOut ? ' (timeout)' : '';
      return `[runner] exit ${exitCode}${elapsedPart}${timeoutPart}`;
    }
  }
}

// Joins the command args in a shell-friendly way for log display.
function formatDisplayCommand(commandArgs: string[]): string {
  return commandArgs.map((token) => (token.includes(' ') ? `"${token}"` : token)).join(' ');
}

// Tells whether the runner is already executing inside the tmux guard.
function isRunnerTmuxSession(): boolean {
  const value = process.env.RUNNER_TMUX;
  if (value) {
    return value !== '0' && value.toLowerCase() !== 'false';
  }
  return Boolean(process.env.TMUX);
}


--- scripts/test-browser.ts ---
#!/usr/bin/env tsx
/**
 * Lightweight browser connectivity smoke test.
 * - Launches Chrome headful with a fixed DevTools port (default 45871 or env ORACLE_BROWSER_PORT/ORACLE_BROWSER_DEBUG_PORT).
 * - Verifies the DevTools /json/version endpoint responds.
 * - Prints a WSL-friendly firewall hint if the port is unreachable.
 */

import { setTimeout as sleep } from 'node:timers/promises';
import { launch } from 'chrome-launcher';
import os from 'node:os';
import { readFileSync } from 'node:fs';

const DEFAULT_PORT = 45871;
const port = normalizePort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT) ?? DEFAULT_PORT;
const hostHint = resolveWslHost();
const targetHost = hostHint ?? '127.0.0.1';

function normalizePort(raw?: string | null): number | null {
  if (!raw) return null;
  const value = Number.parseInt(raw, 10);
  if (!Number.isFinite(value) || value <= 0 || value > 65535) return null;
  return value;
}

function isWsl(): boolean {
  if (process.platform !== 'linux') return false;
  if (process.env.WSL_DISTRO_NAME) return true;
  return os.release().toLowerCase().includes('microsoft');
}

function resolveWslHost(): string | null {
  if (!isWsl()) return null;
  try {
    const resolv = readFileSync('/etc/resolv.conf', 'utf8');
    for (const line of resolv.split('\n')) {
      const match = line.match(/^nameserver\s+([0-9.]+)/);
      if (match?.[1]) return match[1];
    }
  } catch {
    // ignore
  }
  return null;
}

function firewallHint(host: string, devtoolsPort: number): string | null {
  if (!isWsl()) return null;
  return [
    `DevTools port ${host}:${devtoolsPort} is blocked from WSL.`,
    '',
    'PowerShell (admin):',
    `New-NetFirewallRule -DisplayName 'Chrome DevTools ${devtoolsPort}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${devtoolsPort}`,
    "New-NetFirewallRule -DisplayName 'Chrome DevTools (chrome.exe)' -Direction Inbound -Action Allow -Program 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' -Protocol TCP",
    '',
    'Re-run ./runner pnpm test:browser after adding the rule.',
  ].join('\n');
}

async function fetchVersion(host: string, devtoolsPort: number): Promise<boolean> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 5000);
  try {
    const res = await fetch(`http://${host}:${devtoolsPort}/json/version`, { signal: controller.signal });
    if (!res.ok) return false;
    const json = (await res.json()) as { webSocketDebuggerUrl?: string };
    return Boolean(json.webSocketDebuggerUrl);
  } catch {
    return false;
  } finally {
    clearTimeout(timer);
  }
}

async function main() {
  console.log(`[browser-test] launching Chrome on ${targetHost}:${port} (headful)…`);
  const chrome = await launch({
    port,
    chromeFlags: ['--remote-debugging-address=0.0.0.0'],
  });

  let ok = await fetchVersion(targetHost, chrome.port);
  if (!ok) {
    await sleep(500);
    ok = await fetchVersion(targetHost, chrome.port);
  }

  await chrome.kill();

  if (ok) {
    console.log(`[browser-test] PASS: DevTools responding on ${targetHost}:${chrome.port}`);
    process.exit(0);
  }

  const hint = firewallHint(targetHost, chrome.port);
  console.error(`[browser-test] FAIL: DevTools not reachable at ${targetHost}:${chrome.port}`);
  if (hint) {
    console.error(hint);
  }
  process.exit(1);
}

main().catch((error) => {
  console.error('[browser-test] Unexpected failure:', error instanceof Error ? error.message : String(error));
  process.exit(1);
});


--- scripts/test-remote-chrome.ts ---
#!/usr/bin/env npx tsx
/**
 * POC: Test connecting to remote Chrome instance
 *
 * On remote machine with display, run:
 *   google-chrome --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0
 *
 * Then run this script:
 *   npx tsx scripts/test-remote-chrome.ts <remote-host> [port]
 */

import CDP from 'chrome-remote-interface';

async function main() {
  const host = process.argv[2] || 'localhost';
  const port = parseInt(process.argv[3] || '9222', 10);

  console.log(`Attempting to connect to Chrome at ${host}:${port}...`);

  try {
    // Test connection
    const client = await CDP({ host, port });
    console.log('✓ Connected to Chrome DevTools Protocol');

    const { Network, Page, Runtime } = client;

    // Enable domains
    await Promise.all([Network.enable(), Page.enable()]);
    console.log('✓ Enabled Network and Page domains');

    // Get browser version info
    const version = await CDP.Version({ host, port });
    console.log(`✓ Browser: ${version.Browser}`);
    console.log(`✓ Protocol: ${version['Protocol-Version']}`);

    // Navigate to ChatGPT
    console.log('\nNavigating to ChatGPT...');
    await Page.navigate({ url: 'https://chatgpt.com/' });
    await Page.loadEventFired();
    console.log('✓ Page loaded');

    // Check current URL
    const evalResult = await Runtime.evaluate({ expression: 'window.location.href' });
    console.log(`✓ Current URL: ${evalResult.result.value}`);

    // Check if logged in (look for specific elements)
    const checkLogin = await Runtime.evaluate({
      expression: `
        // Check for composer textarea (indicates logged in)
        const composer = document.querySelector('textarea, [contenteditable="true"]');
        const hasComposer = !!composer;

        // Check for login button (indicates logged out)
        const loginBtn = document.querySelector('a[href*="login"], button[data-testid*="login"]');
        const hasLogin = !!loginBtn;

        ({ hasComposer, hasLogin, loggedIn: hasComposer && !hasLogin })
      `,
    });
    console.log(`✓ Login status: ${JSON.stringify(checkLogin.result.value)}`);

    await client.close();
    console.log('\n✓ POC successful! Remote Chrome connection works.');
    console.log('\nTo use Oracle with remote Chrome, you would need to:');
    console.log('1. Ensure cookies are loaded in remote Chrome');
    console.log('2. Configure Oracle with --remote-chrome <host:port> to use this instance');
    console.log('3. Ensure Oracle skips local Chrome launch when --remote-chrome is specified');

  } catch (error) {
    console.error('✗ Connection failed:', error instanceof Error ? error.message : error);
    console.log('\nTroubleshooting:');
    console.log('1. Ensure Chrome is running on remote machine with:');
    console.log(`   google-chrome --remote-debugging-port=${port} --remote-debugging-address=0.0.0.0`);
    console.log('2. Check firewall allows connections to port', port);
    console.log('3. Verify network connectivity to', host);
    process.exit(1);
  }
}

void main();


--- src/browserMode.ts ---
export type {
  BrowserAutomationConfig,
  BrowserRunOptions,
  BrowserRunResult,
} from './browser/index.js';

export {
  runBrowserMode,
  CHATGPT_URL,
  DEFAULT_MODEL_STRATEGY,
  DEFAULT_MODEL_TARGET,
  parseDuration,
  normalizeChatgptUrl,
  isTemporaryChatUrl,
} from './browser/index.js';


--- src/config.ts ---
import fs from 'node:fs/promises';
import path from 'node:path';
import JSON5 from 'json5';
import { getOracleHomeDir } from './oracleHome.js';
import type { BrowserModelStrategy } from './browser/types.js';
import type { ThinkingTimeLevel } from './oracle/types.js';

export type EnginePreference = 'api' | 'browser';

export interface NotifyConfig {
  enabled?: boolean;
  sound?: boolean;
  muteIn?: Array<'CI' | 'SSH'>;
}

export interface BrowserConfigDefaults {
  chromeProfile?: string | null;
  chromePath?: string | null;
  chromeCookiePath?: string | null;
  chatgptUrl?: string | null;
  url?: string;
  timeoutMs?: number;
  debugPort?: number | null;
  inputTimeoutMs?: number;
  cookieSyncWaitMs?: number;
  headless?: boolean;
  hideWindow?: boolean;
  keepBrowser?: boolean;
  modelStrategy?: BrowserModelStrategy;
  /** Thinking time intensity (ChatGPT Thinking/Pro models): 'light', 'standard', 'extended', 'heavy' */
  thinkingTime?: ThinkingTimeLevel;
  /** Skip cookie sync and reuse a persistent automation profile (waits for manual ChatGPT login). */
  manualLogin?: boolean;
  /** Manual-login profile directory override (also available via ORACLE_BROWSER_PROFILE_DIR). */
  manualLoginProfileDir?: string | null;
}

export interface AzureConfig {
  endpoint?: string;
  deployment?: string;
  apiVersion?: string;
}

export interface RemoteServiceConfig {
  host?: string;
  token?: string;
}

export interface UserConfig {
  engine?: EnginePreference;
  model?: string;
  search?: 'on' | 'off';
  notify?: NotifyConfig;
  browser?: BrowserConfigDefaults;
  heartbeatSeconds?: number;
  filesReport?: boolean;
  background?: boolean;
  promptSuffix?: string;
  apiBaseUrl?: string;
  azure?: AzureConfig;
  sessionRetentionHours?: number;
  remote?: RemoteServiceConfig;
  remoteHost?: string;
  remoteToken?: string;
}

function resolveConfigPath(): string {
  return path.join(getOracleHomeDir(), 'config.json');
}

export interface LoadConfigResult {
  config: UserConfig;
  path: string;
  loaded: boolean;
}

export async function loadUserConfig(): Promise<LoadConfigResult> {
  const CONFIG_PATH = resolveConfigPath();
  try {
    const raw = await fs.readFile(CONFIG_PATH, 'utf8');
    const parsed = JSON5.parse(raw) as UserConfig;
    return { config: parsed ?? {}, path: CONFIG_PATH, loaded: true };
  } catch (error) {
    const code = (error as { code?: string }).code;
    if (code === 'ENOENT') {
      return { config: {}, path: CONFIG_PATH, loaded: false };
    }
    console.warn(`Failed to read ${CONFIG_PATH}: ${error instanceof Error ? error.message : String(error)}`);
    return { config: {}, path: CONFIG_PATH, loaded: false };
  }
}
export function configPath(): string {
  return resolveConfigPath();
}


--- src/heartbeat.ts ---
export interface HeartbeatConfig {
  intervalMs?: number;
  log: (message: string) => void;
  isActive: () => boolean;
  makeMessage: (elapsedMs: number) => Promise<string | null> | string | null;
}

export function startHeartbeat(config: HeartbeatConfig): () => void {
  const { intervalMs, log, isActive, makeMessage } = config;
  if (!intervalMs || intervalMs <= 0) {
    return () => {};
  }
  let stopped = false;
  let pending = false;
  const start = Date.now();
  const timer = setInterval(async () => {
    // stop flag flips asynchronously
    if (stopped || pending) {
      return;
    }
    if (!isActive()) {
      stop();
      return;
    }
    pending = true;
    try {
      const elapsed = Date.now() - start;
      const message = await makeMessage(elapsed);
      if (message && !stopped) {
        log(message);
      }
    } catch {
      // ignore heartbeat errors
    } finally {
      pending = false;
    }
  }, intervalMs);
  timer.unref?.();
  const stop = () => {
    // multiple callers may race to stop
    if (stopped) {
      return;
    }
    stopped = true;
    clearInterval(timer);
  };
  return stop;
}


--- src/oracle.ts ---
export * from './oracle/types.js';
export {
  MODEL_CONFIGS,
  DEFAULT_MODEL,
  PRO_MODELS,
  DEFAULT_SYSTEM_PROMPT,
  TOKENIZER_OPTIONS,
} from './oracle/config.js';
export { readFiles, createFileSections } from './oracle/files.js';
export { buildPrompt, buildRequestBody, renderPromptMarkdown } from './oracle/request.js';
export { estimateRequestTokens } from './oracle/tokenEstimate.js';
export { formatUSD, formatNumber, formatElapsed } from './oracle/format.js';
export { formatFileSection } from './oracle/markdown.js';
export { getFileTokenStats, printFileTokenStats } from './oracle/tokenStats.js';
export {
  OracleResponseError,
  OracleTransportError,
  OracleUserError,
  FileValidationError,
  BrowserAutomationError,
  PromptValidationError,
  describeTransportError,
  extractResponseMetadata,
  asOracleUserError,
  toTransportError,
} from './oracle/errors.js';
export { createDefaultClientFactory } from './oracle/client.js';
export { runOracle, extractTextOutput } from './oracle/run.js';
export { resolveGeminiModelId } from './oracle/gemini.js';


--- src/oracleHome.ts ---
import os from 'node:os';
import path from 'node:path';

let oracleHomeDirOverride: string | null = null;

/**
 * Test-only hook: avoid mutating process.env (shared across Vitest worker threads).
 * This override is scoped to the current Node worker.
 */
export function setOracleHomeDirOverrideForTest(dir: string | null): void {
  oracleHomeDirOverride = dir;
}

export function getOracleHomeDir(): string {
  return oracleHomeDirOverride ?? process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), '.oracle');
}



--- src/sessionManager.ts ---
import path from 'node:path';
import fs from 'node:fs/promises';
import { createWriteStream } from 'node:fs';
import type { WriteStream } from 'node:fs';
import net from 'node:net';
import type { BrowserModelStrategy, CookieParam } from './browser/types.js';
import type { TransportFailureReason, AzureOptions, ModelName, ThinkingTimeLevel } from './oracle.js';
import { DEFAULT_MODEL } from './oracle.js';
import { safeModelSlug } from './oracle/modelResolver.js';
import { getOracleHomeDir } from './oracleHome.js';

export type SessionMode = 'api' | 'browser';

export interface BrowserSessionConfig {
  chromeProfile?: string | null;
  chromePath?: string | null;
  chromeCookiePath?: string | null;
  chatgptUrl?: string | null;
  url?: string;
  timeoutMs?: number;
  debugPort?: number | null;
  inputTimeoutMs?: number;
  cookieSync?: boolean;
  cookieNames?: string[] | null;
  cookieSyncWaitMs?: number;
  inlineCookies?: CookieParam[] | null;
  inlineCookiesSource?: string | null;
  headless?: boolean;
  keepBrowser?: boolean;
  hideWindow?: boolean;
  desiredModel?: string | null;
  modelStrategy?: BrowserModelStrategy;
  debug?: boolean;
  allowCookieErrors?: boolean;
  remoteChrome?: { host: string; port: number } | null;
  manualLogin?: boolean;
  manualLoginProfileDir?: string | null;
  manualLoginCookieSync?: boolean;
  /** Thinking time intensity: 'light', 'standard', 'extended', 'heavy' */
  thinkingTime?: ThinkingTimeLevel;
}

export interface BrowserRuntimeMetadata {
  chromePid?: number;
  chromePort?: number;
  chromeHost?: string;
  userDataDir?: string;
  chromeTargetId?: string;
  tabUrl?: string;
  conversationId?: string;
  /** PID of the controller process that launched this browser run. Helps detect orphaned sessions. */
  controllerPid?: number;
}

export interface BrowserMetadata {
  config?: BrowserSessionConfig;
  runtime?: BrowserRuntimeMetadata;
}

export interface SessionResponseMetadata {
  id?: string;
  requestId?: string | null;
  status?: string;
  incompleteReason?: string | null;
}

export interface SessionTransportMetadata {
  reason?: TransportFailureReason;
}

export interface SessionUserErrorMetadata {
  category?: string;
  message?: string;
  details?: Record<string, unknown>;
}

export interface StoredRunOptions {
  prompt?: string;
  file?: string[];
  model?: string;
  models?: ModelName[];
  maxInput?: number;
  system?: string;
  maxOutput?: number;
  silent?: boolean;
  filesReport?: boolean;
  slug?: string;
  mode?: SessionMode;
  browserConfig?: BrowserSessionConfig;
  verbose?: boolean;
  heartbeatIntervalMs?: number;
  browserAttachments?: 'auto' | 'never' | 'always';
  browserInlineFiles?: boolean;
  browserBundleFiles?: boolean;
  background?: boolean;
  search?: boolean;
  baseUrl?: string;
  azure?: AzureOptions;
  effectiveModelId?: string;
  renderPlain?: boolean;
  writeOutputPath?: string;
}

export interface SessionMetadata {
  id: string;
  createdAt: string;
  status: string;
  promptPreview?: string;
  model?: string;
  models?: SessionModelRun[];
  cwd?: string;
  options: StoredRunOptions;
  notifications?: SessionNotifications;
  startedAt?: string;
  completedAt?: string;
  mode?: SessionMode;
  usage?: {
    inputTokens: number;
    outputTokens: number;
    reasoningTokens: number;
    totalTokens: number;
    cost?: number;
  };
  errorMessage?: string;
  elapsedMs?: number;
  browser?: BrowserMetadata;
  response?: SessionResponseMetadata;
  transport?: SessionTransportMetadata;
  error?: SessionUserErrorMetadata;
}

export type SessionStatus = 'pending' | 'running' | 'completed' | 'error' | 'cancelled';

export interface SessionModelRun {
  model: string;
  status: SessionStatus;
  queuedAt?: string;
  startedAt?: string;
  completedAt?: string;
  usage?: {
    inputTokens: number;
    outputTokens: number;
    reasoningTokens: number;
    totalTokens: number;
    cost?: number;
  };
  response?: SessionResponseMetadata;
  transport?: SessionTransportMetadata;
  error?: SessionUserErrorMetadata;
  log?: {
    path: string;
    bytes?: number;
  };
}

export interface SessionNotifications {
  enabled: boolean;
  sound: boolean;
}

interface SessionLogWriter {
  stream: WriteStream;
  logLine: (line?: string) => void;
  writeChunk: (chunk: string) => boolean;
  logPath: string;
}

interface InitializeSessionOptions extends StoredRunOptions {
  prompt?: string;
  model: string;
}

export function getSessionsDir(): string {
  return path.join(getOracleHomeDir(), 'sessions');
}
const METADATA_FILENAME = 'meta.json';
const LEGACY_SESSION_FILENAME = 'session.json';
const LEGACY_REQUEST_FILENAME = 'request.json';
const MODELS_DIRNAME = 'models';
const MODEL_JSON_EXTENSION = '.json';
const MODEL_LOG_EXTENSION = '.log';
const MAX_STATUS_LIMIT = 1000;
const ZOMBIE_MAX_AGE_MS = 60 * 60 * 1000; // 60 minutes
const CHROME_RUNTIME_TIMEOUT_MS = 250;
const DEFAULT_SLUG = 'session';
const MAX_SLUG_WORDS = 5;
const MIN_CUSTOM_SLUG_WORDS = 3;
const MAX_SLUG_WORD_LENGTH = 10;

async function ensureDir(dirPath: string): Promise<void> {
  await fs.mkdir(dirPath, { recursive: true });
}

export async function ensureSessionStorage(): Promise<void> {
  await ensureDir(getSessionsDir());
}

function slugify(text: string | undefined, maxWords = MAX_SLUG_WORDS): string {
  const normalized = text?.toLowerCase() ?? '';
  const words = normalized.match(/[a-z0-9]+/g) ?? [];
  const trimmed = words
    .slice(0, maxWords)
    .map((word) => word.slice(0, MAX_SLUG_WORD_LENGTH));
  return trimmed.length > 0 ? trimmed.join('-') : DEFAULT_SLUG;
}

function countSlugWords(slug: string): number {
  return slug.split('-').filter(Boolean).length;
}

function normalizeCustomSlug(candidate: string): string {
  const slug = slugify(candidate, MAX_SLUG_WORDS);
  const wordCount = countSlugWords(slug);
  if (wordCount < MIN_CUSTOM_SLUG_WORDS || wordCount > MAX_SLUG_WORDS) {
    throw new Error(`Custom slug must include between ${MIN_CUSTOM_SLUG_WORDS} and ${MAX_SLUG_WORDS} words.`);
  }
  return slug;
}

export function createSessionId(prompt: string, customSlug?: string): string {
  if (customSlug) {
    return normalizeCustomSlug(customSlug);
  }
  return slugify(prompt);
}

function sessionDir(id: string): string {
  return path.join(getSessionsDir(), id);
}

function metaPath(id: string): string {
  return path.join(sessionDir(id), METADATA_FILENAME);
}

function requestPath(id: string): string {
  return path.join(sessionDir(id), LEGACY_REQUEST_FILENAME);
}

function legacySessionPath(id: string): string {
  return path.join(sessionDir(id), LEGACY_SESSION_FILENAME);
}

function logPath(id: string): string {
  return path.join(sessionDir(id), 'output.log');
}

function modelsDir(id: string): string {
  return path.join(sessionDir(id), MODELS_DIRNAME);
}

function modelJsonPath(id: string, model: string): string {
  const slug = safeModelSlug(model);
  return path.join(modelsDir(id), `${slug}${MODEL_JSON_EXTENSION}`);
}

function modelLogPath(id: string, model: string): string {
  const slug = safeModelSlug(model);
  return path.join(modelsDir(id), `${slug}${MODEL_LOG_EXTENSION}`);
}

async function fileExists(targetPath: string): Promise<boolean> {
  try {
    await fs.access(targetPath);
    return true;
  } catch {
    return false;
  }
}

async function ensureUniqueSessionId(baseSlug: string): Promise<string> {
  let candidate = baseSlug;
  let suffix = 2;
  while (await fileExists(sessionDir(candidate))) {
    candidate = `${baseSlug}-${suffix}`;
    suffix += 1;
  }
  return candidate;
}

async function listModelRunFiles(sessionId: string): Promise<SessionModelRun[]> {
  const dir = modelsDir(sessionId);
  const entries = await fs.readdir(dir).catch(() => []);
  const result: SessionModelRun[] = [];
  for (const entry of entries) {
    if (!entry.endsWith(MODEL_JSON_EXTENSION)) {
      continue;
    }
    const jsonPath = path.join(dir, entry);
    try {
      const raw = await fs.readFile(jsonPath, 'utf8');
      const parsed = JSON.parse(raw) as SessionModelRun;
      const normalized = ensureModelLogReference(sessionId, parsed);
      result.push(normalized);
    } catch {
      // ignore malformed model files
    }
  }
  return result;
}

function ensureModelLogReference(sessionId: string, record: SessionModelRun): SessionModelRun {
  const logPathRelative =
    record.log?.path ?? path.relative(sessionDir(sessionId), modelLogPath(sessionId, record.model));
  return {
    ...record,
    log: { path: logPathRelative, bytes: record.log?.bytes },
  };
}

async function readModelRunFile(sessionId: string, model: string): Promise<SessionModelRun | null> {
  try {
    const raw = await fs.readFile(modelJsonPath(sessionId, model), 'utf8');
    const parsed = JSON.parse(raw) as SessionModelRun;
    return ensureModelLogReference(sessionId, parsed);
  } catch {
    return null;
  }
}

export async function updateModelRunMetadata(
  sessionId: string,
  model: string,
  updates: Partial<SessionModelRun>,
): Promise<SessionModelRun> {
  await ensureDir(modelsDir(sessionId));
  const existing = (await readModelRunFile(sessionId, model)) ?? {
    model,
    status: 'pending',
  };
  const next: SessionModelRun = ensureModelLogReference(sessionId, {
    ...existing,
    ...updates,
    model,
  });
  await fs.writeFile(modelJsonPath(sessionId, model), JSON.stringify(next, null, 2), 'utf8');
  return next;
}

export async function readModelRunMetadata(sessionId: string, model: string): Promise<SessionModelRun | null> {
  return readModelRunFile(sessionId, model);
}

export async function initializeSession(
  options: InitializeSessionOptions,
  cwd: string,
  notifications?: SessionNotifications,
): Promise<SessionMetadata> {
  await ensureSessionStorage();
  const baseSlug = createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
  const sessionId = await ensureUniqueSessionId(baseSlug);
  const dir = sessionDir(sessionId);
  await ensureDir(dir);
  const mode = options.mode ?? 'api';
  const browserConfig = options.browserConfig;
  const modelList: ModelName[] =
    Array.isArray(options.models) && options.models.length > 0
      ? options.models
      : options.model
        ? [options.model as ModelName]
        : [];

  const metadata: SessionMetadata = {
    id: sessionId,
    createdAt: new Date().toISOString(),
    status: 'pending',
    promptPreview: (options.prompt || '').slice(0, 160),
    model: modelList[0] ?? options.model,
    models: modelList.map((modelName) => ({
      model: modelName,
      status: 'pending',
    })),
    cwd,
    mode,
    browser: browserConfig ? { config: browserConfig } : undefined,
    notifications,
    options: {
      prompt: options.prompt,
      file: options.file ?? [],
      model: options.model,
      models: modelList,
      effectiveModelId: options.effectiveModelId,
      maxInput: options.maxInput,
      system: options.system,
      maxOutput: options.maxOutput,
      silent: options.silent,
      filesReport: options.filesReport,
      slug: sessionId,
      mode,
      browserConfig,
      verbose: options.verbose,
      heartbeatIntervalMs: options.heartbeatIntervalMs,
      browserAttachments: options.browserAttachments,
      browserInlineFiles: options.browserInlineFiles,
      browserBundleFiles: options.browserBundleFiles,
      background: options.background,
      search: options.search,
      baseUrl: options.baseUrl,
      azure: options.azure,
      writeOutputPath: options.writeOutputPath,
    },
  };
  await ensureDir(modelsDir(sessionId));
  await fs.writeFile(metaPath(sessionId), JSON.stringify(metadata, null, 2), 'utf8');
  await Promise.all(
    (modelList.length > 0 ? modelList : [metadata.model ?? DEFAULT_MODEL]).map(async (modelName) => {
      const jsonPath = modelJsonPath(sessionId, modelName);
      const logFilePath = modelLogPath(sessionId, modelName);
      const modelRecord: SessionModelRun = {
        model: modelName,
        status: 'pending',
        log: { path: path.relative(sessionDir(sessionId), logFilePath) },
      };
      await fs.writeFile(jsonPath, JSON.stringify(modelRecord, null, 2), 'utf8');
      await fs.writeFile(logFilePath, '', 'utf8');
    }),
  );
  await fs.writeFile(logPath(sessionId), '', 'utf8');
  return metadata;
}

export async function readSessionMetadata(sessionId: string): Promise<SessionMetadata | null> {
  const modern = await readModernSessionMetadata(sessionId);
  if (modern) {
    return modern;
  }
  const legacy = await readLegacySessionMetadata(sessionId);
  if (legacy) {
    return legacy;
  }
  return null;
}

export async function updateSessionMetadata(
  sessionId: string,
  updates: Partial<SessionMetadata>,
): Promise<SessionMetadata> {
  const existing =
    (await readModernSessionMetadata(sessionId)) ??
    (await readLegacySessionMetadata(sessionId)) ??
    ({ id: sessionId } as SessionMetadata);
  const next = { ...existing, ...updates };
  await fs.writeFile(metaPath(sessionId), JSON.stringify(next, null, 2), 'utf8');
  return next;
}

async function readModernSessionMetadata(sessionId: string): Promise<SessionMetadata | null> {
  try {
    const raw = await fs.readFile(metaPath(sessionId), 'utf8');
    const parsed = JSON.parse(raw) as SessionMetadata | StoredRunOptions;
    if (!isSessionMetadataRecord(parsed)) {
      return null;
    }
    const enriched = await attachModelRuns(parsed, sessionId);
    const runtimeChecked = await markDeadBrowser(enriched, { persist: false });
    return await markZombie(runtimeChecked, { persist: false });
  } catch {
    return null;
  }
}

async function readLegacySessionMetadata(sessionId: string): Promise<SessionMetadata | null> {
  try {
    const raw = await fs.readFile(legacySessionPath(sessionId), 'utf8');
    const parsed = JSON.parse(raw) as SessionMetadata;
    const enriched = await attachModelRuns(parsed, sessionId);
    const runtimeChecked = await markDeadBrowser(enriched, { persist: false });
    return await markZombie(runtimeChecked, { persist: false });
  } catch {
    return null;
  }
}

function isSessionMetadataRecord(value: unknown): value is SessionMetadata {
  return Boolean(value && typeof (value as SessionMetadata).id === 'string' && (value as SessionMetadata).status);
}

async function attachModelRuns(meta: SessionMetadata, sessionId: string): Promise<SessionMetadata> {
  const runs = await listModelRunFiles(sessionId);
  if (runs.length === 0) {
    return meta;
  }
  return { ...meta, models: runs };
}

export function createSessionLogWriter(sessionId: string, model?: string): SessionLogWriter {
  const targetPath = model ? modelLogPath(sessionId, model) : logPath(sessionId);
  if (model) {
    void ensureDir(modelsDir(sessionId));
  }
  const stream = createWriteStream(targetPath, { flags: 'a' });
  const logLine = (line = ''): void => {
    stream.write(`${line}\n`);
  };
  const writeChunk = (chunk: string): boolean => {
    stream.write(chunk);
    return true;
  };
  return { stream, logLine, writeChunk, logPath: targetPath };
}

export async function listSessionsMetadata(): Promise<SessionMetadata[]> {
  await ensureSessionStorage();
  const entries = await fs.readdir(getSessionsDir()).catch(() => []);
  const metas: SessionMetadata[] = [];
  for (const entry of entries) {
    let meta = await readSessionMetadata(entry);
    if (meta) {
      meta = await markDeadBrowser(meta, { persist: true });
      meta = await markZombie(meta, { persist: true }); // keep stored metadata consistent with zombie detection
      metas.push(meta);
    }
  }
  return metas.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
}

export function filterSessionsByRange(
  metas: SessionMetadata[],
  { hours = 24, includeAll = false, limit = 100 }: { hours?: number; includeAll?: boolean; limit?: number },
): { entries: SessionMetadata[]; truncated: boolean; total: number } {
  const maxLimit = Math.min(limit, MAX_STATUS_LIMIT);
  let filtered = metas;
  if (!includeAll) {
    const cutoff = Date.now() - hours * 60 * 60 * 1000;
    filtered = metas.filter((meta) => new Date(meta.createdAt).getTime() >= cutoff);
  }
  const limited = filtered.slice(0, maxLimit);
  const truncated = filtered.length > maxLimit;
  return { entries: limited, truncated, total: filtered.length };
}

export async function readSessionLog(sessionId: string): Promise<string> {
  const runs = await listModelRunFiles(sessionId);
  if (runs.length === 0) {
    try {
      return await fs.readFile(logPath(sessionId), 'utf8');
    } catch {
      return '';
    }
  }
  const sections: string[] = [];
  let hasContent = false;
  const ordered = runs
    .slice()
    .sort((a, b) => (a.startedAt && b.startedAt ? a.startedAt.localeCompare(b.startedAt) : a.model.localeCompare(b.model)));
  for (const run of ordered) {
    const logFile =
      run.log?.path
        ? path.isAbsolute(run.log.path)
          ? run.log.path
          : path.join(sessionDir(sessionId), run.log.path)
        : modelLogPath(sessionId, run.model);
    let body = '';
    try {
      body = await fs.readFile(logFile, 'utf8');
    } catch {
      body = '';
    }
    if (body.length > 0) {
      hasContent = true;
    }
    sections.push(`=== ${run.model} ===\n${body}`.trimEnd());
  }
  if (!hasContent) {
    try {
      return await fs.readFile(logPath(sessionId), 'utf8');
    } catch {
      // ignore and return structured header-only log
    }
  }
  return sections.join('\n\n');
}

export async function readModelLog(sessionId: string, model: string): Promise<string> {
  try {
    return await fs.readFile(modelLogPath(sessionId, model), 'utf8');
  } catch {
    return '';
  }
}

export async function readSessionRequest(sessionId: string): Promise<StoredRunOptions | null> {
  const modern = await readModernSessionMetadata(sessionId);
  if (modern?.options) {
    return modern.options;
  }
  try {
    const raw = await fs.readFile(requestPath(sessionId), 'utf8');
    const parsed = JSON.parse(raw);
    if (isSessionMetadataRecord(parsed)) {
      return parsed.options ?? null;
    }
    return parsed as StoredRunOptions;
  } catch {
    return null;
  }
}

export async function deleteSessionsOlderThan({
  hours = 24,
  includeAll = false,
}: { hours?: number; includeAll?: boolean } = {}): Promise<{ deleted: number; remaining: number }> {
  await ensureSessionStorage();
  const entries = await fs.readdir(getSessionsDir()).catch(() => []);
  if (!entries.length) {
    return { deleted: 0, remaining: 0 };
  }
  const cutoff = includeAll ? Number.NEGATIVE_INFINITY : Date.now() - hours * 60 * 60 * 1000;
  let deleted = 0;

  for (const entry of entries) {
    const dir = sessionDir(entry);
    let createdMs: number | undefined;
    const meta = await readSessionMetadata(entry);
    if (meta?.createdAt) {
      const parsed = Date.parse(meta.createdAt);
      if (!Number.isNaN(parsed)) {
        createdMs = parsed;
      }
    }
    if (createdMs == null) {
      try {
        const stats = await fs.stat(dir);
        createdMs = stats.birthtimeMs || stats.mtimeMs;
      } catch {
        continue;
      }
    }
    if (includeAll || (createdMs != null && createdMs < cutoff)) {
      await fs.rm(dir, { recursive: true, force: true });
      deleted += 1;
    }
  }

  const remaining = Math.max(entries.length - deleted, 0);
  return { deleted, remaining };
}

export async function wait(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export { MAX_STATUS_LIMIT };
export { ZOMBIE_MAX_AGE_MS };

export async function getSessionPaths(sessionId: string): Promise<{
  dir: string;
  metadata: string;
  log: string;
  request: string;
}> {
  const dir = sessionDir(sessionId);
  const metadata = metaPath(sessionId);
  const log = logPath(sessionId);
  const request = requestPath(sessionId);

  const required = [metadata, log];
  const missing: string[] = [];
  for (const file of required) {
    if (!(await fileExists(file))) {
      missing.push(path.basename(file));
    }
  }

  if (missing.length > 0) {
    throw new Error(`Session "${sessionId}" is missing: ${missing.join(', ')}`);
  }
  return { dir, metadata, log, request };
}

async function markZombie(meta: SessionMetadata, { persist }: { persist: boolean }): Promise<SessionMetadata> {
  if (!isZombie(meta)) {
    return meta;
  }
  if (meta.mode === 'browser') {
    const runtime = meta.browser?.runtime;
    if (runtime) {
      const signals: boolean[] = [];
      if (runtime.chromePid) {
        signals.push(isProcessAlive(runtime.chromePid));
      }
      if (runtime.chromePort) {
        const host = runtime.chromeHost ?? '127.0.0.1';
        signals.push(await isPortOpen(host, runtime.chromePort));
      }
      if (signals.some(Boolean)) {
        return meta;
      }
    }
  }
  const updated: SessionMetadata = {
    ...meta,
    status: 'error',
    errorMessage: 'Session marked as zombie (>60m stale)',
    completedAt: new Date().toISOString(),
  };
  if (persist) {
    await fs.writeFile(metaPath(meta.id), JSON.stringify(updated, null, 2), 'utf8');
  }
  return updated;
}

async function markDeadBrowser(meta: SessionMetadata, { persist }: { persist: boolean }): Promise<SessionMetadata> {
  if (meta.status !== 'running' || meta.mode !== 'browser') {
    return meta;
  }
  const runtime = meta.browser?.runtime;
  if (!runtime) {
    return meta;
  }
  const signals: boolean[] = [];
  if (runtime.chromePid) {
    signals.push(isProcessAlive(runtime.chromePid));
  }
  if (runtime.chromePort) {
    const host = runtime.chromeHost ?? '127.0.0.1';
    signals.push(await isPortOpen(host, runtime.chromePort));
  }
  if (signals.length === 0 || signals.some(Boolean)) {
    return meta;
  }
  const response = meta.response
    ? {
        ...meta.response,
        status: 'error',
        incompleteReason: meta.response.incompleteReason ?? 'chrome-disconnected',
      }
    : { status: 'error', incompleteReason: 'chrome-disconnected' };
  const updated: SessionMetadata = {
    ...meta,
    status: 'error',
    errorMessage: 'Browser session ended (Chrome is no longer reachable)',
    completedAt: new Date().toISOString(),
    response,
  };
  if (persist) {
    await fs.writeFile(metaPath(meta.id), JSON.stringify(updated, null, 2), 'utf8');
  }
  return updated;
}

function isZombie(meta: SessionMetadata): boolean {
  if (meta.status !== 'running') {
    return false;
  }
  const reference = meta.startedAt ?? meta.createdAt;
  if (!reference) {
    return false;
  }
  const startedMs = Date.parse(reference);
  if (Number.isNaN(startedMs)) {
    return false;
  }
  return Date.now() - startedMs > ZOMBIE_MAX_AGE_MS;
}

function isProcessAlive(pid?: number): boolean {
  if (!pid) return false;
  try {
    process.kill(pid, 0);
    return true;
  } catch (error) {
    const code = error instanceof Error ? (error as NodeJS.ErrnoException).code : undefined;
    if (code === 'ESRCH' || code === 'EINVAL') {
      return false;
    }
    if (code === 'EPERM') {
      return true;
    }
    return true;
  }
}

async function isPortOpen(host: string, port: number): Promise<boolean> {
  if (!port || port <= 0 || port > 65535) {
    return false;
  }
  return new Promise((resolve) => {
    const socket = net.createConnection({ host, port });
    let settled = false;
    const cleanup = (result: boolean) => {
      if (settled) return;
      settled = true;
      socket.removeAllListeners();
      socket.end();
      socket.destroy();
      socket.unref();
      resolve(result);
    };
    const timer = setTimeout(() => cleanup(false), CHROME_RUNTIME_TIMEOUT_MS);
    socket.once('connect', () => {
      clearTimeout(timer);
      cleanup(true);
    });
    socket.once('error', () => {
      clearTimeout(timer);
      cleanup(false);
    });
  });
}


--- src/sessionStore.ts ---
import type { SessionMetadata, SessionNotifications, StoredRunOptions, SessionModelRun } from './sessionManager.js';
import {
  ensureSessionStorage,
  initializeSession,
  readSessionMetadata,
  updateSessionMetadata,
  createSessionLogWriter,
  readSessionLog,
  readModelLog,
  readSessionRequest,
  listSessionsMetadata,
  filterSessionsByRange,
  deleteSessionsOlderThan,
  updateModelRunMetadata,
  getSessionPaths,
  getSessionsDir,
} from './sessionManager.js';
type InitializeSessionOptionsType = Parameters<typeof initializeSession>[0];

export interface SessionStore {
  ensureStorage(): Promise<void>;
  createSession(
    options: InitializeSessionOptionsType,
    cwd: string,
    notifications?: SessionNotifications,
  ): Promise<SessionMetadata>;
  readSession(sessionId: string): Promise<SessionMetadata | null>;
  updateSession(sessionId: string, updates: Partial<SessionMetadata>): Promise<SessionMetadata>;
  createLogWriter(sessionId: string, model?: string): ReturnType<typeof createSessionLogWriter>;
  updateModelRun(sessionId: string, model: string, updates: Partial<SessionModelRun>): Promise<SessionModelRun>;
  readLog(sessionId: string): Promise<string>;
  readModelLog(sessionId: string, model: string): Promise<string>;
  readRequest(sessionId: string): Promise<StoredRunOptions | null>;
  listSessions(): Promise<SessionMetadata[]>;
  filterSessions(
    metas: SessionMetadata[],
    options: { hours?: number; includeAll?: boolean; limit?: number },
  ): ReturnType<typeof filterSessionsByRange>;
  deleteOlderThan(options?: { hours?: number; includeAll?: boolean }): Promise<{ deleted: number; remaining: number }>;
  getPaths(sessionId: string): Promise<{ dir: string; metadata: string; log: string; request: string }>;
  sessionsDir(): string;
}

class FileSessionStore implements SessionStore {
  ensureStorage(): Promise<void> {
    return ensureSessionStorage();
  }

  createSession(
    options: InitializeSessionOptionsType,
    cwd: string,
    notifications?: SessionNotifications,
  ): Promise<SessionMetadata> {
    return initializeSession(options, cwd, notifications);
  }

  readSession(sessionId: string): Promise<SessionMetadata | null> {
    return readSessionMetadata(sessionId);
  }

  updateSession(sessionId: string, updates: Partial<SessionMetadata>): Promise<SessionMetadata> {
    return updateSessionMetadata(sessionId, updates);
  }

  createLogWriter(sessionId: string, model?: string): ReturnType<typeof createSessionLogWriter> {
    return createSessionLogWriter(sessionId, model);
  }

  updateModelRun(sessionId: string, model: string, updates: Partial<SessionModelRun>): Promise<SessionModelRun> {
    return updateModelRunMetadata(sessionId, model, updates);
  }

  readLog(sessionId: string): Promise<string> {
    return readSessionLog(sessionId);
  }

  readModelLog(sessionId: string, model: string): Promise<string> {
    return readModelLog(sessionId, model);
  }

  readRequest(sessionId: string): Promise<StoredRunOptions | null> {
    return readSessionRequest(sessionId);
  }

  listSessions(): Promise<SessionMetadata[]> {
    return listSessionsMetadata();
  }

  filterSessions(
    metas: SessionMetadata[],
    options: { hours?: number; includeAll?: boolean; limit?: number },
  ): ReturnType<typeof filterSessionsByRange> {
    return filterSessionsByRange(metas, options);
  }

  deleteOlderThan(options?: { hours?: number; includeAll?: boolean }): Promise<{ deleted: number; remaining: number }> {
    return deleteSessionsOlderThan(options);
  }

  getPaths(sessionId: string): Promise<{ dir: string; metadata: string; log: string; request: string }> {
    return getSessionPaths(sessionId);
  }

  sessionsDir(): string {
    return getSessionsDir();
  }
}

export const sessionStore: SessionStore = new FileSessionStore();
export { wait } from './sessionManager.js';
export type {
  SessionMetadata,
  SessionMode,
  BrowserSessionConfig,
  BrowserRuntimeMetadata,
  SessionTransportMetadata,
  SessionUserErrorMetadata,
  SessionStatus,
  SessionModelRun,
} from './sessionManager.js';

export async function pruneOldSessions(
  hours?: number,
  log?: (message: string) => void,
): Promise<void> {
  if (typeof hours !== 'number' || Number.isNaN(hours) || hours <= 0) {
    return;
  }
  const result = await sessionStore.deleteOlderThan({ hours });
  if (result.deleted > 0) {
    log?.(`Pruned ${result.deleted} stored sessions older than ${hours}h.`);
  }
}


--- src/version.ts ---
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

let cachedVersion: string | null = null;

export function getCliVersion(): string {
  if (cachedVersion) {
    return cachedVersion;
  }
  cachedVersion = readVersionFromPackage();
  return cachedVersion;
}

function readVersionFromPackage(): string {
  const modulePath = fileURLToPath(import.meta.url);
  let currentDir = path.dirname(modulePath);
  const filesystemRoot = path.parse(currentDir).root;

  // biome-ignore lint/nursery/noUnnecessaryConditions: deliberate sentinel loop to walk up directories
  while (true) {
    const candidate = path.join(currentDir, 'package.json');
    try {
      const raw = readFileSync(candidate, 'utf8');
      const parsed = JSON.parse(raw) as { version?: unknown };
      const version =
        typeof parsed.version === 'string' && parsed.version.trim().length > 0
          ? parsed.version.trim()
          : '0.0.0';
      return version;
    } catch (error) {
      const code = error instanceof Error && 'code' in error ? (error as { code?: string }).code : undefined;
      if (code && code !== 'ENOENT') {
        break;
      }
    }
    if (currentDir === filesystemRoot) {
      break;
    }
    currentDir = path.dirname(currentDir);
  }
  return '0.0.0';
}


--- src/oracle/background.ts ---
import { APIConnectionError, APIConnectionTimeoutError } from 'openai';
import chalk from 'chalk';
import { formatElapsed } from './format.js';
import { startHeartbeat } from '../heartbeat.js';
import {
  OracleResponseError,
  OracleTransportError,
  describeTransportError,
  toTransportError,
} from './errors.js';
import type { ClientLike, OracleResponse, OracleRequestBody } from './types.js';

const BACKGROUND_MAX_WAIT_MS = 30 * 60 * 1000;
const BACKGROUND_POLL_INTERVAL_MS = 5000;
const BACKGROUND_RETRY_BASE_MS = 3000;
const BACKGROUND_RETRY_MAX_MS = 15000;

interface BackgroundExecutionParams {
  client: ClientLike;
  requestBody: OracleRequestBody;
  log: (message: string) => void;
  wait: (ms: number) => Promise<void>;
  heartbeatIntervalMs?: number;
  now: () => number;
  maxWaitMs: number;
}

export async function executeBackgroundResponse(params: BackgroundExecutionParams): Promise<OracleResponse> {
  const { client, requestBody, log, wait, heartbeatIntervalMs, now, maxWaitMs } = params;
  let initialResponse: OracleResponse;
  try {
    initialResponse = await client.responses.create(requestBody);
  } catch (error) {
    const transportError = toTransportError(error, requestBody.model);
    log(chalk.yellow(describeTransportError(transportError, maxWaitMs)));
    throw transportError;
  }
  if (!initialResponse || !initialResponse.id) {
    throw new OracleResponseError('API did not return a response ID for the background run.', initialResponse);
  }
  const responseId = initialResponse.id;
  log(
    chalk.dim(
      `API scheduled background response ${responseId} (status=${initialResponse.status ?? 'unknown'}). Monitoring up to ${Math.round(
        BACKGROUND_MAX_WAIT_MS / 60000,
      )} minutes for completion...`,
    ),
  );
  let heartbeatActive = false;
  let stopHeartbeat: (() => void) | null = null;
  const stopHeartbeatNow = () => {
    if (!heartbeatActive) return;
    heartbeatActive = false;
    stopHeartbeat?.();
    stopHeartbeat = null;
  };
  if (heartbeatIntervalMs && heartbeatIntervalMs > 0) {
    heartbeatActive = true;
    stopHeartbeat = startHeartbeat({
      intervalMs: heartbeatIntervalMs,
      log: (message) => log(message),
      isActive: () => heartbeatActive,
      makeMessage: (elapsedMs) => {
        const elapsedText = formatElapsed(elapsedMs);
        return `API background run still in progress — ${elapsedText} elapsed.`;
      },
    });
  }
  try {
    return await pollBackgroundResponse({
      client,
      responseId,
      initialResponse,
      log,
      wait,
      now,
      maxWaitMs,
    });
  } finally {
    stopHeartbeatNow();
  }
}

interface BackgroundPollParams {
  client: ClientLike;
  responseId: string;
  initialResponse: OracleResponse;
  log: (message: string) => void;
  wait: (ms: number) => Promise<void>;
  now: () => number;
  maxWaitMs: number;
}

async function pollBackgroundResponse(params: BackgroundPollParams): Promise<OracleResponse> {
  const { client, responseId, initialResponse, log, wait, now, maxWaitMs } = params;
  const startMark = now();
  let response = initialResponse;
  let firstCycle = true;
  let lastStatus: string | undefined = response.status;
  // biome-ignore lint/nursery/noUnnecessaryConditions: intentional polling loop.
  while (true) {
    const status = response.status ?? 'completed';
    // firstCycle toggles immediately; keep for clarity in logs.
    if (firstCycle) {
      firstCycle = false;
      log(chalk.dim(`API background response status=${status}. We'll keep retrying automatically.`));
    } else if (status !== lastStatus && status !== 'completed') {
      log(chalk.dim(`API background response status=${status}.`));
    }
    lastStatus = status;

    if (status === 'completed') {
      return response;
    }
    if (status !== 'in_progress' && status !== 'queued') {
      const detail = response.error?.message || response.incomplete_details?.reason || status;
      throw new OracleResponseError(`Response did not complete: ${detail}`, response);
    }
    if (now() - startMark >= maxWaitMs) {
      throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
    }

    await wait(BACKGROUND_POLL_INTERVAL_MS);
    if (now() - startMark >= maxWaitMs) {
      throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
    }
    const { response: nextResponse, reconnected } = await retrieveBackgroundResponseWithRetry({
      client,
      responseId,
      wait,
      now,
      maxWaitMs,
      startMark,
      log,
    });
    if (reconnected) {
      const nextStatus = nextResponse.status ?? 'in_progress';
      log(chalk.dim(`Reconnected to API background response (status=${nextStatus}). API is still working...`));
    }
    response = nextResponse;
  }
}

interface RetrieveRetryParams {
  client: ClientLike;
  responseId: string;
  wait: (ms: number) => Promise<void>;
  now: () => number;
  maxWaitMs: number;
  startMark: number;
  log: (message: string) => void;
}

async function retrieveBackgroundResponseWithRetry(
  params: RetrieveRetryParams,
): Promise<{ response: OracleResponse; reconnected: boolean }> {
  const { client, responseId, wait, now, maxWaitMs, startMark, log } = params;
  let retries = 0;
  // biome-ignore lint/nursery/noUnnecessaryConditions: intentional retry loop
  while (true) {
    try {
      const next = await client.responses.retrieve(responseId);
      return { response: next, reconnected: retries > 0 };
    } catch (error) {
      const transportError = asRetryableTransportError(error);
      if (!transportError) {
        throw error;
      }
      retries += 1;
      const delay = Math.min(BACKGROUND_RETRY_BASE_MS * 2 ** (retries - 1), BACKGROUND_RETRY_MAX_MS);
      log(chalk.yellow(`${describeTransportError(transportError, maxWaitMs)} Retrying in ${formatElapsed(delay)}...`));
      await wait(delay);
      if (now() - startMark >= maxWaitMs) {
        throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
      }
    }
  }
}

function asRetryableTransportError(error: unknown): OracleTransportError | null {
  if (error instanceof OracleTransportError) {
    return error;
  }
  if (error instanceof APIConnectionError || error instanceof APIConnectionTimeoutError) {
    return toTransportError(error);
  }
  return null;
}


--- src/cli/browserConfig.ts ---
import fs from 'node:fs/promises';
import path from 'node:path';
import type { BrowserSessionConfig } from '../sessionStore.js';
import type { ModelName, ThinkingTimeLevel } from '../oracle.js';
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET, isTemporaryChatUrl, normalizeChatgptUrl, parseDuration } from '../browserMode.js';
import { normalizeBrowserModelStrategy } from '../browser/modelStrategy.js';
import type { BrowserModelStrategy } from '../browser/types.js';
import type { CookieParam } from '../browser/types.js';
import { getOracleHomeDir } from '../oracleHome.js';

const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
const DEFAULT_BROWSER_INPUT_TIMEOUT_MS = 60_000;
const DEFAULT_CHROME_PROFILE = 'Default';

// Ordered array: most specific models first to ensure correct selection.
// The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
const BROWSER_MODEL_LABELS: [ModelName, string][] = [
  // Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
  ['gpt-5.2-thinking', 'GPT-5.2 Thinking'],
  ['gpt-5.2-instant', 'GPT-5.2 Instant'],
  ['gpt-5.2-pro', 'GPT-5.2 Pro'],
  ['gpt-5.1-pro', 'GPT-5.2 Pro'],
  ['gpt-5-pro', 'GPT-5.2 Pro'],
  // Base models last (least specific)
  ['gpt-5.2', 'GPT-5.2'],       // Selects "Auto" in ChatGPT UI
  ['gpt-5.1', 'GPT-5.2'],       // Legacy alias → Auto
  ['gemini-3-pro', 'Gemini 3 Pro'],
];

export interface BrowserFlagOptions {
  browserChromeProfile?: string;
  browserChromePath?: string;
  browserCookiePath?: string;
  chatgptUrl?: string;
  browserUrl?: string;
  browserTimeout?: string;
  browserInputTimeout?: string;
  browserCookieWait?: string;
  browserNoCookieSync?: boolean;
  browserInlineCookiesFile?: string;
  browserCookieNames?: string;
  browserInlineCookies?: string;
  browserHeadless?: boolean;
  browserHideWindow?: boolean;
  browserKeepBrowser?: boolean;
  browserManualLogin?: boolean;
  browserManualLoginProfileDir?: string | null;
  /** Thinking time intensity: 'light', 'standard', 'extended', 'heavy' */
  browserThinkingTime?: ThinkingTimeLevel;
  browserModelLabel?: string;
  browserModelStrategy?: BrowserModelStrategy;
  browserAllowCookieErrors?: boolean;
  remoteChrome?: string;
  browserPort?: number;
  browserDebugPort?: number;
  model: ModelName;
  verbose?: boolean;
}

export function normalizeChatGptModelForBrowser(model: ModelName): ModelName {
  const normalized = model.toLowerCase() as ModelName;
  if (!normalized.startsWith('gpt-') || normalized.includes('codex')) {
    return model;
  }

  // Pro variants: always resolve to the latest Pro model in ChatGPT.
  if (normalized === 'gpt-5-pro' || normalized === 'gpt-5.1-pro' || normalized.endsWith('-pro')) {
    return 'gpt-5.2-pro';
  }

  // Explicit model variants: keep as-is (they have their own browser labels)
  if (normalized === 'gpt-5.2-thinking' || normalized === 'gpt-5.2-instant') {
    return normalized;
  }

  // Legacy aliases: map to base GPT-5.2 (Auto)
  if (normalized === 'gpt-5.1') {
    return 'gpt-5.2';
  }

  return model;
}

export async function buildBrowserConfig(options: BrowserFlagOptions): Promise<BrowserSessionConfig> {
  const desiredModelOverride = options.browserModelLabel?.trim();
  const normalizedOverride = desiredModelOverride?.toLowerCase() ?? '';
  const baseModel = options.model.toLowerCase();
  const isChatGptModel = baseModel.startsWith('gpt-') && !baseModel.includes('codex');
  const shouldUseOverride = !isChatGptModel && normalizedOverride.length > 0 && normalizedOverride !== baseModel;
  const modelStrategy =
    normalizeBrowserModelStrategy(options.browserModelStrategy) ?? DEFAULT_MODEL_STRATEGY;
  const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
  let inline = await resolveInlineCookies({
    inlineArg: options.browserInlineCookies,
    inlineFileArg: options.browserInlineCookiesFile,
    envPayload: process.env.ORACLE_BROWSER_COOKIES_JSON,
    envFile: process.env.ORACLE_BROWSER_COOKIES_FILE,
    cwd: process.cwd(),
  });
  if (inline?.source?.startsWith('home:') && options.browserNoCookieSync !== true) {
    inline = undefined;
  }

  let remoteChrome: { host: string; port: number } | undefined;
  if (options.remoteChrome) {
    remoteChrome = parseRemoteChromeTarget(options.remoteChrome);
  }
  const rawUrl = options.chatgptUrl ?? options.browserUrl;
  const url = rawUrl ? normalizeChatgptUrl(rawUrl, CHATGPT_URL) : undefined;

  const desiredModel = isChatGptModel
    ? mapModelToBrowserLabel(options.model)
    : shouldUseOverride
      ? desiredModelOverride
      : mapModelToBrowserLabel(options.model);

  if (modelStrategy === 'select' && url && isTemporaryChatUrl(url) && /\bpro\b/i.test(desiredModel ?? '')) {
    throw new Error(
      'Temporary Chat mode does not expose Pro models in the ChatGPT model picker. ' +
        'Remove "temporary-chat=true" from --chatgpt-url (or omit --chatgpt-url), or use a non-Pro model (e.g. --model gpt-5.2).',
    );
  }

  return {
    chromeProfile: options.browserChromeProfile ?? DEFAULT_CHROME_PROFILE,
    chromePath: options.browserChromePath ?? null,
    chromeCookiePath: options.browserCookiePath ?? null,
    url,
    debugPort: selectBrowserPort(options),
    timeoutMs: options.browserTimeout ? parseDuration(options.browserTimeout, DEFAULT_BROWSER_TIMEOUT_MS) : undefined,
    inputTimeoutMs: options.browserInputTimeout
      ? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
      : undefined,
    cookieSyncWaitMs: options.browserCookieWait ? parseDuration(options.browserCookieWait, 0) : undefined,
    cookieSync: options.browserNoCookieSync ? false : undefined,
    cookieNames,
    inlineCookies: inline?.cookies,
    inlineCookiesSource: inline?.source ?? null,
    headless: undefined, // disable headless; Cloudflare blocks it
    keepBrowser: options.browserKeepBrowser ? true : undefined,
    manualLogin: options.browserManualLogin === undefined ? undefined : options.browserManualLogin,
    manualLoginProfileDir: options.browserManualLoginProfileDir ?? undefined,
    hideWindow: options.browserHideWindow ? true : undefined,
    desiredModel,
    modelStrategy,
    debug: options.verbose ? true : undefined,
    // Allow cookie failures by default so runs can continue without Chrome/Keychain secrets.
    allowCookieErrors: options.browserAllowCookieErrors ?? true,
    remoteChrome,
    thinkingTime: options.browserThinkingTime,
  };
}

function selectBrowserPort(options: BrowserFlagOptions): number | null {
  const candidate = options.browserPort ?? options.browserDebugPort;
  if (candidate === undefined || candidate === null) return null;
  if (!Number.isFinite(candidate) || candidate <= 0 || candidate > 65_535) {
    throw new Error(`Invalid browser port: ${candidate}. Expected a number between 1 and 65535.`);
  }
  return candidate;
}

export function mapModelToBrowserLabel(model: ModelName): string {
  const normalized = normalizeChatGptModelForBrowser(model);
  // Iterate ordered array to find first match (most specific first)
  for (const [key, label] of BROWSER_MODEL_LABELS) {
    if (key === normalized) {
      return label;
    }
  }
  return DEFAULT_MODEL_TARGET;
}

export function resolveBrowserModelLabel(input: string | undefined, model: ModelName): string {
  const trimmed = input?.trim?.() ?? '';
  if (!trimmed) {
    return mapModelToBrowserLabel(model);
  }
  const normalizedInput = trimmed.toLowerCase();
  if (normalizedInput === model.toLowerCase()) {
    return mapModelToBrowserLabel(model);
  }
  return trimmed;
}

function parseRemoteChromeTarget(raw: string): { host: string; port: number } {
  const target = raw.trim();
  if (!target) {
    throw new Error('Invalid remote-chrome value: expected host:port but received an empty string.');
  }

  const ipv6Match = target.match(/^\[(.+)]:(\d+)$/);
  let host: string | undefined;
  let portSegment: string | undefined;

  if (ipv6Match) {
    host = ipv6Match[1]?.trim();
    portSegment = ipv6Match[2]?.trim();
  } else {
    const lastColon = target.lastIndexOf(':');
    if (lastColon === -1) {
      throw new Error(
        `Invalid remote-chrome format: ${target}. Expected host:port (IPv6 must use [host]:port notation).`
      );
    }
    host = target.slice(0, lastColon).trim();
    portSegment = target.slice(lastColon + 1).trim();
    if (host.includes(':')) {
      throw new Error(
        `Invalid remote-chrome format: ${target}. Wrap IPv6 addresses in brackets, e.g. --remote-chrome "[2001:db8::1]:9222".`
      );
    }
  }

  if (!host) {
    throw new Error(
      `Invalid remote-chrome format: ${target}. Host portion is missing; expected host:port.`
    );
  }
  const port = Number.parseInt(portSegment ?? '', 10);
  if (!Number.isFinite(port) || port <= 0 || port > 65_535) {
    throw new Error(
      `Invalid remote-chrome port: "${portSegment ?? ''}". Expected a number between 1 and 65535.`
    );
  }
  return { host, port };
}

function parseCookieNames(raw?: string | null): string[] | undefined {
  if (!raw) return undefined;
  const names = raw
    .split(',')
    .map((entry) => entry.trim())
    .filter(Boolean);
  return names.length ? names : undefined;
}

async function resolveInlineCookies({
  inlineArg,
  inlineFileArg,
  envPayload,
  envFile,
  cwd,
}: {
  inlineArg?: string | null;
  inlineFileArg?: string | null;
  envPayload?: string | null;
  envFile?: string | null;
  cwd: string;
}): Promise<{ cookies: CookieParam[]; source: string } | undefined> {
  const tryLoad = async (source: string | undefined | null, allowPathResolution: boolean) => {
    if (!source) return undefined;
    const trimmed = source.trim();
    if (!trimmed) return undefined;
    if (allowPathResolution) {
      const resolved = path.isAbsolute(trimmed) ? trimmed : path.join(cwd, trimmed);
      try {
        const stat = await fs.stat(resolved);
        if (stat.isFile()) {
          const fileContent = await fs.readFile(resolved, 'utf8');
          const parsed = parseInlineCookiesPayload(fileContent);
          if (parsed) return parsed;
        }
      } catch {
        // not a file; treat as payload below
      }
    }
    return parseInlineCookiesPayload(trimmed);
  };

  const sources = [
    { value: inlineFileArg, allowPath: true, source: 'inline-file' },
    { value: inlineArg, allowPath: true, source: 'inline-arg' },
    { value: envFile, allowPath: true, source: 'env-file' },
    { value: envPayload, allowPath: false, source: 'env-payload' },
  ];

  for (const { value, allowPath, source } of sources) {
    const parsed = await tryLoad(value, allowPath);
    if (parsed) return { cookies: parsed, source };
  }

  // fallback: ~/.oracle/cookies.{json,base64}
  const oracleHome = getOracleHomeDir();
  const candidates = ['cookies.json', 'cookies.base64'];
  for (const file of candidates) {
    const fullPath = path.join(oracleHome, file);
    try {
      const stat = await fs.stat(fullPath);
      if (!stat.isFile()) continue;
      const content = await fs.readFile(fullPath, 'utf8');
      const parsed = parseInlineCookiesPayload(content);
      if (parsed) return { cookies: parsed, source: `home:${file}` };
    } catch {
      // ignore missing/invalid
    }
  }
  return undefined;
}

function parseInlineCookiesPayload(raw?: string | null): CookieParam[] | undefined {
  if (!raw) return undefined;
  const text = raw.trim();
  if (!text) return undefined;
  let jsonPayload = text;
  // Attempt base64 decode first; fall back to raw text on failure.
  try {
    const decoded = Buffer.from(text, 'base64').toString('utf8');
    if (decoded.trim().startsWith('[')) {
      jsonPayload = decoded;
    }
  } catch {
    // not base64; continue with raw text
  }
  try {
    const parsed = JSON.parse(jsonPayload) as unknown;
    if (Array.isArray(parsed)) {
      return parsed as CookieParam[];
    }
  } catch {
    // invalid json; skip silently to keep this hidden flag non-fatal
  }
  return undefined;
}


--- tests/config.test.ts ---
import { afterAll, beforeEach, describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { loadUserConfig } from '../src/config.js';
import { setOracleHomeDirOverrideForTest } from '../src/oracleHome.js';

describe('loadUserConfig', () => {
  let tempDir: string;

  beforeEach(async () => {
    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-config-'));
    setOracleHomeDirOverrideForTest(tempDir);
  });

  it('parses JSON5 config with comments', async () => {
    const configPath = path.join(tempDir, 'config.json');
    await fs.writeFile(
      configPath,
      `// comment\n{
        engine: "browser",
        notify: { sound: true },
        heartbeatSeconds: 15,
        remote: { host: "host:1234", token: "abc" },
      }`,
      'utf8',
    );

    const result = await loadUserConfig();
    expect(result.loaded).toBe(true);
    expect(result.config.engine).toBe('browser');
    expect(result.config.notify?.sound).toBe(true);
    expect(result.config.heartbeatSeconds).toBe(15);
    expect(result.config.remote?.host).toBe('host:1234');
    expect(result.config.remote?.token).toBe('abc');
  });

  it('supports top-level remoteHost/remoteToken aliases', async () => {
    const configPath = path.join(tempDir, 'config.json');
    await fs.writeFile(
      configPath,
      `{
        remoteHost: "alias:9999",
        remoteToken: "secret"
      }`,
      'utf8',
    );

    const result = await loadUserConfig();
    expect(result.loaded).toBe(true);
    expect(result.config.remoteHost).toBe('alias:9999');
    expect(result.config.remoteToken).toBe('secret');
  });

  it('returns empty config when file is missing', async () => {
    const result = await loadUserConfig();
    expect(result.loaded).toBe(false);
    expect(result.config).toEqual({});
  });

  afterAll(() => {
    setOracleHomeDirOverrideForTest(null);
  });
});


--- tests/engine.test.ts ---
import { describe, expect, it } from 'vitest';
import { resolveEngine, defaultWaitPreference, type EngineMode } from '../src/cli/engine.js';

// biome-ignore lint/style/useNamingConvention: env var names are uppercase with underscores
const envWithKey = { ...process.env, OPENAI_API_KEY: 'sk-test' } as NodeJS.ProcessEnv;
const envWithoutKey = { ...process.env } as NodeJS.ProcessEnv;
delete envWithoutKey.OPENAI_API_KEY;

describe('resolveEngine', () => {
  it('prefers api when no flags and OPENAI_API_KEY is set', () => {
    const engine = resolveEngine({ engine: undefined, browserFlag: false, env: envWithKey });
    expect(engine).toBe<EngineMode>('api');
  });

  it('falls back to browser when no flags and no OPENAI_API_KEY', () => {
    const engine = resolveEngine({ engine: undefined, browserFlag: false, env: envWithoutKey });
    expect(engine).toBe<EngineMode>('browser');
  });

  it('respects explicit --engine api even without OPENAI_API_KEY', () => {
    const engine = resolveEngine({ engine: 'api', browserFlag: false, env: envWithoutKey });
    expect(engine).toBe<EngineMode>('api');
  });

  it('lets legacy --browser override everything', () => {
    const engine = resolveEngine({ engine: 'api', browserFlag: true, env: envWithKey });
    expect(engine).toBe<EngineMode>('browser');
  });
});

describe('defaultWaitPreference', () => {
  it('disables wait for pro API runs', () => {
    expect(defaultWaitPreference('gpt-5.2-pro', 'api')).toBe(false);
  });

  it('keeps wait enabled for Codex and browser models', () => {
    expect(defaultWaitPreference('gpt-5.1-codex', 'api')).toBe(true);
    expect(defaultWaitPreference('gpt-5.2-pro', 'browser')).toBe(true);
  });
});


--- tests/gemini.test.ts ---
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createGeminiClient, resolveGeminiModelId } from '../src/oracle/gemini.js';
import type { OracleRequestBody } from '../src/oracle.js';
import { GoogleGenAI } from '@google/genai';

const { mockGenerateContent, mockGenerateContentStream } = vi.hoisted(() => {
  const mockGenerateContent = vi.fn();
  const mockGenerateContentStream = vi.fn();
  return { mockGenerateContent, mockGenerateContentStream };
});

vi.mock('@google/genai', () => {
  const MOCK_GOOGLE_GENAI = vi.fn().mockImplementation(function GoogleGenAIMock() {
    return {
      models: {
        generateContent: mockGenerateContent,
        generateContentStream: mockGenerateContentStream,
      },
    };
  });

  return {
    // biome-ignore lint/style/useNamingConvention: keep SDK casing
    GoogleGenAI: MOCK_GOOGLE_GENAI,
    // biome-ignore lint/style/useNamingConvention: keep SDK casing
    HarmCategory: {
      // biome-ignore lint/style/useNamingConvention: keep SDK casing
      HARM_CATEGORY_HARASSMENT: 'HARM_CATEGORY_HARASSMENT',
      // biome-ignore lint/style/useNamingConvention: keep SDK casing
      HARM_CATEGORY_HATE_SPEECH: 'HARM_CATEGORY_HATE_SPEECH',
      // biome-ignore lint/style/useNamingConvention: keep SDK casing
      HARM_CATEGORY_SEXUALLY_EXPLICIT: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
      // biome-ignore lint/style/useNamingConvention: keep SDK casing
      HARM_CATEGORY_DANGEROUS_CONTENT: 'HARM_CATEGORY_DANGEROUS_CONTENT',
    },
    // biome-ignore lint/style/useNamingConvention: keep SDK casing
    HarmBlockThreshold: {
      // biome-ignore lint/style/useNamingConvention: keep SDK casing
      BLOCK_NONE: 'BLOCK_NONE',
    },
  };
});

describe('Gemini Client', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('initializes with the correct model', () => {
    createGeminiClient('fake-key');
    expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: 'fake-key' });
    expect(mockGenerateContent).not.toHaveBeenCalled();
  });

  it('maps 3-pro through resolver', () => {
    expect(resolveGeminiModelId('gemini-3-pro')).toBe('gemini-3-pro-preview');
  });

  it('adapts create request correctly', async () => {
    const client = createGeminiClient('fake-key');
    const mockResponse = {
      candidates: [
        {
          content: {
            parts: [{ text: 'Gemini response' }],
          },
        },
      ],
      usageMetadata: {
        promptTokenCount: 10,
        candidatesTokenCount: 20,
      },
    };
    mockGenerateContent.mockResolvedValue(mockResponse);

    const requestBody: OracleRequestBody = {
      model: 'gemini-3-pro',
      instructions: 'System prompt',
      input: [
        {
          role: 'user',
          content: [{ type: 'input_text', text: 'User prompt' }],
        },
      ],
      max_output_tokens: 100,
    };

    const result = await client.responses.create(requestBody);

    expect(mockGenerateContent).toHaveBeenCalledWith({
      model: 'gemini-3-pro-preview',
      contents: [
        {
          role: 'user',
          parts: [{ text: 'User prompt' }],
        },
      ],
      config: {
        systemInstruction: { role: 'system', parts: [{ text: 'System prompt' }] },
        tools: undefined,
        maxOutputTokens: 100,
        safetySettings: expect.any(Array),
      },
    });

    expect(result).toEqual({
      id: expect.stringMatching(/^gemini-/),
      status: 'completed',
      output_text: ['Gemini response'],
      output: [{ type: 'text', text: 'Gemini response' }],
      usage: {
        input_tokens: 10,
        output_tokens: 20,
        total_tokens: 30,
      },
    });
  });

  it('adapts streaming request correctly', async () => {
    const client = createGeminiClient('fake-key');
    
    const mockStream = async function* () {
      yield { text: 'Chunk 1 A' };
      yield { text: 'Chunk 2', usageMetadata: { promptTokenCount: 5, candidatesTokenCount: 5 }, responseId: 'resp-123' };
    };

    mockGenerateContentStream.mockResolvedValue(mockStream());

    const requestBody: OracleRequestBody = {
      model: 'gemini-3-pro',
      instructions: 'System',
      input: [
        {
          role: 'user',
          content: [{ type: 'input_text', text: 'Stream me' }],
        },
      ],
      tools: [{ type: 'web_search_preview' }],
    };

    const stream = await client.responses.stream(requestBody);
    const chunks: string[] = [];

    for await (const event of stream) {
      if (event.type === 'chunk' && event.delta) {
        chunks.push(event.delta);
      }
    }

    expect(chunks).toEqual(['Chunk 1 A', 'Chunk 2']);

    expect(mockGenerateContentStream).toHaveBeenCalledWith(expect.objectContaining({
      config: expect.objectContaining({ tools: [{ googleSearch: {} }] }),
    }));
    
    const final = await stream.finalResponse();
    expect(final).toMatchObject({
      id: 'resp-123',
      usage: {
        input_tokens: 5,
        output_tokens: 5,
        total_tokens: 10,
      },
      output_text: ['Chunk 1 AChunk 2'],
    });
  });

  it('maps web_search_preview to googleSearch tool and keeps safety settings', async () => {
    const client = createGeminiClient('fake-key');
    mockGenerateContent.mockResolvedValue({
      candidates: [],
      usageMetadata: {},
    });

    const requestBody: OracleRequestBody = {
      model: 'gemini-3-pro',
      instructions: '',
      input: [
        {
          role: 'user',
          content: [{ type: 'input_text', text: 'search please' }],
        },
      ],
      tools: [{ type: 'web_search_preview' }],
    };

    await client.responses.create(requestBody);

    const call = mockGenerateContent.mock.calls[0]?.[0];
    expect(call?.config?.tools).toEqual([{ googleSearch: {} }]);
    expect(call?.config?.safetySettings).toEqual([
      { category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_NONE' },
      { category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_NONE' },
      { category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_NONE' },
      { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_NONE' },
    ]);
  });

  it('prefers explicitly resolved model id when provided', async () => {
    const client = createGeminiClient('fake-key', 'gemini-3-pro', 'custom-model-id');
    mockGenerateContent.mockResolvedValue({ candidates: [], usageMetadata: {} });

    await client.responses.create({
      model: 'gemini-3-pro',
      instructions: '',
      input: [
        {
          role: 'user',
          content: [{ type: 'input_text', text: 'hi' }],
        },
      ],
    });

    expect(mockGenerateContent).toHaveBeenCalledWith(
      expect.objectContaining({ model: 'custom-model-id' }),
    );
  });

  it('returns finalResponse even when not iterated', async () => {
    const client = createGeminiClient('fake-key');

    const mockStream = async function* () {
      yield { text: 'Only chunk', responseId: 'resp-999', usageMetadata: { promptTokenCount: 2, candidatesTokenCount: 3 } };
    };

    mockGenerateContentStream.mockResolvedValue(mockStream());

    const requestBody: OracleRequestBody = {
      model: 'gemini-3-pro',
      instructions: '',
      input: [
        {
          role: 'user',
          content: [{ type: 'input_text', text: 'Ping' }],
        },
      ],
    };

    const stream = await client.responses.stream(requestBody);
    const final = await stream.finalResponse();

    expect(final).toEqual({
      id: 'resp-999',
      status: 'completed',
      output_text: ['Only chunk'],
      output: [{ type: 'text', text: 'Only chunk' }],
      usage: { input_tokens: 2, output_tokens: 3, total_tokens: 5 },
    });
  });

  it('includes system prompt even when empty tools array is provided', async () => {
    const client = createGeminiClient('fake-key');
    mockGenerateContent.mockResolvedValue({
      response: {
        candidates: [{ content: { parts: [{ text: 'ok' }] } }],
        usageMetadata: {},
      },
    });

    const requestBody: OracleRequestBody = {
      model: 'gemini-3-pro',
      instructions: 'Sys',
      input: [
        {
          role: 'user',
          content: [{ type: 'input_text', text: 'Ping' }],
        },
      ],
      tools: [],
    };

    await client.responses.create(requestBody);
    expect(mockGenerateContent.mock.calls[0]?.[0]).toMatchObject({
      config: { systemInstruction: { role: 'system', parts: [{ text: 'Sys' }] }, tools: [] },
    });
  });
});


--- tests/logging.test.ts ---
import { describe, expect, test } from 'vitest';
import { formatBaseUrlForLog, maskApiKey } from '../src/oracle/logging.js';

describe('maskApiKey', () => {
  test('masks long keys with first/last 4 chars', () => {
    expect(maskApiKey('sk-abcdef1234567890')).toBe('sk-a****7890');
  });

  test('handles short keys gracefully', () => {
    expect(maskApiKey('abc')).toBe('a***c');
    expect(maskApiKey(undefined)).toBeNull();
    expect(maskApiKey(null)).toBeNull();
  });
});

describe('formatBaseUrlForLog', () => {
  test('redacts credentials, deep paths, and query values', () => {
    const formatted = formatBaseUrlForLog(
      'https://user:pass@proxy.test/v1/deep/path?api-version=2024-10-01&token=secret',
    );
    expect(formatted).toBe('https://proxy.test/v1/...?api-version=***');
  });

  test('truncates unparseable strings', () => {
    const formatted = formatBaseUrlForLog('not a url but extremely long'.repeat(4));
    expect(formatted.startsWith('not a url but extremely longnot ')).toBe(true);
    expect(formatted.includes('…')).toBe(true);
    expect(formatted.endsWith('ely long')).toBe(true);
    expect(formatted.length).toBeLessThan(80);
  });
});


--- tests/mcp.integration.test.ts ---
import { beforeAll, afterAll, describe, expect, it } from 'vitest';
import { spawn } from 'node:child_process';
import path from 'node:path';
import { once } from 'node:events';

function startOracleMcp(): { proc: ReturnType<typeof spawn>; waitReady: () => Promise<void> } {
  const entry = path.join(process.cwd(), 'dist/bin/oracle-mcp.js');
  const proc = spawn(process.execPath, [entry], { stdio: ['pipe', 'pipe', 'pipe'] });
  const waitReady = async () => {
    // Give the stdio transport a moment to attach; MCP stdio has no explicit ready signal.
    await new Promise((resolve) => setTimeout(resolve, 200));
  };
  return { proc, waitReady };
}

describe('oracle-mcp stdio smoke', () => {
  let proc: ReturnType<typeof spawn>;

  beforeAll(async () => {
    // @ts-expect-error built artifact has no d.ts
    await import('../dist/bin/oracle-mcp.js'); // ensure built artifacts exist
    const started = startOracleMcp();
    proc = started.proc;
    await started.waitReady();
  }, 30_000);

  afterAll(async () => {
    if (proc) {
      const exitPromise = once(proc, 'exit');
      proc.kill('SIGTERM');
      await Promise.race([exitPromise, new Promise((resolve) => setTimeout(resolve, 1000))]);
    }
  });

  it('exposes stdio (process stays alive)', () => {
    expect(proc.killed).toBe(false);
    expect(proc.pid).toBeDefined();
  });
});


--- tests/mcp.resources.test.ts ---
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import path from 'node:path';

async function callResource(proc: ReturnType<typeof spawn>, method: string, id: number, params: object) {
  if (!proc.stdin || !proc.stdout) {
    throw new Error('stdio unavailable');
  }
  // Simple one-request/one-response framing over stdio JSON-RPC.
  const req = `${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`;
  proc.stdin.write(req);
  const [data] = (await once(proc.stdout, 'data')) as [Buffer];
  const parsed = JSON.parse(data.toString());
  return parsed;
}

describe('oracle-session resources via stdio', () => {
  let proc: ReturnType<typeof spawn>;

  beforeAll(async () => {
    const entry = path.join(process.cwd(), 'dist/bin/oracle-mcp.js');
    proc = spawn(process.execPath, [entry], { stdio: ['pipe', 'pipe', 'pipe'] });
    await new Promise((resolve) => setTimeout(resolve, 200)); // give stdio transport time
  }, 10_000);

  afterAll(async () => {
    if (proc) {
      proc.kill('SIGTERM');
      await Promise.race([once(proc, 'exit'), new Promise((r) => setTimeout(r, 500))]);
    }
  });

  it('responds to resource/read (metadata)', async () => {
    const res = await callResource(proc, 'resource/read', 1, { uri: 'oracle-session://nonexistent/metadata' });
    expect(res.error?.message || res.result?.contents).toBeDefined();
  }, 15_000);
});


--- tests/mcp.schema.test.ts ---
import { beforeAll, afterAll, describe, expect, it } from 'vitest';
import path from 'node:path';
import type { ChildProcess } from 'node:child_process';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

const entry = path.join(process.cwd(), 'dist/bin/oracle-mcp.js');

describe('oracle-mcp schemas', () => {
  let client: Client | null = null;
  let transport: StdioClientTransport | null = null;
  const stderrLog: string[] = [];
  const exitLog: string[] = [];
  const parsedMessages: unknown[] = [];
  const parseErrors: string[] = [];
  let stdoutRemainder = '';

  // Keep the daemon quiet and skip optional native deps in CI.
  process.env.ORACLE_DISABLE_KEYTAR = '1';

  const attachStderr = (proc: ChildProcess | undefined): void => {
    proc?.stderr?.on('data', (chunk) => stderrLog.push(String(chunk)));
    proc?.stdout?.on('data', (chunk) => {
      exitLog.push(`stdout: ${String(chunk)}`);
      stdoutRemainder += String(chunk);
      const lines = stdoutRemainder.split('\n');
      stdoutRemainder = lines.pop() ?? '';
      for (const line of lines) {
        if (!line.trim()) continue;
        try {
          const msg = JSON.parse(line);
          parsedMessages.push(msg);
        } catch (error) {
          parseErrors.push(`parse error for line "${line}": ${String(error)}`);
        }
      }
    });
    proc?.on('exit', (code, signal) => exitLog.push(`exit ${code ?? 'null'} signal ${signal ?? 'null'}`));
    proc?.on('error', (err) => exitLog.push(`error ${String(err)}`));
  };

  beforeAll(async () => {
    let lastError: unknown;
    for (let attempt = 0; attempt < 5; attempt += 1) {
      const candidateClient = new Client({ name: 'schema-smoke', version: '0.0.0' });
      const candidateTransport = new StdioClientTransport({
        command: process.execPath,
        args: [entry],
        stderr: 'pipe',
        cwd: path.dirname(entry),
        env: {
          ...process.env,
          // biome-ignore lint/style/useNamingConvention: environment variables stay upper snake case
          'ORACLE_DISABLE_KEYTAR': '1',
        },
      });
      try {
        await candidateClient.connect(candidateTransport);
        const proc = (candidateTransport as unknown as { proc?: ChildProcess }).proc;
        attachStderr(proc);
        client = candidateClient;
        transport = candidateTransport;
        return;
      } catch (error) {
        lastError = error;
        const proc = (candidateTransport as unknown as { proc?: ChildProcess }).proc;
        attachStderr(proc);
        proc?.kill?.('SIGKILL');
        await candidateClient.close().catch(() => {});
        await new Promise((resolve) => setTimeout(resolve, 500));
      }
    }
    const detail = [...stderrLog, ...exitLog].join('') || String(lastError);
    throw new Error(`oracle-mcp failed to start: ${detail}`);
  }, 20_000);

  afterAll(async () => {
    await client?.close().catch(() => {});
    const proc = (transport as unknown as { proc?: ChildProcess })?.proc;
    proc?.kill?.('SIGKILL');
  });

  it('exposes object schemas for tools', async () => {
    if (!client) throw new Error('MCP client not connected');
    const { tools } = await client.listTools({}, { timeout: 10_000 });
    expect(tools.length).toBeGreaterThan(0);
    for (const tool of tools) {
      for (const schema of [tool.inputSchema, tool.outputSchema]) {
        if (!schema) continue;
        expect(schema.type).toBe('object');
      }
    }
  });

  it('emits only JSON-RPC lines on stdout during handshake', async () => {
    // Give the transport a moment to flush handshake traffic.
    await new Promise((resolve) => setTimeout(resolve, 200));
    expect(parseErrors).toHaveLength(0);
    if (parsedMessages.length > 0) {
      for (const msg of parsedMessages) {
        expect((msg as { jsonrpc?: unknown }).jsonrpc).toBe('2.0');
      }
    }
    expect(stdoutRemainder.trim()).toBe('');
  });
});


--- tests/mcp.stdout.test.ts ---
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { spawn } from 'node:child_process';
import path from 'node:path';
import { once } from 'node:events';

describe('oracle-mcp stdout hygiene', () => {
  let proc: ReturnType<typeof spawn>;
  const stdoutChunks: string[] = [];

  beforeAll(async () => {
    const entry = path.join(process.cwd(), 'dist/bin/oracle-mcp.js');
    proc = spawn(process.execPath, [entry], { stdio: ['pipe', 'pipe', 'pipe'] });
    proc.stdout?.on('data', (chunk) => stdoutChunks.push(String(chunk)));
    // give the process time to start; we deliberately do not send any input
    await new Promise((resolve) => setTimeout(resolve, 300));
  }, 10_000);

  afterAll(async () => {
    if (proc) {
      proc.kill('SIGTERM');
      await Promise.race([once(proc, 'exit'), new Promise((r) => setTimeout(r, 500))]);
    }
  });

  it('does not emit non-JSON noise on startup', () => {
    const combined = stdoutChunks.join('').trim();
    expect(combined).toBe('');
  });
});


--- tests/mcp.test.ts ---
import { describe, it, expect } from 'vitest';
import { mapConsultToRunOptions } from '../src/mcp/utils.js';

describe('mcp utils', () => {
  it('maps api defaults', () => {
    const { runOptions, resolvedEngine } = mapConsultToRunOptions({ prompt: 'hi', files: [], model: 'gpt-5.2-pro', engine: 'api' });
    expect(resolvedEngine).toBe('api');
    expect(runOptions.model).toBe('gpt-5.2-pro');
  });

  it('infers browser labels', () => {
    const { runOptions, resolvedEngine } = mapConsultToRunOptions({ prompt: 'hi', files: [], model: '5.1 instant', engine: 'browser' });
    expect(resolvedEngine).toBe('browser');
    expect(runOptions.model).toBe('gpt-5.2');
  });
});


--- tests/notifier.test.ts ---
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { resolveNotificationSettings, testHelpers, sendSessionNotification } from '../src/cli/notifier.js';
import type { NotificationContent, NotificationSettings } from '../src/cli/notifier.js';

vi.mock('toasted-notifier', () => ({ default: { notify: vi.fn(async () => undefined) } }));
vi.mock('node:child_process', () => ({
  spawn: vi.fn(() => ({
    on: (event: string, cb: (exit?: number) => void) => {
      if (event === 'exit') cb(0);
    },
  })),
}));

const baseEnv = { ...process.env };

async function getMocks() {
  const notifier = (await import('toasted-notifier')).default as { notify: ReturnType<typeof vi.fn> };
  const { spawn } = await import('node:child_process');
  return { notifier, spawn: spawn as unknown as ReturnType<typeof vi.fn> };
}

beforeEach(async () => {
  const { notifier, spawn } = await getMocks();
  notifier.notify.mockClear();
  spawn.mockClear?.();
});

afterEach(() => {
  vi.restoreAllMocks();
  for (const key of Object.keys(process.env)) {
    if (!(key in baseEnv)) {
      delete process.env[key];
    }
  }
  Object.assign(process.env, baseEnv);
});

describe('resolveNotificationSettings', () => {
  it('defaults to enabled when not in CI or SSH', () => {
    const result = resolveNotificationSettings({ cliNotify: undefined, cliNotifySound: undefined, env: {} });
    expect(result.enabled).toBe(true);
    expect(result.sound).toBe(false);
  });

  it('disables by default in CI', () => {
    // biome-ignore lint/style/useNamingConvention: environment variable name
    const result = resolveNotificationSettings({ cliNotify: undefined, cliNotifySound: undefined, env: { CI: '1' } });
    expect(result.enabled).toBe(false);
  });

  it('honors explicit CLI override', () => {
    // biome-ignore lint/style/useNamingConvention: environment variable name
    const result = resolveNotificationSettings({ cliNotify: true, cliNotifySound: true, env: { CI: '1' } });
    expect(result.enabled).toBe(true);
    expect(result.sound).toBe(true);
  });

  it('parses env toggles', () => {
    // biome-ignore lint/style/useNamingConvention: environment variable name
    const result = resolveNotificationSettings({ cliNotify: undefined, cliNotifySound: undefined, env: { ORACLE_NOTIFY: 'off' } });
    expect(result.enabled).toBe(false);
  });

  it('sanitizes and truncates previews to 200 characters', () => {
    const longPreview = `\`code\` ${'a'.repeat(300)}`;
    const sanitized = testHelpers.sanitizePreview(longPreview);
    expect(sanitized).toBeDefined();
    expect(sanitized?.length).toBe(200);
    expect(sanitized?.includes('code')).toBe(true);
    expect(sanitized?.endsWith('…')).toBe(true);
  });

  it('sends notifications in non-test envs and sanitizes output', async () => {
    // Allow notifications by clearing test env markers.
    delete process.env.VITEST;
    delete process.env.VITEST_WORKER_ID;
    delete process.env.JEST_WORKER_ID;
    process.env.NODE_ENV = 'development';
    process.env.ORACLE_DISABLE_NOTIFICATIONS = '0';
    vi.spyOn(process, 'platform', 'get').mockReturnValue('linux' as NodeJS.Platform);

    const payload: NotificationContent = {
      sessionId: 'sess-1',
      sessionName: 'demo run',
      mode: 'api',
      model: 'gpt-5.1',
      usage: { inputTokens: 1000, outputTokens: 500 },
      characters: 1500,
    };
    const settings: NotificationSettings = { enabled: true, sound: false };
    const log = vi.fn();

    await sendSessionNotification(payload, settings, log, 'Preview **bold** with `code` and a [link](https://x.test)');

    const { notifier } = await getMocks();
    expect(notifier.notify).toHaveBeenCalledTimes(1);
    const call = notifier.notify.mock.calls[0]?.[0];
    expect(call?.title).toContain('Oracle');
    expect(call?.message).toContain('demo run');
    expect(call?.message).toContain('chars');
    expect(call?.message).not.toContain('**');
    expect(call?.message).not.toContain('`code`');
    expect(call?.sound).toBe(false);
    expect(log).not.toHaveBeenCalled();
  });
});


## Links discovered
- [link](https://x.test)

--- types/oracle.d.ts ---
declare module '../src/oracle.js' {
  export function buildPrompt(...args: unknown[]): string;
  export function runOracle(...args: unknown[]): Promise<any>;
  export function renderPromptMarkdown(...args: unknown[]): Promise<string>;
}


--- types/pty.d.ts ---
declare module '@cdktf/node-pty-prebuilt-multiarch';
declare module '@homebridge/node-pty-prebuilt-multiarch';


--- types/toasted-notifier.d.ts ---
declare module 'toasted-notifier' {
  export interface ToastOptions {
    title?: string;
    subtitle?: string;
    message: string;
    sound?: boolean | string;
    icon?: string;
    wait?: boolean;
    appID?: string;
    timeout?: number;
    closeLabel?: string;
    actions?: string[];
    reply?: boolean;
    open?: string;
    id?: string;
    suppressOSD?: boolean;
  }

  const notifier: {
    notify(options: ToastOptions): Promise<void> | void;
  };

  export default notifier;
}


## Links discovered
- [docs/chromium-forks.md](https://github.com/AcidicSoil/oraclepack/blob/main/docs/oracle/chromium-forks.md)
- [docs/chromium-forks.md](https://github.com/steipete/oracle/blob/main/docs/chromium-forks.md)
- [Google AI Studio](https://aistudio.google.com/)
- [docs/linux.md](https://github.com/AcidicSoil/oraclepack/blob/main/docs/oracle/docs/linux.md)
- [docs/chromium-forks.md](https://github.com/AcidicSoil/oraclepack/blob/main/docs/oracle/docs/chromium-forks.md)
- [docs/linux.md](https://github.com/steipete/oracle/blob/main/docs/linux.md)
- [steipete/mcporter](https://github.com/steipete/mcporter)
- [docs/mcp.md](https://github.com/AcidicSoil/oraclepack/blob/main/docs/oracle/docs/mcp.md)
- [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)
- [docs/configuration.md](https://github.com/AcidicSoil/oraclepack/blob/main/docs/oracle/docs/configuration.md)
- [docs/browser-mode.md](https://github.com/AcidicSoil/oraclepack/blob/main/docs/oracle/docs/browser-mode.md)
- [docs/openai-endpoints.md](https://github.com/AcidicSoil/oraclepack/blob/main/docs/oracle/docs/openai-endpoints.md)
- [docs/openrouter.md](https://github.com/AcidicSoil/oraclepack/blob/main/docs/oracle/docs/openrouter.md)
- [docs/manual-tests.md](https://github.com/AcidicSoil/oraclepack/blob/main/docs/oracle/docs/manual-tests.md)
- [docs/testing.md](https://github.com/AcidicSoil/oraclepack/blob/main/docs/oracle/docs/testing.md)
- [Trimmy](https://trimmy.app)
- [CodexBar](https://codexbar.app)
- [MCPorter](https://mcporter.dev)
- [docs/mcp.md](https://github.com/steipete/oracle/blob/main/docs/mcp.md)
- [docs/configuration.md](https://github.com/steipete/oracle/blob/main/docs/configuration.md)
- [docs/browser-mode.md](https://github.com/steipete/oracle/blob/main/docs/browser-mode.md)
- [docs/openai-endpoints.md](https://github.com/steipete/oracle/blob/main/docs/openai-endpoints.md)
- [docs/openrouter.md](https://github.com/steipete/oracle/blob/main/docs/openrouter.md)
- [docs/manual-tests.md](https://github.com/steipete/oracle/blob/main/docs/manual-tests.md)
- [docs/testing.md](https://github.com/steipete/oracle/blob/main/docs/testing.md)
- [<img src="https://img.shields.io/npm/v/@steipete/oracle?style=for-the-badge&logo=npm&logoColor=white" alt="npm version">](https://www.npmjs.com/package/@steipete/oracle)
- [<img src="https://img.shields.io/github/actions/workflow/status/steipete/oracle/ci.yml?branch=main&style=for-the-badge&label=tests" alt="CI Status">](https://github.com/steipete/oracle/actions/workflows/ci.yml)
- [<img src="https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20Windows-blue?style=for-the-badge" alt="Platforms">](https://github.com/steipete/oracle)
- [<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License">](https://github.com/steipete/oracle/blob/main/LICENSE.md)
- [link](https://x.test)
- [<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License">](https://github.com/AcidicSoil/oraclepack/blob/main/docs/oracle/LICENSE.md)

--- .taskmaster/docs/PRD.md ---
## 1) Overview

### Problem

You have “oracle question packs” stored as Markdown that contain a single fenced `bash` block with:

* a **prelude** (e.g., `set -euo pipefail`, `out_dir=...`, `mkdir -p ...`)
* **numbered steps** `# 01)` … `# 20)` that each run an `oracle ...` invocation

Today you can run these with an interactive Bash script, but you want a **polished, first-class CLI/TUI** built with Charm’s ecosystem.

### Target users

* Engineers / operators running packs repeatedly, wanting safe confirm-before-run, logs, resume, and good terminal UX.
* Maintainers who want pack validation and predictable behavior across machines/CI.

### Success metrics

* **Safety**: zero accidental executions (every step is explicitly confirmed unless `--yes` is set).
* **Reliability**: deterministic step parsing; clear non-zero exit codes on failure.
* **Observability**: each step yields a timestamped log file + a machine-readable run summary (JSON).
* **Usability**: fast navigation/filtering, preview of command and `--write-output`, resume after quit/crash.

### Constraints / assumptions

* Pack format is “one ` ```bash ` fence + numbered `# NN)` headers” as in your current pack output .
* `oracle` is installed and available in `PATH`, or provided via `--oracle-bin`.
* Default shell is `bash`; Windows support may require WSL/Git Bash or using `--shell` explicitly.

### “How hard is it?”

* **Low** effort for a “nice enough” wrapper using **gum** prompts around your existing script. Gum is explicitly designed to add “glamorous” prompts to shell scripts without writing Go. ([GitHub][1])
* **Moderate** effort for a **production-grade compiled CLI** with a full-screen TUI using **Bubble Tea** + **Bubbles** + **Lip Gloss**, with robust parsing, streaming logs, resume/state, and packaging polish. Bubble Tea is a Go TUI framework (Elm-style architecture) suited for full-window apps. ([GitHub][2])

**This PRD specifies the “most polished” option (compiled Go CLI + Bubble Tea TUI).**

---

## 2) Capability Tree (Functional Decomposition)

### Capability: Pack ingestion

#### Feature: Load pack from Markdown (MVP)

* **Description**: Read a Markdown pack file and extract the first ` ```bash ` fenced block.
* **Inputs**: `pack_path`
* **Outputs**: `bash_block_text`
* **Behavior**: Find the first matching fence start/end; error if missing/empty.

#### Feature: Parse steps + prelude (MVP)

* **Description**: Split extracted bash block into prelude and ordered step blocks based on `# NN)` headers.
* **Inputs**: `bash_block_text`
* **Outputs**: `Prelude`, `[]Step{number,title,body}`
* **Behavior**: Everything before first `# NN)` is prelude; each subsequent header starts a new step.

#### Feature: Detect derived metadata (MVP)

* **Description**: Best-effort parse of `out_dir="..."` (prelude) and `--write-output "..."` (step body).
* **Inputs**: `Prelude`, `Step.body`
* **Outputs**: `Pack.outDir?`, `Step.writeOutput?`
* **Behavior**: Regex-based extraction; never blocks execution if missing.

#### Feature: Validate pack structure (MVP)

* **Description**: Validate required structure and produce actionable errors.
* **Inputs**: `Pack`
* **Outputs**: `[]ValidationIssue`
* **Behavior**: Enforce “>=1 step”, step numbers are two-digit, no duplicates; warn on missing `--write-output`.

---

### Capability: Execution orchestration

#### Feature: Execute prelude once per run (MVP)

* **Description**: Run prelude in the chosen shell before running step bodies.
* **Inputs**: `Prelude`, `shell`, `workdir`, `env`
* **Outputs**: `PreludeResult`
* **Behavior**: If prelude fails, stop and mark run failed.

#### Feature: Execute a single step with streaming logs (MVP)

* **Description**: Run one step in a subprocess and capture stdout/stderr lines.
* **Inputs**: `Step`, `shell`, `workdir`, `env`
* **Outputs**: `StepResult{exitCode, startedAt, endedAt, logPath}`
* **Behavior**: Stream lines to UI/console and persist to a log file.

#### Feature: Inject flags into oracle invocation (MVP)

* **Description**: Optionally inject user-provided flags after `oracle` in each step.
* **Inputs**: `Step.body`, `injectFlags`
* **Outputs**: transformed `Step.body`
* **Behavior**: Rewrite only lines beginning with `oracle` (conservative, predictable).

#### Feature: Stop/continue policy on failures (MVP)

* **Description**: Define behavior when a step fails.
* **Inputs**: `StepResult`, `policy`
* **Outputs**: next action (stop / prompt / continue)
* **Behavior**: Default: stop and require explicit user action to continue.

---

### Capability: User interaction (TUI + CLI)

#### Feature: Full-screen TUI step list (MVP)

* **Description**: Show steps with status (Pending/Running/OK/Failed/Skipped) and metadata.
* **Inputs**: `Pack`, `RunState`
* **Outputs**: terminal UI
* **Behavior**: Navigate, filter, select; show `--write-output` and command preview.

#### Feature: Confirm-before-run per step (MVP)

* **Description**: Require confirmation before executing a step (unless `--yes`).
* **Inputs**: selected `Step`, `mode`
* **Outputs**: run / skip decision
* **Behavior**: Prompt with clear consequences and output path.

#### Feature: Plain mode (no TUI) (MVP)

* **Description**: Provide `--no-tui` mode that prompts in-line (or runs with `--yes`).
* **Inputs**: same as TUI
* **Outputs**: console output
* **Behavior**: Deterministic text output and exit codes.

#### Feature: Markdown rendering in preview (Non-MVP)

* **Description**: Render relevant Markdown (e.g., step prompt text) in a styled terminal view.
* **Inputs**: markdown snippet
* **Outputs**: ANSI-rendered text
* **Behavior**: Use Glamour for stylesheet-based terminal Markdown rendering. ([GitHub][3])

---

### Capability: State, resume, and reporting

#### Feature: Persist run state (MVP)

* **Description**: Save step statuses and pointers so a run can be resumed.
* **Inputs**: `RunState`
* **Outputs**: `state.json` in a deterministic directory
* **Behavior**: Atomic writes; load on startup when `--resume` is set.

#### Feature: Machine-readable summary report (MVP)

* **Description**: Write `run.json` with per-step results and log paths.
* **Inputs**: `RunState`, `Pack`
* **Outputs**: JSON file
* **Behavior**: Stable schema version; include environment metadata (shell, cwd, start time).

---

### Capability: Packaging and operational polish

#### Feature: Shell completions + man/help text (Non-MVP)

* **Description**: Provide completions and high-quality help UX.
* **Inputs**: CLI command model
* **Outputs**: completion scripts, man page
* **Behavior**: Generated during release; consistent flag naming.

#### Feature: Release automation (Non-MVP)

* **Description**: Produce multi-platform binaries and package manifests (brew/scoop).
* **Inputs**: tags/releases
* **Outputs**: release artifacts
* **Behavior**: Use GoReleaser capabilities for Homebrew/Scoop publishing. ([GoReleaser][4])

---

## 3) Repository Structure + Module Definitions (Structural Decomposition)

### Proposed repository layout

```
orpack/
  cmd/orpack/
    main.go
  internal/
    app/            # application composition (wiring)
    cli/            # command model, flags, help text
    pack/           # markdown fence extraction + step parsing + validation
    exec/           # subprocess execution + streaming + log persistence
    state/          # run state model + persistence
    report/         # run summary JSON schema + writer
    tui/            # Bubble Tea model/view; Bubbles components usage
    render/         # markdown-to-ANSI rendering (Glamour)
    errors/         # typed errors + exit codes
```

### Module: `internal/pack`

* **Responsibility**: Parse pack Markdown into `Pack{Prelude, Steps}` and validate format.
* **Exports**:

  * `Load(path string) (Pack, error)`
  * `Validate(p Pack) []Issue`
  * `DeriveMetadata(p *Pack)` (fills `outDir`, `writeOutput` best-effort)

### Module: `internal/exec`

* **Responsibility**: Execute prelude/steps in a shell; stream output; write log files.
* **Exports**:

  * `Runner{Shell, WorkDir, Env, InjectFlags}`
  * `RunPrelude(ctx, prelude string) (Result, error)`
  * `RunStep(ctx, prelude string, step Step, logDir string, onLine func(Line)) (Result, error)`

### Module: `internal/state`

* **Responsibility**: Track statuses and persist/restore run state.
* **Exports**:

  * `RunState` (pack hash, start time, per-step status/result)
  * `LoadState(path string) (RunState, error)`
  * `SaveStateAtomic(path string, s RunState) error`

### Module: `internal/report`

* **Responsibility**: Write stable JSON summary for automation/CI.
* **Exports**:

  * `ReportV1`
  * `WriteReport(path string, r ReportV1) error`

### Module: `internal/tui`

* **Responsibility**: Full-screen terminal UX using Bubble Tea + Bubbles + Lip Gloss.
* **Exports**:

  * `Run(p Pack, runner *exec.Runner, opts Options) error`
* **Notes**: Bubble Tea provides the stateful TUI architecture; Bubbles provides ready components; Lip Gloss provides styling/layout. ([GitHub][2])

### Module: `internal/render`

* **Responsibility**: Render Markdown snippets to ANSI for previews/help panes.
* **Exports**:

  * `RenderMarkdown(md string, style Style) (string, error)`
* **Notes**: Use Glamour for stylesheet-based terminal markdown rendering. ([GitHub][3])

### Module: `internal/cli`

* **Responsibility**: Parse args, define subcommands, map flags to app behavior.
* **Exports**:

  * `Execute(argv []string) int` (returns exit code)

### Module: `internal/errors`

* **Responsibility**: Centralize error types and exit code mapping.
* **Exports**:

  * `type Code int`
  * `ExitCode(err error) int`

### Module: `internal/app`

* **Responsibility**: Wire modules into a runnable application.
* **Exports**:

  * `New(...) *App`
  * `RunTUI(...) error`
  * `RunPlain(...) error`
  * `Validate(...) error`

---

## 4) Dependency Chain (layers, explicit “Depends on: […]”)

### Foundation Layer

* **errors**: exit codes and typed errors. Depends on: []
* **pack**: parsing + validation. Depends on: [errors]
* **state**: state model + persistence. Depends on: [errors]
* **report**: report schema + writer. Depends on: [errors]
* **render**: markdown rendering adapter. Depends on: [errors]

### Execution Layer

* **exec**: shell execution + streaming + logs. Depends on: [errors, pack]

### Application Layer

* **app**: composition and run orchestration. Depends on: [pack, exec, state, report, render, errors]

### Interaction Layer

* **cli**: command/flags and entrypoints. Depends on: [app, errors]
* **tui**: Bubble Tea UI. Depends on: [app, pack, state, render, errors]

(Acyclic by construction; `app` is the orchestration hub, `errors` is the base.)

---

## 5) Development Phases (Phase 0…N; entry/exit criteria; tasks with dependencies + acceptance criteria + test strategy)

### Phase 0: Foundation

**Entry criteria**: repo initialized; CI runs `go test ./...`

* **Task**: Implement `internal/errors`

  * Depends on: []
  * Acceptance criteria: stable exit codes; unit tests for mapping
  * Test strategy: unit tests
* **Task**: Implement `internal/pack` parsing + validation

  * Depends on: [errors]
  * Acceptance criteria: parses packs shaped like your current output ; clear issues for missing fence/steps
  * Test strategy: golden tests with fixture Markdown files; fuzz-ish tests for malformed inputs
* **Task**: Implement `internal/state` persistence

  * Depends on: [errors]
  * Acceptance criteria: atomic save/load; schema version field present
  * Test strategy: unit tests with temp dirs; corruption handling tests
* **Exit criteria**: `pack.Load + Validate`, `state.Save/Load`, exit codes proven by tests.

### Phase 1: Execution core

**Entry criteria**: Phase 0 complete

* **Task**: Implement `internal/exec` runner (prelude + step)

  * Depends on: [errors, pack]
  * Acceptance criteria: streams stdout/stderr; writes log file; returns accurate exit code
  * Test strategy: integration tests using small deterministic shell scripts (`printf`, `exit 1`)
* **Task**: Implement flag injection transform

  * Depends on: [pack]
  * Acceptance criteria: only modifies intended `oracle` lines; does not rewrite arbitrary commands
  * Test strategy: unit tests with representative step bodies
* **Exit criteria**: can execute a single parsed step and produce logs deterministically.

### Phase 2: Non-TUI CLI (usable early)

**Entry criteria**: Phase 1 complete

* **Task**: Implement `internal/app` orchestration (plain mode)

  * Depends on: [pack, exec, state, report, errors]
  * Acceptance criteria: `orpack run --no-tui` supports confirm/skip, stop-on-fail policy, writes state + report
  * Test strategy: integration tests running the CLI against fixture packs (mock oracle via dummy commands)
* **Task**: Implement `internal/cli` with subcommands

  * Depends on: [app, errors]
  * Acceptance criteria: `run`, `validate`, `list` (optional) produce predictable text output
  * Test strategy: CLI snapshot tests (stdout/stderr) + exit code assertions
* **Exit criteria**: tool is useful without TUI.

### Phase 3: TUI (polish)

**Entry criteria**: Phase 2 complete

* **Task**: Implement `internal/tui` step list + status rendering

  * Depends on: [app, pack, state, errors]
  * Acceptance criteria: navigate/filter steps; run/skip; show per-step metadata
  * Test strategy: model-level tests for update messages and state transitions
* **Task**: Live log viewport

  * Depends on: [tui, exec]
  * Acceptance criteria: streaming output appears during execution; persisted logs still written
  * Test strategy: integration-ish tests using a fake line stream injected into model
* **Exit criteria**: full-screen TUI supports the primary workflow.

### Phase 4: Rendering + reporting polish

**Entry criteria**: Phase 3 complete

* **Task**: Implement `internal/render` (Markdown preview)

  * Depends on: [errors]
  * Acceptance criteria: renders markdown snippets to ANSI using Glamour. ([GitHub][3])
  * Test strategy: golden tests (ANSI output normalized)
* **Task**: Stabilize report schema v1

  * Depends on: [report, state]
  * Acceptance criteria: `run.json` includes per-step results, log paths, pack hash, schema version
  * Test strategy: schema conformance tests
* **Exit criteria**: consistent artifacts for both humans and automation.

### Phase 5: Packaging and distribution

**Entry criteria**: stable CLI behavior + docs

* **Task**: Add release pipeline (GoReleaser)

  * Depends on: [cli]
  * Acceptance criteria: reproducible builds; generates Homebrew/Scoop metadata as configured. ([GoReleaser][4])
  * Test strategy: CI dry-run of release config
* **Exit criteria**: users can install via release artifacts with minimal friction.

---

## 6) User Experience

### Personas

* **Operator**: runs packs, wants minimal mistakes; values confirmations, resume, clear failure context.
* **Maintainer**: authors packs; values `validate`, actionable parse errors, predictable numbering.

### Key flows

1. **Validate a pack**

   * `orpack validate pack.md` → prints issues + derived summary (`out_dir`, step count, detected outputs).
2. **Run in TUI**

   * `orpack run pack.md` → list steps → select → preview command and `--write-output` → confirm → stream logs → mark OK/Failed.
3. **Resume**

   * `orpack run --resume pack.md` → loads saved state → cursor moves to next pending step.
4. **Plain mode / CI**

   * `orpack run --no-tui --yes pack.md` → runs remaining steps deterministically; writes `run.json`.

### UX notes tied to capabilities

* Always display the detected `--write-output` prominently before confirmation.
* Provide explicit “Stop on failure / Continue?” behavior; never silently continue after errors.
* Keep keybindings discoverable (help line), consistent with common Bubble Tea apps. ([GitHub][2])

---

## 7) Technical Architecture

### System components

* **Parser**: Markdown → bash fence → prelude/steps.
* **Executor**: builds script = prelude + step; runs via `bash -lc` (default); streams lines.
* **State/Report**: write `state.json` (resume) + `run.json` (audit).
* **TUI**: Bubble Tea program + Bubbles list/viewport/spinner; Lip Gloss layout/styling. ([GitHub][2])
* **Markdown rendering**: Glamour (optional) for previews/help. ([GitHub][3])

### Data models

* `Pack{path, hash, outDir?, prelude, steps[]}`
* `Step{number, title, body, writeOutput?}`
* `RunState{schemaVersion, packHash, startedAt, steps: map[number]StepState}`
* `StepState{status, lastResult?}`
* `Result{exitCode, startedAt, endedAt, logPath}`

### External integrations

* `oracle` binary (invoked as-is)
* shell runtime (`bash` default)
* filesystem for logs/state/report

### Key decisions and trade-offs

* **Compiled Go + Bubble Tea** over shell-only: enables richer UX, better structure, cross-platform releases; more implementation surface area. Bubble Tea is explicitly intended for rich TUIs. ([GitHub][2])
* **Gum as fallback/alternative**: fastest path but limited extensibility; still valuable for “shell-first” users. ([GitHub][1])
* **Glamour for Markdown rendering**: consistent ANSI rendering with stylesheets for preview panes. ([GitHub][3])

---

## 8) Test Strategy

### Test pyramid targets

* **Unit**: 70% (parser, validators, injection transform, state/report serialization)
* **Integration**: 25% (executor running controlled shell snippets; CLI end-to-end with fixture packs)
* **E2E**: 5% (smoke tests in CI for major OS targets as feasible)

### Coverage minimums

* Pack parsing + validation: near-100% branch coverage (high risk surface)
* Executor: focus on error paths (timeouts, non-zero exits, stderr handling)

### Critical scenarios per module

* **pack**

  * Missing/empty fence; multiple fences; malformed headers; duplicate step numbers; no steps.
* **exec**

  * Prelude fails; step fails; long output; stderr-only output; non-UTF8 handling strategy (define behavior).
* **state/report**

  * Partial runs; resume from saved state; atomic write; schema version mismatch.
* **tui**

  * Status transitions; run/skip flows; failure stop policy; log rendering under high volume.

### Integration points

* Parsing → execution uses the same step bodies as read from pack fixtures derived from your known format.

---

## 9) Risks and Mitigations

### Risk: Pack format drift

* **Impact**: High
* **Likelihood**: Medium
* **Mitigation**: strict `validate` with clear errors; keep parser conservative; add fixture packs as regression tests.
* **Fallback**: allow `--format=legacy|strict` modes if needed.

### Risk: Executing arbitrary shell content (safety/security)

* **Impact**: High
* **Likelihood**: Medium
* **Mitigation**: confirm-by-default; show command preview; provide `--dry-run` and `--print-script` modes; never auto-run without `--yes`.
* **Fallback**: sandboxing is out-of-scope; document threat model.

### Risk: Cross-platform shell assumptions

* **Impact**: Medium
* **Likelihood**: Medium
* **Mitigation**: `--shell` flag; detect Windows and warn; document WSL/Git Bash expectations.
* **Fallback**: support `pwsh` only if pack bodies are compatible (likely not for bash-specific prelude).

### Risk: High-volume output overwhelms TUI

* **Impact**: Medium
* **Likelihood**: Medium
* **Mitigation**: ring buffer for in-memory viewport; always persist full logs to disk; provide “open log file path” UX.

---

## 10) Appendix

### Source format baseline

* Your existing pack structure and interactive-run expectations are exemplified in the uploaded script/pack discussion.

### Charm ecosystem references

* Bubble Tea (TUI framework; Elm-style architecture). ([GitHub][2])
* Bubbles (production-used components for Bubble Tea). ([GitHub][5])
* Lip Gloss (terminal styling/layout). ([GitHub][6])
* Gum (glamorous shell prompts without writing Go). ([GitHub][1])
* Glow (Markdown in terminal) and Glamour (Markdown rendering library for CLI apps). ([GitHub][7])
* GoReleaser packaging docs (Homebrew/Scoop/Actions). ([GoReleaser][4])

[1]: https://github.com/charmbracelet/gum?utm_source=chatgpt.com "charmbracelet/gum: A tool for glamorous shell scripts"
[2]: https://github.com/charmbracelet/bubbletea?utm_source=chatgpt.com "charmbracelet/bubbletea: A powerful little TUI framework"
[3]: https://github.com/charmbracelet/glamour?utm_source=chatgpt.com "charmbracelet/glamour: Stylesheet-based markdown ..."
[4]: https://goreleaser.com/customization/homebrew_casks/?utm_source=chatgpt.com "Homebrew Casks"
[5]: https://github.com/charmbracelet/bubbles?utm_source=chatgpt.com "charmbracelet/bubbles: TUI components for Bubble Tea"
[6]: https://github.com/charmbracelet/lipgloss?utm_source=chatgpt.com "charmbracelet/lipgloss: Style definitions for nice terminal ..."
[7]: https://github.com/charmbracelet/glow?utm_source=chatgpt.com "charmbracelet/glow: Render markdown on the CLI, with ..."


--- .taskmaster/docs/_PRD.md ---
## 1) Overview

### Problem

`oraclepack` is a Bubble Tea TUI for running “oracle packs” (bash steps embedded in Markdown) with state/reporting and automatic flag injection into `oracle` commands.
Today, changing **oracle CLI flags** or the **ChatGPT project URL/endpoint** typically requires editing configs or re-invoking the CLI differently per run, and there is no TUI flow to (a) pick overrides, (b) choose which steps they apply to, and (c) validate the override set before running.

### Who has it (target users)

* Developers using `oraclepack` to run repeatable AI-assisted workflows but needing per-run customization (different ChatGPT project folders, different oracle flags for debugging, different remotes).
* Pack authors who want packs to remain stable while allowing users to safely tweak runtime behavior.

### Why current solutions fail

* Current injection is global and line-based (`InjectFlags` rewrites lines starting with `oracle`), which can break common multi-line `oracle \` formatting and cannot selectively apply overrides per step.
* No TUI affordance exists to stage overrides and confirm/validate them before returning to the main run screen.

### Success metrics

* Override workflow completion rate: ≥ 95% of users can (flags + URL + step targeting) and return to main run screen without errors.
* Preflight prevention: ≥ 90% of invalid flag/url combinations are caught by validation before any step execution.
* UX efficiency: Configure overrides in ≤ 30 seconds for typical cases (multi-select flags + pick steps + confirm).
* Execution correctness: Overrides apply only to selected steps; non-selected steps run with baseline behavior.

Assumptions (explicit):

* Upstream `oracle` supports `--dry-run` and `--chatgpt-url` for safe validation and URL targeting. ([GitHub][1])
* Overrides are **ephemeral per TUI session/run** (no persistence beyond the run) unless added later (out of scope).

---

## 2) Capability Tree (Functional Decomposition)

### Capability: Runtime Overrides Management (MVP)

Manage a staged set of runtime overrides (flags + project URL) and their step targets.

#### Feature: Overrides data model

* **Description**: Represent user-selected flags, selected project URL, and selected step targets.
* **Inputs**: Baseline runner flags (current), user selections, pack step IDs.
* **Outputs**: `RuntimeOverrides` object usable by TUI + execution.
* **Behavior**: Store “added flags”, “removed flags”, `chatgptURL` (optional), and `applyToSteps` set.

#### Feature: Merge baseline flags + runtime overrides (step-aware)

* **Description**: Compute effective flags per step.
* **Inputs**: Baseline flags (existing `Runner.OracleFlags`), overrides, step ID.
* **Outputs**: Effective flag list and any key-value flag pairs (e.g., `--chatgpt-url <url>`).
* **Behavior**: If step not targeted, return baseline; else baseline + adds − removes, plus URL flag injection when set.

---

### Capability: Flag Picker UI (MVP)

Allow choosing additional oracle flags at runtime.

#### Feature: Multi-select flags picker

* **Description**: Pick known/common flags (multi-select) and optionally remove baseline flags.
* **Inputs**: Baseline flags, a curated list of known flags, current overrides.
* **Outputs**: Updated overrides (added/removed flags).
* **Behavior**: Toggle selection; show “added” vs “removed” vs “baseline unchanged”.

---

### Capability: Project URL (Endpoint) UI (MVP)

Allow entering/selecting a ChatGPT project URL for browser runs.

#### Feature: Project URL input screen

* **Description**: Enter a ChatGPT project URL string.
* **Inputs**: Text input (URL), existing selected URL.
* **Outputs**: Updated overrides `chatgptURL`.
* **Behavior**: Basic validation (non-empty, looks like URL); store exactly as entered.

#### Feature: Project URL selection menu (optional within MVP if simple list is kept in-memory)

* **Description**: Choose among recently entered URLs within the same session.
* **Inputs**: Session list, selected value.
* **Outputs**: Updated overrides `chatgptURL`.
* **Behavior**: Select one; allow “clear”.

Upstream basis: `oracle` supports `--chatgpt-url` and config `browser.chatgptUrl` for targeting a ChatGPT project folder. ([GitHub][1])

---

### Capability: Step Targeting UI (MVP)

Choose which steps receive overrides.

#### Feature: Multi-select step picker

* **Description**: Select pack steps that should receive runtime overrides.
* **Inputs**: Pack steps (IDs/titles), current selection set.
* **Outputs**: Updated overrides `applyToSteps`.
* **Behavior**: Toggle step selection; “select all / none”.

---

### Capability: Confirmation + Mode 2 Validation (MVP)

Validate selected overrides against upstream `oracle` before returning to main run screen.

#### Feature: Confirmation summary screen

* **Description**: Show flags added/removed, URL, and affected steps before applying.
* **Inputs**: Overrides + pack step list.
* **Outputs**: User confirmation or cancel.
* **Behavior**: Render a diff-style summary; confirm triggers validation.

#### Feature: Mode 2 validation runner

* **Description**: Validate that the override set is accepted by upstream `oracle` CLI.
* **Inputs**: Pack steps targeted, effective flags per targeted step, extracted oracle invocations.
* **Outputs**: Pass/fail with actionable error text including step + failing invocation.
* **Behavior**: For each targeted step, run each `oracle ...` invocation with overrides plus `--dry-run summary` to ensure CLI parsing succeeds without spending tokens. ([GitHub][1])

Requirement basis: “Mode 2 validation” must run before returning to run screen; failures must block readiness and show actionable errors.

---

### Capability: Execution Integration (MVP)

Apply overrides during actual run.

#### Feature: Step execution uses effective flags (step-aware)

* **Description**: Inject computed flags only into targeted steps.
* **Inputs**: Step code, step ID, effective flags.
* **Outputs**: Executed step with correct oracle invocations.
* **Behavior**: Replace current global injection behavior with per-step effective flags. Current injection is via `Runner.RunStep` → `InjectFlags`.

#### Feature: Hardened oracle injection (multi-line tolerant)

* **Description**: Inject flags without breaking common `oracle \` multi-line formatting.
* **Inputs**: Step script text, flags to inject.
* **Outputs**: Rewritten script.
* **Behavior**: Detect oracle invocations across line continuations and inject before trailing `\` on the invocation line when present.

Risk basis: current line-based approach can break multi-line commands.

---

## 3) Repository Structure + Module Definitions (Structural Decomposition)

Current structure includes `internal/exec`, `internal/tui`, `internal/app`, etc.

### Proposed additions/changes

```
internal/
  overrides/
    types.go
    merge.go
    validate.go
  exec/
    inject.go          (extend/replace with multiline-safe injector)
    oracle_scan.go     (new: extract oracle invocations)
    oracle_validate.go (new: mode-2 validation runner)
  tui/
    overrides_flow.go  (new: state machine for overrides screens)
    overrides_flags.go (new: flags picker model)
    overrides_steps.go (new: step picker model)
    overrides_url.go   (new: project URL input/selector)
    overrides_confirm.go (new: summary + validation screen)
```

### Module: `internal/overrides`

* **Maps to capability**: Runtime Overrides Management
* **Responsibility**: Own the runtime override data model and step-aware flag resolution.
* **Exports**:

  * `type RuntimeOverrides`
  * `func (o RuntimeOverrides) EffectiveFlags(stepID string, baseline []string) []string`
  * `func (o RuntimeOverrides) Targeted(stepID string) bool`
  * `func (o RuntimeOverrides) Summary(packSteps []pack.Step) OverridesSummary`

### Module: `internal/exec/oracle_scan.go`

* **Maps to capability**: Mode 2 Validation (oracle invocation extraction)
* **Responsibility**: Extract oracle invocations from bash step code robustly enough for validation + injection.
* **Exports**:

  * `type OracleInvocation { Raw string; Display string }`
  * `func ExtractOracleInvocations(script string) []OracleInvocation`

### Module: `internal/exec/inject.go` (updated)

* **Maps to capability**: Execution Integration + Hardened Injection
* **Responsibility**: Inject flags into oracle invocations without breaking multi-line formatting.
* **Exports**:

  * `func InjectFlags(script string, flags []string) string` (same signature; improved implementation)

### Module: `internal/exec/oracle_validate.go`

* **Maps to capability**: Mode 2 validation runner
* **Responsibility**: Execute safe validations (`--dry-run summary`) for extracted invocations with overrides.
* **Exports**:

  * `type ValidationError { StepID, Invocation string; Output string }`
  * `func ValidateOverrides(ctx context.Context, shell string, workDir string, env []string, steps []pack.Step, baseline []string, ov overrides.RuntimeOverrides) error`

### Module: `internal/tui/overrides_flow.go` (+ related files)

* **Maps to capability**: Flag Picker UI, Project URL UI, Step Targeting UI, Confirmation UX
* **Responsibility**: Bubble Tea models for the overrides wizard and integration back to main TUI model.
* **Exports**:

  * `func NewOverridesFlowModel(...) tea.Model` (or integrated into existing `tui.Model` as substates)
  * Messages: `OverridesAppliedMsg`, `OverridesCancelledMsg`, `OverridesValidationFailedMsg`

### Module changes: `internal/tui/tui.go`

* **Maps to capability**: Execution Integration + UX integration
* **Responsibility change**: Add a new view state for overrides flow; show current overrides status on main screen; pass overrides into runner step execution path.
* **Exports**: unchanged (existing `NewModel`), but internal state extended.

---

## 4) Dependency Chain (layers, explicit “Depends on: […]”)

### Foundation Layer (Phase 0)

* **internal/overrides**: override types + merge logic.
  Depends on: []
* **internal/exec/oracle_scan**: extract oracle invocations.
  Depends on: []
* **internal/exec/inject (improved)**: multiline-safe injection.
  Depends on: [internal/exec/oracle_scan] (for shared detection rules)

### Validation Layer (Phase 1)

* **internal/exec/oracle_validate**: mode-2 validation runner (`--dry-run summary`). ([GitHub][1])
  Depends on: [internal/overrides, internal/exec/oracle_scan]

### TUI Flow Layer (Phase 2)

* **internal/tui/overrides_* models**: flags picker, url input, step picker, confirm/validate screen.
  Depends on: [internal/overrides, internal/exec/oracle_validate]

### Integration Layer (Phase 3)

* **internal/tui/tui.go integration**: entrypoint keybinding/menu, apply overrides to step execution, display summary.
  Depends on: [internal/tui/overrides_*]
* **internal/app / internal/exec runner integration**: ensure `RunStep` can accept per-step effective flags (or TUI updates Runner flags before each step).
  Depends on: [internal/overrides, internal/exec/inject]

No cycles: all dependencies flow from overrides/scan → validate → tui flow → integration.

---

## 5) Development Phases (Phase 0…N; entry/exit criteria; tasks with dependencies + acceptance criteria + test strategy)

### Phase 0: Foundations (Overrides model + scanning + injection)

**Entry criteria**: Current pack parsing and TUI build clean.
**Tasks**:

* [ ] Implement `internal/overrides` data model + step-aware merge

  * **Depends on**: []
  * **Acceptance criteria**: Given baseline flags and overrides, `EffectiveFlags(stepID)` matches expected for targeted vs non-targeted steps.
  * **Test strategy**: Unit tests for add/remove precedence, empty sets, step targeting.

* [ ] Implement `internal/exec/oracle_scan.ExtractOracleInvocations`

  * **Depends on**: []
  * **Acceptance criteria**: Extracts oracle invocations from (a) single-line `oracle ...`, (b) `oracle \` + continued lines, (c) indented variants.
  * **Test strategy**: Unit tests with representative scripts; snapshot extracted `Display`.

* [ ] Upgrade `internal/exec/InjectFlags` to be multiline-tolerant

  * **Depends on**: [internal/exec/oracle_scan]
  * **Acceptance criteria**: Injects flags into multiline `oracle \` form without breaking trailing backslash; preserves non-oracle lines.
  * **Test strategy**: Extend `inject_test.go` with multiline continuation fixtures (current tests exist).

**Exit criteria**: Per-step effective flag computation and injection behave correctly under unit tests.

---

### Phase 1: Mode 2 validation runner

**Goal**: Validate overrides safely before returning to run screen.
**Entry criteria**: Phase 0 complete.

**Tasks**:

* [ ] Implement `internal/exec/oracle_validate.ValidateOverrides` using `--dry-run summary`

  * **Depends on**: [internal/overrides, internal/exec/oracle_scan]
  * **Acceptance criteria**:

    * For each targeted step, each extracted oracle invocation is executed with overrides + `--dry-run summary` and fails fast on first error.
    * Error includes step ID, invocation, and captured output.
  * **Test strategy**:

    * Unit tests for command construction (pure functions).
    * Integration test with a fake `oracle` binary on PATH (test harness script) that asserts received args.

Upstream basis for dry run: `oracle --dry-run [summary|json|full]` previews without sending. ([GitHub][1])

**Exit criteria**: Validation produces deterministic pass/fail with actionable error payload.

---

### Phase 2: Overrides wizard TUI

**Goal**: Provide the requested picker flow and confirmation/validation gating.
**Entry criteria**: Phase 1 complete.

**Tasks**:

* [ ] Add overrides flow state machine + entrypoint from main run screen

  * **Depends on**: [internal/overrides, internal/exec/oracle_validate]
  * **Acceptance criteria**: From main steps screen, user can enter overrides flow and return (cancel or apply).
  * **Test strategy**: Bubble Tea model tests for state transitions (message-driven).

* [ ] Implement flags picker (multi-select)

  * **Depends on**: [overrides flow state machine]
  * **Acceptance criteria**: Select/deselect flags; mark removed baseline flags; navigation works.
  * **Test strategy**: Model tests simulate keypresses; verify overrides state.

* [ ] Implement step picker (multi-select)

  * **Depends on**: [overrides flow state machine]
  * **Acceptance criteria**: Can select any subset; “all/none” supported.
  * **Test strategy**: Model tests; verify `applyToSteps` set.

* [ ] Implement project URL input/selection menu

  * **Depends on**: [overrides flow state machine]
  * **Acceptance criteria**: Enter URL; clear URL; appears in summary.
  * **Test strategy**: Model tests for textinput.

* [ ] Implement confirmation screen that runs validation on confirm

  * **Depends on**: [flags picker, step picker, url input, internal/exec/oracle_validate]
  * **Acceptance criteria**:

    * Summary includes flags added/removed, affected steps, selected URL.
    * Confirm triggers validation; failure shows error and does not apply.
  * **Test strategy**: Model tests with stubbed validator (inject interface).

**Exit criteria**: Full wizard: flags → steps → confirm/validate → return to run screen.

---

### Phase 3: Execution integration + UX polish

**Goal**: Overrides affect actual execution exactly as selected.

**Tasks**:

* [ ] Apply step-aware effective flags during `runStep` execution path

  * **Depends on**: [Phase 0 injection, Phase 2 applied overrides messages]
  * **Acceptance criteria**: Running a non-targeted step uses baseline flags only; targeted step includes overrides.
  * **Test strategy**: Integration test with fake `oracle` that logs args for selected step only.

* [ ] Display staged overrides summary on main run screen

  * **Depends on**: [Phase 2]
  * **Acceptance criteria**: Main view indicates overrides active and shows brief summary (e.g., “Overrides: +2 flags, URL set, 3 steps”).
  * **Test strategy**: View snapshot tests (string contains markers).

**Exit criteria**: End-to-end usable: configure overrides in TUI, validated, then run steps with correct behavior.

---

## 6) User Experience

### Personas

* **Pack runner**: Wants to run a known pack but direct output into a specific ChatGPT project folder and enable debug flags temporarily.
* **Pack author**: Wants users to safely override runtime settings without editing the pack or shared config.

### Key flows

1. **Open overrides flow** from main steps screen (new keybinding/menu entry).
2. **Flags picker**: multi-select adds/removes.
3. **Steps picker**: choose steps receiving overrides.
4. **Project URL menu**: enter/select/clear URL (separate screen within flow).
5. **Confirm**: show full summary; confirm runs validation.
6. **Return to main run screen**: overrides staged; run steps as normal.

### UI/UX notes (tied to capabilities)

* Always show an “Overrides staged” indicator on the main screen to avoid hidden behavior changes.
* Validation errors must include: step ID + the failing oracle invocation + raw output.
* Keep “full config menu” out of scope; only runtime pickers + project URL.

---

## 7) Technical Architecture

### System components

* **TUI (Bubble Tea)**: collects overrides and step targets; triggers validation; applies overrides to execution.
* **Overrides core**: step-aware merge + summary formatting.
* **Exec layer**:

  * Oracle invocation extraction
  * Multiline-safe injection
  * Mode 2 validation runner (`oracle --dry-run summary ...`) ([GitHub][1])
* **Runner**: executes rewritten step scripts (current `Runner.RunStep` injects flags).

### Data models

* `RuntimeOverrides`:

  * `AddedFlags []string`
  * `RemovedFlags []string`
  * `ChatGPTURL string` (optional)
  * `ApplyToSteps map[string]bool`
* `OverridesSummary`:

  * counts + formatted lists for confirm screen and main screen indicator

### Key decision: validate via `--dry-run summary` (Mode 2)

* **Rationale**: Upstream `oracle` provides a dry-run mode specifically to preview without sending tokens, so it exercises real CLI parsing. ([GitHub][1])
* **Trade-offs**: Requires executing `oracle` during validation; may still read files and can fail if referenced paths/vars are unresolved.
* **Alternative**: `oracle --help`-based validation (weaker; doesn’t validate combos). Not chosen for MVP.

### Key decision: heuristic multiline-safe injection (not full shell parsing)

* **Rationale**: Addresses the most common breakage (`oracle \` continuation) without introducing a full bash AST parser.
* **Trade-offs**: Some exotic shell constructs may not be perfectly handled; mitigate with tests and clear error output.

---

## 8) Test Strategy

### Test pyramid targets

* Unit: ~70% (overrides merge, scan, injection)
* Integration: ~25% (validation runner + fake oracle binary, runner integration)
* E2E: ~5% (optional: simulate TUI flow at message level)

### Coverage minimums

* Unit-tested modules (overrides, scan, inject): ≥ 90% line coverage
* Validation runner: ≥ 80% line coverage with integration fixtures

### Critical scenarios (by module)

* **internal/overrides**

  * Happy: targeted step gets baseline+adds−removes + url flag
  * Edge: empty overrides; remove non-existent flag; applyToSteps empty
* **internal/exec/oracle_scan**

  * Happy: single-line oracle
  * Edge: multiline with backslashes and indentation
  * Error: no oracle invocations → validation is a no-op success (if targeted steps contain none)
* **internal/exec/inject**

  * Multiline injection preserves `\`
  * Does not inject into non-oracle commands
* **internal/exec/oracle_validate**

  * Fails fast and returns structured error with output
  * Uses `--dry-run summary` consistently ([GitHub][1])
* **internal/tui overrides flow**

  * Cancel returns without applying
  * Confirm runs validator; failure stays in flow with readable error
  * Apply sends message to main model and updates indicator

---

## 9) Risks and Mitigations

### Risk: Injection breaks uncommon shell patterns

* **Impact**: Medium (step execution could change meaning)
* **Likelihood**: Medium
* **Mitigation**: Focus on well-defined heuristics (start-of-command + multiline `\`), add fixture tests for real packs, provide clear docs/limitations.
* **Fallback**: Allow disabling runtime injection for a step (not in MVP; could be added later).

### Risk: Validation executes `oracle` but fails due to environment/path assumptions

* **Impact**: Medium
* **Likelihood**: Medium
* **Mitigation**: Validation runs only extracted oracle invocations; capture and display full output; allow user to adjust overrides/targets.
* **Fallback**: Provide “skip validation” escape hatch (not in MVP unless necessary; conflicts with requirement).

### Risk: Upstream `oracle` flags change

* **Impact**: Medium
* **Likelihood**: Low/Medium
* **Mitigation**: Treat the flags list in the picker as curated (best-effort) and allow manual entry for advanced flags; keep validation authoritative.
* **Fallback**: Update curated list independently; validation remains source of truth.

---

## 10) Appendix

### References

* Runtime overrides requirements and acceptance criteria.
* Current `oraclepack` code structure, current injection path (`Runner.RunStep` → `InjectFlags`).
* `oraclepack` product description and existing CLI behavior.
* Upstream `oracle` CLI supports `--dry-run` and `--chatgpt-url` configuration patterns. ([GitHub][1])

### Glossary

* **Oracle Pack**: Markdown file containing one `bash` block with numbered steps executed by `oraclepack`.
* **Mode 2 validation**: For this PRD, defined as executing extracted oracle invocations with overrides using `--dry-run summary` to validate CLI parsing without sending. ([GitHub][1])

### Open questions

* Should project URL injection also support remote browser service flags (`--remote-host/--remote-token`) in addition to `--chatgpt-url`? Upstream supports these. ([GitHub][1])
* Should entered project URLs be persisted across runs (state/config) or remain session-only? (Currently assumed session-only per scope.)

[1]: https://github.com/steipete/oracle "GitHub - steipete/oracle: Ask the oracle when you're stuck. Invoke GPT-5 Pro with a custom context and files."


--- .taskmaster/templates/example_prd.txt ---
<context>
# Overview  
[Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.]

# Core Features  
[List and describe the main features of your product. For each feature, include:
- What it does
- Why it's important
- How it works at a high level]

# User Experience  
[Describe the user journey and experience. Include:
- User personas
- Key user flows
- UI/UX considerations]
</context>
<PRD>
# Technical Architecture  
[Outline the technical implementation details:
- System components
- Data models
- APIs and integrations
- Infrastructure requirements]

# Development Roadmap  
[Break down the development process into phases:
- MVP requirements
- Future enhancements
- Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks]

# Logical Dependency Chain
[Define the logical order of development:
- Which features need to be built first (foundation)
- Getting as quickly as possible to something usable/visible front end that works
- Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches]

# Risks and Mitigations  
[Identify potential risks and how they'll be addressed:
- Technical challenges
- Figuring out the MVP that we can build upon
- Resource constraints]

# Appendix  
[Include any additional information:
- Research findings
- Technical specifications]
</PRD>

--- .taskmaster/templates/example_prd_rpg.txt ---
<rpg-method>
# Repository Planning Graph (RPG) Method - PRD Template

This template teaches you (AI or human) how to create structured, dependency-aware PRDs using the RPG methodology from Microsoft Research. The key insight: separate WHAT (functional) from HOW (structural), then connect them with explicit dependencies.

## Core Principles

1. **Dual-Semantics**: Think functional (capabilities) AND structural (code organization) separately, then map them
2. **Explicit Dependencies**: Never assume - always state what depends on what
3. **Topological Order**: Build foundation first, then layers on top
4. **Progressive Refinement**: Start broad, refine iteratively

## How to Use This Template

- Follow the instructions in each `<instruction>` block
- Look at `<example>` blocks to see good vs bad patterns
- Fill in the content sections with your project details
- The AI reading this will learn the RPG method by following along
- Task Master will parse the resulting PRD into dependency-aware tasks

## Recommended Tools for Creating PRDs

When using this template to **create** a PRD (not parse it), use **code-context-aware AI assistants** for best results:

**Why?** The AI needs to understand your existing codebase to make good architectural decisions about modules, dependencies, and integration points.

**Recommended tools:**
- **Claude Code** (claude-code CLI) - Best for structured reasoning and large contexts
- **Cursor/Windsurf** - IDE integration with full codebase context
- **Gemini CLI** (gemini-cli) - Massive context window for large codebases
- **Codex/Grok CLI** - Strong code generation with context awareness

**Note:** Once your PRD is created, `task-master parse-prd` works with any configured AI model - it just needs to read the PRD text itself, not your codebase.
</rpg-method>

---

<overview>
<instruction>
Start with the problem, not the solution. Be specific about:
- What pain point exists?
- Who experiences it?
- Why existing solutions don't work?
- What success looks like (measurable outcomes)?

Keep this section focused - don't jump into implementation details yet.
</instruction>

## Problem Statement
[Describe the core problem. Be concrete about user pain points.]

## Target Users
[Define personas, their workflows, and what they're trying to achieve.]

## Success Metrics
[Quantifiable outcomes. Examples: "80% task completion via autopilot", "< 5% manual intervention rate"]

</overview>

---

<functional-decomposition>
<instruction>
Now think about CAPABILITIES (what the system DOES), not code structure yet.

Step 1: Identify high-level capability domains
- Think: "What major things does this system do?"
- Examples: Data Management, Core Processing, Presentation Layer

Step 2: For each capability, enumerate specific features
- Use explore-exploit strategy:
  * Exploit: What features are REQUIRED for core value?
  * Explore: What features make this domain COMPLETE?

Step 3: For each feature, define:
- Description: What it does in one sentence
- Inputs: What data/context it needs
- Outputs: What it produces/returns
- Behavior: Key logic or transformations

<example type="good">
Capability: Data Validation
  Feature: Schema validation
    - Description: Validate JSON payloads against defined schemas
    - Inputs: JSON object, schema definition
    - Outputs: Validation result (pass/fail) + error details
    - Behavior: Iterate fields, check types, enforce constraints

  Feature: Business rule validation
    - Description: Apply domain-specific validation rules
    - Inputs: Validated data object, rule set
    - Outputs: Boolean + list of violated rules
    - Behavior: Execute rules sequentially, short-circuit on failure
</example>

<example type="bad">
Capability: validation.js
  (Problem: This is a FILE, not a CAPABILITY. Mixing structure into functional thinking.)

Capability: Validation
  Feature: Make sure data is good
  (Problem: Too vague. No inputs/outputs. Not actionable.)
</example>
</instruction>

## Capability Tree

### Capability: [Name]
[Brief description of what this capability domain covers]

#### Feature: [Name]
- **Description**: [One sentence]
- **Inputs**: [What it needs]
- **Outputs**: [What it produces]
- **Behavior**: [Key logic]

#### Feature: [Name]
- **Description**:
- **Inputs**:
- **Outputs**:
- **Behavior**:

### Capability: [Name]
...

</functional-decomposition>

---

<structural-decomposition>
<instruction>
NOW think about code organization. Map capabilities to actual file/folder structure.

Rules:
1. Each capability maps to a module (folder or file)
2. Features within a capability map to functions/classes
3. Use clear module boundaries - each module has ONE responsibility
4. Define what each module exports (public interface)

The goal: Create a clear mapping between "what it does" (functional) and "where it lives" (structural).

<example type="good">
Capability: Data Validation
  → Maps to: src/validation/
    ├── schema-validator.js      (Schema validation feature)
    ├── rule-validator.js         (Business rule validation feature)
    └── index.js                  (Public exports)

Exports:
  - validateSchema(data, schema)
  - validateRules(data, rules)
</example>

<example type="bad">
Capability: Data Validation
  → Maps to: src/utils.js
  (Problem: "utils" is not a clear module boundary. Where do I find validation logic?)

Capability: Data Validation
  → Maps to: src/validation/everything.js
  (Problem: One giant file. Features should map to separate files for maintainability.)
</example>
</instruction>

## Repository Structure

```
project-root/
├── src/
│   ├── [module-name]/       # Maps to: [Capability Name]
│   │   ├── [file].js        # Maps to: [Feature Name]
│   │   └── index.js         # Public exports
│   └── [module-name]/
├── tests/
└── docs/
```

## Module Definitions

### Module: [Name]
- **Maps to capability**: [Capability from functional decomposition]
- **Responsibility**: [Single clear purpose]
- **File structure**:
  ```
  module-name/
  ├── feature1.js
  ├── feature2.js
  └── index.js
  ```
- **Exports**:
  - `functionName()` - [what it does]
  - `ClassName` - [what it does]

</structural-decomposition>

---

<dependency-graph>
<instruction>
This is THE CRITICAL SECTION for Task Master parsing.

Define explicit dependencies between modules. This creates the topological order for task execution.

Rules:
1. List modules in dependency order (foundation first)
2. For each module, state what it depends on
3. Foundation modules should have NO dependencies
4. Every non-foundation module should depend on at least one other module
5. Think: "What must EXIST before I can build this module?"

<example type="good">
Foundation Layer (no dependencies):
  - error-handling: No dependencies
  - config-manager: No dependencies
  - base-types: No dependencies

Data Layer:
  - schema-validator: Depends on [base-types, error-handling]
  - data-ingestion: Depends on [schema-validator, config-manager]

Core Layer:
  - algorithm-engine: Depends on [base-types, error-handling]
  - pipeline-orchestrator: Depends on [algorithm-engine, data-ingestion]
</example>

<example type="bad">
- validation: Depends on API
- API: Depends on validation
(Problem: Circular dependency. This will cause build/runtime issues.)

- user-auth: Depends on everything
(Problem: Too many dependencies. Should be more focused.)
</example>
</instruction>

## Dependency Chain

### Foundation Layer (Phase 0)
No dependencies - these are built first.

- **[Module Name]**: [What it provides]
- **[Module Name]**: [What it provides]

### [Layer Name] (Phase 1)
- **[Module Name]**: Depends on [[module-from-phase-0], [module-from-phase-0]]
- **[Module Name]**: Depends on [[module-from-phase-0]]

### [Layer Name] (Phase 2)
- **[Module Name]**: Depends on [[module-from-phase-1], [module-from-foundation]]

[Continue building up layers...]

</dependency-graph>

---

<implementation-roadmap>
<instruction>
Turn the dependency graph into concrete development phases.

Each phase should:
1. Have clear entry criteria (what must exist before starting)
2. Contain tasks that can be parallelized (no inter-dependencies within phase)
3. Have clear exit criteria (how do we know phase is complete?)
4. Build toward something USABLE (not just infrastructure)

Phase ordering follows topological sort of dependency graph.

<example type="good">
Phase 0: Foundation
  Entry: Clean repository
  Tasks:
    - Implement error handling utilities
    - Create base type definitions
    - Setup configuration system
  Exit: Other modules can import foundation without errors

Phase 1: Data Layer
  Entry: Phase 0 complete
  Tasks:
    - Implement schema validator (uses: base types, error handling)
    - Build data ingestion pipeline (uses: validator, config)
  Exit: End-to-end data flow from input to validated output
</example>

<example type="bad">
Phase 1: Build Everything
  Tasks:
    - API
    - Database
    - UI
    - Tests
  (Problem: No clear focus. Too broad. Dependencies not considered.)
</example>
</instruction>

## Development Phases

### Phase 0: [Foundation Name]
**Goal**: [What foundational capability this establishes]

**Entry Criteria**: [What must be true before starting]

**Tasks**:
- [ ] [Task name] (depends on: [none or list])
  - Acceptance criteria: [How we know it's done]
  - Test strategy: [What tests prove it works]

- [ ] [Task name] (depends on: [none or list])

**Exit Criteria**: [Observable outcome that proves phase complete]

**Delivers**: [What can users/developers do after this phase?]

---

### Phase 1: [Layer Name]
**Goal**:

**Entry Criteria**: Phase 0 complete

**Tasks**:
- [ ] [Task name] (depends on: [[tasks-from-phase-0]])
- [ ] [Task name] (depends on: [[tasks-from-phase-0]])

**Exit Criteria**:

**Delivers**:

---

[Continue with more phases...]

</implementation-roadmap>

---

<test-strategy>
<instruction>
Define how testing will be integrated throughout development (TDD approach).

Specify:
1. Test pyramid ratios (unit vs integration vs e2e)
2. Coverage requirements
3. Critical test scenarios
4. Test generation guidelines for Surgical Test Generator

This section guides the AI when generating tests during the RED phase of TDD.

<example type="good">
Critical Test Scenarios for Data Validation module:
  - Happy path: Valid data passes all checks
  - Edge cases: Empty strings, null values, boundary numbers
  - Error cases: Invalid types, missing required fields
  - Integration: Validator works with ingestion pipeline
</example>
</instruction>

## Test Pyramid

```
        /\
       /E2E\       ← [X]% (End-to-end, slow, comprehensive)
      /------\
     /Integration\ ← [Y]% (Module interactions)
    /------------\
   /  Unit Tests  \ ← [Z]% (Fast, isolated, deterministic)
  /----------------\
```

## Coverage Requirements
- Line coverage: [X]% minimum
- Branch coverage: [X]% minimum
- Function coverage: [X]% minimum
- Statement coverage: [X]% minimum

## Critical Test Scenarios

### [Module/Feature Name]
**Happy path**:
- [Scenario description]
- Expected: [What should happen]

**Edge cases**:
- [Scenario description]
- Expected: [What should happen]

**Error cases**:
- [Scenario description]
- Expected: [How system handles failure]

**Integration points**:
- [What interactions to test]
- Expected: [End-to-end behavior]

## Test Generation Guidelines
[Specific instructions for Surgical Test Generator about what to focus on, what patterns to follow, project-specific test conventions]

</test-strategy>

---

<architecture>
<instruction>
Describe technical architecture, data models, and key design decisions.

Keep this section AFTER functional/structural decomposition - implementation details come after understanding structure.
</instruction>

## System Components
[Major architectural pieces and their responsibilities]

## Data Models
[Core data structures, schemas, database design]

## Technology Stack
[Languages, frameworks, key libraries]

**Decision: [Technology/Pattern]**
- **Rationale**: [Why chosen]
- **Trade-offs**: [What we're giving up]
- **Alternatives considered**: [What else we looked at]

</architecture>

---

<risks>
<instruction>
Identify risks that could derail development and how to mitigate them.

Categories:
- Technical risks (complexity, unknowns)
- Dependency risks (blocking issues)
- Scope risks (creep, underestimation)
</instruction>

## Technical Risks
**Risk**: [Description]
- **Impact**: [High/Medium/Low - effect on project]
- **Likelihood**: [High/Medium/Low]
- **Mitigation**: [How to address]
- **Fallback**: [Plan B if mitigation fails]

## Dependency Risks
[External dependencies, blocking issues]

## Scope Risks
[Scope creep, underestimation, unclear requirements]

</risks>

---

<appendix>
## References
[Papers, documentation, similar systems]

## Glossary
[Domain-specific terms]

## Open Questions
[Things to resolve during development]
</appendix>

---

<task-master-integration>
# How Task Master Uses This PRD

When you run `task-master parse-prd <file>.txt`, the parser:

1. **Extracts capabilities** → Main tasks
   - Each `### Capability:` becomes a top-level task

2. **Extracts features** → Subtasks
   - Each `#### Feature:` becomes a subtask under its capability

3. **Parses dependencies** → Task dependencies
   - `Depends on: [X, Y]` sets task.dependencies = ["X", "Y"]

4. **Orders by phases** → Task priorities
   - Phase 0 tasks = highest priority
   - Phase N tasks = lower priority, properly sequenced

5. **Uses test strategy** → Test generation context
   - Feeds test scenarios to Surgical Test Generator during implementation

**Result**: A dependency-aware task graph that can be executed in topological order.

## Why RPG Structure Matters

Traditional flat PRDs lead to:
- ❌ Unclear task dependencies
- ❌ Arbitrary task ordering
- ❌ Circular dependencies discovered late
- ❌ Poorly scoped tasks

RPG-structured PRDs provide:
- ✅ Explicit dependency chains
- ✅ Topological execution order
- ✅ Clear module boundaries
- ✅ Validated task graph before implementation

## Tips for Best Results

1. **Spend time on dependency graph** - This is the most valuable section for Task Master
2. **Keep features atomic** - Each feature should be independently testable
3. **Progressive refinement** - Start broad, use `task-master expand` to break down complex tasks
4. **Use research mode** - `task-master parse-prd --research` leverages AI for better task generation
</task-master-integration>


--- .config/mcp/oraclepack-gold-pack/references/attachment-minimization.md ---
# Attachment minimization rules (Stage 1 packs)

Objective: keep oracle calls fast, portable, and deterministic by attaching the minimum evidence per step.

## Hard limits

- Default: **0–2 attachments per step** (`-f/--file`).
- If you need more than 2, the step is not scoped tightly enough: split or reduce.
- Any `extra_files` the user provides must be appended **literally** (do not reinterpret), but you should still keep the step’s own attachments ≤2.

## What to attach (rule of thumb)

For a given step, prefer:
1) One file that *defines* the concept (contract/schema/config/type)
2) One file that *enforces/uses* the concept (handler/service/policy)

If you can’t find both confidently, attach only the “definition” file.

## Common attachment choices by category (patterns, not requirements)

Use these as **patterns** to recognize likely candidates in a repo; do not assume these paths exist.

- contracts/interfaces:
  - route registration, API schema/spec, public type definitions, CLI command registry

- invariants:
  - domain model definitions, validation layer, schema types, critical service functions that enforce rules

- caching/state:
  - cache client config, state container/store, session manager, any TTL/invalidation logic

- background jobs:
  - worker entrypoint, job registry, scheduler configuration, queue client config

- observability:
  - logger initialization/config, metrics/tracing setup, middleware that injects correlation ids

- permissions:
  - authn/authz middleware, policy definitions, role/scope mapping, guard functions

- migrations:
  - migrations folder index, migration runner config, schema definition file (if present)

- UX flows:
  - key UI/router flow code, top-level workflow orchestrator, controller/handler representing the flow

- failure modes:
  - error handling utilities, retry/backoff config, boundary middleware, circuit breaker wrappers (if any)

- feature flags:
  - flag config/registry, evaluation hook, rollout/targeting logic

## If you cannot find good attachments

- Attach nothing or only 1 file.
- Set `reference=Unknown`.
- Make the prompt request “exact missing file/path pattern(s) to attach next.”

## Avoid these attachment anti-patterns

- Attaching entire directories when one file is enough.
- Attaching multiple duplicates (e.g., the same config in three copies).
- Attaching generated/lock files unless the question is explicitly about them.
- Attaching secrets.


--- .config/skills/oracle-pack/references/attachment-minimization.md ---
<!-- path: ~/.codex/skills/oracle-pack/references/attachment-minimization.md -->
# Attachment minimization rules (evidence-driven)

Objective: keep Oracle inputs small and high-signal. Attach only what Oracle needs to answer the single question.

## Always exclude

- Secrets (`.env`, key files, tokens)
- Large generated dirs unless explicitly needed (`node_modules`, `dist`, `build`, `coverage`, etc.)
- Broad globs like `src/**` unless the question has `Unknown` reference and no better anchor exists

## Core mapping: reference → files to attach

### Reference = `{path}:{symbol}`

Attach:
1) `path` (the file containing the symbol)

Optionally attach **one** more file (only if it materially affects correctness):
- nearest router/entrypoint that calls into `path`
- config file that wires the subsystem (`config/*`, `settings.*`, `env` loader)
- interface/schema referenced by the symbol (e.g., `openapi.*`, `schema.*`)

Avoid attaching more than 2 files unless the repo uses very small modules and it’s clearly required.

### Reference = `{endpoint}`

Attach:
1) the route map / router file where the endpoint is declared
2) the handler file (if different)

Prefer literal files over globs.

### Reference = `{event}`

Attach:
1) the job/worker registration file where the event is scheduled/declared
2) the worker implementation file (if different)

### Reference = `Unknown`

Attach only “index” evidence:
- README (or closest equivalent)
- primary manifest (`package.json`, `pyproject.toml`, etc.)
- one best-guess entrypoint file (if you found one)

In the prompt, explicitly request the missing artifact pattern to attach next (e.g., “attach `**/migrations/**`”).

## Optional global extras (`extra_files`)

If the user provided `extra_files`, append those attachments to every command *after* the minimal evidence attachments. Use this sparingly.


--- .config/skills/strategist-questions-oracle-pack/references/attachment-minimization.md ---
<!-- path: ~/.codex/skills/strategist-questions-oracle-pack/references/attachment-minimization.md -->
# Attachment minimization rules (evidence-driven)

Objective: keep Oracle inputs small and high-signal. Attach only what Oracle needs to answer the single question.

## Always exclude

- Secrets (`.env`, key files, tokens)
- Large generated dirs unless explicitly needed (`node_modules`, `dist`, `build`, `coverage`, etc.)
- Broad globs like `src/**` unless the question has `Unknown` reference and no better anchor exists

## Core mapping: reference → files to attach

### Reference = `{path}:{symbol}`

Attach:
1) `path` (the file containing the symbol)

Optionally attach **one** more file (only if it materially affects correctness):
- nearest router/entrypoint that calls into `path`
- config file that wires the subsystem (`config/*`, `settings.*`, `env` loader)
- interface/schema referenced by the symbol (e.g., `openapi.*`, `schema.*`)

Avoid attaching more than 2 files unless the repo uses very small modules and it’s clearly required.

### Reference = `{endpoint}`

Attach:
1) the route map / router file where the endpoint is declared
2) the handler file (if different)

Prefer literal files over globs.

### Reference = `{event}`

Attach:
1) the job/worker registration file where the event is scheduled/declared
2) the worker implementation file (if different)

### Reference = `Unknown`

Attach only “index” evidence:
- README (or closest equivalent)
- primary manifest (`package.json`, `pyproject.toml`, etc.)
- one best-guess entrypoint file (if you found one)

In the prompt, explicitly request the missing artifact pattern to attach next (e.g., “attach `**/migrations/**`”).

## Optional global extras (`extra_files`)

If the user provided `extra_files`, append those attachments to every command *after* the minimal evidence attachments. Use this sparingly.


--- .config/mcp/oraclepack-taskify/references/determinism-and-safety.md ---
# Determinism and safety guardrails

## Determinism rules

- Always select inputs by prefix ordering:
  - exactly one match for each: `01-*.md` … `20-*.md`
- If any prefix is missing or has multiple matches, exit non-zero with a precise error.
- Keep generated JSON normalized:
  - items sorted by id ascending (01..20)
  - stable ordering for arrays
- Keep output paths explicit and stable:
  - do not rely on shared environment variables across steps
  - each step re-declares its constants

## Safety rules

- No interactive prompts in the Action Pack.
- Fail fast when prerequisites are missing:
  - `task-master` (or override)
  - `oracle` (or override)
  - `tm` only in autopilot mode (default)
- Always `mkdir -p` parent directories before writing files.
- Avoid destructive operations:
  - do not delete
  - do not force push
  - do not commit to main/master in autopilot mode
- Autopilot mode:
  - require a clean working tree
  - if on main/master, create a new work branch before starting autopilot
  - write a state file to support resumption

## Failure behavior

- Prefer explicit, early errors over partial or ambiguous outputs.
- If Task Master output paths differ from defaults, print warnings but keep the pack deterministic.


--- .config/mcp/mcp-builder/reference/evaluation.md ---
# MCP Server Evaluation Guide

## Overview

This document provides guidance on creating comprehensive evaluations for MCP servers. Evaluations test whether LLMs can effectively use your MCP server to answer realistic, complex questions using only the tools provided.

---

## Quick Reference

### Evaluation Requirements

- Create 10 human-readable questions
- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE
- Each question requires multiple tool calls (potentially dozens)
- Answers must be single, verifiable values
- Answers must be STABLE (won't change over time)

### Output Format

```xml
<evaluation>
   <qa_pair>
      <question>Your question here</question>
      <answer>Single verifiable answer</answer>
   </qa_pair>
</evaluation>
```

---

## Purpose of Evaluations

The measure of quality of an MCP server is NOT how well or comprehensively the server implements tools, but how well these implementations (input/output schemas, docstrings/descriptions, functionality) enable LLMs with no other context and access ONLY to the MCP servers to answer realistic and difficult questions.

## Evaluation Overview

Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, and IDEMPOTENT operations to answer. Each question should be:

- Realistic
- Clear and concise
- Unambiguous
- Complex, requiring potentially dozens of tool calls or steps
- Answerable with a single, verifiable value that you identify in advance

## Question Guidelines

### Core Requirements

1. **Questions MUST be independent**
   - Each question should NOT depend on the answer to any other question
   - Should not assume prior write operations from processing another question

2. **Questions MUST require ONLY NON-DESTRUCTIVE AND IDEMPOTENT tool use**
   - Should not instruct or require modifying state to arrive at the correct answer

3. **Questions must be REALISTIC, CLEAR, CONCISE, and COMPLEX**
   - Must require another LLM to use multiple (potentially dozens of) tools or steps to answer

### Complexity and Depth

4. **Questions must require deep exploration**
   - Consider multi-hop questions requiring multiple sub-questions and sequential tool calls
   - Each step should benefit from information found in previous questions

5. **Questions may require extensive paging**
   - May need paging through multiple pages of results
   - May require querying old data (1-2 years out-of-date) to find niche information
   - The questions must be DIFFICULT

6. **Questions must require deep understanding**
   - Rather than surface-level knowledge
   - May pose complex ideas as True/False questions requiring evidence
   - May use multiple-choice format where LLM must search different hypotheses

7. **Questions must not be solvable with straightforward keyword search**
   - Do not include specific keywords from the target content
   - Use synonyms, related concepts, or paraphrases
   - Require multiple searches, analyzing multiple related items, extracting context, then deriving the answer

### Tool Testing

8. **Questions should stress-test tool return values**
   - May elicit tools returning large JSON objects or lists, overwhelming the LLM
   - Should require understanding multiple modalities of data:
     - IDs and names
     - Timestamps and datetimes (months, days, years, seconds)
     - File IDs, names, extensions, and mimetypes
     - URLs, GIDs, etc.
   - Should probe the tool's ability to return all useful forms of data

9. **Questions should MOSTLY reflect real human use cases**
   - The kinds of information retrieval tasks that HUMANS assisted by an LLM would care about

10. **Questions may require dozens of tool calls**
    - This challenges LLMs with limited context
    - Encourages MCP server tools to reduce information returned

11. **Include ambiguous questions**
    - May be ambiguous OR require difficult decisions on which tools to call
    - Force the LLM to potentially make mistakes or misinterpret
    - Ensure that despite AMBIGUITY, there is STILL A SINGLE VERIFIABLE ANSWER

### Stability

12. **Questions must be designed so the answer DOES NOT CHANGE**
    - Do not ask questions that rely on "current state" which is dynamic
    - For example, do not count:
      - Number of reactions to a post
      - Number of replies to a thread
      - Number of members in a channel

13. **DO NOT let the MCP server RESTRICT the kinds of questions you create**
    - Create challenging and complex questions
    - Some may not be solvable with the available MCP server tools
    - Questions may require specific output formats (datetime vs. epoch time, JSON vs. MARKDOWN)
    - Questions may require dozens of tool calls to complete

## Answer Guidelines

### Verification

1. **Answers must be VERIFIABLE via direct string comparison**
   - If the answer can be re-written in many formats, clearly specify the output format in the QUESTION
   - Examples: "Use YYYY/MM/DD.", "Respond True or False.", "Answer A, B, C, or D and nothing else."
   - Answer should be a single VERIFIABLE value such as:
     - User ID, user name, display name, first name, last name
     - Channel ID, channel name
     - Message ID, string
     - URL, title
     - Numerical quantity
     - Timestamp, datetime
     - Boolean (for True/False questions)
     - Email address, phone number
     - File ID, file name, file extension
     - Multiple choice answer
   - Answers must not require special formatting or complex, structured output
   - Answer will be verified using DIRECT STRING COMPARISON

### Readability

2. **Answers should generally prefer HUMAN-READABLE formats**
   - Examples: names, first name, last name, datetime, file name, message string, URL, yes/no, true/false, a/b/c/d
   - Rather than opaque IDs (though IDs are acceptable)
   - The VAST MAJORITY of answers should be human-readable

### Stability

3. **Answers must be STABLE/STATIONARY**
   - Look at old content (e.g., conversations that have ended, projects that have launched, questions answered)
   - Create QUESTIONS based on "closed" concepts that will always return the same answer
   - Questions may ask to consider a fixed time window to insulate from non-stationary answers
   - Rely on context UNLIKELY to change
   - Example: if finding a paper name, be SPECIFIC enough so answer is not confused with papers published later

4. **Answers must be CLEAR and UNAMBIGUOUS**
   - Questions must be designed so there is a single, clear answer
   - Answer can be derived from using the MCP server tools

### Diversity

5. **Answers must be DIVERSE**
   - Answer should be a single VERIFIABLE value in diverse modalities and formats
   - User concept: user ID, user name, display name, first name, last name, email address, phone number
   - Channel concept: channel ID, channel name, channel topic
   - Message concept: message ID, message string, timestamp, month, day, year

6. **Answers must NOT be complex structures**
   - Not a list of values
   - Not a complex object
   - Not a list of IDs or strings
   - Not natural language text
   - UNLESS the answer can be straightforwardly verified using DIRECT STRING COMPARISON
   - And can be realistically reproduced
   - It should be unlikely that an LLM would return the same list in any other order or format

## Evaluation Process

### Step 1: Documentation Inspection

Read the documentation of the target API to understand:

- Available endpoints and functionality
- If ambiguity exists, fetch additional information from the web
- Parallelize this step AS MUCH AS POSSIBLE
- Ensure each subagent is ONLY examining documentation from the file system or on the web

### Step 2: Tool Inspection

List the tools available in the MCP server:

- Inspect the MCP server directly
- Understand input/output schemas, docstrings, and descriptions
- WITHOUT calling the tools themselves at this stage

### Step 3: Developing Understanding

Repeat steps 1 & 2 until you have a good understanding:

- Iterate multiple times
- Think about the kinds of tasks you want to create
- Refine your understanding
- At NO stage should you READ the code of the MCP server implementation itself
- Use your intuition and understanding to create reasonable, realistic, but VERY challenging tasks

### Step 4: Read-Only Content Inspection

After understanding the API and tools, USE the MCP server tools:

- Inspect content using READ-ONLY and NON-DESTRUCTIVE operations ONLY
- Goal: identify specific content (e.g., users, channels, messages, projects, tasks) for creating realistic questions
- Should NOT call any tools that modify state
- Will NOT read the code of the MCP server implementation itself
- Parallelize this step with individual sub-agents pursuing independent explorations
- Ensure each subagent is only performing READ-ONLY, NON-DESTRUCTIVE, and IDEMPOTENT operations
- BE CAREFUL: SOME TOOLS may return LOTS OF DATA which would cause you to run out of CONTEXT
- Make INCREMENTAL, SMALL, AND TARGETED tool calls for exploration
- In all tool call requests, use the `limit` parameter to limit results (<10)
- Use pagination

### Step 5: Task Generation

After inspecting the content, create 10 human-readable questions:

- An LLM should be able to answer these with the MCP server
- Follow all question and answer guidelines above

## Output Format

Each QA pair consists of a question and an answer. The output should be an XML file with this structure:

```xml
<evaluation>
   <qa_pair>
      <question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question>
      <answer>Website Redesign</answer>
   </qa_pair>
   <qa_pair>
      <question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question>
      <answer>sarah_dev</answer>
   </qa_pair>
   <qa_pair>
      <question>Look for pull requests that modified files in the /api directory and were merged between January 1 and January 31, 2024. How many different contributors worked on these PRs?</question>
      <answer>7</answer>
   </qa_pair>
   <qa_pair>
      <question>Find the repository with the most stars that was created before 2023. What is the repository name?</question>
      <answer>data-pipeline</answer>
   </qa_pair>
</evaluation>
```

## Evaluation Examples

### Good Questions

**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)**

```xml
<qa_pair>
   <question>Find the repository that was archived in Q3 2023 and had previously been the most forked project in the organization. What was the primary programming language used in that repository?</question>
   <answer>Python</answer>
</qa_pair>
```

This question is good because:

- Requires multiple searches to find archived repositories
- Needs to identify which had the most forks before archival
- Requires examining repository details for the language
- Answer is a simple, verifiable value
- Based on historical (closed) data that won't change

**Example 2: Requires understanding context without keyword matching (Project Management MCP)**

```xml
<qa_pair>
   <question>Locate the initiative focused on improving customer onboarding that was completed in late 2023. The project lead created a retrospective document after completion. What was the lead's role title at that time?</question>
   <answer>Product Manager</answer>
</qa_pair>
```

This question is good because:

- Doesn't use specific project name ("initiative focused on improving customer onboarding")
- Requires finding completed projects from specific timeframe
- Needs to identify the project lead and their role
- Requires understanding context from retrospective documents
- Answer is human-readable and stable
- Based on completed work (won't change)

**Example 3: Complex aggregation requiring multiple steps (Issue Tracker MCP)**

```xml
<qa_pair>
   <question>Among all bugs reported in January 2024 that were marked as critical priority, which assignee resolved the highest percentage of their assigned bugs within 48 hours? Provide the assignee's username.</question>
   <answer>alex_eng</answer>
</qa_pair>
```

This question is good because:

- Requires filtering bugs by date, priority, and status
- Needs to group by assignee and calculate resolution rates
- Requires understanding timestamps to determine 48-hour windows
- Tests pagination (potentially many bugs to process)
- Answer is a single username
- Based on historical data from specific time period

**Example 4: Requires synthesis across multiple data types (CRM MCP)**

```xml
<qa_pair>
   <question>Find the account that upgraded from the Starter to Enterprise plan in Q4 2023 and had the highest annual contract value. What industry does this account operate in?</question>
   <answer>Healthcare</answer>
</qa_pair>
```

This question is good because:

- Requires understanding subscription tier changes
- Needs to identify upgrade events in specific timeframe
- Requires comparing contract values
- Must access account industry information
- Answer is simple and verifiable
- Based on completed historical transactions

### Poor Questions

**Example 1: Answer changes over time**

```xml
<qa_pair>
   <question>How many open issues are currently assigned to the engineering team?</question>
   <answer>47</answer>
</qa_pair>
```

This question is poor because:

- The answer will change as issues are created, closed, or reassigned
- Not based on stable/stationary data
- Relies on "current state" which is dynamic

**Example 2: Too easy with keyword search**

```xml
<qa_pair>
   <question>Find the pull request with title "Add authentication feature" and tell me who created it.</question>
   <answer>developer123</answer>
</qa_pair>
```

This question is poor because:

- Can be solved with a straightforward keyword search for exact title
- Doesn't require deep exploration or understanding
- No synthesis or analysis needed

**Example 3: Ambiguous answer format**

```xml
<qa_pair>
   <question>List all the repositories that have Python as their primary language.</question>
   <answer>repo1, repo2, repo3, data-pipeline, ml-tools</answer>
</qa_pair>
```

This question is poor because:

- Answer is a list that could be returned in any order
- Difficult to verify with direct string comparison
- LLM might format differently (JSON array, comma-separated, newline-separated)
- Better to ask for a specific aggregate (count) or superlative (most stars)

## Verification Process

After creating evaluations:

1. **Examine the XML file** to understand the schema
2. **Load each task instruction** and in parallel using the MCP server and tools, identify the correct answer by attempting to solve the task YOURSELF
3. **Flag any operations** that require WRITE or DESTRUCTIVE operations
4. **Accumulate all CORRECT answers** and replace any incorrect answers in the document
5. **Remove any `<qa_pair>`** that require WRITE or DESTRUCTIVE operations

Remember to parallelize solving tasks to avoid running out of context, then accumulate all answers and make changes to the file at the end.

## Tips for Creating Quality Evaluations

1. **Think Hard and Plan Ahead** before generating tasks
2. **Parallelize Where Opportunity Arises** to speed up the process and manage context
3. **Focus on Realistic Use Cases** that humans would actually want to accomplish
4. **Create Challenging Questions** that test the limits of the MCP server's capabilities
5. **Ensure Stability** by using historical data and closed concepts
6. **Verify Answers** by solving the questions yourself using the MCP server tools
7. **Iterate and Refine** based on what you learn during the process

---

# Running Evaluations

After creating your evaluation file, you can use the provided evaluation harness to test your MCP server.

## Setup

1. **Install Dependencies**

   ```bash
   pip install -r scripts/requirements.txt
   ```

   Or install manually:

   ```bash
   pip install anthropic mcp
   ```

2. **Set API Key**

   ```bash
   export ANTHROPIC_API_KEY=your_api_key_here
   ```

## Evaluation File Format

Evaluation files use XML format with `<qa_pair>` elements:

```xml
<evaluation>
   <qa_pair>
      <question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question>
      <answer>Website Redesign</answer>
   </qa_pair>
   <qa_pair>
      <question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question>
      <answer>sarah_dev</answer>
   </qa_pair>
</evaluation>
```

## Running Evaluations

The evaluation script (`scripts/evaluation.py`) supports three transport types:

**Important:**

- **stdio transport**: The evaluation script automatically launches and manages the MCP server process for you. Do not run the server manually.
- **sse/http transports**: You must start the MCP server separately before running the evaluation. The script connects to the already-running server at the specified URL.

### 1. Local STDIO Server

For locally-run MCP servers (script launches the server automatically):

```bash
python scripts/evaluation.py \
  -t stdio \
  -c python \
  -a my_mcp_server.py \
  evaluation.xml
```

With environment variables:

```bash
python scripts/evaluation.py \
  -t stdio \
  -c python \
  -a my_mcp_server.py \
  -e API_KEY=abc123 \
  -e DEBUG=true \
  evaluation.xml
```

### 2. Server-Sent Events (SSE)

For SSE-based MCP servers (you must start the server first):

```bash
python scripts/evaluation.py \
  -t sse \
  -u https://example.com/mcp \
  -H "Authorization: Bearer token123" \
  -H "X-Custom-Header: value" \
  evaluation.xml
```

### 3. HTTP (Streamable HTTP)

For HTTP-based MCP servers (you must start the server first):

```bash
python scripts/evaluation.py \
  -t http \
  -u https://example.com/mcp \
  -H "Authorization: Bearer token123" \
  evaluation.xml
```

## Command-Line Options

```
usage: evaluation.py [-h] [-t {stdio,sse,http}] [-m MODEL] [-c COMMAND]
                     [-a ARGS [ARGS ...]] [-e ENV [ENV ...]] [-u URL]
                     [-H HEADERS [HEADERS ...]] [-o OUTPUT]
                     eval_file

positional arguments:
  eval_file             Path to evaluation XML file

optional arguments:
  -h, --help            Show help message
  -t, --transport       Transport type: stdio, sse, or http (default: stdio)
  -m, --model           codex model to use (default: codex-3-7-sonnet-20250219)
  -o, --output          Output file for report (default: print to stdout)

stdio options:
  -c, --command         Command to run MCP server (e.g., python, node)
  -a, --args            Arguments for the command (e.g., server.py)
  -e, --env             Environment variables in KEY=VALUE format

sse/http options:
  -u, --url             MCP server URL
  -H, --header          HTTP headers in 'Key: Value' format
```

## Output

The evaluation script generates a detailed report including:

- **Summary Statistics**:
  - Accuracy (correct/total)
  - Average task duration
  - Average tool calls per task
  - Total tool calls

- **Per-Task Results**:
  - Prompt and expected response
  - Actual response from the agent
  - Whether the answer was correct (✅/❌)
  - Duration and tool call details
  - Agent's summary of its approach
  - Agent's feedback on the tools

### Save Report to File

```bash
python scripts/evaluation.py \
  -t stdio \
  -c python \
  -a my_server.py \
  -o evaluation_report.md \
  evaluation.xml
```

## Complete Example Workflow

Here's a complete example of creating and running an evaluation:

1. **Create your evaluation file** (`my_evaluation.xml`):

```xml
<evaluation>
   <qa_pair>
      <question>Find the user who created the most issues in January 2024. What is their username?</question>
      <answer>alice_developer</answer>
   </qa_pair>
   <qa_pair>
      <question>Among all pull requests merged in Q1 2024, which repository had the highest number? Provide the repository name.</question>
      <answer>backend-api</answer>
   </qa_pair>
   <qa_pair>
      <question>Find the project that was completed in December 2023 and had the longest duration from start to finish. How many days did it take?</question>
      <answer>127</answer>
   </qa_pair>
</evaluation>
```

2. **Install dependencies**:

```bash
pip install -r scripts/requirements.txt
export ANTHROPIC_API_KEY=your_api_key
```

3. **Run evaluation**:

```bash
python scripts/evaluation.py \
  -t stdio \
  -c python \
  -a github_mcp_server.py \
  -e GITHUB_TOKEN=ghp_xxx \
  -o github_eval_report.md \
  my_evaluation.xml
```

4. **Review the report** in `github_eval_report.md` to:
   - See which questions passed/failed
   - Read the agent's feedback on your tools
   - Identify areas for improvement
   - Iterate on your MCP server design

## Troubleshooting

### Connection Errors

If you get connection errors:

- **STDIO**: Verify the command and arguments are correct
- **SSE/HTTP**: Check the URL is accessible and headers are correct
- Ensure any required API keys are set in environment variables or headers

### Low Accuracy

If many evaluations fail:

- Review the agent's feedback for each task
- Check if tool descriptions are clear and comprehensive
- Verify input parameters are well-documented
- Consider whether tools return too much or too little data
- Ensure error messages are actionable

### Timeout Issues

If tasks are timing out:

- Use a more capable model (e.g., `codex-3-7-sonnet-20250219`)
- Check if tools are returning too much data
- Verify pagination is working correctly
- Consider simplifying complex questions


--- .config/mcp/oraclepack-gold-pack/references/inference-first-discovery.md ---
# Inference-first discovery (Stage 1 packs)

Goal: pick the *right* 1–2 attachments per step without over-attaching, by inferring repo shape from a small set of anchors.

## Principles

- Prefer **evidence** (actual files) over assumptions.
- Start broad with **cheap, high-signal** files; only then zoom in.
- If a file/path doesn’t exist: record `Unknown` and continue.
- Keep steps self-contained: each step should succeed without relying on shared shell setup.

## Deterministic discovery order

1) **Repo identity + entrypoints**
- `README*` (first ~200 lines)
- top-level manifests (language/framework/package)
- main entrypoints (server start, CLI main, app bootstrap) if obvious from tree

2) **Configuration + environment**
- example config files
- `.env.example` or equivalent (if present)
- CI config files (to infer build/test and deploy steps)

3) **Public surface**
- routing tables / controllers / handlers
- schema/contract definitions (API specs, message schemas, type definitions)
- CLI command registration (if applicable)

4) **Data + jobs + operations**
- data models and storage adapters
- migrations directory (if present)
- background job definitions and worker entrypoints (if present)
- logging/metrics/tracing configuration (if present)

## Turning discovery into step-specific attachments

For each planned step:
- Choose 1 “definition” file (where the thing is declared).
- Optionally choose 1 “use-site” file (where the thing is used/enforced).
- If you can’t confidently pick: attach fewer files and use `reference=Unknown`.

Then write the prompt so the oracle can request missing artifact patterns when needed.

## What to do when evidence is insufficient

- Set `reference=Unknown` in the step header.
- In the prompt, explicitly ask for:
  - “the exact missing file/path pattern(s) to attach next”
- Keep attachments minimal; do not guess file paths.


--- .config/skills/oracle-pack/references/inference-first-discovery.md ---
<!-- path: ~/.codex/skills/oracle-pack/references/inference-first-discovery.md -->
# Inference-first discovery (adaptive search ladder, ck-first)

Goal: avoid wasting time on hard-targeted globs/greps that may not exist by deriving the search plan from repo evidence,
using the codebase-search rules (ck → ast-grep when structure matters → deterministic output shaping).

## Step 0 — Discover the search interface (required)

- Always run: `ck --help`
- Use only flags confirmed by `ck --help`.
- If `ck` indicates indexing/setup is needed, follow its printed instructions.

## Step 1 — Read “index” artifacts first (cheap, high-signal)

Use `fd` to locate these quickly:

- Repo docs/index:
  - `fd -p README.md`
  - `fd -e md . docs | head`
- Manifests/config (choose the ones that exist):
  - `fd -p package.json`
  - `fd -p pyproject.toml`
  - `fd -p go.mod`
  - `fd -p Cargo.toml`
  - `fd -p pom.xml`
  - `fd -p build.gradle`

Read the smallest set of files that explain structure and entrypoints.

## Step 2 — Infer subsystem locations from what you actually saw

Build a short “signals” list from evidence:
- Router/framework name (from dependencies and scripts)
- Job system
- Migration tool
- Feature flag system
- Observability stack
- Cache layer

Then: follow imports/registration code rather than searching the whole repo.

## Step 3 — Derive targeted `ck` queries (conceptual-first)

Start conceptual (meaning-based), scoped to inferred roots:

- Conceptual: `ck --sem "<intent phrase>"`
  - Examples of intent phrases (adapt to the repo’s vocabulary):
    - “route registration” / “router setup”
    - “auth middleware” / “permission check”
    - “queue worker registration”
    - “migration runner” / “schema change”
    - “feature flag evaluation”
    - “telemetry initialization” / “logger setup”

When unsure, use hybrid:
- `ck --hybrid "<intent phrase>"`

For literal hits (exact tokens you already saw), use regex:
- `ck --regex "<literal-or-regex>"`

### Deterministic narrowing

If results are broad/noisy, narrow deterministically:

- Use `ck --jsonl` (if available) and cap results:
  - `ck --jsonl ... | jq ... | sort | head -n 20`
- Otherwise cap with `head` and re-run `ck` constrained to the top-hit directories/files.

## Step 4 — Structural search with `ast-grep` (when structure matters)

Use `ast-grep` when you need syntax-aware matching (AST-level), such as:
- finding a specific call pattern (e.g., permission checks around handlers)
- finding registration shapes (e.g., route definitions)
- reducing false positives vs text search

Examples:

- Find code structure:
  - `ast-grep --lang <language> -p '<pattern>'`
- List matching files (cap output):
  - `ast-grep -l --lang <language> -p '<pattern>' | head -n 10`

Use `ck` first to locate candidate files/modules, then `ast-grep` to enforce structure inside those.

## Step 5 — Progressive widening (fallback ladder)

If inference cannot locate a subsystem:

1) Run a small set of conceptual `ck --sem` / `ck --hybrid` queries using generic intent phrases.
2) If still nothing, broaden to repo-wide `ck --hybrid` for that intent.
3) If still nothing, mark evidence for that category as missing and record the most likely missing artifact pattern.

## Step 6 — Early stopping (don’t over-search)

Stop harvesting once:
- you have >= 20 candidate anchors total, and
- every required category has at least one candidate (or a clearly documented “missing artifact pattern”)

Then generate questions; do not keep scanning “just in case”.


--- .config/skills/strategist-questions-oracle-pack/references/inference-first-discovery.md ---
<!-- path: ~/.codex/skills/strategist-questions-oracle-pack/references/inference-first-discovery.md -->
# Inference-first discovery (adaptive search ladder, ck-first)

Goal: avoid wasting time on hard-targeted globs/greps that may not exist by deriving the search plan from repo evidence,
using the codebase-search rules (ck → ast-grep when structure matters → deterministic output shaping).

## Step 0 — Discover the search interface (required)

- Always run: `ck --help`
- Use only flags confirmed by `ck --help`.
- If `ck` indicates indexing/setup is needed, follow its printed instructions.

## Step 1 — Read “index” artifacts first (cheap, high-signal)

Use `fd` to locate these quickly:

- Repo docs/index:
  - `fd -p README.md`
  - `fd -e md . docs | head`
- Manifests/config (choose the ones that exist):
  - `fd -p package.json`
  - `fd -p pyproject.toml`
  - `fd -p go.mod`
  - `fd -p Cargo.toml`
  - `fd -p pom.xml`
  - `fd -p build.gradle`

Read the smallest set of files that explain structure and entrypoints.

## Step 2 — Infer subsystem locations from what you actually saw

Build a short “signals” list from evidence:
- Router/framework name (from dependencies and scripts)
- Job system
- Migration tool
- Feature flag system
- Observability stack
- Cache layer

Then: follow imports/registration code rather than searching the whole repo.

## Step 3 — Derive targeted `ck` queries (conceptual-first)

Start conceptual (meaning-based), scoped to inferred roots:

- Conceptual: `ck --sem "<intent phrase>"`
  - Examples of intent phrases (adapt to the repo’s vocabulary):
    - “route registration” / “router setup”
    - “auth middleware” / “permission check”
    - “queue worker registration”
    - “migration runner” / “schema change”
    - “feature flag evaluation”
    - “telemetry initialization” / “logger setup”

When unsure, use hybrid:
- `ck --hybrid "<intent phrase>"`

For literal hits (exact tokens you already saw), use regex:
- `ck --regex "<literal-or-regex>"`

### Deterministic narrowing

If results are broad/noisy, narrow deterministically:

- Use `ck --jsonl` (if available) and cap results:
  - `ck --jsonl ... | jq ... | sort | head -n 20`
- Otherwise cap with `head` and re-run `ck` constrained to the top-hit directories/files.

## Step 4 — Structural search with `ast-grep` (when structure matters)

Use `ast-grep` when you need syntax-aware matching (AST-level), such as:
- finding a specific call pattern (e.g., permission checks around handlers)
- finding registration shapes (e.g., route definitions)
- reducing false positives vs text search

Examples:

- Find code structure:
  - `ast-grep --lang <language> -p '<pattern>'`
- List matching files (cap output):
  - `ast-grep -l --lang <language> -p '<pattern>' | head -n 10`

Use `ck` first to locate candidate files/modules, then `ast-grep` to enforce structure inside those.

## Step 5 — Progressive widening (fallback ladder)

If inference cannot locate a subsystem:

1) Run a small set of conceptual `ck --sem` / `ck --hybrid` queries using generic intent phrases.
2) If still nothing, broaden to repo-wide `ck --hybrid` for that intent.
3) If still nothing, mark evidence for that category as missing and record the most likely missing artifact pattern.

## Step 6 — Early stopping (don’t over-search)

Stop harvesting once:
- you have >= 20 candidate anchors total, and
- every required category has at least one candidate (or a clearly documented “missing artifact pattern”)

Then generate questions; do not keep scanning “just in case”.


--- .config/mcp/mcp-builder/reference/mcp_best_practices.md ---
# MCP Server Best Practices

## Quick Reference

### Server Naming
- **Python**: `{service}_mcp` (e.g., `slack_mcp`)
- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`)

### Tool Naming
- Use snake_case with service prefix
- Format: `{service}_{action}_{resource}`
- Example: `slack_send_message`, `github_create_issue`

### Response Formats
- Support both JSON and Markdown formats
- JSON for programmatic processing
- Markdown for human readability

### Pagination
- Always respect `limit` parameter
- Return `has_more`, `next_offset`, `total_count`
- Default to 20-50 items

### Transport
- **Streamable HTTP**: For remote servers, multi-client scenarios
- **stdio**: For local integrations, command-line tools
- Avoid SSE (deprecated in favor of streamable HTTP)

---

## Server Naming Conventions

Follow these standardized naming patterns:

**Python**: Use format `{service}_mcp` (lowercase with underscores)
- Examples: `slack_mcp`, `github_mcp`, `jira_mcp`

**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens)
- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server`

The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers.

---

## Tool Naming and Design

### Tool Naming

1. **Use snake_case**: `search_users`, `create_project`, `get_channel_info`
2. **Include service prefix**: Anticipate that your MCP server may be used alongside other MCP servers
   - Use `slack_send_message` instead of just `send_message`
   - Use `github_create_issue` instead of just `create_issue`
3. **Be action-oriented**: Start with verbs (get, list, search, create, etc.)
4. **Be specific**: Avoid generic names that could conflict with other servers

### Tool Design

- Tool descriptions must narrowly and unambiguously describe functionality
- Descriptions must precisely match actual functionality
- Provide tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
- Keep tool operations focused and atomic

---

## Response Formats

All tools that return data should support multiple formats:

### JSON Format (`response_format="json"`)
- Machine-readable structured data
- Include all available fields and metadata
- Consistent field names and types
- Use for programmatic processing

### Markdown Format (`response_format="markdown"`, typically default)
- Human-readable formatted text
- Use headers, lists, and formatting for clarity
- Convert timestamps to human-readable format
- Show display names with IDs in parentheses
- Omit verbose metadata

---

## Pagination

For tools that list resources:

- **Always respect the `limit` parameter**
- **Implement pagination**: Use `offset` or cursor-based pagination
- **Return pagination metadata**: Include `has_more`, `next_offset`/`next_cursor`, `total_count`
- **Never load all results into memory**: Especially important for large datasets
- **Default to reasonable limits**: 20-50 items is typical

Example pagination response:
```json
{
  "total": 150,
  "count": 20,
  "offset": 0,
  "items": [...],
  "has_more": true,
  "next_offset": 20
}
```

---

## Transport Options

### Streamable HTTP

**Best for**: Remote servers, web services, multi-client scenarios

**Characteristics**:
- Bidirectional communication over HTTP
- Supports multiple simultaneous clients
- Can be deployed as a web service
- Enables server-to-client notifications

**Use when**:
- Serving multiple clients simultaneously
- Deploying as a cloud service
- Integration with web applications

### stdio

**Best for**: Local integrations, command-line tools

**Characteristics**:
- Standard input/output stream communication
- Simple setup, no network configuration needed
- Runs as a subprocess of the client

**Use when**:
- Building tools for local development environments
- Integrating with desktop applications
- Single-user, single-session scenarios

**Note**: stdio servers should NOT log to stdout (use stderr for logging)

### Transport Selection

| Criterion | stdio | Streamable HTTP |
|-----------|-------|-----------------|
| **Deployment** | Local | Remote |
| **Clients** | Single | Multiple |
| **Complexity** | Low | Medium |
| **Real-time** | No | Yes |

---

## Security Best Practices

### Authentication and Authorization

**OAuth 2.1**:
- Use secure OAuth 2.1 with certificates from recognized authorities
- Validate access tokens before processing requests
- Only accept tokens specifically intended for your server

**API Keys**:
- Store API keys in environment variables, never in code
- Validate keys on server startup
- Provide clear error messages when authentication fails

### Input Validation

- Sanitize file paths to prevent directory traversal
- Validate URLs and external identifiers
- Check parameter sizes and ranges
- Prevent command injection in system calls
- Use schema validation (Pydantic/Zod) for all inputs

### Error Handling

- Don't expose internal errors to clients
- Log security-relevant errors server-side
- Provide helpful but not revealing error messages
- Clean up resources after errors

### DNS Rebinding Protection

For streamable HTTP servers running locally:
- Enable DNS rebinding protection
- Validate the `Origin` header on all incoming connections
- Bind to `127.0.0.1` rather than `0.0.0.0`

---

## Tool Annotations

Provide annotations to help clients understand tool behavior:

| Annotation | Type | Default | Description |
|-----------|------|---------|-------------|
| `readOnlyHint` | boolean | false | Tool does not modify its environment |
| `destructiveHint` | boolean | true | Tool may perform destructive updates |
| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect |
| `openWorldHint` | boolean | true | Tool interacts with external entities |

**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations.

---

## Error Handling

- Use standard JSON-RPC error codes
- Report tool errors within result objects (not protocol-level errors)
- Provide helpful, specific error messages with suggested next steps
- Don't expose internal implementation details
- Clean up resources properly on errors

Example error handling:
```typescript
try {
  const result = performOperation();
  return { content: [{ type: "text", text: result }] };
} catch (error) {
  return {
    isError: true,
    content: [{
      type: "text",
      text: `Error: ${error.message}. Try using filter='active_only' to reduce results.`
    }]
  };
}
```

---

## Testing Requirements

Comprehensive testing should cover:

- **Functional testing**: Verify correct execution with valid/invalid inputs
- **Integration testing**: Test interaction with external systems
- **Security testing**: Validate auth, input sanitization, rate limiting
- **Performance testing**: Check behavior under load, timeouts
- **Error handling**: Ensure proper error reporting and cleanup

---

## Documentation Requirements

- Provide clear documentation of all tools and capabilities
- Include working examples (at least 3 per major feature)
- Document security considerations
- Specify required permissions and access levels
- Document rate limits and performance characteristics


--- .config/mcp/mcp-builder/reference/node_mcp_server.md ---
# Node/TypeScript MCP Server Implementation Guide

## Overview

This document provides Node/TypeScript-specific best practices and examples for implementing MCP servers using the MCP TypeScript SDK. It covers project structure, server setup, tool registration patterns, input validation with Zod, error handling, and complete working examples.

---

## Quick Reference

### Key Imports
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import express from "express";
import { z } from "zod";
```

### Server Initialization
```typescript
const server = new McpServer({
  name: "service-mcp-server",
  version: "1.0.0"
});
```

### Tool Registration Pattern
```typescript
server.registerTool(
  "tool_name",
  {
    title: "Tool Display Name",
    description: "What the tool does",
    inputSchema: { param: z.string() },
    outputSchema: { result: z.string() }
  },
  async ({ param }) => {
    const output = { result: `Processed: ${param}` };
    return {
      content: [{ type: "text", text: JSON.stringify(output) }],
      structuredContent: output // Modern pattern for structured data
    };
  }
);
```

---

## MCP TypeScript SDK

The official MCP TypeScript SDK provides:
- `McpServer` class for server initialization
- `registerTool` method for tool registration
- Zod schema integration for runtime input validation
- Type-safe tool handler implementations

**IMPORTANT - Use Modern APIs Only:**
- **DO use**: `server.registerTool()`, `server.registerResource()`, `server.registerPrompt()`
- **DO NOT use**: Old deprecated APIs such as `server.tool()`, `server.setRequestHandler(ListToolsRequestSchema, ...)`, or manual handler registration
- The `register*` methods provide better type safety, automatic schema handling, and are the recommended approach

See the MCP SDK documentation in the references for complete details.

## Server Naming Convention

Node/TypeScript MCP servers must follow this naming pattern:
- **Format**: `{service}-mcp-server` (lowercase with hyphens)
- **Examples**: `github-mcp-server`, `jira-mcp-server`, `stripe-mcp-server`

The name should be:
- General (not tied to specific features)
- Descriptive of the service/API being integrated
- Easy to infer from the task description
- Without version numbers or dates

## Project Structure

Create the following structure for Node/TypeScript MCP servers:

```
{service}-mcp-server/
├── package.json
├── tsconfig.json
├── README.md
├── src/
│   ├── index.ts          # Main entry point with McpServer initialization
│   ├── types.ts          # TypeScript type definitions and interfaces
│   ├── tools/            # Tool implementations (one file per domain)
│   ├── services/         # API clients and shared utilities
│   ├── schemas/          # Zod validation schemas
│   └── constants.ts      # Shared constants (API_URL, CHARACTER_LIMIT, etc.)
└── dist/                 # Built JavaScript files (entry point: dist/index.js)
```

## Tool Implementation

### Tool Naming

Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names.

**Avoid Naming Conflicts**: Include the service context to prevent overlaps:
- Use "slack_send_message" instead of just "send_message"
- Use "github_create_issue" instead of just "create_issue"
- Use "asana_list_tasks" instead of just "list_tasks"

### Tool Structure

Tools are registered using the `registerTool` method with the following requirements:
- Use Zod schemas for runtime input validation and type safety
- The `description` field must be explicitly provided - JSDoc comments are NOT automatically extracted
- Explicitly provide `title`, `description`, `inputSchema`, and `annotations`
- The `inputSchema` must be a Zod schema object (not a JSON schema)
- Type all parameters and return values explicitly

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({
  name: "example-mcp",
  version: "1.0.0"
});

// Zod schema for input validation
const UserSearchInputSchema = z.object({
  query: z.string()
    .min(2, "Query must be at least 2 characters")
    .max(200, "Query must not exceed 200 characters")
    .describe("Search string to match against names/emails"),
  limit: z.number()
    .int()
    .min(1)
    .max(100)
    .default(20)
    .describe("Maximum results to return"),
  offset: z.number()
    .int()
    .min(0)
    .default(0)
    .describe("Number of results to skip for pagination"),
  response_format: z.nativeEnum(ResponseFormat)
    .default(ResponseFormat.MARKDOWN)
    .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
}).strict();

// Type definition from Zod schema
type UserSearchInput = z.infer<typeof UserSearchInputSchema>;

server.registerTool(
  "example_search_users",
  {
    title: "Search Example Users",
    description: `Search for users in the Example system by name, email, or team.

This tool searches across all user profiles in the Example platform, supporting partial matches and various search filters. It does NOT create or modify users, only searches existing ones.

Args:
  - query (string): Search string to match against names/emails
  - limit (number): Maximum results to return, between 1-100 (default: 20)
  - offset (number): Number of results to skip for pagination (default: 0)
  - response_format ('markdown' | 'json'): Output format (default: 'markdown')

Returns:
  For JSON format: Structured data with schema:
  {
    "total": number,           // Total number of matches found
    "count": number,           // Number of results in this response
    "offset": number,          // Current pagination offset
    "users": [
      {
        "id": string,          // User ID (e.g., "U123456789")
        "name": string,        // Full name (e.g., "John Doe")
        "email": string,       // Email address
        "team": string,        // Team name (optional)
        "active": boolean      // Whether user is active
      }
    ],
    "has_more": boolean,       // Whether more results are available
    "next_offset": number      // Offset for next page (if has_more is true)
  }

Examples:
  - Use when: "Find all marketing team members" -> params with query="team:marketing"
  - Use when: "Search for John's account" -> params with query="john"
  - Don't use when: You need to create a user (use example_create_user instead)

Error Handling:
  - Returns "Error: Rate limit exceeded" if too many requests (429 status)
  - Returns "No users found matching '<query>'" if search returns empty`,
    inputSchema: UserSearchInputSchema,
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      idempotentHint: true,
      openWorldHint: true
    }
  },
  async (params: UserSearchInput) => {
    try {
      // Input validation is handled by Zod schema
      // Make API request using validated parameters
      const data = await makeApiRequest<any>(
        "users/search",
        "GET",
        undefined,
        {
          q: params.query,
          limit: params.limit,
          offset: params.offset
        }
      );

      const users = data.users || [];
      const total = data.total || 0;

      if (!users.length) {
        return {
          content: [{
            type: "text",
            text: `No users found matching '${params.query}'`
          }]
        };
      }

      // Prepare structured output
      const output = {
        total,
        count: users.length,
        offset: params.offset,
        users: users.map((user: any) => ({
          id: user.id,
          name: user.name,
          email: user.email,
          ...(user.team ? { team: user.team } : {}),
          active: user.active ?? true
        })),
        has_more: total > params.offset + users.length,
        ...(total > params.offset + users.length ? {
          next_offset: params.offset + users.length
        } : {})
      };

      // Format text representation based on requested format
      let textContent: string;
      if (params.response_format === ResponseFormat.MARKDOWN) {
        const lines = [`# User Search Results: '${params.query}'`, "",
          `Found ${total} users (showing ${users.length})`, ""];
        for (const user of users) {
          lines.push(`## ${user.name} (${user.id})`);
          lines.push(`- **Email**: ${user.email}`);
          if (user.team) lines.push(`- **Team**: ${user.team}`);
          lines.push("");
        }
        textContent = lines.join("\n");
      } else {
        textContent = JSON.stringify(output, null, 2);
      }

      return {
        content: [{ type: "text", text: textContent }],
        structuredContent: output // Modern pattern for structured data
      };
    } catch (error) {
      return {
        content: [{
          type: "text",
          text: handleApiError(error)
        }]
      };
    }
  }
);
```

## Zod Schemas for Input Validation

Zod provides runtime type validation:

```typescript
import { z } from "zod";

// Basic schema with validation
const CreateUserSchema = z.object({
  name: z.string()
    .min(1, "Name is required")
    .max(100, "Name must not exceed 100 characters"),
  email: z.string()
    .email("Invalid email format"),
  age: z.number()
    .int("Age must be a whole number")
    .min(0, "Age cannot be negative")
    .max(150, "Age cannot be greater than 150")
}).strict();  // Use .strict() to forbid extra fields

// Enums
enum ResponseFormat {
  MARKDOWN = "markdown",
  JSON = "json"
}

const SearchSchema = z.object({
  response_format: z.nativeEnum(ResponseFormat)
    .default(ResponseFormat.MARKDOWN)
    .describe("Output format")
});

// Optional fields with defaults
const PaginationSchema = z.object({
  limit: z.number()
    .int()
    .min(1)
    .max(100)
    .default(20)
    .describe("Maximum results to return"),
  offset: z.number()
    .int()
    .min(0)
    .default(0)
    .describe("Number of results to skip")
});
```

## Response Format Options

Support multiple output formats for flexibility:

```typescript
enum ResponseFormat {
  MARKDOWN = "markdown",
  JSON = "json"
}

const inputSchema = z.object({
  query: z.string(),
  response_format: z.nativeEnum(ResponseFormat)
    .default(ResponseFormat.MARKDOWN)
    .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
});
```

**Markdown format**:
- Use headers, lists, and formatting for clarity
- Convert timestamps to human-readable format
- Show display names with IDs in parentheses
- Omit verbose metadata
- Group related information logically

**JSON format**:
- Return complete, structured data suitable for programmatic processing
- Include all available fields and metadata
- Use consistent field names and types

## Pagination Implementation

For tools that list resources:

```typescript
const ListSchema = z.object({
  limit: z.number().int().min(1).max(100).default(20),
  offset: z.number().int().min(0).default(0)
});

async function listItems(params: z.infer<typeof ListSchema>) {
  const data = await apiRequest(params.limit, params.offset);

  const response = {
    total: data.total,
    count: data.items.length,
    offset: params.offset,
    items: data.items,
    has_more: data.total > params.offset + data.items.length,
    next_offset: data.total > params.offset + data.items.length
      ? params.offset + data.items.length
      : undefined
  };

  return JSON.stringify(response, null, 2);
}
```

## Character Limits and Truncation

Add a CHARACTER_LIMIT constant to prevent overwhelming responses:

```typescript
// At module level in constants.ts
export const CHARACTER_LIMIT = 25000;  // Maximum response size in characters

async function searchTool(params: SearchInput) {
  let result = generateResponse(data);

  // Check character limit and truncate if needed
  if (result.length > CHARACTER_LIMIT) {
    const truncatedData = data.slice(0, Math.max(1, data.length / 2));
    response.data = truncatedData;
    response.truncated = true;
    response.truncation_message =
      `Response truncated from ${data.length} to ${truncatedData.length} items. ` +
      `Use 'offset' parameter or add filters to see more results.`;
    result = JSON.stringify(response, null, 2);
  }

  return result;
}
```

## Error Handling

Provide clear, actionable error messages:

```typescript
import axios, { AxiosError } from "axios";

function handleApiError(error: unknown): string {
  if (error instanceof AxiosError) {
    if (error.response) {
      switch (error.response.status) {
        case 404:
          return "Error: Resource not found. Please check the ID is correct.";
        case 403:
          return "Error: Permission denied. You don't have access to this resource.";
        case 429:
          return "Error: Rate limit exceeded. Please wait before making more requests.";
        default:
          return `Error: API request failed with status ${error.response.status}`;
      }
    } else if (error.code === "ECONNABORTED") {
      return "Error: Request timed out. Please try again.";
    }
  }
  return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`;
}
```

## Shared Utilities

Extract common functionality into reusable functions:

```typescript
// Shared API request function
async function makeApiRequest<T>(
  endpoint: string,
  method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
  data?: any,
  params?: any
): Promise<T> {
  try {
    const response = await axios({
      method,
      url: `${API_BASE_URL}/${endpoint}`,
      data,
      params,
      timeout: 30000,
      headers: {
        "Content-Type": "application/json",
        "Accept": "application/json"
      }
    });
    return response.data;
  } catch (error) {
    throw error;
  }
}
```

## Async/Await Best Practices

Always use async/await for network requests and I/O operations:

```typescript
// Good: Async network request
async function fetchData(resourceId: string): Promise<ResourceData> {
  const response = await axios.get(`${API_URL}/resource/${resourceId}`);
  return response.data;
}

// Bad: Promise chains
function fetchData(resourceId: string): Promise<ResourceData> {
  return axios.get(`${API_URL}/resource/${resourceId}`)
    .then(response => response.data);  // Harder to read and maintain
}
```

## TypeScript Best Practices

1. **Use Strict TypeScript**: Enable strict mode in tsconfig.json
2. **Define Interfaces**: Create clear interface definitions for all data structures
3. **Avoid `any`**: Use proper types or `unknown` instead of `any`
4. **Zod for Runtime Validation**: Use Zod schemas to validate external data
5. **Type Guards**: Create type guard functions for complex type checking
6. **Error Handling**: Always use try-catch with proper error type checking
7. **Null Safety**: Use optional chaining (`?.`) and nullish coalescing (`??`)

```typescript
// Good: Type-safe with Zod and interfaces
interface UserResponse {
  id: string;
  name: string;
  email: string;
  team?: string;
  active: boolean;
}

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
  team: z.string().optional(),
  active: z.boolean()
});

type User = z.infer<typeof UserSchema>;

async function getUser(id: string): Promise<User> {
  const data = await apiCall(`/users/${id}`);
  return UserSchema.parse(data);  // Runtime validation
}

// Bad: Using any
async function getUser(id: string): Promise<any> {
  return await apiCall(`/users/${id}`);  // No type safety
}
```

## Package Configuration

### package.json

```json
{
  "name": "{service}-mcp-server",
  "version": "1.0.0",
  "description": "MCP server for {Service} API integration",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "start": "node dist/index.js",
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "clean": "rm -rf dist"
  },
  "engines": {
    "node": ">=18"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.6.1",
    "axios": "^1.7.9",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^22.10.0",
    "tsx": "^4.19.2",
    "typescript": "^5.7.2"
  }
}
```

### tsconfig.json

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "allowSyntheticDefaultImports": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
```

## Complete Example

```typescript
#!/usr/bin/env node
/**
 * MCP Server for Example Service.
 *
 * This server provides tools to interact with Example API, including user search,
 * project management, and data export capabilities.
 */

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import axios, { AxiosError } from "axios";

// Constants
const API_BASE_URL = "https://api.example.com/v1";
const CHARACTER_LIMIT = 25000;

// Enums
enum ResponseFormat {
  MARKDOWN = "markdown",
  JSON = "json"
}

// Zod schemas
const UserSearchInputSchema = z.object({
  query: z.string()
    .min(2, "Query must be at least 2 characters")
    .max(200, "Query must not exceed 200 characters")
    .describe("Search string to match against names/emails"),
  limit: z.number()
    .int()
    .min(1)
    .max(100)
    .default(20)
    .describe("Maximum results to return"),
  offset: z.number()
    .int()
    .min(0)
    .default(0)
    .describe("Number of results to skip for pagination"),
  response_format: z.nativeEnum(ResponseFormat)
    .default(ResponseFormat.MARKDOWN)
    .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
}).strict();

type UserSearchInput = z.infer<typeof UserSearchInputSchema>;

// Shared utility functions
async function makeApiRequest<T>(
  endpoint: string,
  method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
  data?: any,
  params?: any
): Promise<T> {
  try {
    const response = await axios({
      method,
      url: `${API_BASE_URL}/${endpoint}`,
      data,
      params,
      timeout: 30000,
      headers: {
        "Content-Type": "application/json",
        "Accept": "application/json"
      }
    });
    return response.data;
  } catch (error) {
    throw error;
  }
}

function handleApiError(error: unknown): string {
  if (error instanceof AxiosError) {
    if (error.response) {
      switch (error.response.status) {
        case 404:
          return "Error: Resource not found. Please check the ID is correct.";
        case 403:
          return "Error: Permission denied. You don't have access to this resource.";
        case 429:
          return "Error: Rate limit exceeded. Please wait before making more requests.";
        default:
          return `Error: API request failed with status ${error.response.status}`;
      }
    } else if (error.code === "ECONNABORTED") {
      return "Error: Request timed out. Please try again.";
    }
  }
  return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`;
}

// Create MCP server instance
const server = new McpServer({
  name: "example-mcp",
  version: "1.0.0"
});

// Register tools
server.registerTool(
  "example_search_users",
  {
    title: "Search Example Users",
    description: `[Full description as shown above]`,
    inputSchema: UserSearchInputSchema,
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      idempotentHint: true,
      openWorldHint: true
    }
  },
  async (params: UserSearchInput) => {
    // Implementation as shown above
  }
);

// Main function
// For stdio (local):
async function runStdio() {
  if (!process.env.EXAMPLE_API_KEY) {
    console.error("ERROR: EXAMPLE_API_KEY environment variable is required");
    process.exit(1);
  }

  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP server running via stdio");
}

// For streamable HTTP (remote):
async function runHTTP() {
  if (!process.env.EXAMPLE_API_KEY) {
    console.error("ERROR: EXAMPLE_API_KEY environment variable is required");
    process.exit(1);
  }

  const app = express();
  app.use(express.json());

  app.post('/mcp', async (req, res) => {
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
      enableJsonResponse: true
    });
    res.on('close', () => transport.close());
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  });

  const port = parseInt(process.env.PORT || '3000');
  app.listen(port, () => {
    console.error(`MCP server running on http://localhost:${port}/mcp`);
  });
}

// Choose transport based on environment
const transport = process.env.TRANSPORT || 'stdio';
if (transport === 'http') {
  runHTTP().catch(error => {
    console.error("Server error:", error);
    process.exit(1);
  });
} else {
  runStdio().catch(error => {
    console.error("Server error:", error);
    process.exit(1);
  });
}
```

---

## Advanced MCP Features

### Resource Registration

Expose data as resources for efficient, URI-based access:

```typescript
import { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js";

// Register a resource with URI template
server.registerResource(
  {
    uri: "file://documents/{name}",
    name: "Document Resource",
    description: "Access documents by name",
    mimeType: "text/plain"
  },
  async (uri: string) => {
    // Extract parameter from URI
    const match = uri.match(/^file:\/\/documents\/(.+)$/);
    if (!match) {
      throw new Error("Invalid URI format");
    }

    const documentName = match[1];
    const content = await loadDocument(documentName);

    return {
      contents: [{
        uri,
        mimeType: "text/plain",
        text: content
      }]
    };
  }
);

// List available resources dynamically
server.registerResourceList(async () => {
  const documents = await getAvailableDocuments();
  return {
    resources: documents.map(doc => ({
      uri: `file://documents/${doc.name}`,
      name: doc.name,
      mimeType: "text/plain",
      description: doc.description
    }))
  };
});
```

**When to use Resources vs Tools:**
- **Resources**: For data access with simple URI-based parameters
- **Tools**: For complex operations requiring validation and business logic
- **Resources**: When data is relatively static or template-based
- **Tools**: When operations have side effects or complex workflows

### Transport Options

The TypeScript SDK supports two main transport mechanisms:

#### Streamable HTTP (Recommended for Remote Servers)

```typescript
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";

const app = express();
app.use(express.json());

app.post('/mcp', async (req, res) => {
  // Create new transport for each request (stateless, prevents request ID collisions)
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
    enableJsonResponse: true
  });

  res.on('close', () => transport.close());

  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000);
```

#### stdio (For Local Integrations)

```typescript
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const transport = new StdioServerTransport();
await server.connect(transport);
```

**Transport selection:**
- **Streamable HTTP**: Web services, remote access, multiple clients
- **stdio**: Command-line tools, local development, subprocess integration

### Notification Support

Notify clients when server state changes:

```typescript
// Notify when tools list changes
server.notification({
  method: "notifications/tools/list_changed"
});

// Notify when resources change
server.notification({
  method: "notifications/resources/list_changed"
});
```

Use notifications sparingly - only when server capabilities genuinely change.

---

## Code Best Practices

### Code Composability and Reusability

Your implementation MUST prioritize composability and code reuse:

1. **Extract Common Functionality**:
   - Create reusable helper functions for operations used across multiple tools
   - Build shared API clients for HTTP requests instead of duplicating code
   - Centralize error handling logic in utility functions
   - Extract business logic into dedicated functions that can be composed
   - Extract shared markdown or JSON field selection & formatting functionality

2. **Avoid Duplication**:
   - NEVER copy-paste similar code between tools
   - If you find yourself writing similar logic twice, extract it into a function
   - Common operations like pagination, filtering, field selection, and formatting should be shared
   - Authentication/authorization logic should be centralized

## Building and Running

Always build your TypeScript code before running:

```bash
# Build the project
npm run build

# Run the server
npm start

# Development with auto-reload
npm run dev
```

Always ensure `npm run build` completes successfully before considering the implementation complete.

## Quality Checklist

Before finalizing your Node/TypeScript MCP server implementation, ensure:

### Strategic Design
- [ ] Tools enable complete workflows, not just API endpoint wrappers
- [ ] Tool names reflect natural task subdivisions
- [ ] Response formats optimize for agent context efficiency
- [ ] Human-readable identifiers used where appropriate
- [ ] Error messages guide agents toward correct usage

### Implementation Quality
- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented
- [ ] All tools registered using `registerTool` with complete configuration
- [ ] All tools include `title`, `description`, `inputSchema`, and `annotations`
- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
- [ ] All tools use Zod schemas for runtime input validation with `.strict()` enforcement
- [ ] All Zod schemas have proper constraints and descriptive error messages
- [ ] All tools have comprehensive descriptions with explicit input/output types
- [ ] Descriptions include return value examples and complete schema documentation
- [ ] Error messages are clear, actionable, and educational

### TypeScript Quality
- [ ] TypeScript interfaces are defined for all data structures
- [ ] Strict TypeScript is enabled in tsconfig.json
- [ ] No use of `any` type - use `unknown` or proper types instead
- [ ] All async functions have explicit Promise<T> return types
- [ ] Error handling uses proper type guards (e.g., `axios.isAxiosError`, `z.ZodError`)

### Advanced Features (where applicable)
- [ ] Resources registered for appropriate data endpoints
- [ ] Appropriate transport configured (stdio or streamable HTTP)
- [ ] Notifications implemented for dynamic server capabilities
- [ ] Type-safe with SDK interfaces

### Project Configuration
- [ ] Package.json includes all necessary dependencies
- [ ] Build script produces working JavaScript in dist/ directory
- [ ] Main entry point is properly configured as dist/index.js
- [ ] Server name follows format: `{service}-mcp-server`
- [ ] tsconfig.json properly configured with strict mode

### Code Quality
- [ ] Pagination is properly implemented where applicable
- [ ] Large responses check CHARACTER_LIMIT constant and truncate with clear messages
- [ ] Filtering options are provided for potentially large result sets
- [ ] All network operations handle timeouts and connection errors gracefully
- [ ] Common functionality is extracted into reusable functions
- [ ] Return types are consistent across similar operations

### Testing and Build
- [ ] `npm run build` completes successfully without errors
- [ ] dist/index.js created and executable
- [ ] Server runs: `node dist/index.js --help`
- [ ] All imports resolve correctly
- [ ] Sample tool calls work as expected

--- README.md ---
# oraclepack

<p align="center">
  <a href="https://github.com/acidicsoil/oraclepack/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/acidicsoil/oraclepack/actions/workflows/ci.yml/badge.svg" /></a>
  <a href="https://github.com/acidicsoil/oraclepack/actions/workflows/release.yml"><img alt="Release" src="https://github.com/acidicsoil/oraclepack/actions/workflows/release.yml/badge.svg" /></a>
  <a href="https://github.com/acidicsoil/oraclepack/releases/latest"><img alt="Release Version" src="https://img.shields.io/github/v/release/acidicsoil/oraclepack?sort=semver" /></a>
  <a href="https://github.com/acidicsoil/oraclepack/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/acidicsoil/oraclepack" /></a>
  <a href="https://github.com/acidicsoil/oraclepack/blob/main/go.mod"><img alt="Go Version" src="https://img.shields.io/github/go-mod/go-version/acidicsoil/oraclepack" /></a>
</p>

`oraclepack` is a polished, TUI-driven wrapper/runner for **Oracle Packs**—interactive bash workflows embedded in Markdown utilizing [oracle](https://github.com/steipete/oracle). It lets teams ship runbooks, audits, migrations, and LLM evaluation scripts as self-documenting `.md` files that can be validated, resumed, and executed with real-time feedback.

## 🎯 Project Scope & Purpose

Oraclepack is built to make **multi-step operational workflows** reproducible and safe:

- **Runbooks you can execute**: keep instructions and commands in one Markdown file.
- **LLM evaluation flows**: wrap `oracle` CLI calls in steps and validate them with dry runs.
- **Team-friendly automation**: share a pack as documentation, then execute it as a guided TUI.
- **Repeatable ops**: state + report files make it easy to resume or audit past runs.

## 🚀 Features

- **Interactive TUI**: browse steps, view live output, and manage execution with keyboard shortcuts.
- **Run All / Resume**: execute sequentially or continue from the last successful step.
- **Overrides Wizard**: select which steps receive extra official `oracle` flags and validate via dry-run.
- **Step Preview**: view a full step (no truncation), toggle wrap, and copy contents.
- **ROI Filtering**: include/exclude steps by ROI with over/under mode.
- **Project URL Management**: save ChatGPT project URLs globally or per project and pick quickly.
- **State + Report Files**: persistent `.state.json` and `.report.json` outputs for traceability.
- **Plain Mode**: run without TUI for CI or automated environments.
- **Markdown Native**: packs live in a single `.md` file with a bash block.

## 📦 Installation

### Install oracle

Install oracle and setup/configure. Get it [here!](https://github.com/steipete/oracle)

### Setting up for creating oracle-packs

### From Source

Ensure you have [Go](https://go.dev/) 1.24+ installed:

```bash
git clone https://github.com/user/oraclepack.git
cd oraclepack
go build -o oraclepack ./cmd/oraclepack
```

### Building for Windows (.exe)

#### On Windows (PowerShell/CMD)

```powershell
go build -o oraclepack.exe ./cmd/oraclepack
```

#### Cross-Compiling for Windows (from Linux/macOS)

```bash
GOOS=windows GOARCH=amd64 go build -o oraclepack.exe ./cmd/oraclepack
```

### Global Installation (Run from Anywhere)

To run `oraclepack` from any directory, move the binary to a location in your system's `PATH`.

#### Linux & macOS

```bash
# Move the binary to /usr/local/bin (requires sudo)
sudo mv oraclepack /usr/local/bin/

# OR install to your local bin (no sudo required)
mkdir -p ~/.local/bin
mv oraclepack ~/.local/bin/
# Note: Ensure ~/.local/bin is in your shell's PATH
```

#### Windows (PowerShell)

1. Create a directory for your tools (e.g., `C:\Tools`).
2. Move `oraclepack.exe` into that directory.
3. Add that directory to your PATH:

```powershell
$env:Path += ";C:\Tools"
[Environment]::SetEnvironmentVariable("Path", $env:Path, [EnvironmentVariableTarget]::User)
```

```bash
oraclepack completion bash >& oraclepack.completion.sh
mkdir -p ~/.local/share/bash-completion/completions
install -m 0644 oraclepack.completion.sh \
  ~/.local/share/bash-completion/completions/oraclepack

# reload shell
exec bash

```

## Fix: Git Bash wrapper with path-conversion disabled

### Run in Git Bash on Windows

```bash
cat > "$HOME/bin/oraclepack" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

# Git for Windows (Git Bash) path-conversion off for this exec call.
# Required so /home/... is not rewritten into C:/Program Files/Git/...
MSYS_NO_PATHCONV=1 exec wsl.exe -d Ubuntu-24.04 -u user -- /home/user/.local/bin/oraclepack "$@"
EOF

# ensure LF line endings + executable
sed -i 's/\r$//' "$HOME/bin/oraclepack"
chmod +x "$HOME/bin/oraclepack"
hash -r

```

#### WSL (Windows Subsystem for Linux)

Follow the **Linux** instructions above.

## 🛠 Usage

### 1. Run a Pack (Interactive TUI)

```bash
oraclepack run examples/setup-project.md
```

### 2. Run All Steps Sequentially

```bash
oraclepack run examples/setup-project.md --run-all
```

### 3. Resume a Previous Run

```bash
oraclepack run examples/setup-project.md --resume
```

### 4. Plain Mode (Non-Interactive)

```bash
oraclepack run examples/setup-project.md --no-tui
```

### 5. List Steps

```bash
oraclepack list examples/setup-project.md
```

### 6. Validate a Pack

```bash
oraclepack validate examples/setup-project.md
```

### CLI Flags (run)

```bash
oraclepack run <pack.md> \
  --roi-threshold 2.0 \
  --roi-mode over \
  --run-all \
  --resume \
  --stop-on-fail=true \
  --no-tui \
  --out-dir ./out
```

`oraclepack` expects the `oracle` CLI to be available on your PATH. Overrides let you append official `oracle` flags at runtime.

## ⌨️ TUI Controls (Core)

- `enter`: run selected step
- `a`: run all visible steps sequentially
- `f`: set ROI threshold
- `m`: toggle ROI mode (over/under)
- `v`: step preview (full view)
- `o`: overrides wizard (oracle flags + step targeting)
- `u`: edit ChatGPT project URL
- `U`: open saved URL picker (project/global)
- `q`: quit

### Step Preview Controls

- `b` / `esc`: back to list
- `t`: wrap/un-wrap preview
- `c`: copy step contents (falls back to temp file if clipboard fails)

## 🧭 Overrides Wizard

The overrides flow lets you:

- Select official `oracle` flags (e.g., `--files-report`, `--render`, `--render-plain`, `--copy`, `--wait`).
- Target which steps receive those flags.
- Validate the overridden commands with `oracle --dry-run summary` before running.

## 🔗 Project URL Management

Oraclepack can store ChatGPT project URLs in two scopes:

- **Project scope**: `your-pack.chatgpt-urls.json`
- **Global scope**: `~/.oraclepack/chatgpt-urls.json`

Use `U` to pick, add, edit, delete, or set a default URL.

## 📝 Oracle Pack Format

An Oracle Pack is a Markdown file containing exactly one `bash` code block. Steps are identified by a specific header pattern: `# NN)`.

````markdown
# Project Setup Pack

This pack sets up the development environment.

```bash
# Prelude: Variables defined here are available to all steps
out_dir="dist"

# 01) Initialize dependencies
npm install

# 02) Build the project
npm run build

# 03) Run oracle query (ROI=4.5)
oracle query "check-integrity"
```
````

### Rules

1. Steps must start with `# NN)` (e.g., `# 01)`, `# 02)`).
2. Step numbering must be sequential starting from `01`.
3. The first bash code block in the file is the one executed.
4. Everything before the first `# 01)` is the **prelude**, which runs once.
5. Optional ROI tags (`ROI=2.5`) can be embedded in the step header for filtering.

## 📊 Reports and State

- **State File:** `[pack-name].state.json` tracks step statuses and filter settings.
- **Report File:** `[pack-name].report.json` includes timing, exit codes, and metadata for each step.

## 🤝 Contributing

1. Fork the repo
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## 📄 License

Distributed under the MIT License. See `LICENSE` for more information.


## Links discovered
- [oracle](https://github.com/steipete/oracle)
- [here!](https://github.com/steipete/oracle)
- [Go](https://go.dev/)
- [<img alt="CI" src="https://github.com/acidicsoil/oraclepack/actions/workflows/ci.yml/badge.svg" />](https://github.com/acidicsoil/oraclepack/actions/workflows/ci.yml)
- [<img alt="Release" src="https://github.com/acidicsoil/oraclepack/actions/workflows/release.yml/badge.svg" />](https://github.com/acidicsoil/oraclepack/actions/workflows/release.yml)
- [<img alt="Release Version" src="https://img.shields.io/github/v/release/acidicsoil/oraclepack?sort=semver" />](https://github.com/acidicsoil/oraclepack/releases/latest)
- [<img alt="License" src="https://img.shields.io/github/license/acidicsoil/oraclepack" />](https://github.com/acidicsoil/oraclepack/blob/main/LICENSE)
- [<img alt="Go Version" src="https://img.shields.io/github/go-mod/go-version/acidicsoil/oraclepack" />](https://github.com/acidicsoil/oraclepack/blob/main/go.mod)

--- .config/mcp/mcp-builder/SKILL.md ---
---
name: mcp-builder
description: Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
license: Complete terms in LICENSE.txt
---

# MCP Server Development Guide

## Overview

Create MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. The quality of an MCP server is measured by how well it enables LLMs to accomplish real-world tasks.

---

# Process

## 🚀 High-Level Workflow

Creating a high-quality MCP server involves four main phases:

### Phase 1: Deep Research and Planning

#### 1.1 Understand Modern MCP Design

**API Coverage vs. Workflow Tools:**
Balance comprehensive API endpoint coverage with specialized workflow tools. Workflow tools can be more convenient for specific tasks, while comprehensive coverage gives agents flexibility to compose operations. Performance varies by client—some clients benefit from code execution that combines basic tools, while others work better with higher-level workflows. When uncertain, prioritize comprehensive API coverage.

**Tool Naming and Discoverability:**
Clear, descriptive tool names help agents find the right tools quickly. Use consistent prefixes (e.g., `github_create_issue`, `github_list_repos`) and action-oriented naming.

**Context Management:**
Agents benefit from concise tool descriptions and the ability to filter/paginate results. Design tools that return focused, relevant data. Some clients support code execution which can help agents filter and process data efficiently.

**Actionable Error Messages:**
Error messages should guide agents toward solutions with specific suggestions and next steps.

#### 1.2 Study MCP Protocol Documentation

**Navigate the MCP specification:**

Start with the sitemap to find relevant pages: `https://modelcontextprotocol.io/sitemap.xml`

Then fetch specific pages with `.md` suffix for markdown format (e.g., `https://modelcontextprotocol.io/specification/draft.md`).

Key pages to review:
- Specification overview and architecture
- Transport mechanisms (streamable HTTP, stdio)
- Tool, resource, and prompt definitions

#### 1.3 Study Framework Documentation

**Recommended stack:**
- **Language**: TypeScript (high-quality SDK support and good compatibility in many execution environments e.g. MCPB. Plus AI models are good at generating TypeScript code, benefiting from its broad usage, static typing and good linting tools)
- **Transport**: Streamable HTTP for remote servers, using stateless JSON (simpler to scale and maintain, as opposed to stateful sessions and streaming responses). stdio for local servers.

**Load framework documentation:**

- **MCP Best Practices**: [📋 View Best Practices](./reference/mcp_best_practices.md) - Core guidelines

**For TypeScript (recommended):**
- **TypeScript SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md`
- [⚡ TypeScript Guide](./reference/node_mcp_server.md) - TypeScript patterns and examples

**For Python:**
- **Python SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md`
- [🐍 Python Guide](./reference/python_mcp_server.md) - Python patterns and examples

#### 1.4 Plan Your Implementation

**Understand the API:**
Review the service's API documentation to identify key endpoints, authentication requirements, and data models. Use web search and WebFetch as needed.

**Tool Selection:**
Prioritize comprehensive API coverage. List endpoints to implement, starting with the most common operations.

---

### Phase 2: Implementation

#### 2.1 Set Up Project Structure

See language-specific guides for project setup:
- [⚡ TypeScript Guide](./reference/node_mcp_server.md) - Project structure, package.json, tsconfig.json
- [🐍 Python Guide](./reference/python_mcp_server.md) - Module organization, dependencies

#### 2.2 Implement Core Infrastructure

Create shared utilities:
- API client with authentication
- Error handling helpers
- Response formatting (JSON/Markdown)
- Pagination support

#### 2.3 Implement Tools

For each tool:

**Input Schema:**
- Use Zod (TypeScript) or Pydantic (Python)
- Include constraints and clear descriptions
- Add examples in field descriptions

**Output Schema:**
- Define `outputSchema` where possible for structured data
- Use `structuredContent` in tool responses (TypeScript SDK feature)
- Helps clients understand and process tool outputs

**Tool Description:**
- Concise summary of functionality
- Parameter descriptions
- Return type schema

**Implementation:**
- Async/await for I/O operations
- Proper error handling with actionable messages
- Support pagination where applicable
- Return both text content and structured data when using modern SDKs

**Annotations:**
- `readOnlyHint`: true/false
- `destructiveHint`: true/false
- `idempotentHint`: true/false
- `openWorldHint`: true/false

---

### Phase 3: Review and Test

#### 3.1 Code Quality

Review for:
- No duplicated code (DRY principle)
- Consistent error handling
- Full type coverage
- Clear tool descriptions

#### 3.2 Build and Test

**TypeScript:**
- Run `npm run build` to verify compilation
- Test with MCP Inspector: `npx @modelcontextprotocol/inspector`

**Python:**
- Verify syntax: `python -m py_compile your_server.py`
- Test with MCP Inspector

See language-specific guides for detailed testing approaches and quality checklists.

---

### Phase 4: Create Evaluations

After implementing your MCP server, create comprehensive evaluations to test its effectiveness.

**Load [✅ Evaluation Guide](./reference/evaluation.md) for complete evaluation guidelines.**

#### 4.1 Understand Evaluation Purpose

Use evaluations to test whether LLMs can effectively use your MCP server to answer realistic, complex questions.

#### 4.2 Create 10 Evaluation Questions

To create effective evaluations, follow the process outlined in the evaluation guide:

1. **Tool Inspection**: List available tools and understand their capabilities
2. **Content Exploration**: Use READ-ONLY operations to explore available data
3. **Question Generation**: Create 10 complex, realistic questions
4. **Answer Verification**: Solve each question yourself to verify answers

#### 4.3 Evaluation Requirements

Ensure each question is:
- **Independent**: Not dependent on other questions
- **Read-only**: Only non-destructive operations required
- **Complex**: Requiring multiple tool calls and deep exploration
- **Realistic**: Based on real use cases humans would care about
- **Verifiable**: Single, clear answer that can be verified by string comparison
- **Stable**: Answer won't change over time

#### 4.4 Output Format

Create an XML file with this structure:

```xml
<evaluation>
  <qa_pair>
    <question>Find discussions about AI model launches with animal codenames. One model needed a specific safety designation that uses the format ASL-X. What number X was being determined for the model named after a spotted wild cat?</question>
    <answer>3</answer>
  </qa_pair>
<!-- More qa_pairs... -->
</evaluation>
```

---

# Reference Files

## 📚 Documentation Library

Load these resources as needed during development:

### Core MCP Documentation (Load First)
- **MCP Protocol**: Start with sitemap at `https://modelcontextprotocol.io/sitemap.xml`, then fetch specific pages with `.md` suffix
- [📋 MCP Best Practices](./reference/mcp_best_practices.md) - Universal MCP guidelines including:
  - Server and tool naming conventions
  - Response format guidelines (JSON vs Markdown)
  - Pagination best practices
  - Transport selection (streamable HTTP vs stdio)
  - Security and error handling standards

### SDK Documentation (Load During Phase 1/2)
- **Python SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md`
- **TypeScript SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md`

### Language-Specific Implementation Guides (Load During Phase 2)
- [🐍 Python Implementation Guide](./reference/python_mcp_server.md) - Complete Python/FastMCP guide with:
  - Server initialization patterns
  - Pydantic model examples
  - Tool registration with `@mcp.tool`
  - Complete working examples
  - Quality checklist

- [⚡ TypeScript Implementation Guide](./reference/node_mcp_server.md) - Complete TypeScript guide with:
  - Project structure
  - Zod schema patterns
  - Tool registration with `server.registerTool`
  - Complete working examples
  - Quality checklist

### Evaluation Guide (Load During Phase 4)
- [✅ Evaluation Guide](./reference/evaluation.md) - Complete evaluation creation guide with:
  - Question creation guidelines
  - Answer verification strategies
  - XML format specifications
  - Example questions and answers
  - Running an evaluation with the provided scripts


## Links discovered
- [📋 View Best Practices](https://github.com/AcidicSoil/oraclepack/blob/main/.config/mcp/mcp-builder/reference/mcp_best_practices.md)
- [⚡ TypeScript Guide](https://github.com/AcidicSoil/oraclepack/blob/main/.config/mcp/mcp-builder/reference/node_mcp_server.md)
- [🐍 Python Guide](https://github.com/AcidicSoil/oraclepack/blob/main/.config/mcp/mcp-builder/reference/python_mcp_server.md)
- [✅ Evaluation Guide](https://github.com/AcidicSoil/oraclepack/blob/main/.config/mcp/mcp-builder/reference/evaluation.md)
- [📋 MCP Best Practices](https://github.com/AcidicSoil/oraclepack/blob/main/.config/mcp/mcp-builder/reference/mcp_best_practices.md)
- [🐍 Python Implementation Guide](https://github.com/AcidicSoil/oraclepack/blob/main/.config/mcp/mcp-builder/reference/python_mcp_server.md)
- [⚡ TypeScript Implementation Guide](https://github.com/AcidicSoil/oraclepack/blob/main/.config/mcp/mcp-builder/reference/node_mcp_server.md)

--- .config/mcp/oraclepack-gold-pack/SKILL.md ---
---
name: oraclepack-gold-pack
description: Generate a single canonical Stage-1 oraclepack question pack as Markdown:exactly one ```bash fence containing exactly 20 steps (01..20) with strict ROI header tokens, per-step --write-output, fixed categories + coverage check. Use when you need a gold, runner-ingestible pack template (Stage 1 only; not Stage 3 taskify).
metadata:
  short-description: Gold Stage-1 oraclepack pack generator + validators
---

# Oraclepack Gold Pack (Stage 1)

This skill produces the **canonical Stage-1** oraclepack question pack (20 Oracle CLI calls). It is intentionally strict to prevent schema drift.

**Non-negotiable contract (pack output):**
- Exactly **one** fenced code block: starts with exactly ` ```bash` on its own line, and ends with exactly ` ````
- No other fenced code blocks anywhere in the pack.
- Exactly **20** steps, numbered **01..20** in order.
- Every step has a header line matching:
  - `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes `--write-output "<out_dir>/<nn>-<slug>.md"`.
- Categories are fixed to this exact set (no additions/renames):
  - `contracts/interfaces`
  - `invariants`
  - `caching/state`
  - `background jobs`
  - `observability`
  - `permissions`
  - `migrations`
  - `UX flows`
  - `failure modes`
  - `feature flags`
- Pack ends with a **Coverage check** section listing all 10 categories as `OK` or `Missing(<step ids>)`.

The pack template is the contract. The scratch doc is **not** a pack format.

References:
- Contract template: `references/oracle-pack-template.md`
- Repo discovery: `references/inference-first-discovery.md`
- Attachment rules: `references/attachment-minimization.md`
- Scratch playbook (not pack): `references/oracle-scratch-format.md`

## Quick start

1) Generate a pack file (intended path):
- `docs/oracle-pack-YYYY-MM-DD.md`

1) Validate it (recommended before running oraclepack):
- `python3 scripts/validate_pack.py docs/oracle-pack-YYYY-MM-DD.md`

1) Optional attachment lint:
- `python3 scripts/lint_attachments.py docs/oracle-pack-YYYY-MM-DD.md`

## Inputs (do not ask follow-ups)

Interpret the user’s trailing text as conceptual `{{args}}`. Extract:

- `codebase_name` (default `Unknown`)
- `constraints` (default `None`)
- `non_goals` (default `None`)
- `team_size` (default `Unknown`)
- `deadline` (default `Unknown`)
- `out_dir` (default `docs/oracle-questions-YYYY-MM-DD`)
- `oracle_cmd` (default `oracle`)
- `oracle_flags` (default `--files-report`)
- `engine` (`api|browser`; optional; if provided, append to flags *only if your oracle CLI supports it*, else omit and record `TODO(engine flag unknown)`)
- `model` (optional; if provided, append to flags *only if your oracle CLI supports it*, else omit and record `TODO(model flag unknown)`)
- `extra_files` (default empty; if provided, append **literally** to every command)

If any value is missing: use defaults and proceed.

## Workflow (deterministic)

### 1) Read the contract template first
Open `references/oracle-pack-template.md` and treat it as the **single source of truth** for formatting.

### 2) Repo discovery (inference-first)
Follow `references/inference-first-discovery.md`:
- Read a small set of “anchors” first.
- Infer what’s present in the repo.
- Only then choose the best 1–2 attachments per step.

### 3) Plan the 20 probes (2 per category)
Use the fixed categories and produce **exactly 2 steps per category** (20 total). Keep each step’s prompt focused and non-overlapping.

For each step:
- Pick a **reference anchor** (`reference=` token): `{path}:{symbol}` OR `{path}` OR `Unknown`.
- Pick ≤2 attachments (or fewer) using `references/attachment-minimization.md`.
- Ensure the prompt asks for:
  - Direct answer (bullets)
  - Risks/unknowns
  - Next smallest concrete experiment
  - Missing artifact patterns to request if evidence is insufficient

### 4) Emit the pack (single file)
Produce exactly one Markdown document with:
- Title + parsed args section (plain markdown; no code fences)
- Exactly one ` ```bash` fence containing the 20 steps
- A Coverage check section after the fence

### 5) Validate and correct drift
Run:
- `python3 scripts/validate_pack.py <pack.md>`
If it fails, fix the pack until it passes.

Optionally run:
- `python3 scripts/lint_attachments.py <pack.md>`
If it fails, reduce attachments to ≤2 per step (before any literal `extra_files`).

## Output contract

When invoked, you produce:
- One runner-ingestible Markdown pack (intended filename: `docs/oracle-pack-YYYY-MM-DD.md`)

You do **not**:
- Run oraclepack
- Generate Stage-3 “action packs” (that is `oraclepack-taskify`)

## Failure modes (do not guess)

- Missing repo evidence → set `reference=Unknown`, attach fewer files, and explicitly request missing file/path patterns in the prompt.
- Uncertain CLI flag support (`engine`, `model`) → omit flags and write `TODO(engine/model flag unknown)` in the pack’s parsed args notes (do not invent flags).
- Any schema drift → fix until `scripts/validate_pack.py` passes.

## Invocation examples

1) “Generate a gold oraclepack Stage-1 pack for this repo. out_dir=docs/oracle-questions-2026-01-06”
2) “Make the strict 20-step pack for codebase_name=AcmeAPI constraints=‘no DB changes’”
3) “Create the canonical pack; engine=browser model=gpt-5.2-pro (if supported)”
4) “Produce the gold pack; add extra_files='-f docs/ARCHITECTURE.md -f docs/API.md'”
5) “Regenerate this pack but fix headers and coverage check so it validates.”


--- .config/mcp/oraclepack-taskify/SKILL.md ---
---
name: oraclepack-taskify
description: Generate a Stage-3 Action Pack from oraclepack output (oracle-out 01–20 .md answers) that synthesizes a canonical actions plan + Task Master PRD, runs Task Master to create/expand tasks, and by default starts a guarded autopilot to begin implementation.
metadata:
  short-description: Stage 3:answers → tasks → pipelines/autopilot
---

# oraclepack-taskify

## Use when

Use this skill only after **oraclepack Stage 2** has produced **20 answer files** under an output directory (default `oracle-out/`), and the user wants Stage 3 work products such as:

- “Stage 3” / “taskify” / “actionize” / “turn oracle outputs into tasks”
- “Task Master follow-up” / “PRD from oracle-out” / “implementation plan”
- “Start work automatically from oracle-out” / “autopilot top tasks”

## Deliverable

When invoked, produce exactly one primary deliverable:

- A single Markdown doc at `pack_path` (default `docs/oracle-actions-pack-YYYY-MM-DD.md`)

The generated Action Pack MUST be oraclepack-ingestible and MUST obey:

- Exactly **one** fenced code block labeled `bash` in the entire document.
- No other code fences anywhere in the document.
- Inside the bash fence, include sequential step headers exactly like: `# NN)` where `NN` is `01, 02, 03...`
- Each step is self-contained and must **not rely on shell variables created in previous steps**.
- Each step writes artifacts to explicit, deterministic paths.

## Skill interface (args)

Parse user trailing text as whitespace-separated `KEY=value` tokens (last-one-wins). Do not ask follow-ups.

Supported args (all optional):

- `out_dir` (default `oracle-out`)
- `pack_path` (default `docs/oracle-actions-pack-YYYY-MM-DD.md`)
- `actions_json` (default `<out_dir>/_actions.json`)
- `actions_md` (default `<out_dir>/_actions.md`)
- `prd_path` (default `.taskmaster/docs/oracle-actions-prd.md`)
- `tag` (default `oraclepack`)
- `mode` (default `autopilot`; allowed `backlog|pipelines|autopilot`)
- `top_n` (default `10`; clamp to `1..20`)
- `oracle_cmd` (default `oracle`; allow a multi-word command like `npx -y @steipete/oracle` only if the user requests it)
- `task_master_cmd` (default `task-master`)
- `tm_cmd` (default `tm`; used only in autopilot mode)
- `extra_files` (default empty; if provided, treat as a comma-separated list of file paths; attach them wherever the synthesis step accepts file inputs)

If any referenced file/path does not exist, the skill still outputs the Action Pack, but includes an early step that prints a clear error and exits non-zero.

## Workflow (what to do when invoked)

1) Parse args from `KEY=value` tokens (no follow-ups; last-one-wins; unknown keys ignored).

2) Resolve defaults deterministically:
   - Compute `YYYY-MM-DD` using the local date at generation time.
   - Apply defaults and clamp `top_n` to `1..20`.
   - Default `mode=autopilot` unless explicitly overridden.
   - Derive:
     - `actions_json = <out_dir>/_actions.json` unless user overrides
     - `actions_md = <out_dir>/_actions.md` unless user overrides

3) Render the Action Pack:
   - Start from `assets/action-pack-template.md`.
   - Substitute placeholders:
     - `{{pack_date}}`, `{{out_dir}}`, `{{pack_path}}`, `{{actions_json}}`, `{{actions_md}}`, `{{prd_path}}`, `{{tag}}`, `{{mode}}`, `{{top_n}}`, `{{oracle_cmd}}`, `{{task_master_cmd}}`, `{{tm_cmd}}`
   - Expand `extra_files` into literal bash lines of the form:
     - `oracle_file_flags+=( -f "<path>" )`
     - and insert them only where the template indicates “Extra attachments”.

4) Ensure Action Pack contract:
   - Exactly one `bash` code fence in the document.
   - No other code fences.
   - Step headers `# 01)`.. are sequential and stable.
   - Each step re-declares its variables and does not depend on variables from earlier steps.

5) Write the final pack to `pack_path` (create parent dirs).

## Modes

### mode=backlog

Action Pack should:
- Synthesize canonical `_actions.json` + `_actions.md`
- Write PRD to `prd_path`
- Run:
  - `task-master parse-prd <prd_path>` (attempt with tag scoping if supported)
  - `task-master analyze-complexity --output <out_dir>/tm-complexity.json`
  - `task-master expand --all`

### mode=pipelines

Do everything in `backlog`, then:
- Generate deterministic pipelines from tasks.json
- Write: `docs/oracle-actions-pipelines.md`

### mode=autopilot (default)

Do everything in `backlog`, then:
- Enforce branch safety and tests-first guardrails
- Start a guarded autopilot entrypoint (expects `tm`-style autopilot tooling)
- Never commit to the default branch (do not run `git commit` on main/master; create a work branch first)

Important: if the environment does not provide a compatible `tm autopilot`, Step 08 will fail fast with a clear error. To avoid that, run Stage 3 with `mode=backlog` or `mode=pipelines`.

## Determinism + safety rules

- No interactive prompts in the generated pack.
- Stable ordering: select exactly the 20 outputs by filename prefix ordering `01`..`20`.
- Fail fast when required tools are missing:
  - `command -v <task_master_cmd first word>`
  - `command -v <oracle_cmd first word>`
  - `command -v <tm_cmd first word>` only in autopilot mode
- Always create directories before writing files.
- Avoid destructive commands:
  - Do not delete files.
  - Do not force-push.
  - Do not commit to main/master (autopilot mode creates a new branch).
- If multiple outputs exist for a prefix (e.g., `01-*.md` expands to more than one file), exit non-zero with an explicit error listing the matches.

## Failure modes (handle without questions)

- Missing `out_dir` → pack Step 01 exits non-zero with a clear message.
- Missing any of `01-*.md`..`20-*.md` → Step 01 exits non-zero (and prints which one is missing).
- Missing required tools → Step 01 exits non-zero (and prints which command is missing).
- Unknown `mode` → treat as `autopilot` (clamp at generation time) and render `mode=autopilot` in the pack.

## Resources

- `assets/action-pack-template.md`: base template for the Action Pack deliverable.
- `assets/actions-json-schema.md`: canonical schema spec for `_actions.json` normalization.
- `assets/prd-synthesis-prompt.md`: exact prompt text embedded into the Action Pack for synthesis.
- `references/*`: Stage 3 overview and guardrails for future maintenance.
- `scripts/*`: optional helpers (standalone); may also be embedded verbatim into packs if desired.

## Invocation examples

- `$oraclepack-taskify out_dir=oracle-out` (defaults to mode=autopilot)
- `$oraclepack-taskify out_dir=oracle-out mode=backlog`
- `$oraclepack-taskify out_dir=oracle-out mode=pipelines tag=oraclepack-top top_n=10`
- `$oraclepack-taskify out_dir=oracle-out mode=autopilot tag=oraclepack-top pack_path=docs/oracle-actions-pack-2026-01-05.md`
- `$oraclepack-taskify out_dir=oracle-out extra_files=README.md,package.json`


--- .config/skills/oracle-pack-rpg-infused/SKILL.md ---
---
name: oracle-pack-rpg-infused
description: Generate a strictly oraclepack-ingestible “oracle strategist question pack” Markdown file under docs/oracle-pack-YYYY-MM-DD.md containing exactly one fenced bash block with exactly 20 numbered oracle commands (01..20), ROI-scored/sorted and category-covered, with an RPG block (WHAT/HOW + dependency edges) embedded in each -p prompt for graph extraction.
---

# Oracle Pack (RPG-infused)

## Quick start

1) Read `assets/oracle-pack-template.md` and keep its structure exactly.
2) Produce exactly one Markdown deliverable and output it verbatim in your final response:
   - First line: `Output file: docs/oracle-pack-YYYY-MM-DD.md`
   - Then the exact Markdown content (no extra commentary, no additional code fences).
3) Ensure the bash block contains exactly 20 steps, numbered `01..20`, each with:
   - One header comment line in the required format
   - Exactly one runnable oracle command line

For the full spec, follow `references/oracle-pack-spec.md`.

## Inputs and defaults

Interpret any user-provided details as “args” and apply these defaults when missing:

- `codebase_name=Unknown`
- `constraints=None`
- `non_goals=None`
- `team_size=Unknown`
- `deadline=Unknown`
- `out_dir=oracle-out`
- `oracle_cmd=oracle`
- `oracle_flags=--files-report`
- `extra_files=empty` (only use if the user provides an explicit mechanism/flag; otherwise omit and keep evidence handling inside the prompt body as text)

If the user supplies their own values, use them verbatim unless they would break the strict output contract.

## Workflow

1) Establish minimal repo understanding (inference-first)
   - Prefer reading index artifacts (e.g., README, top-level config/manifests) before proposing file sweeps.
   - If you cannot access the repo contents, keep `reference=Unknown` and make the “missing artifact pattern(s)” explicit in the prompt body.

2) Plan the 20 questions (before writing any command lines)
   - Use only the required categories (no additions, no removals).
   - Ensure horizon mix: exactly 12 `Immediate` and 8 `Strategic`.
   - Ensure RPG infusion in every step’s `-p` prompt (WHAT/HOW + DependsOn + Phase).
   - Ensure dependencies only point backward (DependsOn may reference only earlier step IDs).
   - Ensure coverage: across all 20 steps, each required category appears at least once.

3) Assign scoring and compute ROI
   - Pick `impact`, `confidence`, `effort` each as one decimal in `[0.1..1.0]`.
   - Compute `ROI = (impact * confidence) / effort` (one decimal).
   - Sort all 20 steps by ROI descending; tie-break by lower effort.

4) Emit the pack Markdown
   - Copy `assets/oracle-pack-template.md` structure exactly.
   - Fill in metadata values and the 20 steps.
   - Keep exactly ONE fenced bash block.
   - Keep exactly 20 steps (01..20) inside that block.

5) Emit the coverage check section
   - List the 10 required categories exactly once each with `OK` or `Missing(...)`.

6) Validate (optional but recommended when execution is available)
   - Run `scripts/validate_oracle_pack.py docs/oracle-pack-YYYY-MM-DD.md` and fix any reported issues.

## Output contract (must hold)

- Exactly one Markdown deliverable.
- Matches `assets/oracle-pack-template.md` structure exactly.
- Exactly one fenced bash block.
- Bash block contains exactly 20 steps, numbered 01..20.
- Each step has:
  - Header line with: `ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
  - Exactly one runnable oracle command including:
    - `${oracle_cmd}` (default `oracle`)
    - `${oracle_flags}` (default `--files-report`)
    - `--write-output "<out_dir>/<nn>-<slug>.md"`
- Steps sorted by ROI desc; tie-break by lower effort.
- Coverage check section present and correctly formatted.

## Failure modes (do not guess)

- Unknown subsystem/file references: set `reference=Unknown` and include explicit missing file/path patterns in section (4) of the prompt body.
- Missing or unclear user args: apply defaults; do not invent tool flags or repo-specific details.
- Potential contract violations: prioritize strict compliance over richer prose.

## Invocation examples

1) “Generate an RPG-infused oracle strategist pack for this repo. out_dir=oracle-out.”
2) “Create the oracle-pack for a monorepo; prioritize caching/state, observability, and permissions. Use oracle_flags=--files-report.”
3) “Make a pack for codebase_name=AcmeWeb, constraints=‘No DB migrations this sprint’, non_goals=‘No UI redesign’.”
4) “Produce a pack that emphasizes feature flags and invariants, but still covers all required categories.”
5) “Generate the pack with conservative confidence values (0.2–0.6) and document missing artifact patterns explicitly.”


--- .config/skills/oracle-pack/SKILL.md ---
---
name: oracle-pack
description: Generate exactly 20 evidence-cited, ROI-ranked strategist questions for the current repository (12 immediate + 8 strategic), then emit them as runnable @steipete/oracle CLI commands with minimal targeted file attachments and deterministic per-question output paths (a copy/paste-ready “oracle question pack” in the same scratch-style format as oracle-scratch.md).
---

## Quick start

Use this skill when the user wants strategist questions **and** wants to run each question through Oracle as a separate command (so a second model can answer with repo context).

Invocation:

- `$oracle-pack <free-text args>`

Primary output: a Markdown doc whose main content is a **single** fenced `bash` block containing **exactly 20** `oracle ...` commands (copy/paste-ready), in the same “scratch-style” format shown in `references/oracle-scratch-format.md`.

## Inputs

- Repository contents (current working directory).
- Free-text args (may include): `codebase_name`, `constraints`, `non_goals`, `team_size`, `deadline`, plus Oracle pack controls:
  - `out_dir` (default: `oracle-out`)
  - `oracle_cmd` (default: `oracle`)
  - `oracle_flags` (default: `--files-report`)
  - `extra_files` (optional: comma-separated extra `-f/--file` entries to include in *every* command; default: none)

## Deterministic search/tooling rules (must follow)

- Prefer deterministic, non-interactive commands so runs are reproducible.
- Before using `ck` in a session, **always run**: `ck --help` and only rely on flags that `ck --help` confirms.
- Default search tool is `ck`:
  - Conceptual: `ck --sem "<query>"`
  - Literal: `ck --regex "<pattern>"`
  - Best of both: `ck --hybrid "<query>"`
  - For post-processing: prefer `ck --jsonl | jq ... | sort | head`
- Structural search: use `ast-grep` when syntax/shape matters (see `references/inference-first-discovery.md`).
- File finding: prefer `fd` (filename/path/extension filters).

## Args parsing (no follow-ups)

Extract values if present; otherwise set defaults:

- `codebase_name`: `Unknown`
- `constraints`: `None`
- `non_goals`: `None`
- `team_size`: `Unknown`
- `deadline`: `Unknown`
- `out_dir`: `oracle-out`
- `oracle_cmd`: `oracle`
- `oracle_flags`: `--files-report`
- `extra_files`: (empty)

Honor `constraints`. Explicitly exclude `non_goals`.

## Workflow (inference-first discovery → evidence → questions → oracle pack)

### 1) Inference-first discovery (adaptive, codebase-search compliant)

Goal: infer *what this repo is* and *where the truth likely lives* before broad sweeps.

Do this in order:

1. **Discover the search interface (required)**
   - Run: `ck --help`
   - If `ck` indicates it needs setup/indexing, follow the instructions printed by `ck`.

2. **Map the repo surface (cheap, high-signal)**
   - Use `fd` to list likely roots deterministically:
     - `fd . -d 2`
     - `fd -p README.md`
     - `fd -p package.json`
     - `fd -p pyproject.toml`
     - `fd -p go.mod`
     - `fd -p Cargo.toml`
   - Read the smallest set of “index” files that describe structure:
     - README / docs index if present
     - primary manifests/config
     - obvious entrypoint referenced by scripts/manifests

3. **Infer stack + conventions from evidence**
   - From manifests/config, infer runtime + framework signals (dependencies, filenames, directory conventions).
   - From entrypoints, infer wiring patterns (router, DI/container, job scheduler, migration tool).

4. **Derive a “search plan” from inferred signals**
   - Prefer following imports/references from entrypoints into:
     - routing/handlers
     - auth middleware/policies
     - job/queue registration
     - data layer/migrations
     - feature flags config
     - observability bootstrap
   - Use `ck` in the smallest inferred app roots first:
     - Conceptual: `ck --sem "<subsystem intent>"`
     - Hybrid when unsure: `ck --hybrid "<subsystem intent>"`
   - If results are broad/noisy:
     - Narrow to directories/files from the top hits, then re-run `ck`.
     - Use `ast-grep` when you need structure, not text matches.

This improves efficiency vs. hard-targeting a long list of patterns up front: you start with the repo’s own “self-description” and expand only as needed.

### 2) Evidence harvesting (collect anchors before writing questions)

Collect **at least 20 candidate anchors** as one of:

- `{path}:{symbol}` (preferred: nearest function/class/type/handler)
- `{endpoint}` (literal endpoint string)
- `{event}` (job/queue/event name)

For each required category, attempt to identify at least one anchor:

- contracts/interfaces
- invariants
- caching/state
- background jobs
- observability
- permissions
- migrations
- UX flows
- failure modes
- feature flags

Use deterministic selection of candidates:
- If using `ck --jsonl`, post-process to stable top-N:
  - `ck --jsonl ... | jq ... | sort | head -n 20`
- If using plain output, cap deterministically with `head`.

If evidence for a category cannot be found:

- still generate a question for it with reference `Unknown`
- explicitly state which missing artifact pattern would likely provide it (e.g., `**/migrations/**` not found)

### 3) Generate exactly 20 strategist questions (evidence-cited + minimal experiments)

Produce exactly:

- **12** Immediate Exploration
- **8** Strategic Planning

Each question must include:

- **Reference**: `{path}:{symbol}` OR `{endpoint}` OR `{event}` OR `Unknown`
- **Question**: incisive, feasibility-focused, no scope creep
- **Rationale**: exactly one sentence
- **Smallest experiment today**: exactly one concrete action runnable today (targeted `ck` query, targeted `ast-grep` pattern, trace a codepath, add a log line, minimal unit/integration test, run migration dry-run, execute a single endpoint, etc.)

No duplicates (by intent or reference).

### 4) Score and order by near-term ROI (required math + sorting)

For each question compute:

- `ROI = (impact * confidence) / effort`
- `impact`, `confidence`, `effort` ∈ `{0.1..1.0}` (one decimal)

Sort all 20 descending by ROI; break ties by lower effort.

### 5) Convert the 20 questions into an Oracle “question pack” (final deliverable)

For each question (in final sorted order), emit a runnable command using:

- command: `{{oracle_cmd}}` (default `oracle`)
- include `{{oracle_flags}}` (default `--files-report`)
- deterministic output file: `--write-output "<out_dir>/<nn>-<slug>.md"`
  - `<nn>` = zero-padded 2-digit index (01..20)
  - `<slug>` = short, filesystem-safe slug derived from `{category}` + a hint of `{reference}` (fallback to category only)
- prompt: a structured prompt that includes the strategist question fields and constraints:
  - reference, question, rationale, smallest experiment today
  - constraints + non_goals (explicit)
  - requested Oracle answer shape (concise, evidence-cited, and actionable)

#### Attachment selection (must be minimal, evidence-driven)

For each command, attach only the smallest set of files that lets Oracle answer correctly:

- If reference is `{path}:{symbol}`:
  - attach that `path`
  - optionally attach **one** upstream config/router/entrypoint file that shows how it’s invoked (only if needed)
- If reference is `{endpoint}` or `{event}`:
  - attach the file(s) where you found the literal endpoint/event definition
- If reference is `Unknown`:
  - attach only “index” files (README + manifest + 1 best-guess entrypoint), and in the prompt state what artifact pattern is missing

Follow `references/attachment-minimization.md`.

Also:
- If `extra_files` was provided in args, include those `-f` entries in every command (after the evidence-minimal attachments).

### 6) Render final output using the pack template

Use `assets/oracle-pack-template.md` as the strict shape for the final Markdown deliverable.

## Output contract (strict)

Produce exactly one Markdown deliverable that:

1. Matches the structure in `assets/oracle-pack-template.md`
2. Contains **exactly 20** `oracle` invocations total
3. Commands are sorted by ROI (desc; ties by lower effort)
4. Each command:
   - includes `--write-output "<out_dir>/<nn>-<slug>.md"`
   - includes minimal targeted `-f/--file` attachments
   - embeds the strategist question (reference, question, rationale, experiment) in the `-p/--prompt` text

## Failure modes

- Missing/insufficient evidence:
  - use reference `Unknown`
  - state the missing artifact pattern that would provide evidence
  - keep the question actionable and experiment minimal
  - keep attachments limited to index files (don’t attach huge globs)
- Search tool limitations (`ck` missing/setup required):
  - run `ck --help` and follow printed setup; if unavailable, fall back to reading index files + minimal `fd`-based locating
  - proceed with partial evidence; mark affected references `Unknown`
- Ambiguous args:
  - apply defaults without follow-ups

## References

- `assets/oracle-pack-template.md` — exact final output shape
- `references/inference-first-discovery.md` — inference-driven search plan aligned to `ck`/`ast-grep`/`fd`
- `references/attachment-minimization.md` — deterministic rules for minimal `-f` selections
- `references/oracle-scratch-format.md` — the target “scratch-style” formatting to mimic

## Invocation examples

- “Generate an oracle question pack for this repo. codebase_name=Payments API; constraints=no new infra; non_goals=mobile app; team_size=3; deadline=2 weeks; out_dir=artifacts/oracle”
- “Create oracle strategist questions; constraints=ship bugfixes only; deadline=Friday; out_dir=oracle-out”
- “Produce an oracle pack: focus on auth + jobs + migrations; non_goals=refactor; out_dir=oracle-q”
- “Generate strategist oracle commands; constraints=keep DB schema stable; team_size=2; deadline=1 week”
- “Make an Oracle question pack; oracle_cmd='npx -y @steipete/oracle'; oracle_flags='--engine browser  --files-report'; out_dir=oracle-out”

### Write output to `docs/` (required)

- Ensure a `docs/` directory exists at the repo root (create it if missing).
- Write the complete final deliverable to:
  - `docs/oracle-pack-YYYY-MM-DD.md` (use today’s date; ISO 8601)
  - Fallback if date is unavailable: `docs/oracle-pack.md`
- The file must contain only the deliverable Markdown (no extra preamble/epilogue).
- In the assistant response, print the chosen path first on a single line: `Output file: <path>`, then print the same Markdown content.


--- .config/skills/oracle-strategist-commands/SKILL.md ---
---

name: oracle-strategist-commands
description: Generate exactly 20 evidence-anchored strategist questions as runnable @steipete/oracle bash commands (12 immediate + 8 strategic), using inference-first repo discovery to avoid wasted searching.
metadata:
  short-description: 20 ROI-ranked Oracle CLI question commands for a repo (inference-first discovery)
  output: bash
  count: 20
---

## Use when

Use this skill when the user wants incisive, feasibility-focused strategist questions, but **as an executable pack** of **exactly 20** `oracle` CLI commands.

## Deliverable (strict)

Output MUST be:

- A single fenced code block: ```bash
- Optional header lines (env vars, `mkdir -p`, shared preamble)
- Then **exactly 20** Oracle invocations (lines beginning with `oracle` or `npx -y @steipete/oracle`)
- Counts:
  - **12** commands that are “Immediate” (01–12)
  - **8** commands that are “Strategic” (13–20)
- No prose outside the bash code fence.

Each command MUST include:

- `-p/--prompt`
- at least one `-f/--file`
- `--files-report`
- `--write-output "<out_dir>/<nn>-<slug>.md"`
- `--wait` (unless user explicitly requests omitting it)

Optional but recommended:

- a commented preflight line using `--dry-run summary` before each real command.

## Inputs (do not ask follow-ups)

Interpret free-text args as optionally including:

- `codebase_name` (default `Unknown`)
- `constraints` (default `None`)
- `non_goals` (default `None`)
- `team_size` (default `Unknown`)
- `deadline` (default `Unknown`)
- `engine` (`api|browser`; if unspecified, omit)
- `model` (if unspecified, omit)
- `out_dir` (default `docs/oracle-questions-YYYY-MM-DD`)

If missing, apply defaults and proceed.

## Core principle: inference-first discovery

Avoid “hard-targeting” specific artifact types up front (e.g., hunting for migrations/auth/jobs) because many repos won’t have them. Instead:

1) Gather cheap high-signal evidence (anchors).
2) Infer the stack and architecture hypotheses.
3) Run small, evidence-gated probes to confirm.
4) Only search/read deeper for categories that now have supporting evidence.

This reduces wasted probing and keeps attachments minimal.

## Workflow

### 1) Build a repo fingerprint (anchors only)

Read the smallest set of files that often exist and immediately reveal stack/shape:

- `README*` (first ~200 lines)
- root manifests that exist (one or more): `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, `pom.xml`, `build.gradle`, `composer.json`, `Gemfile`
- `Dockerfile`, `docker-compose.*` (if present)
- `.github/workflows/*` (names + one representative workflow file)
- `Makefile` / `Taskfile.*` (if present)

Also capture a lightweight file index summary (without reading everything):

- top-level directories
- dominant file extensions
- any “obvious entrypoint” indicators from scripts/config (e.g., package.json scripts, go `cmd/`, rust `src/main.rs`)

### 2) Form hypotheses (stack + entrypoints)

From the fingerprint, infer:

- primary language/runtime
- framework candidates (e.g., Express/Nest/Next, FastAPI/Django, chi/gin, Rails, Spring)
- likely entrypoints and routing surfaces
- where “truth” lives (schema, API contracts, config)

Write down 3–6 candidate entrypoints (paths) you will validate.

### 3) Evidence-gated probing loop (bounded)

Run a small loop of probes; each probe must have a clear purpose and a stop condition.

**Budgets (defaults):**

- max probes: 12
- max file reads: 25
- max search passes: 6

**Allowed probes (choose the cheapest next step):**

- Open an inferred entrypoint and follow imports to one routing/handler layer.
- Read a config file referenced by an entrypoint.
- Perform a targeted search ONLY for confirmed-framework idioms (e.g., “router”, “app.get”, “FastAPI()”, “chi.NewRouter”) after framework is evidenced.
- If you see a strong signal (e.g., `prisma` in deps), then and only then probe its likely files (`prisma/schema.prisma`, migrations).

**Do NOT** scan for categories (auth/jobs/migrations/feature flags/observability) unless you have at least one of:

- a dependency/config signal (library name, config key)
- an import path signal
- a directory naming signal found naturally via the fingerprint or followed imports

### 4) Build the evidence index (mandatory)

Create an internal evidence index with **>= 20** references that actually exist, as one of:

- `path:symbol` (function/class/type/handler name)
- explicit endpoint strings (e.g., `/api/users`)
- job/event names (queue/topic/schedule identifiers)
- config keys/flags (feature gates, env var keys)

For each reference, also record a “best attachment set” (1–6 files) that makes it intelligible.

If the repo is too small to reach 20, pad with `Unknown` entries; those questions must explicitly call out missing evidence.

### 5) Draft exactly 20 strategist questions (12 immediate + 8 strategic)

Each question must:

- be feasibility-focused and grounded in repo reality
- respect `constraints`
- explicitly exclude `non_goals`
- be actionable for `team_size` and `deadline`
- cite at least one evidence reference (or `Unknown` with a clear gap)
- fall into one of these areas (only when evidenced; otherwise treat as a “risk/unknowns” question):
  - contracts/interfaces
  - invariants/data consistency
  - auth/permissions
  - migrations/schema
  - request/UX flows (if applicable)
  - failure modes & reliability
  - feature flags/config gating
  - observability (logs/metrics/tracing)

Guidance:

- Immediate (01–12): unblockers, correctness, runtime risk, deployment friction, quick wins.
- Strategic (13–20): architectural constraints, roadmap leverage, risk retirement, scalability, maintainability.

### 6) Emit the question pack as bash commands (final output)

#### Common command shape

Each command MUST:

- call Oracle with `-p/--prompt` and `-f/--file` (repeat `-f` as needed)
- include `--files-report`
- include `--write-output "<out_dir>/<nn>-<slug>.md"`
- include `--wait` unless user opted out
- attach minimal files:
  - small base context: 1–3 anchor files (entrypoint + config)
  - focus context: files containing the cited evidence reference(s)

Avoid broad `**/*` globs. Prefer explicit files and small globs with negation excludes.

#### Prompt micro-format (required in every `-p`)

Each `-p` must require Oracle to respond using exactly:

- **Answer**
- **Evidence** (bullets; cite concrete refs)
- **Risks / unknowns**
- **Smallest experiment** (runnable today)

Keep prompts short and specific.

#### Optional preflight (recommended)

For each command, optionally emit a commented “dry-run” line immediately above it:

- same command + `--dry-run summary`
This lets the user validate attachments/token weights without sending.

## Failure handling rules

- Missing evidence for a category:
  - do NOT hunt for it
  - convert into a “risk/unknowns” question
  - attach best available anchors + nearest relevant files
- Very large repos:
  - tighten budgets
  - reduce base context to 1–2 files
  - prefer following import chains over directory scans
- Unclear entrypoint:
  - use build scripts/config to infer; otherwise choose the most central “composition root” file you can find and proceed

## Write output to docs (required)

When running this skill, also write the complete deliverable (the single `bash` fence) to:

- `docs/oracle-strategist-commands-YYYY-MM-DD.md` (ISO date)
Fallback:
- `docs/oracle-strategist-commands.md`

In the assistant response:

1) Print: `Output file: <path>`
2) Then print the exact same `bash` fenced deliverable.


--- .config/skills/oraclepack-usecase-rpg/SKILL.md ---
---
name: oraclepack-usecase-rpg
description: Generate a strict, oraclepack-ingestible 20-step Oracle strategist pack (single ```bash fence) with ROI-ranked step headers and RPG (Repository Planning Graph) metadata embedded in each -p prompt body.
metadata:
  short-description: Strict oraclepack pack generator (RPG-infused)
---

## Quick start

Use this skill when the user asks for any of the following:
- an “oraclepack pack”, “oracle strategist question pack”, or “20-step ROI-ranked pack”
- “oraclepack use cases” that must keep the strict runner-ingestible output
- the same, but with RPG/RPG graph/Repository Planning Graph infusion

What you produce:
- Exactly one Markdown deliverable (intended path: `docs/oracle-pack-YYYY-MM-DD.md`)
- The deliverable contains **exactly one** fenced code block that starts with **exactly** ` ```bash` then a newline (no extra tokens after `bash`)
- The fenced `bash` block contains **exactly 20** sequential steps, `01..20`

Reference template: `references/oracle-pack-template.md`

Optional validator:
- `python3 scripts/validate_oraclepack_pack.py docs/oracle-pack-YYYY-MM-DD.md`

## Workflow

### 1) Parse inputs (free-text args)

Interpret the user’s trailing text as conceptual `{{args}}` and extract:
- `codebase_name` (default: `Unknown`)
- `constraints` (default: `None`)
- `non_goals` (default: `None`)
- `team_size` (default: `Unknown`)
- `deadline` (default: `Unknown`)
- `out_dir` (default: `oracle-out`)
- `oracle_cmd` (default: `oracle`)
- `oracle_flags` (default: `--files-report`)
- `extra_files` (default: empty; append **literally** to every command if provided)

If any value is missing or ambiguous, use the defaults above (do not ask questions).

### 2) Repo discovery (inference-first, deterministic)

Read small, high-signal “index” artifacts first (if they exist):
- README / overview docs
- manifests / dependency lockfiles
- entrypoints (main binaries, CLI, server startup)
- config examples
- CI configuration

Then do targeted discovery (only as needed):
- Prefer deterministic searches (e.g., `ck`, `ast-grep`, `fd`) if available.
- Cap results deterministically (fixed max hits).
- If a file/path does not exist: record `Unknown` and continue.

### 3) Plan the 20 probes (what to cover)

You must keep categories **exactly** to this set (do not add/remove/rename):
- contracts/interfaces
- invariants
- caching/state
- background jobs
- observability
- permissions
- migrations
- UX flows
- failure modes
- feature flags

Across the 20 steps, ensure at least one step explicitly targets each sub-layer below (embed it in the *Question* text; map to the best required category above):
- supply chain/dependencies
- secrets/credentials
- CI/CD or release process
- runtime configuration/IaC (if present)
- privacy/PII/retention (if applicable)
- SLO/alerting/runbooks
- cost/performance hotspots
- testing strategy/coverage

Horizon mix requirement:
- Exactly **12** steps with `horizon=Immediate`
- Exactly **8** steps with `horizon=Strategic`

### 4) RPG infusion (non-breaking)

Inside each step’s `-p` prompt body, include an `RPG:` block with:
- `FunctionalNode: <Capability>::<Feature> (WHAT)`
- `StructuralNode: <module-or-dir> :: <public surface> (HOW)`
- `DependsOn: [<prior step ids, optional>]`
- `Phase: <P0|P1|P2|P3>`

Dependency rule (to preserve ROI-sorted executability):
- `DependsOn` may reference **only earlier step IDs**.

Phase mapping guidance (do not create new categories):
- `P0`: contracts/interfaces, invariants, permissions, observability
- `P1`: caching/state, background jobs, failure modes
- `P2`: migrations, feature flags
- `P3`: UX flows

### 5) ROI scoring and ordering

For each step choose:
- `impact`, `confidence`, `effort` as **one decimal** in `0.1..1.0`

Compute:
- `ROI = (impact * confidence) / effort`

Ordering requirement:
- Sort all 20 steps by ROI descending
- Tie-break by lower effort
- After sorting, assign IDs `01..20` in that sorted order

If ordering conflicts with RPG dependencies:
- Adjust impact/confidence/effort (and thus ROI) so prerequisite/foundation steps float earlier.
- Keep `DependsOn` pointing backwards only.

### 6) Render the pack (strict output)

You must output exactly one Markdown deliverable matching the structure in `references/oracle-pack-template.md`:
- Title section
- Parsed args list
- Commands section with **exactly one** fenced code block:
  - opening fence must be exactly: <code>```bash</code>
  - closing fence must be exactly: <code>```</code>
- Coverage check section

Inside the fenced `bash` block:
- Include a small prelude setting `out_dir="..."` (so pack metadata can be derived).
- Each of the 20 steps must start with a header line matching one of:
  - `# 01) ...`
  - `# 01 — ...`
  - `# 01 - ...`

Each step header line must include all tokens:
- `ROI=<number>`
- `impact=<i>`
- `confidence=<c>`
- `effort=<e>`
- `horizon=<Immediate|Strategic>`
- `category=<one of required categories>`
- `reference=<...>` (use `Unknown` if needed)

Each step must invoke the Oracle CLI once and must include:
- `--write-output "<out_dir>/<nn>-<slug>.md"`
- A `-p` prompt body that includes (in this exact section order):
  - `Strategist question #NN`
  - `RPG:` block (fields listed above)
  - `Reference: ...`
  - `Category: ...`
  - `Horizon: ...`
  - `ROI: ... (impact=..., confidence=..., effort=...)`
  - `Question: ...`
  - `Rationale: ...` (exactly one sentence)
  - `Smallest experiment today: ...` (exactly one action)
  - `Constraints: ...`
  - `Non-goals: ...`
  - `Answer format:` with the 4 required numbered items

Quoting guidance for `-p` (recommended, robust):
- Use a heredoc in command substitution so the prompt stays readable while remaining a single oracle invocation:
  - `-p "$(cat <<'PROMPT' ... PROMPT)"`

### 7) Validate before finalizing

Run the included validator if possible:
- `python3 scripts/validate_oraclepack_pack.py docs/oracle-pack-YYYY-MM-DD.md`

Fix any failures by editing the pack until it passes.

## Output contract

When invoked, produce:

1) A single line:
- `Output file: docs/oracle-pack-YYYY-MM-DD.md`

2) Immediately after, the **exact** Markdown content for that file (no extra commentary).

If the current date is not available in the execution context:
- Use `docs/oracle-pack.md`.

## Failure modes (do not guess)

- Missing repo evidence → use `Reference: Unknown` and in the prompt’s item (4) name the exact missing file/path patterns to attach next.
- Missing args → apply defaults; do not ask follow-ups.
- Conflicting constraints (ordering vs dependencies vs horizon mix) → preserve strict ingestion first:
  1) single `bash` fence
  2) 20 sequential steps
  3) required header tokens
  4) horizon mix 12/8
  Then adjust ROI inputs and dependency edges.

## Invocation examples

1) “Generate an RPG-infused oraclepack strategist pack for this repo; focus on permissions boundaries and CI/CD; keep strict oraclepack output.”

2) “Create a 20-step ROI-ranked oraclepack pack for `promptbench` with constraints=None and non_goals=None; default out_dir; strict format.”

3) “Produce an oraclepack pack emphasizing caching/state correctness and observability; include at least one step about cost/performance hotspots; strict template; RPG nodes.”

4) “Generate an oraclepack use-case pack for a Go CLI; include secrets/credentials handling and release process; keep categories fixed; strict single ```bash fence.”

5) “Make an oraclepack pack with explicit dependency edges (DependsOn) that only point backwards; preserve ROI-sorted order; strict output.”


--- .config/skills/strategist-questions-oracle-pack/SKILL.md ---
---
name: strategist-questions-oracle-pack
description: Generate exactly 20 evidence-cited, ROI-ranked strategist questions for the current repository (12 immediate + 8 strategic), then emit them as runnable @steipete/oracle CLI commands with minimal targeted file attachments and deterministic per-question output paths (a copy/paste-ready “oracle question pack” in the same scratch-style format as oracle-scratch.md).
---

## Quick start

Use this skill when the user wants strategist questions **and** wants to run each question through Oracle as a separate command (so a second model can answer with repo context).

Invocation:

- `$strategist-questions-oracle-pack <free-text args>`

Primary output: a Markdown doc whose main content is a **single** fenced `bash` block containing **exactly 20** `oracle ...` commands (copy/paste-ready), in the same “scratch-style” format shown in `references/oracle-scratch-format.md`.

## Inputs

- Repository contents (current working directory).
- Free-text args (may include): `codebase_name`, `constraints`, `non_goals`, `team_size`, `deadline`, plus Oracle pack controls:
  - `out_dir` (default: `oracle-out`)
  - `oracle_cmd` (default: `oracle`)
  - `oracle_flags` (default: `--browser-attachments always --files-report`)
  - `extra_files` (optional: comma-separated extra `-f/--file` entries to include in *every* command; default: none)

## Deterministic search/tooling rules (must follow)

- Prefer deterministic, non-interactive commands so runs are reproducible.
- Before using `ck` in a session, **always run**: `ck --help` and only rely on flags that `ck --help` confirms.
- Default search tool is `ck`:
  - Conceptual: `ck --sem "<query>"`
  - Literal: `ck --regex "<pattern>"`
  - Best of both: `ck --hybrid "<query>"`
  - For post-processing: prefer `ck --jsonl | jq ... | sort | head`
- Structural search: use `ast-grep` when syntax/shape matters (see `references/inference-first-discovery.md`).
- File finding: prefer `fd` (filename/path/extension filters).

## Args parsing (no follow-ups)

Extract values if present; otherwise set defaults:

- `codebase_name`: `Unknown`
- `constraints`: `None`
- `non_goals`: `None`
- `team_size`: `Unknown`
- `deadline`: `Unknown`
- `out_dir`: `oracle-out`
- `oracle_cmd`: `oracle`
- `oracle_flags`: `--browser-attachments always --files-report`
- `extra_files`: (empty)

Honor `constraints`. Explicitly exclude `non_goals`.

## Workflow (inference-first discovery → evidence → questions → oracle pack)

### 1) Inference-first discovery (adaptive, codebase-search compliant)

Goal: infer *what this repo is* and *where the truth likely lives* before broad sweeps.

Do this in order:

1. **Discover the search interface (required)**
   - Run: `ck --help`
   - If `ck` indicates it needs setup/indexing, follow the instructions printed by `ck`.

2. **Map the repo surface (cheap, high-signal)**
   - Use `fd` to list likely roots deterministically:
     - `fd . -d 2`
     - `fd -p README.md`
     - `fd -p package.json`
     - `fd -p pyproject.toml`
     - `fd -p go.mod`
     - `fd -p Cargo.toml`
   - Read the smallest set of “index” files that describe structure:
     - README / docs index if present
     - primary manifests/config
     - obvious entrypoint referenced by scripts/manifests

3. **Infer stack + conventions from evidence**
   - From manifests/config, infer runtime + framework signals (dependencies, filenames, directory conventions).
   - From entrypoints, infer wiring patterns (router, DI/container, job scheduler, migration tool).

4. **Derive a “search plan” from inferred signals**
   - Prefer following imports/references from entrypoints into:
     - routing/handlers
     - auth middleware/policies
     - job/queue registration
     - data layer/migrations
     - feature flags config
     - observability bootstrap
   - Use `ck` in the smallest inferred app roots first:
     - Conceptual: `ck --sem "<subsystem intent>"`
     - Hybrid when unsure: `ck --hybrid "<subsystem intent>"`
   - If results are broad/noisy:
     - Narrow to directories/files from the top hits, then re-run `ck`.
     - Use `ast-grep` when you need structure, not text matches.

This improves efficiency vs. hard-targeting a long list of patterns up front: you start with the repo’s own “self-description” and expand only as needed.

### 2) Evidence harvesting (collect anchors before writing questions)

Collect **at least 20 candidate anchors** as one of:

- `{path}:{symbol}` (preferred: nearest function/class/type/handler)
- `{endpoint}` (literal endpoint string)
- `{event}` (job/queue/event name)

For each required category, attempt to identify at least one anchor:

- contracts/interfaces
- invariants
- caching/state
- background jobs
- observability
- permissions
- migrations
- UX flows
- failure modes
- feature flags

Use deterministic selection of candidates:
- If using `ck --jsonl`, post-process to stable top-N:
  - `ck --jsonl ... | jq ... | sort | head -n 20`
- If using plain output, cap deterministically with `head`.

If evidence for a category cannot be found:

- still generate a question for it with reference `Unknown`
- explicitly state which missing artifact pattern would likely provide it (e.g., `**/migrations/**` not found)

### 3) Generate exactly 20 strategist questions (evidence-cited + minimal experiments)

Produce exactly:

- **12** Immediate Exploration
- **8** Strategic Planning

Each question must include:

- **Reference**: `{path}:{symbol}` OR `{endpoint}` OR `{event}` OR `Unknown`
- **Question**: incisive, feasibility-focused, no scope creep
- **Rationale**: exactly one sentence
- **Smallest experiment today**: exactly one concrete action runnable today (targeted `ck` query, targeted `ast-grep` pattern, trace a codepath, add a log line, minimal unit/integration test, run migration dry-run, execute a single endpoint, etc.)

No duplicates (by intent or reference).

### 4) Score and order by near-term ROI (required math + sorting)

For each question compute:

- `ROI = (impact * confidence) / effort`
- `impact`, `confidence`, `effort` ∈ `{0.1..1.0}` (one decimal)

Sort all 20 descending by ROI; break ties by lower effort.

### 5) Convert the 20 questions into an Oracle “question pack” (final deliverable)

For each question (in final sorted order), emit a runnable command using:

- command: `{{oracle_cmd}}` (default `oracle`)
- include `{{oracle_flags}}` (default `--browser-attachments always --files-report`)
- deterministic output file: `--write-output "<out_dir>/<nn>-<slug>.md"`
  - `<nn>` = zero-padded 2-digit index (01..20)
  - `<slug>` = short, filesystem-safe slug derived from `{category}` + a hint of `{reference}` (fallback to category only)
- prompt: a structured prompt that includes the strategist question fields and constraints:
  - reference, question, rationale, smallest experiment today
  - constraints + non_goals (explicit)
  - requested Oracle answer shape (concise, evidence-cited, and actionable)

#### Attachment selection (must be minimal, evidence-driven)

For each command, attach only the smallest set of files that lets Oracle answer correctly:

- If reference is `{path}:{symbol}`:
  - attach that `path`
  - optionally attach **one** upstream config/router/entrypoint file that shows how it’s invoked (only if needed)
- If reference is `{endpoint}` or `{event}`:
  - attach the file(s) where you found the literal endpoint/event definition
- If reference is `Unknown`:
  - attach only “index” files (README + manifest + 1 best-guess entrypoint), and in the prompt state what artifact pattern is missing

Follow `references/attachment-minimization.md`.

Also:
- If `extra_files` was provided in args, include those `-f` entries in every command (after the evidence-minimal attachments).

### 6) Render final output using the pack template

Use `assets/oracle-pack-template.md` as the strict shape for the final Markdown deliverable.

## Output contract (strict)

Produce exactly one Markdown deliverable that:

1. Matches the structure in `assets/oracle-pack-template.md`
2. Contains **exactly 20** `oracle` invocations total
3. Commands are sorted by ROI (desc; ties by lower effort)
4. Each command:
   - includes `--write-output "<out_dir>/<nn>-<slug>.md"`
   - includes minimal targeted `-f/--file` attachments
   - embeds the strategist question (reference, question, rationale, experiment) in the `-p/--prompt` text

## Failure modes

- Missing/insufficient evidence:
  - use reference `Unknown`
  - state the missing artifact pattern that would provide evidence
  - keep the question actionable and experiment minimal
  - keep attachments limited to index files (don’t attach huge globs)
- Search tool limitations (`ck` missing/setup required):
  - run `ck --help` and follow printed setup; if unavailable, fall back to reading index files + minimal `fd`-based locating
  - proceed with partial evidence; mark affected references `Unknown`
- Ambiguous args:
  - apply defaults without follow-ups

## References

- `assets/oracle-pack-template.md` — exact final output shape
- `references/inference-first-discovery.md` — inference-driven search plan aligned to `ck`/`ast-grep`/`fd`
- `references/attachment-minimization.md` — deterministic rules for minimal `-f` selections
- `references/oracle-scratch-format.md` — the target “scratch-style” formatting to mimic

## Invocation examples

- “Generate an oracle question pack for this repo. codebase_name=Payments API; constraints=no new infra; non_goals=mobile app; team_size=3; deadline=2 weeks; out_dir=artifacts/oracle”
- “Create oracle strategist questions; constraints=ship bugfixes only; deadline=Friday; out_dir=oracle-out”
- “Produce an oracle pack: focus on auth + jobs + migrations; non_goals=refactor; out_dir=oracle-q”
- “Generate strategist oracle commands; constraints=keep DB schema stable; team_size=2; deadline=1 week”
- “Make an Oracle question pack; oracle_cmd='npx -y @steipete/oracle'; oracle_flags='--engine browser --browser-attachments always --files-report'; out_dir=oracle-out”

### Write output to `docs/` (required)

- Ensure a `docs/` directory exists at the repo root (create it if missing).
- Write the complete final deliverable to:
  - `docs/strategist-questions-oracle-pack-YYYY-MM-DD.md` (use today’s date; ISO 8601)
  - Fallback if date is unavailable: `docs/strategist-questions-oracle-pack.md`
- The file must contain only the deliverable Markdown (no extra preamble/epilogue).
- In the assistant response, print the chosen path first on a single line: `Output file: <path>`, then print the same Markdown content.


--- .config/mcp/oraclepack-taskify/assets/action-pack-template.md ---
# Oraclepack Stage 3 — Action Pack (Taskify)

Generated: {{pack_date}}

Parsed args (resolved):
- out_dir: {{out_dir}}
- pack_path: {{pack_path}}
- actions_json: {{actions_json}}
- actions_md: {{actions_md}}
- prd_path: {{prd_path}}
- tag: {{tag}}
- mode: {{mode}}
- top_n: {{top_n}}
- oracle_cmd: {{oracle_cmd}}
- task_master_cmd: {{task_master_cmd}}
- tm_cmd: {{tm_cmd}}
- extra_files: (embedded where applicable)

This document must contain exactly one bash code fence, and no other code fences.

```bash
# 01) Preflight: verify inputs and tools (fail fast)
set -euo pipefail

OUT_DIR="{{out_dir}}"
MODE="{{mode}}"
TASK_MASTER_CMD="{{task_master_cmd}}"
ORACLE_CMD="{{oracle_cmd}}"
TM_CMD="{{tm_cmd}}"

if [ ! -d "${OUT_DIR}" ]; then
  echo "ERROR: out_dir does not exist: ${OUT_DIR}" >&2
  exit 2
fi

shopt -s nullglob
for n in 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20; do
  matches=( "${OUT_DIR}/${n}-"*.md )
  if [ "${#matches[@]}" -eq 0 ]; then
    echo "ERROR: missing oracle output for prefix ${n}: expected ${OUT_DIR}/${n}-*.md" >&2
    exit 3
  fi
  if [ "${#matches[@]}" -gt 1 ]; then
    echo "ERROR: multiple oracle outputs for prefix ${n}; expected exactly one file." >&2
    printf '%s\n' "${matches[@]}" >&2
    exit 4
  fi
done

if ! command -v "${TASK_MASTER_CMD%% *}" >/dev/null 2>&1; then
  echo "ERROR: required tool missing: ${TASK_MASTER_CMD%% *} (from task_master_cmd='${TASK_MASTER_CMD}')" >&2
  exit 10
fi

if ! command -v "${ORACLE_CMD%% *}" >/dev/null 2>&1; then
  echo "ERROR: required tool missing: ${ORACLE_CMD%% *} (from oracle_cmd='${ORACLE_CMD}')" >&2
  exit 11
fi

if [ "${MODE}" = "autopilot" ]; then
  if ! command -v "${TM_CMD%% *}" >/dev/null 2>&1; then
    echo "ERROR: autopilot mode requires tm_cmd, but tool missing: ${TM_CMD%% *} (from tm_cmd='${TM_CMD}')" >&2
    echo "HINT: rerun Stage 3 with mode=backlog if you only want tasks generated." >&2
    exit 12
  fi
fi

mkdir -p "$(dirname "{{actions_json}}")" "$(dirname "{{actions_md}}")" "$(dirname "{{prd_path}}")" "docs" "${OUT_DIR}"

cat > "${OUT_DIR}/_actions.schema.md" <<'SCHEMA'
# Canonical Actions JSON Schema (human-readable)

This file describes the required structure of `_actions.json` produced in Step 02.

## Root object

- `metadata` (object, required)
  - `generated_at` (string, ISO-8601 recommended)
  - `pack_date` (string, YYYY-MM-DD)
  - `source_out_dir` (string)
  - `repo` (object)
    - `name` (string, optional)
    - `root` (string, optional)
    - `head_sha` (string, optional)
  - `tooling` (object)
    - `oracle_cmd` (string)
    - `task_master_cmd` (string)
  - `top_n` (number)

- `items` (array, required; max 20)
  - Each item is normalized and must include:

## Item fields (required unless marked optional)

- `id` (string): "01".."20"
- `source_file` (string): the exact answer file path used for this item
- `category` (string): a stable label (e.g., `contracts/interfaces`, `permissions`, `observability`, etc.)
- `priority_score` (number): higher means more important (can be ROI if available)
- `recommended_next_action` (string): a single imperative sentence
- `missing_artifacts` (array of strings): file/path globs to locate or create
- `acceptance_criteria` (array of strings): testable, objective conditions
- `risk_notes` (array of strings): specific risks/unknowns, no generic filler
- `estimated_effort` (string): one of `XS|S|M|L|XL` (or a short consistent scale)

## Optional item fields

- `dependencies` (array of strings): other `id` values that should precede this item

## Normalization rules

- Keep `items` sorted by `id` ascending (01..20), regardless of priority.
- Keep all arrays stably ordered (most important first; ties by lexical order).
- Do not include code fences in any string values.
SCHEMA

cat > "${OUT_DIR}/_prd_synthesis_prompt.md" <<'PROMPT'
See the canonical prompt text in the skill asset: assets/prd-synthesis-prompt.md.

This repo-local copy exists for traceability and to keep the Action Pack portable.
PROMPT

echo "OK: Preflight passed."
echo "OK: Inputs: ${OUT_DIR}/01-*.md .. ${OUT_DIR}/20-*.md"
echo "OK: Mode: ${MODE}"


# 02) Synthesize canonical actions JSON + summary MD
set -euo pipefail

OUT_DIR="{{out_dir}}"
ACTIONS_JSON="{{actions_json}}"
ACTIONS_MD="{{actions_md}}"

shopt -s nullglob
oracle_file_flags=()
for n in 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20; do
  matches=( "${OUT_DIR}/${n}-"*.md )
  if [ "${#matches[@]}" -ne 1 ]; then
    echo "ERROR: expected exactly one match for ${OUT_DIR}/${n}-*.md, got ${#matches[@]}" >&2
    printf '%s\n' "${matches[@]:-}" >&2
    exit 20
  fi
  oracle_file_flags+=( -f "${matches[0]}" )
done

# Extra attachments (auto-expanded at pack generation time)
# (If none, this section is empty)
{{EXTRA_FILES_LINES}}

mkdir -p "$(dirname "${ACTIONS_JSON}")" "$(dirname "${ACTIONS_MD}")"

{{oracle_cmd}} \
  --write-output "${ACTIONS_JSON}" \
  "${oracle_file_flags[@]}" \
  -p "$(cat <<'PROMPT'
You are producing a SINGLE JSON document and nothing else.

Task: Normalize 20 oraclepack answer files into a canonical actionable plan.

Hard requirements:
- Output MUST be valid JSON (no markdown, no prose, no code fences).
- Output MUST follow the schema described below.
- Output MUST be deterministic in ordering:
  - items sorted by id ascending: 01..20
  - arrays use stable ordering (highest priority first; ties lexical)
- Extract only actionable work. Do not invent repo facts. If evidence is missing, record it in missing_artifacts/risk_notes explicitly.

Schema (summarized):
Root object:
- metadata: { generated_at, pack_date, source_out_dir, repo?, tooling?, top_n }
- items: array (max 20)
Each item:
- id: "01".."20"
- source_file: string
- category: string
- priority_score: number
- recommended_next_action: string (single imperative sentence)
- missing_artifacts: string[]
- acceptance_criteria: string[] (testable)
- risk_notes: string[]
- estimated_effort: "XS"|"S"|"M"|"L"|"XL"
- dependencies?: string[] of ids

Repo/run context:
- pack_date: {{pack_date}}
- source_out_dir: {{out_dir}}
- top_n: {{top_n}}
- tag: {{tag}}

Output hygiene:
- Do not include backticks or fenced code blocks anywhere.
- Keep strings concise and specific.

Now produce the JSON.
PROMPT
)"

{{oracle_cmd}} \
  --write-output "${ACTIONS_MD}" \
  -f "${ACTIONS_JSON}" \
  -p "$(cat <<'PROMPT'
Write a human-readable Markdown summary of the canonical actions JSON.

Hard requirements:
- Output MUST be Markdown text with headings/bullets only (no code fences).
- Keep ordering aligned with items id ascending (01..20).
- Include:
  - short executive summary (5–10 bullets)
  - top {{top_n}} prioritized list (with id, title inferred from recommended_next_action, category, and why)
  - per-item: recommended_next_action + acceptance_criteria bullets + missing_artifacts bullets
- Do not invent facts; reflect only what is present in the JSON.

Now write the summary Markdown.
PROMPT
)"

echo "OK: Wrote ${ACTIONS_JSON}"
echo "OK: Wrote ${ACTIONS_MD}"


# 03) Generate PRD for Task Master
set -euo pipefail

ACTIONS_JSON="{{actions_json}}"
PRD_PATH="{{prd_path}}"

mkdir -p "$(dirname "${PRD_PATH}")"

{{oracle_cmd}} \
  --write-output "${PRD_PATH}" \
  -f "${ACTIONS_JSON}" \
  -p "$(cat <<'PROMPT'
Write a Task Master-compatible PRD (Markdown) derived from the canonical actions JSON.

Hard requirements:
- Output MUST be Markdown (no code fences).
- Be dependency-aware (use dependencies if present; otherwise infer minimal dependencies cautiously).
- Prioritize focus: select the top N items by priority_score (N = TOP_N), but keep a traceability appendix mapping all ids 01..20.
- Every selected item must become an implementation-ready PRD section with:
  - Goal
  - Scope
  - Non-goals
  - Constraints
  - Acceptance criteria (testable)
  - Risks/unknowns
  - Dependencies (explicit)
- Use the tag value "{{tag}}" in the PRD text where helpful for grouping.

Constants:
- TOP_N={{top_n}}
- TAG={{tag}}

Now produce the PRD.
PROMPT
)"

echo "OK: Wrote ${PRD_PATH}"


# 04) Task Master: parse PRD into tasks
set -euo pipefail

PRD_PATH="{{prd_path}}"
TASK_MASTER_CMD="{{task_master_cmd}}"
TAG="{{tag}}"

if "${TASK_MASTER_CMD}" parse-prd "${PRD_PATH}" --tag "${TAG}" 2>/dev/null; then
  echo "OK: task-master parse-prd (tagged) succeeded."
else
  echo "INFO: task-master parse-prd did not accept --tag; retrying without tag."
  "${TASK_MASTER_CMD}" parse-prd "${PRD_PATH}"
fi

if [ -f ".taskmaster/tasks.json" ]; then
  echo "OK: Found .taskmaster/tasks.json"
elif [ -f "tasks.json" ]; then
  echo "OK: Found tasks.json"
else
  echo "WARN: tasks.json not found at .taskmaster/tasks.json or tasks.json. Check your Task Master configuration/output path."
fi


# 05) Task Master: analyze complexity and save report
set -euo pipefail

TASK_MASTER_CMD="{{task_master_cmd}}"
OUT_DIR="{{out_dir}}"

mkdir -p "${OUT_DIR}"

"${TASK_MASTER_CMD}" analyze-complexity --output "${OUT_DIR}/tm-complexity.json"
echo "OK: Wrote ${OUT_DIR}/tm-complexity.json"


# 06) Task Master: expand tasks
set -euo pipefail

TASK_MASTER_CMD="{{task_master_cmd}}"

"${TASK_MASTER_CMD}" expand --all
echo "OK: Expanded tasks."


# 07) Pipelines (pipelines mode only): generate deterministic pipelines from tasks.json
set -euo pipefail

MODE="{{mode}}"

if [ "${MODE}" != "pipelines" ]; then
  echo "SKIP: mode=${MODE} (pipelines step runs only when mode=pipelines)."
else
  tasks_path=""
  if [ -f ".taskmaster/tasks.json" ]; then
    tasks_path=".taskmaster/tasks.json"
  elif [ -f "tasks.json" ]; then
    tasks_path="tasks.json"
  else
    echo "ERROR: tasks.json not found; cannot generate pipelines." >&2
    exit 70
  fi

  mkdir -p "docs"

  {{oracle_cmd}} \
    --write-output "docs/oracle-actions-pipelines.md" \
    -f "${tasks_path}" \
    -p "$(cat <<'PROMPT'
Generate deterministic command pipelines from tasks.json.

Hard requirements:
- Output MUST be Markdown (no code fences).
- Include 3–6 pipelines, each a numbered list of shell commands.
- Each pipeline must be tests-first and avoid destructive operations.
- Commands should be generic and repo-agnostic (no invented scripts).
- Include a short “resume strategy” section explaining how to re-run pipelines safely.

Now write docs/oracle-actions-pipelines.md content.
PROMPT
)"

  echo "OK: Wrote docs/oracle-actions-pipelines.md"
fi


# 08) Autopilot (autopilot mode only): branch safety + guarded entrypoint
set -euo pipefail

MODE="{{mode}}"
TM_CMD="{{tm_cmd}}"
OUT_DIR="{{out_dir}}"
PACK_DATE="{{pack_date}}"
TAG="{{tag}}"

if [ "${MODE}" != "autopilot" ]; then
  echo "SKIP: mode=${MODE} (autopilot step runs only when mode=autopilot)."
else
  if ! command -v git >/dev/null 2>&1; then
    echo "ERROR: autopilot mode requires git on PATH." >&2
    exit 80
  fi

  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    echo "ERROR: not inside a git work tree; autopilot mode requires a git repo." >&2
    exit 81
  fi

  if ! git diff --quiet || ! git diff --cached --quiet; then
    echo "ERROR: working tree not clean. Commit/stash before autopilot." >&2
    exit 82
  fi

  current_branch="$(git rev-parse --abbrev-ref HEAD)"
  if [ "${current_branch}" = "main" ] || [ "${current_branch}" = "master" ]; then
    new_branch="oraclepack/${PACK_DATE}-${TAG}"
    echo "INFO: on default-like branch (${current_branch}); creating work branch: ${new_branch}"
    git checkout -b "${new_branch}"
  else
    echo "OK: current branch is ${current_branch}"
  fi

  mkdir -p "${OUT_DIR}"
  cat > "${OUT_DIR}/tm-autopilot.state.json" <<STATE
{"pack_date":"${PACK_DATE}","tag":"${TAG}","mode":"autopilot","notes":"State file created by Stage-3 Action Pack. Autopilot tooling should resume from this file if supported."}
STATE

  echo "OK: Wrote ${OUT_DIR}/tm-autopilot.state.json"
  echo "INFO: Starting autopilot via: ${TM_CMD} autopilot"
  echo "INFO: If your tm tool uses a different subcommand, edit this step accordingly."

  if ! "${TM_CMD}" --help 2>&1 | grep -qi "autopilot"; then
    echo "ERROR: '${TM_CMD}' does not advertise 'autopilot' in --help output." >&2
    echo "HINT: rerun Stage 3 with mode=backlog if you only want tasks generated." >&2
    exit 83
  fi

  "${TM_CMD}" autopilot
fi
```

--- .config/mcp/oraclepack-taskify/assets/actions-json-schema.md ---
# Canonical Actions JSON Schema (human-readable)

This schema defines the required structure of the canonical actions file:

- Default path: `<out_dir>/_actions.json`

## Root object

The root MUST be a JSON object with:

### metadata (required)

- `generated_at` (string): generation timestamp (ISO-8601 recommended)
- `pack_date` (string): `YYYY-MM-DD`
- `source_out_dir` (string): the oraclepack output dir used (e.g., `oracle-out`)
- `repo` (object, optional):
  - `name` (string, optional)
  - `root` (string, optional)
  - `head_sha` (string, optional)
- `tooling` (object, optional):
  - `oracle_cmd` (string)
  - `task_master_cmd` (string)
- `top_n` (number): the top-N focus value used to build PRD/pipelines

### items (required; max 20)

`items` MUST be an array with up to 20 objects. Each item MUST include:

- `id` (string): `"01"`..`"20"`
- `source_file` (string): the specific answer file used for this item
- `category` (string): stable label describing the domain area
- `priority_score` (number): higher means higher priority (can be ROI if present)
- `recommended_next_action` (string): single imperative sentence
- `missing_artifacts` (array of strings): file/path globs or concrete paths
- `acceptance_criteria` (array of strings): testable, objective conditions
- `risk_notes` (array of strings): risks/unknowns grounded in evidence gaps
- `estimated_effort` (string): use a consistent scale such as `XS|S|M|L|XL`

Optional:

- `dependencies` (array of strings): other ids (e.g., `["03","07"]`) that should precede this item

## Normalization rules

- Items MUST be sorted by `id` ascending (`01..20`) for machine stability.
- Within each item:
  - `missing_artifacts`, `acceptance_criteria`, and `risk_notes` MUST be stably ordered.
- Do not include fenced code blocks in any string values.


--- .cursor/commands/tm/add-dependency.md ---
Add a dependency between tasks.

Arguments: $ARGUMENTS

Parse the task IDs to establish dependency relationship.

## Adding Dependencies

Creates a dependency where one task must be completed before another can start.

## Argument Parsing

Parse natural language or IDs:
- "make 5 depend on 3" → task 5 depends on task 3
- "5 needs 3" → task 5 depends on task 3
- "5 3" → task 5 depends on task 3
- "5 after 3" → task 5 depends on task 3

## Execution

```bash
task-master add-dependency --id=<task-id> --depends-on=<dependency-id>
```

## Validation

Before adding:
1. **Verify both tasks exist**
2. **Check for circular dependencies**
3. **Ensure dependency makes logical sense**
4. **Warn if creating complex chains**

## Smart Features

- Detect if dependency already exists
- Suggest related dependencies
- Show impact on task flow
- Update task priorities if needed

## Post-Addition

After adding dependency:
1. Show updated dependency graph
2. Identify any newly blocked tasks
3. Suggest task order changes
4. Update project timeline

## Example Flows

```
/taskmaster:add-dependency 5 needs 3
→ Task #5 now depends on Task #3
→ Task #5 is now blocked until #3 completes
→ Suggested: Also consider if #5 needs #4
```

--- .cursor/commands/tm/add-subtask.md ---
Add a subtask to a parent task.

Arguments: $ARGUMENTS

Parse arguments to create a new subtask or convert existing task.

## Adding Subtasks

Creates subtasks to break down complex parent tasks into manageable pieces.

## Argument Parsing

Flexible natural language:
- "add subtask to 5: implement login form"
- "break down 5 with: setup, implement, test"
- "subtask for 5: handle edge cases"
- "5: validate user input" → adds subtask to task 5

## Execution Modes

### 1. Create New Subtask
```bash
task-master add-subtask --parent=<id> --title="<title>" --description="<desc>"
```

### 2. Convert Existing Task
```bash
task-master add-subtask --parent=<id> --task-id=<existing-id>
```

## Smart Features

1. **Automatic Subtask Generation**
   - If title contains "and" or commas, create multiple
   - Suggest common subtask patterns
   - Inherit parent's context

2. **Intelligent Defaults**
   - Priority based on parent
   - Appropriate time estimates
   - Logical dependencies between subtasks

3. **Validation**
   - Check parent task complexity
   - Warn if too many subtasks
   - Ensure subtask makes sense

## Creation Process

1. Parse parent task context
2. Generate subtask with ID like "5.1"
3. Set appropriate defaults
4. Link to parent task
5. Update parent's time estimate

## Example Flows

```
/taskmaster:add-subtask to 5: implement user authentication
→ Created subtask #5.1: "implement user authentication"
→ Parent task #5 now has 1 subtask
→ Suggested next subtasks: tests, documentation

/taskmaster:add-subtask 5: setup, implement, test
→ Created 3 subtasks:
  #5.1: setup
  #5.2: implement
  #5.3: test
```

## Post-Creation

- Show updated task hierarchy
- Suggest logical next subtasks
- Update complexity estimates
- Recommend subtask order

--- .cursor/commands/tm/add-task.md ---
Add new tasks with intelligent parsing and context awareness.

Arguments: $ARGUMENTS

## Smart Task Addition

Parse natural language to create well-structured tasks.

### 1. **Input Understanding**

I'll intelligently parse your request:
- Natural language → Structured task
- Detect priority from keywords (urgent, ASAP, important)
- Infer dependencies from context
- Suggest complexity based on description
- Determine task type (feature, bug, refactor, test, docs)

### 2. **Smart Parsing Examples**

**"Add urgent task to fix login bug"**
→ Title: Fix login bug
→ Priority: high
→ Type: bug
→ Suggested complexity: medium

**"Create task for API documentation after task 23 is done"**
→ Title: API documentation
→ Dependencies: [23]
→ Type: documentation
→ Priority: medium

**"Need to refactor auth module - depends on 12 and 15, high complexity"**
→ Title: Refactor auth module
→ Dependencies: [12, 15]
→ Complexity: high
→ Type: refactor

### 3. **Context Enhancement**

Based on current project state:
- Suggest related existing tasks
- Warn about potential conflicts
- Recommend dependencies
- Propose subtasks if complex

### 4. **Interactive Refinement**

```yaml
Task Preview:
─────────────
Title: [Extracted title]
Priority: [Inferred priority]
Dependencies: [Detected dependencies]
Complexity: [Estimated complexity]

Suggestions:
- Similar task #34 exists, consider as dependency?
- This seems complex, break into subtasks?
- Tasks #45-47 work on same module
```

### 5. **Validation & Creation**

Before creating:
- Validate dependencies exist
- Check for duplicates
- Ensure logical ordering
- Verify task completeness

### 6. **Smart Defaults**

Intelligent defaults based on:
- Task type patterns
- Team conventions
- Historical data
- Current sprint/phase

Result: High-quality tasks from minimal input.

--- .cursor/commands/tm/analyze-complexity.md ---
Analyze task complexity and generate expansion recommendations.

Arguments: $ARGUMENTS

Perform deep analysis of task complexity across the project.

## Complexity Analysis

Uses AI to analyze tasks and recommend which ones need breakdown.

## Execution Options

```bash
task-master analyze-complexity [--research] [--threshold=5]
```

## Analysis Parameters

- `--research` → Use research AI for deeper analysis
- `--threshold=5` → Only flag tasks above complexity 5
- Default: Analyze all pending tasks

## Analysis Process

### 1. **Task Evaluation**
For each task, AI evaluates:
- Technical complexity
- Time requirements
- Dependency complexity
- Risk factors
- Knowledge requirements

### 2. **Complexity Scoring**
Assigns score 1-10 based on:
- Implementation difficulty
- Integration challenges
- Testing requirements
- Unknown factors
- Technical debt risk

### 3. **Recommendations**
For complex tasks:
- Suggest expansion approach
- Recommend subtask breakdown
- Identify risk areas
- Propose mitigation strategies

## Smart Analysis Features

1. **Pattern Recognition**
   - Similar task comparisons
   - Historical complexity accuracy
   - Team velocity consideration
   - Technology stack factors

2. **Contextual Factors**
   - Team expertise
   - Available resources
   - Timeline constraints
   - Business criticality

3. **Risk Assessment**
   - Technical risks
   - Timeline risks
   - Dependency risks
   - Knowledge gaps

## Output Format

```
Task Complexity Analysis Report
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

High Complexity Tasks (>7):
📍 #5 "Implement real-time sync" - Score: 9/10
   Factors: WebSocket complexity, state management, conflict resolution
   Recommendation: Expand into 5-7 subtasks
   Risks: Performance, data consistency

📍 #12 "Migrate database schema" - Score: 8/10
   Factors: Data migration, zero downtime, rollback strategy
   Recommendation: Expand into 4-5 subtasks
   Risks: Data loss, downtime

Medium Complexity Tasks (5-7):
📍 #23 "Add export functionality" - Score: 6/10
   Consider expansion if timeline tight

Low Complexity Tasks (<5):
✅ 15 tasks - No expansion needed

Summary:
- Expand immediately: 2 tasks
- Consider expanding: 5 tasks
- Keep as-is: 15 tasks
```

## Actionable Output

For each high-complexity task:
1. Complexity score with reasoning
2. Specific expansion suggestions
3. Risk mitigation approaches
4. Recommended subtask structure

## Integration

Results are:
- Saved to `.taskmaster/reports/complexity-analysis.md`
- Used by expand command
- Inform sprint planning
- Guide resource allocation

## Next Steps

After analysis:
```
/taskmaster:expand 5    # Expand specific task
/taskmaster:expand-all  # Expand all recommended
/taskmaster:complexity-report  # View detailed report
```

--- .cursor/commands/tm/analyze-project.md ---
Advanced project analysis with actionable insights and recommendations.

Arguments: $ARGUMENTS

## Comprehensive Project Analysis

Multi-dimensional analysis based on requested focus area.

### 1. **Analysis Modes**

Based on $ARGUMENTS:
- "velocity" → Sprint velocity and trends
- "quality" → Code quality metrics
- "risk" → Risk assessment and mitigation
- "dependencies" → Dependency graph analysis
- "team" → Workload and skill distribution
- "architecture" → System design coherence
- Default → Full spectrum analysis

### 2. **Velocity Analytics**

```
📊 Velocity Analysis
━━━━━━━━━━━━━━━━━━━
Current Sprint: 24 points/week ↗️ +20%
Rolling Average: 20 points/week
Efficiency: 85% (17/20 tasks on time)

Bottlenecks Detected:
- Code review delays (avg 4h wait)
- Test environment availability
- Dependency on external team

Recommendations:
1. Implement parallel review process
2. Add staging environment
3. Mock external dependencies
```

### 3. **Risk Assessment**

**Technical Risks**
- High complexity tasks without backup assignee
- Single points of failure in architecture
- Insufficient test coverage in critical paths
- Technical debt accumulation rate

**Project Risks**
- Critical path dependencies
- Resource availability gaps
- Deadline feasibility analysis
- Scope creep indicators

### 4. **Dependency Intelligence**

Visual dependency analysis:
```
Critical Path:
#12 → #15 → #23 → #45 → #50 (20 days)
         ↘ #24 → #46 ↗

Optimization: Parallelize #15 and #24
Time Saved: 3 days
```

### 5. **Quality Metrics**

**Code Quality**
- Test coverage trends
- Complexity scores
- Technical debt ratio
- Review feedback patterns

**Process Quality**
- Rework frequency
- Bug introduction rate
- Time to resolution
- Knowledge distribution

### 6. **Predictive Insights**

Based on patterns:
- Completion probability by deadline
- Resource needs projection
- Risk materialization likelihood
- Suggested interventions

### 7. **Executive Dashboard**

High-level summary with:
- Health score (0-100)
- Top 3 risks
- Top 3 opportunities
- Recommended actions
- Success probability

Result: Data-driven decisions with clear action paths.

--- .cursor/commands/tm/auto-implement-tasks.md ---
Enhanced auto-implementation with intelligent code generation and testing.

Arguments: $ARGUMENTS

## Intelligent Auto-Implementation

Advanced implementation with context awareness and quality checks.

### 1. **Pre-Implementation Analysis**

Before starting:
- Analyze task complexity and requirements
- Check codebase patterns and conventions
- Identify similar completed tasks
- Assess test coverage needs
- Detect potential risks

### 2. **Smart Implementation Strategy**

Based on task type and context:

**Feature Tasks**
1. Research existing patterns
2. Design component architecture
3. Implement with tests
4. Integrate with system
5. Update documentation

**Bug Fix Tasks**
1. Reproduce issue
2. Identify root cause
3. Implement minimal fix
4. Add regression tests
5. Verify side effects

**Refactoring Tasks**
1. Analyze current structure
2. Plan incremental changes
3. Maintain test coverage
4. Refactor step-by-step
5. Verify behavior unchanged

### 3. **Code Intelligence**

**Pattern Recognition**
- Learn from existing code
- Follow team conventions
- Use preferred libraries
- Match style guidelines

**Test-Driven Approach**
- Write tests first when possible
- Ensure comprehensive coverage
- Include edge cases
- Performance considerations

### 4. **Progressive Implementation**

Step-by-step with validation:
```
Step 1/5: Setting up component structure ✓
Step 2/5: Implementing core logic ✓
Step 3/5: Adding error handling ⚡ (in progress)
Step 4/5: Writing tests ⏳
Step 5/5: Integration testing ⏳

Current: Adding try-catch blocks and validation...
```

### 5. **Quality Assurance**

Automated checks:
- Linting and formatting
- Test execution
- Type checking
- Dependency validation
- Performance analysis

### 6. **Smart Recovery**

If issues arise:
- Diagnostic analysis
- Suggestion generation
- Fallback strategies
- Manual intervention points
- Learning from failures

### 7. **Post-Implementation**

After completion:
- Generate PR description
- Update documentation
- Log lessons learned
- Suggest follow-up tasks
- Update task relationships

Result: High-quality, production-ready implementations.

--- .cursor/commands/tm/command-pipeline.md ---
Execute a pipeline of commands based on a specification.

Arguments: $ARGUMENTS

## Command Pipeline Execution

Parse pipeline specification from arguments. Supported formats:

### Simple Pipeline
`init → expand-all → sprint-plan`

### Conditional Pipeline
`status → if:pending>10 → sprint-plan → else → next`

### Iterative Pipeline
`for:pending-tasks → expand → complexity-check`

### Smart Pipeline Patterns

**1. Project Setup Pipeline**
```
init [prd] →
expand-all →
complexity-report →
sprint-plan →
show first-sprint
```

**2. Daily Work Pipeline**
```
standup →
if:in-progress → continue →
else → next → start
```

**3. Task Completion Pipeline**
```
complete [id] →
git-commit →
if:blocked-tasks-freed → show-freed →
next
```

**4. Quality Check Pipeline**
```
list in-progress →
for:each → check-idle-time →
if:idle>1day → prompt-update
```

### Pipeline Features

**Variables**
- Store results: `status → $count=pending-count`
- Use in conditions: `if:$count>10`
- Pass between commands: `expand $high-priority-tasks`

**Error Handling**
- On failure: `try:complete → catch:show-blockers`
- Skip on error: `optional:test-run`
- Retry logic: `retry:3:commit`

**Parallel Execution**
- Parallel branches: `[analyze | test | lint]`
- Join results: `parallel → join:report`

### Execution Flow

1. Parse pipeline specification
2. Validate command sequence
3. Execute with state passing
4. Handle conditions and loops
5. Aggregate results
6. Show summary

This enables complex workflows like:
`parse-prd → expand-all → filter:complex>70 → assign:senior → sprint-plan:weighted`

--- .cursor/commands/tm/complexity-report.md ---
Display the task complexity analysis report.

Arguments: $ARGUMENTS

View the detailed complexity analysis generated by analyze-complexity command.

## Viewing Complexity Report

Shows comprehensive task complexity analysis with actionable insights.

## Execution

```bash
task-master complexity-report [--file=<path>]
```

## Report Location

Default: `.taskmaster/reports/complexity-analysis.md`
Custom: Specify with --file parameter

## Report Contents

### 1. **Executive Summary**
```
Complexity Analysis Summary
━━━━━━━━━━━━━━━━━━━━━━━━
Analysis Date: 2024-01-15
Tasks Analyzed: 32
High Complexity: 5 (16%)
Medium Complexity: 12 (37%)
Low Complexity: 15 (47%)

Critical Findings:
- 5 tasks need immediate expansion
- 3 tasks have high technical risk
- 2 tasks block critical path
```

### 2. **Detailed Task Analysis**
For each complex task:
- Complexity score breakdown
- Contributing factors
- Specific risks identified
- Expansion recommendations
- Similar completed tasks

### 3. **Risk Matrix**
Visual representation:
```
Risk vs Complexity Matrix
━━━━━━━━━━━━━━━━━━━━━━━
High Risk  | #5(9) #12(8) | #23(6)
Med Risk   | #34(7)       | #45(5) #67(5)
Low Risk   | #78(8)       | [15 tasks]
           | High Complex  | Med Complex
```

### 4. **Recommendations**

**Immediate Actions:**
1. Expand task #5 - Critical path + high complexity
2. Expand task #12 - High risk + dependencies
3. Review task #34 - Consider splitting

**Sprint Planning:**
- Don't schedule multiple high-complexity tasks together
- Ensure expertise available for complex tasks
- Build in buffer time for unknowns

## Interactive Features

When viewing report:
1. **Quick Actions**
   - Press 'e' to expand a task
   - Press 'd' for task details
   - Press 'r' to refresh analysis

2. **Filtering**
   - View by complexity level
   - Filter by risk factors
   - Show only actionable items

3. **Export Options**
   - Markdown format
   - CSV for spreadsheets
   - JSON for tools

## Report Intelligence

- Compares with historical data
- Shows complexity trends
- Identifies patterns
- Suggests process improvements

## Integration

Use report for:
- Sprint planning sessions
- Resource allocation
- Risk assessment
- Team discussions
- Client updates

## Example Usage

```
/taskmaster:complexity-report
→ Opens latest analysis

/taskmaster:complexity-report --file=archived/2024-01-01.md
→ View historical analysis

After viewing:
/taskmaster:expand 5
→ Expand high-complexity task
```

--- .cursor/commands/tm/convert-task-to-subtask.md ---
Convert an existing task into a subtask.

Arguments: $ARGUMENTS

Parse parent ID and task ID to convert.

## Task Conversion

Converts an existing standalone task into a subtask of another task.

## Argument Parsing

- "move task 8 under 5"
- "make 8 a subtask of 5"
- "nest 8 in 5"
- "5 8" → make task 8 a subtask of task 5

## Execution

```bash
task-master add-subtask --parent=<parent-id> --task-id=<task-to-convert>
```

## Pre-Conversion Checks

1. **Validation**
   - Both tasks exist and are valid
   - No circular parent relationships
   - Task isn't already a subtask
   - Logical hierarchy makes sense

2. **Impact Analysis**
   - Dependencies that will be affected
   - Tasks that depend on converting task
   - Priority alignment needed
   - Status compatibility

## Conversion Process

1. Change task ID from "8" to "5.1" (next available)
2. Update all dependency references
3. Inherit parent's context where appropriate
4. Adjust priorities if needed
5. Update time estimates

## Smart Features

- Preserve task history
- Maintain dependencies
- Update all references
- Create conversion log

## Example

```
/taskmaster:add-subtask/from-task 5 8
→ Converting: Task #8 becomes subtask #5.1
→ Updated: 3 dependency references
→ Parent task #5 now has 1 subtask
→ Note: Subtask inherits parent's priority

Before: #8 "Implement validation" (standalone)
After:  #5.1 "Implement validation" (subtask of #5)
```

## Post-Conversion

- Show new task hierarchy
- List updated dependencies
- Verify project integrity
- Suggest related conversions

--- .cursor/commands/tm/expand-all-tasks.md ---
Expand all pending tasks that need subtasks.

## Bulk Task Expansion

Intelligently expands all tasks that would benefit from breakdown.

## Execution

```bash
task-master expand --all
```

## Smart Selection

Only expands tasks that:
- Are marked as pending
- Have high complexity (>5)
- Lack existing subtasks
- Would benefit from breakdown

## Expansion Process

1. **Analysis Phase**
   - Identify expansion candidates
   - Group related tasks
   - Plan expansion strategy

2. **Batch Processing**
   - Expand tasks in logical order
   - Maintain consistency
   - Preserve relationships
   - Optimize for parallelism

3. **Quality Control**
   - Ensure subtask quality
   - Avoid over-decomposition
   - Maintain task coherence
   - Update dependencies

## Options

- Add `force` to expand all regardless of complexity
- Add `research` for enhanced AI analysis

## Results

After bulk expansion:
- Summary of tasks expanded
- New subtask count
- Updated complexity metrics
- Suggested task order

--- .ruler/AGENTS.md ---
# AGENTS.md

Centralised AI agent instructions. Add coding guidelines, style guides, and project context here.

Ruler concatenates all .md files in this directory (and subdirectories), starting with AGENTS.md (if present), then remaining files in sorted order.


--- .ruler/tm-AGENTS.md ---
# Agent Instructions

## Task Master AI and Workflow Instructions
**Import Task Master's development workflow commands and guidelines, treat as if import is in the main AGENT.md file.**
@./.taskmaster/AGENTS.md @./.rules/dev_workflow.md @./.rules/rules.md @./.rules/self_improve.md @./.rules/taskmaster.md


--- .rules/dev_workflow.md ---
---
description: Guide for using Taskmaster to manage task-driven development workflows
---

# Taskmaster Development Workflow

This guide outlines the standard process for using Taskmaster to manage software development projects. It is written as a set of instructions for you, the AI agent.

- **Your Default Stance**: For most projects, the user can work directly within the `master` task context. Your initial actions should operate on this default context unless a clear pattern for multi-context work emerges.
- **Your Goal**: Your role is to elevate the user's workflow by intelligently introducing advanced features like **Tagged Task Lists** when you detect the appropriate context. Do not force tags on the user; suggest them as a helpful solution to a specific need.

## The Basic Loop
The fundamental development cycle you will facilitate is:
1.  **`list`**: Show the user what needs to be done.
2.  **`next`**: Help the user decide what to work on.
3.  **`show <id>`**: Provide details for a specific task.
4.  **`expand <id>`**: Break down a complex task into smaller, manageable subtasks.
5.  **Implement**: The user writes the code and tests.
6.  **`update-subtask`**: Log progress and findings on behalf of the user.
7.  **`set-status`**: Mark tasks and subtasks as `done` as work is completed.
8.  **Repeat**.

All your standard command executions should operate on the user's current task context, which defaults to `master`.

---

## Standard Development Workflow Process

### Simple Workflow (Default Starting Point)

For new projects or when users are getting started, operate within the `master` tag context:

-   Start new projects by running `initialize_project` tool / `task-master init` or `parse_prd` / `task-master parse-prd --input='<prd-file.txt>'` (see @`taskmaster.md`) to generate initial tasks.json with tagged structure
-   Configure rule sets during initialization with `--rules` flag (e.g., `task-master init --rules <AGENT>,windsurf`) or manage them later with `task-master rules add/remove` commands
-   Begin coding sessions with `get_tasks` / `task-master list` (see @`taskmaster.md`) to see current tasks, status, and IDs
-   Determine the next task to work on using `next_task` / `task-master next` (see @`taskmaster.md`)
-   Analyze task complexity with `analyze_project_complexity` / `task-master analyze-complexity --research` (see @`taskmaster.md`) before breaking down tasks
-   Review complexity report using `complexity_report` / `task-master complexity-report` (see @`taskmaster.md`)
-   Select tasks based on dependencies (all marked 'done'), priority level, and ID order
-   View specific task details using `get_task` / `task-master show <id>` (see @`taskmaster.md`) to understand implementation requirements
-   Break down complex tasks using `expand_task` / `task-master expand --id=<id> --force --research` (see @`taskmaster.md`) with appropriate flags like `--force` (to replace existing subtasks) and `--research`
-   Implement code following task details, dependencies, and project standards
-   Mark completed tasks with `set_task_status` / `task-master set-status --id=<id> --status=done` (see @`taskmaster.md`)
-   Update dependent tasks when implementation differs from original plan using `update` / `task-master update --from=<id> --prompt="..."` or `update_task` / `task-master update-task --id=<id> --prompt="..."` (see @`taskmaster.md`)

---

## Leveling Up: Agent-Led Multi-Context Workflows

While the basic workflow is powerful, your primary opportunity to add value is by identifying when to introduce **Tagged Task Lists**. These patterns are your tools for creating a more organized and efficient development environment for the user, especially if you detect agentic or parallel development happening across the same session.

**Critical Principle**: Most users should never see a difference in their experience. Only introduce advanced workflows when you detect clear indicators that the project has evolved beyond simple task management.

### When to Introduce Tags: Your Decision Patterns

Here are the patterns to look for. When you detect one, you should propose the corresponding workflow to the user.

#### Pattern 1: Simple Git Feature Branching
This is the most common and direct use case for tags.

- **Trigger**: The user creates a new git branch (e.g., `git checkout -b feature/user-auth`).
- **Your Action**: Propose creating a new tag that mirrors the branch name to isolate the feature's tasks from `master`.
- **Your Suggested Prompt**: *"I see you've created a new branch named 'feature/user-auth'. To keep all related tasks neatly organized and separate from your main list, I can create a corresponding task tag for you. This helps prevent merge conflicts in your `tasks.json` file later. Shall I create the 'feature-user-auth' tag?"*
- **Tool to Use**: `task-master add-tag --from-branch`

#### Pattern 2: Team Collaboration
- **Trigger**: The user mentions working with teammates (e.g., "My teammate Alice is handling the database schema," or "I need to review Bob's work on the API.").
- **Your Action**: Suggest creating a separate tag for the user's work to prevent conflicts with shared master context.
- **Your Suggested Prompt**: *"Since you're working with Alice, I can create a separate task context for your work to avoid conflicts. This way, Alice can continue working with the master list while you have your own isolated context. When you're ready to merge your work, we can coordinate the tasks back to master. Shall I create a tag for your current work?"*
- **Tool to Use**: `task-master add-tag my-work --copy-from-current --description="My tasks while collaborating with Alice"`

#### Pattern 3: Experiments or Risky Refactors
- **Trigger**: The user wants to try something that might not be kept (e.g., "I want to experiment with switching our state management library," or "Let's refactor the old API module, but I want to keep the current tasks as a reference.").
- **Your Action**: Propose creating a sandboxed tag for the experimental work.
- **Your Suggested Prompt**: *"This sounds like a great experiment. To keep these new tasks separate from our main plan, I can create a temporary 'experiment-zustand' tag for this work. If we decide not to proceed, we can simply delete the tag without affecting the main task list. Sound good?"*
- **Tool to Use**: `task-master add-tag experiment-zustand --description="Exploring Zustand migration"`

#### Pattern 4: Large Feature Initiatives (PRD-Driven)
This is a more structured approach for significant new features or epics.

- **Trigger**: The user describes a large, multi-step feature that would benefit from a formal plan.
- **Your Action**: Propose a comprehensive, PRD-driven workflow.
- **Your Suggested Prompt**: *"This sounds like a significant new feature. To manage this effectively, I suggest we create a dedicated task context for it. Here's the plan: I'll create a new tag called 'feature-xyz', then we can draft a Product Requirements Document (PRD) together to scope the work. Once the PRD is ready, I'll automatically generate all the necessary tasks within that new tag. How does that sound?"*
- **Your Implementation Flow**:
    1.  **Create an empty tag**: `task-master add-tag feature-xyz --description "Tasks for the new XYZ feature"`. You can also start by creating a git branch if applicable, and then create the tag from that branch.
    2.  **Collaborate & Create PRD**: Work with the user to create a detailed PRD file (e.g., `.taskmaster/docs/feature-xyz-prd.txt`).
    3.  **Parse PRD into the new tag**: `task-master parse-prd .taskmaster/docs/feature-xyz-prd.txt --tag feature-xyz`
    4.  **Prepare the new task list**: Follow up by suggesting `analyze-complexity` and `expand-all` for the newly created tasks within the `feature-xyz` tag.

#### Pattern 5: Version-Based Development
Tailor your approach based on the project maturity indicated by tag names.

- **Prototype/MVP Tags** (`prototype`, `mvp`, `poc`, `v0.x`):
  - **Your Approach**: Focus on speed and functionality over perfection
  - **Task Generation**: Create tasks that emphasize "get it working" over "get it perfect"
  - **Complexity Level**: Lower complexity, fewer subtasks, more direct implementation paths
  - **Research Prompts**: Include context like "This is a prototype - prioritize speed and basic functionality over optimization"
  - **Example Prompt Addition**: *"Since this is for the MVP, I'll focus on tasks that get core functionality working quickly rather than over-engineering."*

- **Production/Mature Tags** (`v1.0+`, `production`, `stable`):
  - **Your Approach**: Emphasize robustness, testing, and maintainability
  - **Task Generation**: Include comprehensive error handling, testing, documentation, and optimization
  - **Complexity Level**: Higher complexity, more detailed subtasks, thorough implementation paths
  - **Research Prompts**: Include context like "This is for production - prioritize reliability, performance, and maintainability"
  - **Example Prompt Addition**: *"Since this is for production, I'll ensure tasks include proper error handling, testing, and documentation."*

### Advanced Workflow (Tag-Based & PRD-Driven)

**When to Transition**: Recognize when the project has evolved (or has initiated a project which existing code) beyond simple task management. Look for these indicators:
- User mentions teammates or collaboration needs
- Project has grown to 15+ tasks with mixed priorities
- User creates feature branches or mentions major initiatives
- User initializes Taskmaster on an existing, complex codebase
- User describes large features that would benefit from dedicated planning

**Your Role in Transition**: Guide the user to a more sophisticated workflow that leverages tags for organization and PRDs for comprehensive planning.

#### Master List Strategy (High-Value Focus)
Once you transition to tag-based workflows, the `master` tag should ideally contain only:
- **High-level deliverables** that provide significant business value
- **Major milestones** and epic-level features
- **Critical infrastructure** work that affects the entire project
- **Release-blocking** items

**What NOT to put in master**:
- Detailed implementation subtasks (these go in feature-specific tags' parent tasks)
- Refactoring work (create dedicated tags like `refactor-auth`)
- Experimental features (use `experiment-*` tags)
- Team member-specific tasks (use person-specific tags)

#### PRD-Driven Feature Development

**For New Major Features**:
1. **Identify the Initiative**: When user describes a significant feature
2. **Create Dedicated Tag**: `add_tag feature-[name] --description="[Feature description]"`
3. **Collaborative PRD Creation**: Work with user to create comprehensive PRD in `.taskmaster/docs/feature-[name]-prd.txt`
4. **Parse & Prepare**:
   - `parse_prd .taskmaster/docs/feature-[name]-prd.txt --tag=feature-[name]`
   - `analyze_project_complexity --tag=feature-[name] --research`
   - `expand_all --tag=feature-[name] --research`
5. **Add Master Reference**: Create a high-level task in `master` that references the feature tag

**For Existing Codebase Analysis**:
When users initialize Taskmaster on existing projects:
1. **Codebase Discovery**: Use your native tools for producing deep context about the code base. You may use `research` tool with `--tree` and `--files` to collect up to date information using the existing architecture as context.
2. **Collaborative Assessment**: Work with user to identify improvement areas, technical debt, or new features
3. **Strategic PRD Creation**: Co-author PRDs that include:
   - Current state analysis (based on your codebase research)
   - Proposed improvements or new features
   - Implementation strategy considering existing code
4. **Tag-Based Organization**: Parse PRDs into appropriate tags (`refactor-api`, `feature-dashboard`, `tech-debt`, etc.)
5. **Master List Curation**: Keep only the most valuable initiatives in master

The parse-prd's `--append` flag enables the user to parse multiple PRDs within tags or across tags. PRDs should be focused and the number of tasks they are parsed into should be strategically chosen relative to the PRD's complexity and level of detail.

### Workflow Transition Examples

**Example 1: Simple → Team-Based**
```
User: "Alice is going to help with the API work"
Your Response: "Great! To avoid conflicts, I'll create a separate task context for your work. Alice can continue with the master list while you work in your own context. When you're ready to merge, we can coordinate the tasks back together."
Action: add_tag my-api-work --copy-from-current --description="My API tasks while collaborating with Alice"
```

**Example 2: Simple → PRD-Driven**
```
User: "I want to add a complete user dashboard with analytics, user management, and reporting"
Your Response: "This sounds like a major feature that would benefit from detailed planning. Let me create a dedicated context for this work and we can draft a PRD together to ensure we capture all requirements."
Actions:
1. add_tag feature-dashboard --description="User dashboard with analytics and management"
2. Collaborate on PRD creation
3. parse_prd dashboard-prd.txt --tag=feature-dashboard
4. Add high-level "User Dashboard" task to master
```

**Example 3: Existing Project → Strategic Planning**
```
User: "I just initialized Taskmaster on my existing React app. It's getting messy and I want to improve it."
Your Response: "Let me research your codebase to understand the current architecture, then we can create a strategic plan for improvements."
Actions:
1. research "Current React app architecture and improvement opportunities" --tree --files=src/
2. Collaborate on improvement PRD based on findings
3. Create tags for different improvement areas (refactor-components, improve-state-management, etc.)
4. Keep only major improvement initiatives in master
```

---

## Primary Interaction: MCP Server vs. CLI

Taskmaster offers two primary ways to interact:

1.  **MCP Server (Recommended for Integrated Tools)**:
    - For AI agents and integrated development environments (like <AGENT>), interacting via the **MCP server is the preferred method**.
    - The MCP server exposes Taskmaster functionality through a set of tools (e.g., `get_tasks`, `add_subtask`).
    - This method offers better performance, structured data exchange, and richer error handling compared to CLI parsing.
    - Refer to @`mcp.md` for details on the MCP architecture and available tools.
    - A comprehensive list and description of MCP tools and their corresponding CLI commands can be found in @`taskmaster.md`.
    - **Restart the MCP server** if core logic in `scripts/modules` or MCP tool/direct function definitions change.
    - **Note**: MCP tools fully support tagged task lists with complete tag management capabilities.

2.  **`task-master` CLI (For Users & Fallback)**:
    - The global `task-master` command provides a user-friendly interface for direct terminal interaction.
    - It can also serve as a fallback if the MCP server is inaccessible or a specific function isn't exposed via MCP.
    - Install globally with `npm install -g task-master-ai` or use locally via `npx task-master-ai ...`.
    - The CLI commands often mirror the MCP tools (e.g., `task-master list` corresponds to `get_tasks`).
    - Refer to @`taskmaster.md` for a detailed command reference.
    - **Tagged Task Lists**: CLI fully supports the new tagged system with seamless migration.

## How the Tag System Works (For Your Reference)

- **Data Structure**: Tasks are organized into separate contexts (tags) like "master", "feature-branch", or "v2.0".
- **Silent Migration**: Existing projects automatically migrate to use a "master" tag with zero disruption.
- **Context Isolation**: Tasks in different tags are completely separate. Changes in one tag do not affect any other tag.
- **Manual Control**: The user is always in control. There is no automatic switching. You facilitate switching by using `use-tag <name>`.
- **Full CLI & MCP Support**: All tag management commands are available through both the CLI and MCP tools for you to use. Refer to @`taskmaster.md` for a full command list.

---

## Task Complexity Analysis

-   Run `analyze_project_complexity` / `task-master analyze-complexity --research` (see @`taskmaster.md`) for comprehensive analysis
-   Review complexity report via `complexity_report` / `task-master complexity-report` (see @`taskmaster.md`) for a formatted, readable version.
-   Focus on tasks with highest complexity scores (8-10) for detailed breakdown
-   Use analysis results to determine appropriate subtask allocation
-   Note that reports are automatically used by the `expand_task` tool/command

## Task Breakdown Process

-   Use `expand_task` / `task-master expand --id=<id>`. It automatically uses the complexity report if found, otherwise generates default number of subtasks.
-   Use `--num=<number>` to specify an explicit number of subtasks, overriding defaults or complexity report recommendations.
-   Add `--research` flag to leverage Perplexity AI for research-backed expansion.
-   Add `--force` flag to clear existing subtasks before generating new ones (default is to append).
-   Use `--prompt="<context>"` to provide additional context when needed.
-   Review and adjust generated subtasks as necessary.
-   Use `expand_all` tool or `task-master expand --all` to expand multiple pending tasks at once, respecting flags like `--force` and `--research`.
-   If subtasks need complete replacement (regardless of the `--force` flag on `expand`), clear them first with `clear_subtasks` / `task-master clear-subtasks --id=<id>`.

## Implementation Drift Handling

-   When implementation differs significantly from planned approach
-   When future tasks need modification due to current implementation choices
-   When new dependencies or requirements emerge
-   Use `update` / `task-master update --from=<futureTaskId> --prompt='<explanation>\nUpdate context...' --research` to update multiple future tasks.
-   Use `update_task` / `task-master update-task --id=<taskId> --prompt='<explanation>\nUpdate context...' --research` to update a single specific task.

## Task Status Management

-   Use 'pending' for tasks ready to be worked on
-   Use 'done' for completed and verified tasks
-   Use 'deferred' for postponed tasks
-   Add custom status values as needed for project-specific workflows

## Task Structure Fields

- **id**: Unique identifier for the task (Example: `1`, `1.1`)
- **title**: Brief, descriptive title (Example: `"Initialize Repo"`)
- **description**: Concise summary of what the task involves (Example: `"Create a new repository, set up initial structure."`)
- **status**: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`)
- **dependencies**: IDs of prerequisite tasks (Example: `[1, 2.1]`)
    - Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending)
    - This helps quickly identify which prerequisite tasks are blocking work
- **priority**: Importance level (Example: `"high"`, `"medium"`, `"low"`)
- **details**: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`)
- **testStrategy**: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`)
- **subtasks**: List of smaller, more specific tasks (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`)
- Refer to task structure details (previously linked to `tasks.md`).

## Configuration Management (Updated)

Taskmaster configuration is managed through two main mechanisms:

1.  **`.taskmaster/config.json` File (Primary):**
    *   Located in the project root directory.
    *   Stores most configuration settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default subtasks/priority, project name, etc.
    *   **Tagged System Settings**: Includes `global.defaultTag` (defaults to "master") and `tags` section for tag management configuration.
    *   **Managed via `task-master models --setup` command.** Do not edit manually unless you know what you are doing.
    *   **View/Set specific models via `task-master models` command or `models` MCP tool.**
    *   Created automatically when you run `task-master models --setup` for the first time or during tagged system migration.

2.  **Environment Variables (`.env` / `mcp.json`):**
    *   Used **only** for sensitive API keys and specific endpoint URLs.
    *   Place API keys (one per provider) in a `.env` file in the project root for CLI usage.
    *   For MCP/<AGENT> integration, configure these keys in the `env` section of `.<AGENT>/mcp.json`.
    *   Available keys/variables: See `assets/env.example` or the Configuration section in the command reference (previously linked to `taskmaster.md`).

3.  **`.taskmaster/state.json` File (Tagged System State):**
    *   Tracks current tag context and migration status.
    *   Automatically created during tagged system migration.
    *   Contains: `currentTag`, `lastSwitched`, `migrationNoticeShown`.

**Important:** Non-API key settings (like model selections, `MAX_TOKENS`, `TASKMASTER_LOG_LEVEL`) are **no longer configured via environment variables**. Use the `task-master models` command (or `--setup` for interactive configuration) or the `models` MCP tool.
**If AI commands FAIL in MCP** verify that the API key for the selected provider is present in the `env` section of `.<AGENT>/mcp.json`.
**If AI commands FAIL in CLI** verify that the API key for the selected provider is present in the `.env` file in the root of the project.

## Rules Management

Taskmaster supports multiple AI coding assistant rule sets that can be configured during project initialization or managed afterward:

- **Available Profiles**: Claude Code, <AGENT>, Codex, <AGENT>, Roo Code, Trae, Windsurf (claude, <AGENT>, codex, <AGENT>, roo, trae, windsurf)
- **During Initialization**: Use `task-master init --rules <AGENT>,windsurf` to specify which rule sets to include
- **After Initialization**: Use `task-master rules add <profiles>` or `task-master rules remove <profiles>` to manage rule sets
- **Interactive Setup**: Use `task-master rules setup` to launch an interactive prompt for selecting rule profiles
- **Default Behavior**: If no `--rules` flag is specified during initialization, all available rule profiles are included
- **Rule Structure**: Each profile creates its own directory (e.g., `.<AGENT>/rules`, `.roo/rules`) with appropriate configuration files

## Determining the Next Task

- Run `next_task` / `task-master next` to show the next task to work on.
- The command identifies tasks with all dependencies satisfied
- Tasks are prioritized by priority level, dependency count, and ID
- The command shows comprehensive task information including:
    - Basic task details and description
    - Implementation details
    - Subtasks (if they exist)
    - Contextual suggested actions
- Recommended before starting any new development work
- Respects your project's dependency structure
- Ensures tasks are completed in the appropriate sequence
- Provides ready-to-use commands for common task actions

## Viewing Specific Task Details

- Run `get_task` / `task-master show <id>` to view a specific task.
- Use dot notation for subtasks: `task-master show 1.2` (shows subtask 2 of task 1)
- Displays comprehensive information similar to the next command, but for a specific task
- For parent tasks, shows all subtasks and their current status
- For subtasks, shows parent task information and relationship
- Provides contextual suggested actions appropriate for the specific task
- Useful for examining task details before implementation or checking status

## Managing Task Dependencies

- Use `add_dependency` / `task-master add-dependency --id=<id> --depends-on=<id>` to add a dependency.
- Use `remove_dependency` / `task-master remove-dependency --id=<id> --depends-on=<id>` to remove a dependency.
- The system prevents circular dependencies and duplicate dependency entries
- Dependencies are checked for existence before being added or removed
- Task files are automatically regenerated after dependency changes
- Dependencies are visualized with status indicators in task listings and files

## Task Reorganization

- Use `move_task` / `task-master move --from=<id> --to=<id>` to move tasks or subtasks within the hierarchy
- This command supports several use cases:
  - Moving a standalone task to become a subtask (e.g., `--from=5 --to=7`)
  - Moving a subtask to become a standalone task (e.g., `--from=5.2 --to=7`)
  - Moving a subtask to a different parent (e.g., `--from=5.2 --to=7.3`)
  - Reordering subtasks within the same parent (e.g., `--from=5.2 --to=5.4`)
  - Moving a task to a new, non-existent ID position (e.g., `--from=5 --to=25`)
  - Moving multiple tasks at once using comma-separated IDs (e.g., `--from=10,11,12 --to=16,17,18`)
- The system includes validation to prevent data loss:
  - Allows moving to non-existent IDs by creating placeholder tasks
  - Prevents moving to existing task IDs that have content (to avoid overwriting)
  - Validates source tasks exist before attempting to move them
- The system maintains proper parent-child relationships and dependency integrity
- Task files are automatically regenerated after the move operation
- This provides greater flexibility in organizing and refining your task structure as project understanding evolves
- This is especially useful when dealing with potential merge conflicts arising from teams creating tasks on separate branches. Solve these conflicts very easily by moving your tasks and keeping theirs.

## Iterative Subtask Implementation

Once a task has been broken down into subtasks using `expand_task` or similar methods, follow this iterative process for implementation:

1.  **Understand the Goal (Preparation):**
    *   Use `get_task` / `task-master show <subtaskId>` (see @`taskmaster.md`) to thoroughly understand the specific goals and requirements of the subtask.

2.  **Initial Exploration & Planning (Iteration 1):**
    *   This is the first attempt at creating a concrete implementation plan.
    *   Explore the codebase to identify the precise files, functions, and even specific lines of code that will need modification.
    *   Determine the intended code changes (diffs) and their locations.
    *   Gather *all* relevant details from this exploration phase.

3.  **Log the Plan:**
    *   Run `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<detailed plan>'`.
    *   Provide the *complete and detailed* findings from the exploration phase in the prompt. Include file paths, line numbers, proposed diffs, reasoning, and any potential challenges identified. Do not omit details. The goal is to create a rich, timestamped log within the subtask's `details`.

4.  **Verify the Plan:**
    *   Run `get_task` / `task-master show <subtaskId>` again to confirm that the detailed implementation plan has been successfully appended to the subtask's details.

5.  **Begin Implementation:**
    *   Set the subtask status using `set_task_status` / `task-master set-status --id=<subtaskId> --status=in-progress`.
    *   Start coding based on the logged plan.

6.  **Refine and Log Progress (Iteration 2+):**
    *   As implementation progresses, you will encounter challenges, discover nuances, or confirm successful approaches.
    *   **Before appending new information**: Briefly review the *existing* details logged in the subtask (using `get_task` or recalling from context) to ensure the update adds fresh insights and avoids redundancy.
    *   **Regularly** use `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<update details>\n- What worked...\n- What didn't work...'` to append new findings.
    *   **Crucially, log:**
        *   What worked ("fundamental truths" discovered).
        *   What didn't work and why (to avoid repeating mistakes).
        *   Specific code snippets or configurations that were successful.
        *   Decisions made, especially if confirmed with user input.
        *   Any deviations from the initial plan and the reasoning.
    *   The objective is to continuously enrich the subtask's details, creating a log of the implementation journey that helps the AI (and human developers) learn, adapt, and avoid repeating errors.

7.  **Review & Update Rules (Post-Implementation):**
    *   Once the implementation for the subtask is functionally complete, review all code changes and the relevant chat history.
    *   Identify any new or modified code patterns, conventions, or best practices established during the implementation.
    *   Create new or update existing rules following internal guidelines (previously linked to `cursor_rules.md` and `self_improve.md`).

8.  **Mark Task Complete:**
    *   After verifying the implementation and updating any necessary rules, mark the subtask as completed: `set_task_status` / `task-master set-status --id=<subtaskId> --status=done`.

9.  **Commit Changes (If using Git):**
    *   Stage the relevant code changes and any updated/new rule files (`git add .`).
    *   Craft a comprehensive Git commit message summarizing the work done for the subtask, including both code implementation and any rule adjustments.
    *   Execute the commit command directly in the terminal (e.g., `git commit -m 'feat(module): Implement feature X for subtask <subtaskId>\n\n- Details about changes...\n- Updated rule Y for pattern Z'`).
    *   Consider if a Changeset is needed according to internal versioning guidelines (previously linked to `changeset.md`). If so, run `npm run changeset`, stage the generated file, and amend the commit or create a new one.

10. **Proceed to Next Subtask:**
    *   Identify the next subtask (e.g., using `next_task` / `task-master next`).

## Code Analysis & Refactoring Techniques

- **Top-Level Function Search**:
    - Useful for understanding module structure or planning refactors.
    - Use grep/ripgrep to find exported functions/constants:
      `rg "export (async function|function|const) \w+"` or similar patterns.
    - Can help compare functions between files during migrations or identify potential naming conflicts.

---
*This workflow provides a general guideline. Adapt it based on your specific project needs and team practices.*

--- .rules/rules.md ---
---
description: Guidelines for creating and maintaining AGENT rules to ensure consistency and effectiveness.
---

- **Required Rule Structure:**
  ```markdown
  ---
  description: Clear, one-line description of what the rule enforces
  ---

  - **Main Points in Bold**
    - Sub-points with details
    - Examples and explanations
  ```

- **File References:**
  - Use `[filename](md:path/to/file)` ([filename](md:filename)) to reference files
  - Example: [prisma.md](.ruler/prisma.md) for rule references
  - Example: [schema.prisma](md:prisma/schema.prisma) for code references

- **Code Examples:**
  - Use language-specific code blocks
  ```typescript
  // ✅ DO: Show good examples
  const goodExample = true;

  // ❌ DON'T: Show anti-patterns
  const badExample = false;
  ```

- **Rule Content Guidelines:**
  - Start with high-level overview
  - Include specific, actionable requirements
  - Show examples of correct implementation
  - Reference existing code when possible
  - Keep rules DRY by referencing other rules

- **Rule Maintenance:**
  - Update rules when new patterns emerge
  - Add examples from actual codebase
  - Remove outdated patterns
  - Cross-reference related rules

- **Best Practices:**
  - Use bullet points for clarity
  - Keep descriptions concise
  - Include both DO and DON'T examples
  - Reference actual code over theoretical examples
  - Use consistent formatting across rules

## Links discovered
- [filename](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/md:path/to/file.md)
- [filename](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/md:filename.md)
- [prisma.md](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/.ruler/prisma.md)
- [schema.prisma](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/md:prisma/schema.prisma)

--- .rules/self_improve.md ---
---
description: Guidelines for continuously improving  rules based on emerging code patterns and best practices.
---

- **Rule Improvement Triggers:**
  - New code patterns not covered by existing rules
  - Repeated similar implementations across files
  - Common error patterns that could be prevented
  - New libraries or tools being used consistently
  - Emerging best practices in the codebase

- **Analysis Process:**
  - Compare new code with existing rules
  - Identify patterns that should be standardized
  - Look for references to external documentation
  - Check for consistent error handling patterns
  - Monitor test patterns and coverage

- **Rule Updates:**
  - **Add New Rules When:**
    - A new technology/pattern is used in 3+ files
    - Common bugs could be prevented by a rule
    - Code reviews repeatedly mention the same feedback
    - New security or performance patterns emerge

  - **Modify Existing Rules When:**
    - Better examples exist in the codebase
    - Additional edge cases are discovered
    - Related rules have been updated
    - Implementation details have changed

- **Example Pattern Recognition:**
  ```typescript
  // If you see repeated patterns like:
  const data = await prisma.user.findMany({
    select: { id: true, email: true },
    where: { status: 'ACTIVE' }
  });

  // Consider adding to [prisma.md](.ruler/prisma.md):
  // - Standard select fields
  // - Common where conditions
  // - Performance optimization patterns
  ```

- **Rule Quality Checks:**
  - Rules should be actionable and specific
  - Examples should come from actual code
  - References should be up to date
  - Patterns should be consistently enforced

- **Continuous Improvement:**
  - Monitor code review comments
  - Track common development questions
  - Update rules after major refactors
  - Add links to relevant documentation
  - Cross-reference related rules

- **Rule Deprecation:**
  - Mark outdated patterns as deprecated
  - Remove rules that no longer apply
  - Update references to deprecated rules
  - Document migration paths for old patterns

- **Documentation Updates:**
  - Keep examples synchronized with code
  - Update references to external docs
  - Maintain links between related rules
  - Document breaking changes
Follow [.ruler.md](.ruler/rules.md) for proper rule formatting and structure.


## Links discovered
- [prisma.md](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/.ruler/prisma.md)
- [.ruler.md](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/.ruler/rules.md)

--- .rules/taskmaster.md ---
---
description: Comprehensive reference for Taskmaster MCP tools and CLI commands.
---

# Taskmaster Tool & Command Reference

This document provides a detailed reference for interacting with Taskmaster, covering both the recommended MCP tools, suitable for integrations like <AGENT>, and the corresponding `task-master` CLI commands, designed for direct user interaction or fallback.

**Note:** For interacting with Taskmaster programmatically or via integrated tools, using the **MCP tools is strongly recommended** due to better performance, structured data, and error handling. The CLI commands serve as a user-friendly alternative and fallback.

**Important:** Several MCP tools involve AI processing... The AI-powered tools include `parse_prd`, `analyze_project_complexity`, `update_subtask`, `update_task`, `update`, `expand_all`, `expand_task`, and `add_task`.

**🏷️ Tagged Task Lists System:** Task Master now supports **tagged task lists** for multi-context task management. This allows you to maintain separate, isolated lists of tasks for different features, branches, or experiments. Existing projects are seamlessly migrated to use a default "master" tag. Most commands now support a `--tag <name>` flag to specify which context to operate on. If omitted, commands use the currently active tag.

---

## Initialization & Setup

### 1. Initialize Project (`init`)

*   **MCP Tool:** `initialize_project`
*   **CLI Command:** `task-master init [options]`
*   **Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project.`
*   **Key CLI Options:**
    *   `--name <name>`: `Set the name for your project in Taskmaster's configuration.`
    *   `--description <text>`: `Provide a brief description for your project.`
    *   `--version <version>`: `Set the initial version for your project, e.g., '0.1.0'.`
    *   `-y, --yes`: `Initialize Taskmaster quickly using default settings without interactive prompts.`
*   **Usage:** Run this once at the beginning of a new project.
*   **MCP Variant Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project by running the 'task-master init' command.`
*   **Key MCP Parameters/Options:**
    *   `projectName`: `Set the name for your project.` (CLI: `--name <name>`)
    *   `projectDescription`: `Provide a brief description for your project.` (CLI: `--description <text>`)
    *   `projectVersion`: `Set the initial version for your project, e.g., '0.1.0'.` (CLI: `--version <version>`)
    *   `authorName`: `Author name.` (CLI: `--author <author>`)
    *   `skipInstall`: `Skip installing dependencies. Default is false.` (CLI: `--skip-install`)
    *   `addAliases`: `Add shell aliases tm, taskmaster, hamster, and ham. Default is false.` (CLI: `--aliases`)
    *   `yes`: `Skip prompts and use defaults/provided arguments. Default is false.` (CLI: `-y, --yes`)
*   **Usage:** Run this once at the beginning of a new project, typically via an integrated tool like <AGENT>. Operates on the current working directory of the MCP server.
*   **Important:** Once complete, you *MUST* parse a prd in order to generate tasks. There will be no tasks files until then. The next step after initializing should be to create a PRD using the example PRD in .taskmaster/templates/example_prd.txt.
*   **Tagging:** Use the `--tag` option to parse the PRD into a specific, non-default tag context. If the tag doesn't exist, it will be created automatically. Example: `task-master parse-prd spec.txt --tag=new-feature`.

### 2. Parse PRD (`parse_prd`)

*   **MCP Tool:** `parse_prd`
*   **CLI Command:** `task-master parse-prd [file] [options]`
*   **Description:** `Parse a Product Requirements Document, PRD, or text file with Taskmaster to automatically generate an initial set of tasks in tasks.json.`
*   **Key Parameters/Options:**
    *   `input`: `Path to your PRD or requirements text file that Taskmaster should parse for tasks.` (CLI: `[file]` positional or `-i, --input <file>`)
    *   `output`: `Specify where Taskmaster should save the generated 'tasks.json' file. Defaults to '.taskmaster/tasks/tasks.json'.` (CLI: `-o, --output <file>`)
    *   `numTasks`: `Approximate number of top-level tasks Taskmaster should aim to generate from the document.` (CLI: `-n, --num-tasks <number>`)
    *   `force`: `Use this to allow Taskmaster to overwrite an existing 'tasks.json' without asking for confirmation.` (CLI: `-f, --force`)
*   **Usage:** Useful for bootstrapping a project from an existing requirements document.
*   **Notes:** Task Master will strictly adhere to any specific requirements mentioned in the PRD, such as libraries, database schemas, frameworks, tech stacks, etc., while filling in any gaps where the PRD isn't fully specified. Tasks are designed to provide the most direct implementation path while avoiding over-engineering.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. If the user does not have a PRD, suggest discussing their idea and then use the example PRD in `.taskmaster/templates/example_prd.txt` as a template for creating the PRD based on their idea, for use with `parse-prd`.

---

## AI Model Configuration

### 2. Manage Models (`models`)
*   **MCP Tool:** `models`
*   **CLI Command:** `task-master models [options]`
*   **Description:** `View the current AI model configuration or set specific models for different roles (main, research, fallback). Allows setting custom model IDs for Ollama and OpenRouter.`
*   **Key MCP Parameters/Options:**
    *   `setMain <model_id>`: `Set the primary model ID for task generation/updates.` (CLI: `--set-main <model_id>`)
    *   `setResearch <model_id>`: `Set the model ID for research-backed operations.` (CLI: `--set-research <model_id>`)
    *   `setFallback <model_id>`: `Set the model ID to use if the primary fails.` (CLI: `--set-fallback <model_id>`)
    *   `ollama <boolean>`: `Indicates the set model ID is a custom Ollama model.` (CLI: `--ollama`)
    *   `openrouter <boolean>`: `Indicates the set model ID is a custom OpenRouter model.` (CLI: `--openrouter`)
    *   `listAvailableModels <boolean>`: `If true, lists available models not currently assigned to a role.` (CLI: No direct equivalent; CLI lists available automatically)
    *   `projectRoot <string>`: `Optional. Absolute path to the project root directory.` (CLI: Determined automatically)
*   **Key CLI Options:**
    *   `--set-main <model_id>`: `Set the primary model.`
    *   `--set-research <model_id>`: `Set the research model.`
    *   `--set-fallback <model_id>`: `Set the fallback model.`
    *   `--ollama`: `Specify that the provided model ID is for Ollama (use with --set-*).`
    *   `--openrouter`: `Specify that the provided model ID is for OpenRouter (use with --set-*). Validates against OpenRouter API.`
    *   `--bedrock`: `Specify that the provided model ID is for AWS Bedrock (use with --set-*).`
    *   `--setup`: `Run interactive setup to configure models, including custom Ollama/OpenRouter IDs.`
*   **Usage (MCP):** Call without set flags to get current config. Use `setMain`, `setResearch`, or `setFallback` with a valid model ID to update the configuration. Use `listAvailableModels: true` to get a list of unassigned models. To set a custom model, provide the model ID and set `ollama: true` or `openrouter: true`.
*   **Usage (CLI):** Run without flags to view current configuration and available models. Use set flags to update specific roles. Use `--setup` for guided configuration, including custom models. To set a custom model via flags, use `--set-<role>=<model_id>` along with either `--ollama` or `--openrouter`.
*   **Notes:** Configuration is stored in `.taskmaster/config.json` in the project root. This command/tool modifies that file. Use `listAvailableModels` or `task-master models` to see internally supported models. OpenRouter custom models are validated against their live API. Ollama custom models are not validated live.
*   **API note:** API keys for selected AI providers (based on their model) need to exist in the mcp.json file to be accessible in MCP context. The API keys must be present in the local .env file for the CLI to be able to read them.
*   **Model costs:** The costs in supported models are expressed in dollars. An input/output value of 3 is $3.00. A value of 0.8 is $0.80.
*   **Warning:** DO NOT MANUALLY EDIT THE .taskmaster/config.json FILE. Use the included commands either in the MCP or CLI format as needed. Always prioritize MCP tools when available and use the CLI as a fallback.

---

## Task Listing & Viewing

### 3. Get Tasks (`get_tasks`)

*   **MCP Tool:** `get_tasks`
*   **CLI Command:** `task-master list [options]`
*   **Description:** `List your Taskmaster tasks, optionally filtering by status and showing subtasks.`
*   **Key Parameters/Options:**
    *   `status`: `Show only Taskmaster tasks matching this status (or multiple statuses, comma-separated), e.g., 'pending' or 'done,in-progress'.` (CLI: `-s, --status <status>`)
    *   `withSubtasks`: `Include subtasks indented under their parent tasks in the list.` (CLI: `--with-subtasks`)
    *   `tag`: `Specify which tag context to list tasks from. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
    *   `watch`: `Watch for changes and auto-refresh the list in real-time. Works with file storage (fs.watch) and API storage (Supabase Realtime).` (CLI: `-w, --watch`)
*   **Usage:** Get an overview of the project status, often used at the start of a work session. Use `--watch` to keep the list live-updating as tasks change.

### 4. Get Next Task (`next_task`)

*   **MCP Tool:** `next_task`
*   **CLI Command:** `task-master next [options]`
*   **Description:** `Ask Taskmaster to show the next available task you can work on, based on status and completed dependencies.`
*   **Key Parameters/Options:**
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
    *   `tag`: `Specify which tag context to use. Defaults to the current active tag.` (CLI: `--tag <name>`)
*   **Usage:** Identify what to work on next according to the plan.

### 5. Get Task Details (`get_task`)

*   **MCP Tool:** `get_task`
*   **CLI Command:** `task-master show [id] [options]`
*   **Description:** `Display detailed information for one or more specific Taskmaster tasks or subtasks by ID.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID of the Taskmaster task (e.g., '15'), subtask (e.g., '15.2'), or a comma-separated list of IDs ('1,5,10.2') you want to view.` (CLI: `[id]` positional or `-i, --id <id>`)
    *   `tag`: `Specify which tag context to get the task(s) from. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Understand the full details for a specific task. When multiple IDs are provided, a summary table is shown.
*   **CRITICAL INFORMATION** If you need to collect information from multiple tasks, use comma-separated IDs (i.e. 1,2,3) to receive an array of tasks. Do not needlessly get tasks one at a time if you need to get many as that is wasteful.

---

## Task Creation & Modification

### 6. Add Task (`add_task`)

*   **MCP Tool:** `add_task`
*   **CLI Command:** `task-master add-task [options]`
*   **Description:** `Add a new task to Taskmaster by describing it; AI will structure it.`
*   **Key Parameters/Options:**
    *   `prompt`: `Required. Describe the new task you want Taskmaster to create, e.g., "Implement user authentication using JWT".` (CLI: `-p, --prompt <text>`)
    *   `dependencies`: `Specify the IDs of any Taskmaster tasks that must be completed before this new one can start, e.g., '12,14'.` (CLI: `-d, --dependencies <ids>`)
    *   `priority`: `Set the priority for the new task: 'high', 'medium', or 'low'. Default is 'medium'.` (CLI: `--priority <priority>`)
    *   `research`: `Enable Taskmaster to use the research role for potentially more informed task creation.` (CLI: `-r, --research`)
    *   `tag`: `Specify which tag context to add the task to. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Quickly add newly identified tasks during development.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 7. Add Subtask (`add_subtask`)

*   **MCP Tool:** `add_subtask`
*   **CLI Command:** `task-master add-subtask [options]`
*   **Description:** `Add a new subtask to a Taskmaster parent task, or convert an existing task into a subtask.`
*   **Key Parameters/Options:**
    *   `id` / `parent`: `Required. The ID of the Taskmaster task that will be the parent.` (MCP: `id`, CLI: `-p, --parent <id>`)
    *   `taskId`: `Use this if you want to convert an existing top-level Taskmaster task into a subtask of the specified parent.` (CLI: `-i, --task-id <id>`)
    *   `title`: `Required if not using taskId. The title for the new subtask Taskmaster should create.` (CLI: `-t, --title <title>`)
    *   `description`: `A brief description for the new subtask.` (CLI: `-d, --description <text>`)
    *   `details`: `Provide implementation notes or details for the new subtask.` (CLI: `--details <text>`)
    *   `dependencies`: `Specify IDs of other tasks or subtasks, e.g., '15' or '16.1', that must be done before this new subtask.` (CLI: `--dependencies <ids>`)
    *   `status`: `Set the initial status for the new subtask. Default is 'pending'.` (CLI: `-s, --status <status>`)
    *   `generate`: `Enable Taskmaster to regenerate markdown task files after adding the subtask.` (CLI: `--generate`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Break down tasks manually or reorganize existing tasks.

### 8. Update Tasks (`update`)

*   **MCP Tool:** `update`
*   **CLI Command:** `task-master update [options]`
*   **Description:** `Update multiple upcoming tasks in Taskmaster based on new context or changes, starting from a specific task ID.`
*   **Key Parameters/Options:**
    *   `from`: `Required. The ID of the first task Taskmaster should update. All tasks with this ID or higher that are not 'done' will be considered.` (CLI: `--from <id>`)
    *   `prompt`: `Required. Explain the change or new context for Taskmaster to apply to the tasks, e.g., "We are now using React Query instead of Redux Toolkit for data fetching".` (CLI: `-p, --prompt <text>`)
    *   `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Handle significant implementation changes or pivots that affect multiple future tasks. Example CLI: `task-master update --from='18' --prompt='Switching to React Query.\nNeed to refactor data fetching...'`
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 9. Update Task (`update_task`)

*   **MCP Tool:** `update_task`
*   **CLI Command:** `task-master update-task [options]`
*   **Description:** `Modify a specific Taskmaster task by ID, incorporating new information or changes. By default, this replaces the existing task details.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The specific ID of the Taskmaster task, e.g., '15', you want to update.` (CLI: `-i, --id <id>`)
    *   `prompt`: `Required. Explain the specific changes or provide the new information Taskmaster should incorporate into this task.` (CLI: `-p, --prompt <text>`)
    *   `append`: `If true, appends the prompt content to the task's details with a timestamp, rather than replacing them. Behaves like update-subtask.` (CLI: `--append`)
    *   `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `tag`: `Specify which tag context the task belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Refine a specific task based on new understanding. Use `--append` to log progress without creating subtasks.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 10. Update Subtask (`update_subtask`)

*   **MCP Tool:** `update_subtask`
*   **CLI Command:** `task-master update-subtask [options]`
*   **Description:** `Append timestamped notes or details to a specific Taskmaster subtask without overwriting existing content. Intended for iterative implementation logging.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID of the Taskmaster subtask, e.g., '5.2', to update with new information.` (CLI: `-i, --id <id>`)
    *   `prompt`: `Required. The information, findings, or progress notes to append to the subtask's details with a timestamp.` (CLI: `-p, --prompt <text>`)
    *   `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `tag`: `Specify which tag context the subtask belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Log implementation progress, findings, and discoveries during subtask development. Each update is timestamped and appended to preserve the implementation journey.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 11. Set Task Status (`set_task_status`)

*   **MCP Tool:** `set_task_status`
*   **CLI Command:** `task-master set-status [options]`
*   **Description:** `Update the status of one or more Taskmaster tasks or subtasks, e.g., 'pending', 'in-progress', 'done'.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID(s) of the Taskmaster task(s) or subtask(s), e.g., '15', '15.2', or '16,17.1', to update.` (CLI: `-i, --id <id>`)
    *   `status`: `Required. The new status to set, e.g., 'done', 'pending', 'in-progress', 'review', 'cancelled'.` (CLI: `-s, --status <status>`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Mark progress as tasks move through the development cycle.

### 12. Remove Task (`remove_task`)

*   **MCP Tool:** `remove_task`
*   **CLI Command:** `task-master remove-task [options]`
*   **Description:** `Permanently remove a task or subtask from the Taskmaster tasks list.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID of the Taskmaster task, e.g., '5', or subtask, e.g., '5.2', to permanently remove.` (CLI: `-i, --id <id>`)
    *   `yes`: `Skip the confirmation prompt and immediately delete the task.` (CLI: `-y, --yes`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Permanently delete tasks or subtasks that are no longer needed in the project.
*   **Notes:** Use with caution as this operation cannot be undone. Consider using 'blocked', 'cancelled', or 'deferred' status instead if you just want to exclude a task from active planning but keep it for reference. The command automatically cleans up dependency references in other tasks.

---

## Task Structure & Breakdown

### 13. Expand Task (`expand_task`)

*   **MCP Tool:** `expand_task`
*   **CLI Command:** `task-master expand [options]`
*   **Description:** `Use Taskmaster's AI to break down a complex task into smaller, manageable subtasks. Appends subtasks by default.`
*   **Key Parameters/Options:**
    *   `id`: `The ID of the specific Taskmaster task you want to break down into subtasks.` (CLI: `-i, --id <id>`)
    *   `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create. Uses complexity analysis/defaults otherwise.` (CLI: `-n, --num <number>`)
    *   `research`: `Enable Taskmaster to use the research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `prompt`: `Optional: Provide extra context or specific instructions to Taskmaster for generating the subtasks.` (CLI: `-p, --prompt <text>`)
    *   `force`: `Optional: If true, clear existing subtasks before generating new ones. Default is false (append).` (CLI: `--force`)
    *   `tag`: `Specify which tag context the task belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Generate a detailed implementation plan for a complex task before starting coding. Automatically uses complexity report recommendations if available and `num` is not specified.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 14. Expand All Tasks (`expand_all`)

*   **MCP Tool:** `expand_all`
*   **CLI Command:** `task-master expand --all [options]` (Note: CLI uses the `expand` command with the `--all` flag)
*   **Description:** `Tell Taskmaster to automatically expand all eligible pending/in-progress tasks based on complexity analysis or defaults. Appends subtasks by default.`
*   **Key Parameters/Options:**
    *   `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create per task.` (CLI: `-n, --num <number>`)
    *   `research`: `Enable research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `prompt`: `Optional: Provide extra context for Taskmaster to apply generally during expansion.` (CLI: `-p, --prompt <text>`)
    *   `force`: `Optional: If true, clear existing subtasks before generating new ones for each eligible task. Default is false (append).` (CLI: `--force`)
    *   `tag`: `Specify which tag context to expand. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Useful after initial task generation or complexity analysis to break down multiple tasks at once.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 15. Clear Subtasks (`clear_subtasks`)

*   **MCP Tool:** `clear_subtasks`
*   **CLI Command:** `task-master clear-subtasks [options]`
*   **Description:** `Remove all subtasks from one or more specified Taskmaster parent tasks.`
*   **Key Parameters/Options:**
    *   `id`: `The ID(s) of the Taskmaster parent task(s) whose subtasks you want to remove, e.g., '15' or '16,18'. Required unless using 'all'.` (CLI: `-i, --id <ids>`)
    *   `all`: `Tell Taskmaster to remove subtasks from all parent tasks.` (CLI: `--all`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Used before regenerating subtasks with `expand_task` if the previous breakdown needs replacement.

### 16. Remove Subtask (`remove_subtask`)

*   **MCP Tool:** `remove_subtask`
*   **CLI Command:** `task-master remove-subtask [options]`
*   **Description:** `Remove a subtask from its Taskmaster parent, optionally converting it into a standalone task.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID(s) of the Taskmaster subtask(s) to remove, e.g., '15.2' or '16.1,16.3'.` (CLI: `-i, --id <id>`)
    *   `convert`: `If used, Taskmaster will turn the subtask into a regular top-level task instead of deleting it.` (CLI: `-c, --convert`)
    *   `generate`: `Enable Taskmaster to regenerate markdown task files after removing the subtask.` (CLI: `--generate`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Delete unnecessary subtasks or promote a subtask to a top-level task.

### 17. Move Task (`move_task`)

*   **MCP Tool:** `move_task`
*   **CLI Command:** `task-master move [options]`
*   **Description:** `Move a task or subtask to a new position within the task hierarchy.`
*   **Key Parameters/Options:**
    *   `from`: `Required. ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated for multiple tasks.` (CLI: `--from <id>`)
    *   `to`: `Required. ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated.` (CLI: `--to <id>`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Reorganize tasks by moving them within the hierarchy. Supports various scenarios like:
    *   Moving a task to become a subtask
    *   Moving a subtask to become a standalone task
    *   Moving a subtask to a different parent
    *   Reordering subtasks within the same parent
    *   Moving a task to a new, non-existent ID (automatically creates placeholders)
    *   Moving multiple tasks at once with comma-separated IDs
*   **Validation Features:**
    *   Allows moving tasks to non-existent destination IDs (creates placeholder tasks)
    *   Prevents moving to existing task IDs that already have content (to avoid overwriting)
    *   Validates that source tasks exist before attempting to move them
    *   Maintains proper parent-child relationships
*   **Example CLI:** `task-master move --from=5.2 --to=7.3` to move subtask 5.2 to become subtask 7.3.
*   **Example Multi-Move:** `task-master move --from=10,11,12 --to=16,17,18` to move multiple tasks to new positions.
*   **Common Use:** Resolving merge conflicts in tasks.json when multiple team members create tasks on different branches.

---

## Dependency Management

### 18. Add Dependency (`add_dependency`)

*   **MCP Tool:** `add_dependency`
*   **CLI Command:** `task-master add-dependency [options]`
*   **Description:** `Define a dependency in Taskmaster, making one task a prerequisite for another.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID of the Taskmaster task that will depend on another.` (CLI: `-i, --id <id>`)
    *   `dependsOn`: `Required. The ID of the Taskmaster task that must be completed first, the prerequisite.` (CLI: `-d, --depends-on <id>`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <path>`)
*   **Usage:** Establish the correct order of execution between tasks.

### 19. Remove Dependency (`remove_dependency`)

*   **MCP Tool:** `remove_dependency`
*   **CLI Command:** `task-master remove-dependency [options]`
*   **Description:** `Remove a dependency relationship between two Taskmaster tasks.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID of the Taskmaster task you want to remove a prerequisite from.` (CLI: `-i, --id <id>`)
    *   `dependsOn`: `Required. The ID of the Taskmaster task that should no longer be a prerequisite.` (CLI: `-d, --depends-on <id>`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Update task relationships when the order of execution changes.

### 20. Validate Dependencies (`validate_dependencies`)

*   **MCP Tool:** `validate_dependencies`
*   **CLI Command:** `task-master validate-dependencies [options]`
*   **Description:** `Check your Taskmaster tasks for dependency issues (like circular references or links to non-existent tasks) without making changes.`
*   **Key Parameters/Options:**
    *   `tag`: `Specify which tag context to validate. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Audit the integrity of your task dependencies.

### 21. Fix Dependencies (`fix_dependencies`)

*   **MCP Tool:** `fix_dependencies`
*   **CLI Command:** `task-master fix-dependencies [options]`
*   **Description:** `Automatically fix dependency issues (like circular references or links to non-existent tasks) in your Taskmaster tasks.`
*   **Key Parameters/Options:**
    *   `tag`: `Specify which tag context to fix dependencies in. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Clean up dependency errors automatically.

---

## Analysis & Reporting

### 22. Analyze Project Complexity (`analyze_project_complexity`)

*   **MCP Tool:** `analyze_project_complexity`
*   **CLI Command:** `task-master analyze-complexity [options]`
*   **Description:** `Have Taskmaster analyze your tasks to determine their complexity and suggest which ones need to be broken down further.`
*   **Key Parameters/Options:**
    *   `output`: `Where to save the complexity analysis report. Default is '.taskmaster/reports/task-complexity-report.json' (or '..._tagname.json' if a tag is used).` (CLI: `-o, --output <file>`)
    *   `threshold`: `The minimum complexity score (1-10) that should trigger a recommendation to expand a task.` (CLI: `-t, --threshold <number>`)
    *   `research`: `Enable research role for more accurate complexity analysis. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `tag`: `Specify which tag context to analyze. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Used before breaking down tasks to identify which ones need the most attention.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 23. View Complexity Report (`complexity_report`)

*   **MCP Tool:** `complexity_report`
*   **CLI Command:** `task-master complexity-report [options]`
*   **Description:** `Display the task complexity analysis report in a readable format.`
*   **Key Parameters/Options:**
    *   `tag`: `Specify which tag context to show the report for. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to the complexity report (default: '.taskmaster/reports/task-complexity-report.json').` (CLI: `-f, --file <file>`)
*   **Usage:** Review and understand the complexity analysis results after running analyze-complexity.

---

## File Management

### 24. Generate Task Files (`generate`)

*   **MCP Tool:** `generate`
*   **CLI Command:** `task-master generate [options]`
*   **Description:** `Create or update individual Markdown files for each task based on your tasks.json.`
*   **Key Parameters/Options:**
    *   `output`: `The directory where Taskmaster should save the task files (default: in a 'tasks' directory).` (CLI: `-o, --output <directory>`)
    *   `tag`: `Specify which tag context to generate files for. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Run this after making changes to tasks.json to keep individual task files up to date. This command is now manual and no longer runs automatically.

---

## AI-Powered Research

### 25. Research (`research`)

*   **MCP Tool:** `research`
*   **CLI Command:** `task-master research [options]`
*   **Description:** `Perform AI-powered research queries with project context to get fresh, up-to-date information beyond the AI's knowledge cutoff.`
*   **Key Parameters/Options:**
    *   `query`: `Required. Research query/prompt (e.g., "What are the latest best practices for React Query v5?").` (CLI: `[query]` positional or `-q, --query <text>`)
    *   `taskIds`: `Comma-separated list of task/subtask IDs from the current tag context (e.g., "15,16.2,17").` (CLI: `-i, --id <ids>`)
    *   `filePaths`: `Comma-separated list of file paths for context (e.g., "src/api.js,docs/readme.md").` (CLI: `-f, --files <paths>`)
    *   `customContext`: `Additional custom context text to include in the research.` (CLI: `-c, --context <text>`)
    *   `includeProjectTree`: `Include project file tree structure in context (default: false).` (CLI: `--tree`)
    *   `detailLevel`: `Detail level for the research response: 'low', 'medium', 'high' (default: medium).` (CLI: `--detail <level>`)
    *   `saveTo`: `Task or subtask ID (e.g., "15", "15.2") to automatically save the research conversation to.` (CLI: `--save-to <id>`)
    *   `saveFile`: `If true, saves the research conversation to a markdown file in '.taskmaster/docs/research/'.` (CLI: `--save-file`)
    *   `noFollowup`: `Disables the interactive follow-up question menu in the CLI.` (CLI: `--no-followup`)
    *   `tag`: `Specify which tag context to use for task-based context gathering. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `projectRoot`: `The directory of the project. Must be an absolute path.` (CLI: Determined automatically)
*   **Usage:** **This is a POWERFUL tool that agents should use FREQUENTLY** to:
    *   Get fresh information beyond knowledge cutoff dates
    *   Research latest best practices, library updates, security patches
    *   Find implementation examples for specific technologies
    *   Validate approaches against current industry standards
    *   Get contextual advice based on project files and tasks
*   **When to Consider Using Research:**
    *   **Before implementing any task** - Research current best practices
    *   **When encountering new technologies** - Get up-to-date implementation guidance (libraries, apis, etc)
    *   **For security-related tasks** - Find latest security recommendations
    *   **When updating dependencies** - Research breaking changes and migration guides
    *   **For performance optimization** - Get current performance best practices
    *   **When debugging complex issues** - Research known solutions and workarounds
*   **Research + Action Pattern:**
    *   Use `research` to gather fresh information
    *   Use `update_subtask` to commit findings with timestamps
    *   Use `update_task` to incorporate research into task details
    *   Use `add_task` with research flag for informed task creation
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. The research provides FRESH data beyond the AI's training cutoff, making it invaluable for current best practices and recent developments.

---

## Tag Management

This new suite of commands allows you to manage different task contexts (tags).

### 26. List Tags (`tags`)

*   **MCP Tool:** `list_tags`
*   **CLI Command:** `task-master tags [options]`
*   **Description:** `List all available tags with task counts, completion status, and other metadata.`
*   **Key Parameters/Options:**
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
    *   `--show-metadata`: `Include detailed metadata in the output (e.g., creation date, description).` (CLI: `--show-metadata`)

### 27. Add Tag (`add_tag`)

*   **MCP Tool:** `add_tag`
*   **CLI Command:** `task-master add-tag <tagName> [options]`
*   **Description:** `Create a new, empty tag context, or copy tasks from another tag.`
*   **Key Parameters/Options:**
    *   `tagName`: `Name of the new tag to create (alphanumeric, hyphens, underscores).` (CLI: `<tagName>` positional)
    *   `--from-branch`: `Creates a tag with a name derived from the current git branch, ignoring the <tagName> argument.` (CLI: `--from-branch`)
    *   `--copy-from-current`: `Copy tasks from the currently active tag to the new tag.` (CLI: `--copy-from-current`)
    *   `--copy-from <tag>`: `Copy tasks from a specific source tag to the new tag.` (CLI: `--copy-from <tag>`)
    *   `--description <text>`: `Provide an optional description for the new tag.` (CLI: `-d, --description <text>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)

### 28. Delete Tag (`delete_tag`)

*   **MCP Tool:** `delete_tag`
*   **CLI Command:** `task-master delete-tag <tagName> [options]`
*   **Description:** `Permanently delete a tag and all of its associated tasks.`
*   **Key Parameters/Options:**
    *   `tagName`: `Name of the tag to delete.` (CLI: `<tagName>` positional)
    *   `--yes`: `Skip the confirmation prompt.` (CLI: `-y, --yes`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)

### 29. Use Tag (`use_tag`)

*   **MCP Tool:** `use_tag`
*   **CLI Command:** `task-master use-tag <tagName>`
*   **Description:** `Switch your active task context to a different tag.`
*   **Key Parameters/Options:**
    *   `tagName`: `Name of the tag to switch to.` (CLI: `<tagName>` positional)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)

### 30. Rename Tag (`rename_tag`)

*   **MCP Tool:** `rename_tag`
*   **CLI Command:** `task-master rename-tag <oldName> <newName>`
*   **Description:** `Rename an existing tag.`
*   **Key Parameters/Options:**
    *   `oldName`: `The current name of the tag.` (CLI: `<oldName>` positional)
    *   `newName`: `The new name for the tag.` (CLI: `<newName>` positional)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)

### 31. Copy Tag (`copy_tag`)

*   **MCP Tool:** `copy_tag`
*   **CLI Command:** `task-master copy-tag <sourceName> <targetName> [options]`
*   **Description:** `Copy an entire tag context, including all its tasks and metadata, to a new tag.`
*   **Key Parameters/Options:**
    *   `sourceName`: `Name of the tag to copy from.` (CLI: `<sourceName>` positional)
    *   `targetName`: `Name of the new tag to create.` (CLI: `<targetName>` positional)
    *   `--description <text>`: `Optional description for the new tag.` (CLI: `-d, --description <text>`)

---

## Miscellaneous

### 32. Sync Readme (`sync-readme`) -- experimental

*   **MCP Tool:** N/A
*   **CLI Command:** `task-master sync-readme [options]`
*   **Description:** `Exports your task list to your project's README.md file, useful for showcasing progress.`
*   **Key Parameters/Options:**
    *   `status`: `Filter tasks by status (e.g., 'pending', 'done').` (CLI: `-s, --status <status>`)
    *   `withSubtasks`: `Include subtasks in the export.` (CLI: `--with-subtasks`)
    *   `tag`: `Specify which tag context to export from. Defaults to the current active tag.` (CLI: `--tag <name>`)

---

## Environment Variables Configuration (Updated)

Taskmaster primarily uses the **`.taskmaster/config.json`** file (in project root) for configuration (models, parameters, logging level, etc.), managed via `task-master models --setup`.

Environment variables are used **only** for sensitive API keys related to AI providers and specific overrides like the Ollama base URL:

*   **API Keys (Required for corresponding provider):**
    *   `ANTHROPIC_API_KEY`
    *   `PERPLEXITY_API_KEY`
    *   `OPENAI_API_KEY`
    *   `GOOGLE_API_KEY`
    *   `MISTRAL_API_KEY`
    *   `AZURE_OPENAI_API_KEY` (Requires `AZURE_OPENAI_ENDPOINT` too)
    *   `OPENROUTER_API_KEY`
    *   `XAI_API_KEY`
    *   `OLLAMA_API_KEY` (Requires `OLLAMA_BASE_URL` too)
*   **Endpoints (Optional/Provider Specific inside .taskmaster/config.json):**
    *   `AZURE_OPENAI_ENDPOINT`
    *   `OLLAMA_BASE_URL` (Default: `http://localhost:11434/api`)

**Set API keys** in your **`.env`** file in the project root (for CLI use) or within the `env` section of your **`<AGENT>/mcp.json`** file (for MCP/<AGENT> integration). All other settings (model choice, max tokens, temperature, log level, custom endpoints) are managed in `.taskmaster/config.json` via `task-master models` command or `models` MCP tool.

---

## MCP Tool Tiers

Default: `core` (7 tools). Set via `TASK_MASTER_TOOLS` env var in MCP config.

| Tier | Count | Tools |
|------|-------|-------|
| `core` | 7 | `get_tasks`, `next_task`, `get_task`, `set_task_status`, `update_subtask`, `parse_prd`, `expand_task` |
| `standard` | 14 | core + `initialize_project`, `analyze_project_complexity`, `expand_all`, `add_subtask`, `remove_task`, `add_task`, `complexity_report` |
| `all` | 44+ | standard + dependencies, tags, research, autopilot, scoping, models, rules |

**Upgrade when tool unavailable:** Edit MCP config (`<AGENT>/mcp.json`, `.mcp.json`, or `.vscode/mcp.json`), change `TASK_MASTER_TOOLS` from `"core"` to `"standard"` or `"all"`, restart MCP.

---

For details on how these commands fit into the development process, see the [dev_workflow.md](<AGENT>rules/dev_workflow.md).

## Links discovered
- [dev_workflow.md](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/<AGENT>rules/dev_workflow.md)

--- .taskmaster/AGENTS.md ---
# Task Master AI - Agent Integration Guide

## Essential Commands

### Core Workflow Commands

```bash
# Project Setup
task-master init                                    # Initialize Task Master in current project
task-master parse-prd .taskmaster/docs/prd.md       # Generate tasks from PRD document
task-master models --setup                        # Configure AI models interactively

# Daily Development Workflow
task-master list                                   # Show all tasks with status
task-master next                                   # Get next available task to work on
task-master show <id>                             # View detailed task information (e.g., task-master show 1.2)
task-master set-status --id=<id> --status=done    # Mark task complete

# Task Management
task-master add-task --prompt="description" --research        # Add new task with AI assistance
task-master expand --id=<id> --research --force              # Break task into subtasks
task-master update-task --id=<id> --prompt="changes"         # Update specific task
task-master update --from=<id> --prompt="changes"            # Update multiple tasks from ID onwards
task-master update-subtask --id=<id> --prompt="notes"        # Add implementation notes to subtask

# Analysis & Planning
task-master analyze-complexity --research          # Analyze task complexity
task-master complexity-report                      # View complexity analysis
task-master expand --all --research               # Expand all eligible tasks

# Dependencies & Organization
task-master add-dependency --id=<id> --depends-on=<id>       # Add task dependency
task-master move --from=<id> --to=<id>                       # Reorganize task hierarchy
task-master validate-dependencies                            # Check for dependency issues
task-master generate                                         # Update task markdown files (usually auto-called)
```

## Key Files & Project Structure

### Core Files

- `.taskmaster/tasks/tasks.json` - Main task data file (auto-managed)
- `.taskmaster/config.json` - AI model configuration (use `task-master models` to modify)
- `.taskmaster/docs/prd.md` - Product Requirements Document for parsing (`.md` extension recommended for better editor support)
- `.taskmaster/tasks/*.txt` - Individual task files (auto-generated from tasks.json)
- `.env` - API keys for CLI usage

**PRD File Format:** While both `.txt` and `.md` extensions work, **`.md` is recommended** because:
- Markdown syntax highlighting in editors improves readability
- Proper rendering when previewing in VS Code, GitHub, or other tools
- Better collaboration through formatted documentation

### Claude Code Integration Files

- `CLAUDE.md` - Auto-loaded context for Claude Code (this file)
- `.claude/settings.json` - Claude Code tool allowlist and preferences
- `.claude/commands/` - Custom slash commands for repeated workflows
- `.mcp.json` - MCP server configuration (project-specific)

### Directory Structure

```
project/
├── .taskmaster/
│   ├── tasks/              # Task files directory
│   │   ├── tasks.json      # Main task database
│   │   ├── task-1.md      # Individual task files
│   │   └── task-2.md
│   ├── docs/              # Documentation directory
│   │   ├── prd.md         # Product requirements (.md recommended)
│   ├── reports/           # Analysis reports directory
│   │   └── task-complexity-report.json
│   ├── templates/         # Template files
│   │   └── example_prd.md  # Example PRD template (.md recommended)
│   └── config.json        # AI models & settings
├── .claude/
│   ├── settings.json      # Claude Code configuration
│   └── commands/         # Custom slash commands
├── .env                  # API keys
├── .mcp.json            # MCP configuration
└── CLAUDE.md            # This file - auto-loaded by Claude Code
```

## MCP Integration

Task Master provides an MCP server that Claude Code can connect to. Configure in `.mcp.json`:

```json
{
  "mcpServers": {
    "task-master-ai": {
      "command": "npx",
      "args": ["-y", "task-master-ai"],
      "env": {
        "TASK_MASTER_TOOLS": "core",
        "ANTHROPIC_API_KEY": "your_key_here",
        "PERPLEXITY_API_KEY": "your_key_here",
        "OPENAI_API_KEY": "OPENAI_API_KEY_HERE",
        "GOOGLE_API_KEY": "GOOGLE_API_KEY_HERE",
        "XAI_API_KEY": "XAI_API_KEY_HERE",
        "OPENROUTER_API_KEY": "OPENROUTER_API_KEY_HERE",
        "MISTRAL_API_KEY": "MISTRAL_API_KEY_HERE",
        "AZURE_OPENAI_API_KEY": "AZURE_OPENAI_API_KEY_HERE",
        "OLLAMA_API_KEY": "OLLAMA_API_KEY_HERE"
      }
    }
  }
}
```

### MCP Tool Tiers

Default: `core` (7 tools). Set via `TASK_MASTER_TOOLS` env var.

| Tier | Count | Tools |
|------|-------|-------|
| `core` | 7 | `get_tasks`, `next_task`, `get_task`, `set_task_status`, `update_subtask`, `parse_prd`, `expand_task` |
| `standard` | 14 | core + `initialize_project`, `analyze_project_complexity`, `expand_all`, `add_subtask`, `remove_task`, `add_task`, `complexity_report` |
| `all` | 44+ | standard + dependencies, tags, research, autopilot, scoping, models, rules |

**Upgrade when tool unavailable:** Edit MCP config, change `TASK_MASTER_TOOLS` from `"core"` to `"standard"` or `"all"`, restart MCP.

### Essential MCP Tools

```javascript
help; // = shows available taskmaster commands
// Project setup
initialize_project; // = task-master init
parse_prd; // = task-master parse-prd

// Daily workflow
get_tasks; // = task-master list
next_task; // = task-master next
get_task; // = task-master show <id>
set_task_status; // = task-master set-status

// Task management
add_task; // = task-master add-task
expand_task; // = task-master expand
update_task; // = task-master update-task
update_subtask; // = task-master update-subtask
update; // = task-master update

// Analysis
analyze_project_complexity; // = task-master analyze-complexity
complexity_report; // = task-master complexity-report
```

## Claude Code Workflow Integration

### Standard Development Workflow

#### 1. Project Initialization

```bash
# Initialize Task Master
task-master init

# Create or obtain PRD, then parse it (use .md extension for better editor support)
task-master parse-prd .taskmaster/docs/prd.md

# Analyze complexity and expand tasks
task-master analyze-complexity --research
task-master expand --all --research
```

If tasks already exist, another PRD can be parsed (with new information only!) using parse-prd with --append flag. This will add the generated tasks to the existing list of tasks..

#### 2. Daily Development Loop

```bash
# Start each session
task-master next                           # Find next available task
task-master show <id>                     # Review task details

# During implementation, check in code context into the tasks and subtasks
task-master update-subtask --id=<id> --prompt="implementation notes..."

# Complete tasks
task-master set-status --id=<id> --status=done
```

#### 3. Multi-Claude Workflows

For complex projects, use multiple Claude Code sessions:

```bash
# Terminal 1: Main implementation
cd project && claude

# Terminal 2: Testing and validation
cd project-test-worktree && claude

# Terminal 3: Documentation updates
cd project-docs-worktree && claude
```

### Custom Slash Commands

Create `.claude/commands/taskmaster-next.md`:

```markdown
Find the next available Task Master task and show its details.

Steps:

1. Run `task-master next` to get the next task
2. If a task is available, run `task-master show <id>` for full details
3. Provide a summary of what needs to be implemented
4. Suggest the first implementation step
```

Create `.claude/commands/taskmaster-complete.md`:

```markdown
Complete a Task Master task: $ARGUMENTS

Steps:

1. Review the current task with `task-master show $ARGUMENTS`
2. Verify all implementation is complete
3. Run any tests related to this task
4. Mark as complete: `task-master set-status --id=$ARGUMENTS --status=done`
5. Show the next available task with `task-master next`
```

## Tool Allowlist Recommendations

Add to `.claude/settings.json`:

```json
{
  "allowedTools": [
    "Edit",
    "Bash(task-master *)",
    "Bash(git commit:*)",
    "Bash(git add:*)",
    "Bash(npm run *)",
    "mcp__task_master_ai__*"
  ]
}
```

## Configuration & Setup

### API Keys Required

At least **one** of these API keys must be configured:

- `ANTHROPIC_API_KEY` (Claude models) - **Recommended**
- `PERPLEXITY_API_KEY` (Research features) - **Highly recommended**
- `OPENAI_API_KEY` (GPT models)
- `GOOGLE_API_KEY` (Gemini models)
- `MISTRAL_API_KEY` (Mistral models)
- `OPENROUTER_API_KEY` (Multiple models)
- `XAI_API_KEY` (Grok models)

An API key is required for any provider used across any of the 3 roles defined in the `models` command.

### Model Configuration

```bash
# Interactive setup (recommended)
task-master models --setup

# Set specific models
task-master models --set-main claude-3-5-sonnet-20241022
task-master models --set-research perplexity-llama-3.1-sonar-large-128k-online
task-master models --set-fallback gpt-4o-mini
```

## Task Structure & IDs

### Task ID Format

- Main tasks: `1`, `2`, `3`, etc.
- Subtasks: `1.1`, `1.2`, `2.1`, etc.
- Sub-subtasks: `1.1.1`, `1.1.2`, etc.

### Task Status Values

- `pending` - Ready to work on
- `in-progress` - Currently being worked on
- `done` - Completed and verified
- `deferred` - Postponed
- `cancelled` - No longer needed
- `blocked` - Waiting on external factors

### Task Fields

```json
{
  "id": "1.2",
  "title": "Implement user authentication",
  "description": "Set up JWT-based auth system",
  "status": "pending",
  "priority": "high",
  "dependencies": ["1.1"],
  "details": "Use bcrypt for hashing, JWT for tokens...",
  "testStrategy": "Unit tests for auth functions, integration tests for login flow",
  "subtasks": []
}
```

## Claude Code Best Practices with Task Master

### Context Management

- Use `/clear` between different tasks to maintain focus
- This CLAUDE.md file is automatically loaded for context
- Use `task-master show <id>` to pull specific task context when needed

### Iterative Implementation

1. `task-master show <subtask-id>` - Understand requirements
2. Explore codebase and plan implementation
3. `task-master update-subtask --id=<id> --prompt="detailed plan"` - Log plan
4. `task-master set-status --id=<id> --status=in-progress` - Start work
5. Implement code following logged plan
6. `task-master update-subtask --id=<id> --prompt="what worked/didn't work"` - Log progress
7. `task-master set-status --id=<id> --status=done` - Complete task

### Complex Workflows with Checklists

For large migrations or multi-step processes:

1. Create a markdown PRD file describing the new changes: `touch task-migration-checklist.md` (prds can be .txt or .md)
2. Use Taskmaster to parse the new prd with `task-master parse-prd --append` (also available in MCP)
3. Use Taskmaster to expand the newly generated tasks into subtasks. Consdier using `analyze-complexity` with the correct --to and --from IDs (the new ids) to identify the ideal subtask amounts for each task. Then expand them.
4. Work through items systematically, checking them off as completed
5. Use `task-master update-subtask` to log progress on each task/subtask and/or updating/researching them before/during implementation if getting stuck

### Git Integration

Task Master works well with `gh` CLI:

```bash
# Create PR for completed task
gh pr create --title "Complete task 1.2: User authentication" --body "Implements JWT auth system as specified in task 1.2"

# Reference task in commits
git commit -m "feat: implement JWT auth (task 1.2)"
```

### Parallel Development with Git Worktrees

```bash
# Create worktrees for parallel task development
git worktree add ../project-auth feature/auth-system
git worktree add ../project-api feature/api-refactor

# Run Claude Code in each worktree
cd ../project-auth && claude    # Terminal 1: Auth work
cd ../project-api && claude     # Terminal 2: API work
```

## Troubleshooting

### AI Commands Failing

```bash
# Check API keys are configured
cat .env                           # For CLI usage

# Verify model configuration
task-master models

# Test with different model
task-master models --set-fallback gpt-4o-mini
```

### MCP Connection Issues

- Check `.mcp.json` configuration
- Verify Node.js installation
- Use `--mcp-debug` flag when starting Claude Code
- Use CLI as fallback if MCP unavailable

### Task File Sync Issues

```bash
# Regenerate task files from tasks.json
task-master generate

# Fix dependency issues
task-master fix-dependencies
```

DO NOT RE-INITIALIZE. That will not do anything beyond re-adding the same Taskmaster core files.

## Important Notes

### AI-Powered Operations

These commands make AI calls and may take up to a minute:

- `parse_prd` / `task-master parse-prd`
- `analyze_project_complexity` / `task-master analyze-complexity`
- `expand_task` / `task-master expand`
- `expand_all` / `task-master expand --all`
- `add_task` / `task-master add-task`
- `update` / `task-master update`
- `update_task` / `task-master update-task`
- `update_subtask` / `task-master update-subtask`

### File Management

- Never manually edit `tasks.json` - use commands instead
- Never manually edit `.taskmaster/config.json` - use `task-master models`
- Task markdown files in `tasks/` are auto-generated
- Run `task-master generate` after manual changes to tasks.json

### Claude Code Session Management

- Use `/clear` frequently to maintain focused context
- Create custom slash commands for repeated Task Master workflows
- Configure tool allowlist to streamline permissions
- Use headless mode for automation: `claude -p "task-master next"`

### Multi-Task Updates

- Use `update --from=<id>` to update multiple future tasks
- Use `update-task --id=<id>` for single task updates
- Use `update-subtask --id=<id>` for implementation logging

### Research Mode

- Add `--research` flag for research-based AI enhancement
- Requires a research model API key like Perplexity (`PERPLEXITY_API_KEY`) in environment
- Provides more informed task creation and updates
- Recommended for complex technical tasks

---

_This guide ensures Claude Code has immediate access to Task Master's essential functionality for agentic development workflows._


--- .taskmaster/oracle-pack-2026-01-02.md ---
# oracle strategist question pack

---

## parsed args

- codebase_name: Unknown
- constraints: None
- non_goals: None
- team_size: Unknown
- deadline: Unknown
- out_dir: oracle-out
- oracle_cmd: oracle
- oracle_flags: --files-report
- extra_files: empty

---

## commands (exactly 20; sorted by ROI desc; ties by lower effort)

```bash
# 01 — ROI=1.75 impact=0.5 confidence=0.7 effort=0.2 horizon=Immediate category=observability reference=internal/report/generate.go:GenerateReport
oracle \
  --files-report \
  --write-output "oracle-out/01-observability-generate-report.md" \
  -p "Strategist question #01
Reference: internal/report/generate.go:GenerateReport
Category: observability
Horizon: Immediate
ROI: 1.75 (impact=0.5, confidence=0.7, effort=0.2)
Question: Should `GenerateReport` produce a deterministic step order (e.g., sorted by step ID) so downstream tooling can diff reports reliably?
Rationale: Reports are the primary audit artifact and nondeterministic ordering makes automation brittle.
Smallest experiment today: Review the map iteration in `GenerateReport` and prototype sorting by step ID.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/report/generate.go"

# 02 — ROI=1.50 impact=0.5 confidence=0.6 effort=0.2 horizon=Immediate category=UX flows reference=internal/tui/tui.go:refreshList
oracle \
  --files-report \
  --write-output "oracle-out/02-ux-flows-roi-filter.md" \
  -p "Strategist question #02
Reference: internal/tui/tui.go:refreshList
Category: UX flows
Horizon: Immediate
ROI: 1.50 (impact=0.5, confidence=0.6, effort=0.2)
Question: Does the ROI filter behave as intended for `under` vs `over` modes and for steps with `ROI==0`?
Rationale: Filtering is a core interaction in large packs and off-by-one logic will confuse users.
Smallest experiment today: Inspect `refreshList` and trace the ROI comparisons for zero-valued ROI.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/tui/tui.go"

# 03 — ROI=1.40 impact=0.7 confidence=0.6 effort=0.3 horizon=Immediate category=invariants reference=internal/pack/parser.go:Validate
oracle \
  --files-report \
  --write-output "oracle-out/03-invariants-validate-steps.md" \
  -p "Strategist question #03
Reference: internal/pack/parser.go:Validate
Category: invariants
Horizon: Immediate
ROI: 1.40 (impact=0.7, confidence=0.6, effort=0.3)
Question: Does `Validate` correctly allow all supported header separators while still enforcing strict sequential numbering?
Rationale: Validation is the gatekeeper for running packs and overly strict rules can reject valid files.
Smallest experiment today: Add a unit test in `internal/pack/parser_test.go` for mixed header separators and sequential numbering.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/pack/parser.go"

# 04 — ROI=1.25 impact=0.5 confidence=0.5 effort=0.2 horizon=Immediate category=caching/state reference=internal/state/types.go:RunState
oracle \
  --files-report \
  --write-output "oracle-out/04-caching-state-schema-version.md" \
  -p "Strategist question #04
Reference: internal/state/types.go:RunState
Category: caching/state
Horizon: Immediate
ROI: 1.25 (impact=0.5, confidence=0.5, effort=0.2)
Question: Is `SchemaVersion` enforced anywhere on resume, and should mismatches block or migrate state loading?
Rationale: State schema drift can silently corrupt resume behavior.
Smallest experiment today: Search for `SchemaVersion` usage and note whether load-time validation exists.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/state/types.go" \
  -f "internal/state/persist.go"

# 05 — ROI=1.20 impact=0.4 confidence=0.6 effort=0.2 horizon=Immediate category=observability reference=internal/exec/stream.go:LineWriter.Write
oracle \
  --files-report \
  --write-output "oracle-out/05-observability-linewriter-flush.md" \
  -p "Strategist question #05
Reference: internal/exec/stream.go:LineWriter.Write
Category: observability
Horizon: Immediate
ROI: 1.20 (impact=0.4, confidence=0.6, effort=0.2)
Question: Do log lines flush correctly when output lacks a trailing newline?
Rationale: Missing final lines makes debugging long runs harder.
Smallest experiment today: Review `LineWriter.Write`/`Close` to confirm buffered output is always emitted.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/exec/stream.go"

# 06 — ROI=1.20 impact=0.6 confidence=0.6 effort=0.3 horizon=Immediate category=caching/state reference=internal/state/persist.go:SaveStateAtomic
oracle \
  --files-report \
  --write-output "oracle-out/06-caching-state-atomic-save.md" \
  -p "Strategist question #06
Reference: internal/state/persist.go:SaveStateAtomic
Category: caching/state
Horizon: Immediate
ROI: 1.20 (impact=0.6, confidence=0.6, effort=0.3)
Question: Is `SaveStateAtomic` safe across different filesystems when `statePath` is set outside the pack directory?
Rationale: Atomic renames are only guaranteed on the same filesystem.
Smallest experiment today: Verify that temp files are created alongside `statePath` and document any cross-filesystem risks.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/state/persist.go"

# 07 — ROI=1.20 impact=0.6 confidence=0.6 effort=0.3 horizon=Immediate category=failure modes reference=internal/exec/runner.go:run
oracle \
  --files-report \
  --write-output "oracle-out/07-failure-modes-runner-run.md" \
  -p "Strategist question #07
Reference: internal/exec/runner.go:run
Category: failure modes
Horizon: Immediate
ROI: 1.20 (impact=0.6, confidence=0.6, effort=0.3)
Question: Does `Runner.run` surface context cancellations distinctly from command failures and preserve the original exit status?
Rationale: Clear failure classification speeds up diagnosis and retry logic.
Smallest experiment today: Trace the error wrapping paths in `Runner.run` for context vs exec errors.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/exec/runner.go"

# 08 — ROI=1.00 impact=0.5 confidence=0.6 effort=0.3 horizon=Immediate category=failure modes reference=internal/errors/errors.go:ExitCode
oracle \
  --files-report \
  --write-output "oracle-out/08-failure-modes-exit-code.md" \
  -p "Strategist question #08
Reference: internal/errors/errors.go:ExitCode
Category: failure modes
Horizon: Immediate
ROI: 1.00 (impact=0.5, confidence=0.6, effort=0.3)
Question: Are all user-visible failures mapped to stable exit codes that CI can rely on?
Rationale: Exit codes are the primary contract for automation.
Smallest experiment today: Trace error returns from CLI/app layers to see which errors reach `ExitCode`.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/errors/errors.go"

# 09 — ROI=1.00 impact=0.6 confidence=0.5 effort=0.3 horizon=Immediate category=invariants reference=internal/pack/parser.go:Parse
oracle \
  --files-report \
  --write-output "oracle-out/09-invariants-stepheader-regex.md" \
  -p "Strategist question #09
Reference: internal/pack/parser.go:Parse
Category: invariants
Horizon: Immediate
ROI: 1.00 (impact=0.6, confidence=0.5, effort=0.3)
Question: Can `stepHeaderRegex` accidentally interpret commented lines inside bash code as new steps?
Rationale: Mis-parsing steps can execute unintended commands.
Smallest experiment today: Add a pack example with a bash comment containing `# 01)` and observe parse behavior.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/pack/parser.go"

# 10 — ROI=0.83 impact=0.5 confidence=0.5 effort=0.3 horizon=Immediate category=UX flows reference=internal/cli/run.go:runCmd
oracle \
  --files-report \
  --write-output "oracle-out/10-ux-flows-run-paths.md" \
  -p "Strategist question #10
Reference: internal/cli/run.go:runCmd
Category: UX flows
Horizon: Immediate
ROI: 0.83 (impact=0.5, confidence=0.5, effort=0.3)
Question: Should `statePath` and `reportPath` be resolved relative to the pack file rather than the current working directory?
Rationale: Users expect outputs to live alongside the pack they ran.
Smallest experiment today: Follow `runCmd` path construction and note the directory used for outputs.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/cli/run.go" \
  -f "internal/app/run.go"

# 11 — ROI=0.75 impact=0.6 confidence=0.5 effort=0.4 horizon=Immediate category=contracts/interfaces reference=internal/app/app.go:Config
oracle \
  --files-report \
  --write-output "oracle-out/11-contracts-config-propagation.md" \
  -p "Strategist question #11
Reference: internal/app/app.go:Config
Category: contracts/interfaces
Horizon: Immediate
ROI: 0.75 (impact=0.6, confidence=0.5, effort=0.4)
Question: Is the `Config` contract missing any CLI/runtime inputs that should be propagated to `exec.Runner` (e.g., `oracle-bin`, per-step flags)?
Rationale: Gaps between CLI flags and runtime config lead to surprising behavior.
Smallest experiment today: Trace which CLI flags are copied into `app.Config` and `exec.Runner`.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/app/app.go" \
  -f "internal/cli/run.go"

# 12 — ROI=0.75 impact=0.6 confidence=0.5 effort=0.4 horizon=Immediate category=UX flows reference=internal/tui/tui.go:Model.Update
oracle \
  --files-report \
  --write-output "oracle-out/12-ux-flows-viewdone-keys.md" \
  -p "Strategist question #12
Reference: internal/tui/tui.go:Model.Update
Category: UX flows
Horizon: Immediate
ROI: 0.75 (impact=0.6, confidence=0.5, effort=0.4)
Question: Do `r`, `b`, and `n` key paths leave the model in a consistent state with cleared logs and correct selection?
Rationale: Inconsistent state transitions cause confusing reruns.
Smallest experiment today: Map the `ViewDone` key handling branch in `Model.Update`.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/tui/tui.go"

# 13 — ROI=0.56 impact=0.7 confidence=0.4 effort=0.5 horizon=Strategic category=permissions reference=Unknown
oracle \
  --files-report \
  --write-output "oracle-out/13-permissions-policy-missing.md" \
  -p "Strategist question #13
Reference: Unknown
Category: permissions
Horizon: Strategic
ROI: 0.56 (impact=0.7, confidence=0.4, effort=0.5)
Question: Should running pack steps require an explicit permission/allowlist policy beyond the existing `--yes` flag?
Rationale: No permission artifacts were found (missing `**/policy/**` or `**/permissions/**`), so safety expectations are unclear.
Smallest experiment today: Run `ck --sem \"permission allowlist confirm\" .` to check for policy files.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "README.md" \
  -f "go.mod" \
  -f "cmd/oraclepack/main.go"

# 14 — ROI=0.48 impact=0.6 confidence=0.4 effort=0.5 horizon=Strategic category=caching/state reference=internal/state/persist.go:LoadState
oracle \
  --files-report \
  --write-output "oracle-out/14-caching-state-migrations.md" \
  -p "Strategist question #14
Reference: internal/state/persist.go:LoadState
Category: caching/state
Horizon: Strategic
ROI: 0.48 (impact=0.6, confidence=0.4, effort=0.5)
Question: Do we need a formal state migration path for future schema changes to keep `--resume` reliable?
Rationale: As features evolve, resume compatibility becomes a long-term risk.
Smallest experiment today: Sketch a `SchemaVersion` switch in `LoadState` for forward/backward compatibility.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/state/persist.go" \
  -f "internal/state/types.go"

# 15 — ROI=0.48 impact=0.6 confidence=0.4 effort=0.5 horizon=Strategic category=UX flows reference=internal/tui/tui.go:Model.Init
oracle \
  --files-report \
  --write-output "oracle-out/15-ux-flows-batch-mode.md" \
  -p "Strategist question #15
Reference: internal/tui/tui.go:Model.Init
Category: UX flows
Horizon: Strategic
ROI: 0.48 (impact=0.6, confidence=0.4, effort=0.5)
Question: Should the TUI support a non-interactive batch mode with step selection to scale large packs?
Rationale: As packs grow, batch workflows could become primary usage.
Smallest experiment today: Review how `run-all` is wired between CLI and `Model.Init` to assess feasibility of persisted selections.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/tui/tui.go" \
  -f "internal/cli/run.go"

# 16 — ROI=0.47 impact=0.7 confidence=0.4 effort=0.6 horizon=Strategic category=contracts/interfaces reference=internal/pack/types.go:Pack
oracle \
  --files-report \
  --write-output "oracle-out/16-contracts-pack-version.md" \
  -p "Strategist question #16
Reference: internal/pack/types.go:Pack
Category: contracts/interfaces
Horizon: Strategic
ROI: 0.47 (impact=0.7, confidence=0.4, effort=0.6)
Question: Should the pack format introduce an explicit version/schema field to enable backward-compatible feature evolution?
Rationale: Versioned contracts prevent breaking older packs when new fields are added.
Smallest experiment today: Draft a minimal `Pack.Version` field and list compatibility rules.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/pack/types.go"

# 17 — ROI=0.40 impact=0.6 confidence=0.4 effort=0.6 horizon=Strategic category=observability reference=internal/report/types.go:ReportV1
oracle \
  --files-report \
  --write-output "oracle-out/17-observability-report-telemetry.md" \
  -p "Strategist question #17
Reference: internal/report/types.go:ReportV1
Category: observability
Horizon: Strategic
ROI: 0.40 (impact=0.6, confidence=0.4, effort=0.6)
Question: What additional telemetry fields (e.g., oracle command count, stderr size) would materially improve report usefulness?
Rationale: Strategic decisions need richer, consistent metrics than today’s summary.
Smallest experiment today: List candidate fields and identify which module would populate each.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "internal/report/types.go"

# 18 — ROI=0.30 impact=0.5 confidence=0.3 effort=0.5 horizon=Strategic category=feature flags reference=Unknown
oracle \
  --files-report \
  --write-output "oracle-out/18-feature-flags-missing.md" \
  -p "Strategist question #18
Reference: Unknown
Category: feature flags
Horizon: Strategic
ROI: 0.30 (impact=0.5, confidence=0.3, effort=0.5)
Question: Should experimental features (ROI filter, run-all, overrides) be gated behind a feature-flag mechanism?
Rationale: No feature-flag system was found (missing `**/flags/**`), which limits safe rollout.
Smallest experiment today: Run `ck --sem \"feature flag toggle\" .` and note any flag infrastructure.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "README.md" \
  -f "go.mod" \
  -f "cmd/oraclepack/main.go"

# 19 — ROI=0.24 impact=0.4 confidence=0.3 effort=0.5 horizon=Strategic category=migrations reference=Unknown
oracle \
  --files-report \
  --write-output "oracle-out/19-migrations-missing.md" \
  -p "Strategist question #19
Reference: Unknown
Category: migrations
Horizon: Strategic
ROI: 0.24 (impact=0.4, confidence=0.3, effort=0.5)
Question: Is there a migration strategy for pack/report/state schema changes, or are breaking changes acceptable?
Rationale: No migration artifacts were found (missing `**/migrations/**`), so upgrade expectations are unclear.
Smallest experiment today: Search for `schema_version` or `migration` mentions across the repo to confirm absence.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "README.md" \
  -f "go.mod" \
  -f "cmd/oraclepack/main.go"

# 20 — ROI=0.20 impact=0.4 confidence=0.3 effort=0.6 horizon=Strategic category=background jobs reference=Unknown
oracle \
  --files-report \
  --write-output "oracle-out/20-background-jobs-missing.md" \
  -p "Strategist question #20
Reference: Unknown
Category: background jobs
Horizon: Strategic
ROI: 0.20 (impact=0.4, confidence=0.3, effort=0.6)
Question: Would a background job runner be valuable for long-running validation or preflight tasks?
Rationale: No job/worker artifacts were found (missing `**/jobs/**`), so long-running tasks may block the TUI.
Smallest experiment today: Run `ck --sem \"queue worker cron\" .` to confirm whether any job system exists.
Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–4 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (1 action) — may differ from the suggested one if you justify it
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next." \
  -f "README.md" \
  -f "go.mod" \
  -f "cmd/oraclepack/main.go"
```

---

## coverage check (must be satisfied)

*   contracts/interfaces: OK

*   invariants: OK

*   caching/state: OK

*   background jobs: Missing (no job artifacts found; attach `**/jobs/**` if present)

*   observability: OK

*   permissions: Missing (no policy artifacts found; attach `**/policy/**` or `**/permissions/**` if present)

*   migrations: Missing (no migration artifacts found; attach `**/migrations/**` if present)

*   UX flows: OK

*   failure modes: OK

*   feature flags: Missing (no flag artifacts found; attach `**/flags/**` if present)


--- .taskmaster/tm_help.txt ---

╭─────────────────────╮
│                     │
│   Task Master CLI   │
│                     │
╰─────────────────────╯


╭─────────────────────────────────╮
│  Project Setup & Configuration  │
╰─────────────────────────────────╯
    init                      [--name=<name>]                          Initialize a new project with Task       
                              [--description=<desc>] [-y]              Master structure                         
    models                                                             View current AI model configuration and  
                                                                       available models                         
    models --setup                                                     Run interactive setup to configure AI    
                                                                       models                                   
    models --set-main         <model_id>                               Set the primary model for task           
                                                                       generation                               
    models                    <model_id>                               Set the model for research operations    
    --set-research                                                                                              
    models                    <model_id>                               Set the fallback model (optional)        
    --set-fallback                                                                                              


╭───────────────────╮
│  Task Generation  │
╰───────────────────╯
    parse-prd                 --input=<file.txt> [--num-tasks=10]      Generate tasks from a PRD document       
    generate                                                           Create individual task files from        
                                                                       tasks.json                               


╭───────────────────╮
│  Task Management  │
╰───────────────────╯
    list                      [<status>|all] [--with-subtasks]         List all tasks - use "all" to show with  
                                                                       subtasks                                 
    set-status                <id> <status>                            Update task status (pending, done,       
                                                                       in-progress, review, deferred,           
                                                                       cancelled)                               
    sync-readme               [--with-subtasks]                        Export tasks to README.md with           
                              [--status=<status>]                      professional formatting                  
    update                    --from=<id> --prompt="<context>"         Update multiple tasks based on new       
                                                                       requirements                             
    update-task               <id> <prompt...>                         Update a single task (no quotes needed   
                                                                       for multi-word prompts)                  
    update-subtask            --id=<parentId.subtaskId>                Append additional information to a       
                              --prompt="<context>"                     subtask                                  
    add-task                  --prompt="<text>"                        Add a new task using AI                  
                              [--dependencies=<ids>]                                                            
                              [--priority=<priority>]                                                           
    remove-task               --id=<id> [-y]                           Permanently remove a task or subtask     


╭──────────────────────╮
│  Subtask Management  │
╰──────────────────────╯
    add-subtask               --parent=<id> --title="<title>"          Add a new subtask to a parent task       
                              [--description="<desc>"]                                                          
    add-subtask               --parent=<id> --task-id=<id>             Convert an existing task into a subtask  
    remove-subtask            --id=<parentId.subtaskId>                Remove a subtask (optionally convert to  
                              [--convert]                              standalone task)                         
    clear-subtasks            --id=<id>                                Remove all subtasks from specified tasks 
    clear-subtasks --all                                               Remove subtasks from all tasks           


╭─────────────────────────────╮
│  Task Analysis & Breakdown  │
╰─────────────────────────────╯
    analyze-complexity        [--research] [--threshold=5]             Analyze tasks and generate expansion     
                                                                       recommendations                          
    complexity-report         [--file=<path>]                          Display the complexity analysis report   
    expand                    --id=<id> [--num=5] [--research]         Break down tasks into detailed subtasks  
                              [--prompt="<context>"]                                                            
    expand --all              [--force] [--research]                   Expand all pending tasks with subtasks   
    research                  "<prompt>" [-i=<task_ids>]               Perform AI-powered research queries with 
                              [-f=<file_paths>] [-c="<context>"]       project context                          
                              [--tree] [-s=<save_file>]                                                         
                              [-d=<detail_level>]                                                               


╭─────────────────────────────╮
│  Task Navigation & Viewing  │
╰─────────────────────────────╯
    next                                                               Show the next task to work on based on   
                                                                       dependencies                             
    show                      <id>                                     Display detailed information about a     
                                                                       specific task                            


╭──────────────────╮
│  Tag Management  │
╰──────────────────╯
    tags                      [--show-metadata]                        List all available tags with task counts 
    add-tag                   <tagName> [--copy-from-current]          Create a new tag context for organizing  
                              [--copy-from=<tag>] [-d="<desc>"]        tasks                                    
    use-tag                   <tagName>                                Switch to a different tag context        
    delete-tag                <tagName> [--yes]                        Delete an existing tag and all its tasks 
    rename-tag                <oldName> <newName>                      Rename an existing tag                   
    copy-tag                  <sourceName> <targetName>                Copy an existing tag to create a new tag 
                              [-d="<desc>"]                            with the same tasks                      


╭─────────────────────────╮
│  Dependency Management  │
╰─────────────────────────╯
    add-dependency            --id=<id> --depends-on=<id>              Add a dependency to a task               
    remove-dependency         --id=<id> --depends-on=<id>              Remove a dependency from a task          
    validate-dependenci…                                               Identify invalid dependencies without    
                                                                       fixing them                              
    fix-dependencies                                                   Fix invalid dependencies automatically   


╭─────────────────╮
│  Configuration  │
╰─────────────────╯
    .taskmaster/config.json        AI model configuration file (project root)         Managed by models cmd     
    API Keys (.env)                API keys for AI providers (ANTHROPIC_API_KEY,      Required in .env file     
                                   etc.)                                                                        
    MCP Keys (mcp.json)            API keys for Cursor integration                    Required in .cursor/      


╭────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                        │
│   Quick Start:                                                                         │
│                                                                                        │
│   1. Create Project: task-master init                                                  │
│   2. Setup Models: task-master models --setup                                          │
│   3. Parse PRD: task-master parse-prd --input=<prd-file>                               │
│   4. List Tasks: task-master list                                                      │
│   5. Find Next Task: task-master next                                                  │
│                                                                                        │
╰────────────────────────────────────────────────────────────────────────────────────────╯


--- .taskmaster/tm_tags-help.txt ---
Usage: task-master tags [options] [command]

Manage tags for task organization

Options:
  -h, --help                        display help for command

Commands:
  list [options]                    List all tags with statistics (default
                                    action)
  add [options] <name>              Create a new tag
  use <name>                        Switch to a different tag
  remove [options] <name>           Remove a tag
  rename <oldName> <newName>        Rename a tag
  copy [options] <source> <target>  Copy a tag with all its tasks
