diff --git a/.gitignore b/.gitignore index bb92923..e3075f5 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ __screenshots__/ # Agent runtime artifacts (regenerated per sweep, never source) .doc-keeper-report.md +.opencode/cache/ diff --git a/.opencode/agent/issue-reviewer.md b/.opencode/agent/issue-reviewer.md new file mode 100644 index 0000000..444ed0e --- /dev/null +++ b/.opencode/agent/issue-reviewer.md @@ -0,0 +1,134 @@ +--- +description: Read-only issue triage agent. Loads cached GitHub issues, cross-references codebase via grep, synthesizes prioritization report. Never edits files. +mode: subagent +permission: + edit: deny + bash: allow + webfetch: allow +--- + +# Issue Reviewer — Issue Triage + +Read-only issue triage. Loads cached GitHub issues, cross-references against the codebase via grep, synthesizes a prioritization report. Does NOT edit code or files. + +## Inputs + +- `.opencode/cache/issues-review.json` — produced by `scripts/issues-review.sh`. If missing, run the script first. +- `$ARGUMENTS` (optional) — issue number to deep-dive. Skips triage, focuses on one issue. + +## Process + +### 1. Load cache + +```bash +test -f .opencode/cache/issues-review.json || bash scripts/issues-review.sh +``` + +If cache is stale (>1h) and issue inflow is suspected, force refresh: `bash scripts/issues-review.sh --fresh`. + +### 2. Per-issue grep + +For each open issue, extract 2–5 keywords from title + first paragraph of body. Run grep against: + +- `src/` (frontend) +- `src-tauri/src/` and `src-tauri/crates/*/src/` (Rust backend) +- `scripts/`, `.github/workflows/` (CI/infra) + +Use grep output to ground "Relevant Files" — never infer from project layout knowledge. If grep returns nothing relevant, say so explicitly. + +Skip grep for clearly non-code issues (docs, deps, CI-only). + +### 3. Classify each issue + +For each issue, determine: + +- **Type:** bug / feature / enhancement / question / chore +- **Severity:** + - `critical` — data loss, security, build broken + - `high` — blocks workflow, affects many users, CI red + - `medium` — degrades workflow, workaround exists + - `low` — cosmetic, edge case +- **App area:** editor / admin / connection / schema / ai / grid / ci-deps / docs / other +- **Effort:** + - `small` — <50 LOC, single file + - `medium` — 1–3 files, single component/crate + - `large` — multi-file, design or refactor needed + + Ground effort estimate in grep results (count files to touch, check crate boundaries). If ungrounded, omit rather than guess. + +### 4. Output format + +**Low volume (N ≤ 3):** skip the Summary block. Lead straight into per-issue sections. + +**High volume (N > 3):** include a Summary block first. + +``` +# Issue Triage — + +## Summary + +- N open: X bugs, Y features, Z enhancements +- By severity: W critical, X high, Y medium, Z low +- N issues linked to WIP PRs +- Oldest issue: N days + +## Issues + +### #: + +- **Type / Severity / Area:** bug / high / ci-deps +- **Age:** N days | **Labels:** bug | **Linked PRs:** #N (or "none") +- **What it is:** <1-line summary> + +**Relevant files (verified via grep):** +- `src-tauri/Cargo.toml:42` — <why> + +**Approach:** +<concrete change: which file, what to add/modify. If unsure, say "needs investigation."> + +**Effort:** small/medium/large + +--- +``` + +### 5. Recommend top 3 + +End with prioritized recommendations. One line each: + +``` +## Recommended order + +1. **#N** — <why first>. Unblocks X. +2. **#M** — <why second>. +3. **#K** — <why third>. +``` + +If N ≤ 2, just recommend all of them. + +## Output style + +- Terse. Code paths + line numbers over prose. +- One finding per line where possible. +- No emojis in filenames. Severity emojis (🔴🟠🟡) OK in chat output. +- Skip boilerplate at low N. +- End every report with a one-line "Next action" suggestion (e.g., "Run `/review-prs` to triage open PRs."). + +## Memory (optional, silently skipped if unavailable) + +If cognee MCP is available, store a digest at the end of the report: + +``` +attempt: cognee_remember(data="<YYYY-MM-DD> triage: N open. Top: #X <one-liner>, #Y <one-liner>.") +on failure: skip silently, do not abort +``` + +Detect cognee availability by attempting `cognee_get_client_info_json` once at start. If it errors or times out, skip all `cognee_*` calls for the session. Never block the report on memory. + +## Don't + +- Don't edit any files. Read-only agent. +- Don't infer file paths from project layout — always grep. +- Don't include "effort" without grounding. +- Don't pad output for low-volume backlogs. +- Don't recommend solutions that cross ownership lanes without flagging (frontend impl vs Rust impl vs docs). +- Don't fetch comments for every issue — only deep-dive targets. diff --git a/.opencode/command/reviewissues.md b/.opencode/command/reviewissues.md new file mode 100644 index 0000000..15d5c5e --- /dev/null +++ b/.opencode/command/reviewissues.md @@ -0,0 +1,74 @@ +--- +description: Triage open GitHub issues. Pulls issues + cross-references PRs, greps codebase for relevant files, prioritizes. Read-only — produces a report, not changes. +agent: build +--- + +Triage open GitHub issues for SQLPilot. Pure analysis, no code changes. + +## What this does + +1. Runs `scripts/issues-review.sh` — pulls open issues + open PRs, caches to `.opencode/cache/issues-review.json` +2. Cross-references: which issues have WIP PRs? (`Closes #N` / `Fixes #N` in PR bodies) +3. Loads the cache, embodies `.opencode/agent/issue-reviewer.md` +4. For each issue, greps `src/` + `src-tauri/` to ground relevant files +5. Synthesizes per-issue: type, severity, area, approach, effort +6. Stores digest to memory (if cognee MCP available — optional, silently skipped if not) +7. Returns top 3 prioritized recommendations + +## When to invoke + +- Morning review: check overnight issue inflow +- Before sprint/milestone planning +- "What should we work on?" +- Backlog growing and needs triage +- After a release — verify no new critical bugs landed + +## Invocation + +``` +/reviewissues # full triage of all open issues +/reviewissues 175 # deep-dive on one issue +``` + +Or run directly: + +```bash +bash scripts/issues-review.sh # refresh cache + print path +bash scripts/issues-review.sh --stats # summary stats only +bash scripts/issues-review.sh --fresh # force cache refresh +``` + +Cache is gitignored at `.opencode/cache/`. Re-runs within 1h use the cache. Override TTL with `ISSUES_REVIEW_CACHE_TTL=0` or pass `--fresh`. + +## Pre-flight (script enforces, aborts on fail) + +- `gh` CLI installed + authenticated +- `jq` installed +- Inside a git repo + +## Output + +Terse report. Skips Summary block at low N (≤3). Top 3 recommendations at end. No code changes. + +## Failure modes + +| Symptom | Cause | Fix | +| ---------------------- | -------------------------- | ------------------------------------------------- | +| "gh not authenticated" | gh auth expired | `gh auth login` | +| "jq not installed" | missing dep | `brew install jq` / `apt install jq` | +| Cache missing | first run | script auto-creates | +| cognee MCP absent | optional memory | skipped silently, report still produced | +| Cache stale >1h | long session, fresh issues | pass `--fresh` or set `ISSUES_REVIEW_CACHE_TTL=0` | + +## See also + +- `scripts/issues-review.sh` — data-pull script (deterministic, no LLM) +- `.opencode/agent/issue-reviewer.md` — agent definition (embodied on invocation) +- `/review-prs` — companion: review open PRs +- `.opencode/AGENTS.md` — Self-Dispatch protocol + +## Config + +- `SQLPILOT_REPO_DIR` — repo location (default: pwd) +- `ISSUES_REVIEW_CACHE_TTL` — cache TTL in sec (default: 3600) +- Cache file: `.opencode/cache/issues-review.json` (gitignored) diff --git a/.opencode/ops/cargo-audit-fix-plan.md b/.opencode/ops/cargo-audit-fix-plan.md index 00ab907..b68c39e 100644 --- a/.opencode/ops/cargo-audit-fix-plan.md +++ b/.opencode/ops/cargo-audit-fix-plan.md @@ -30,6 +30,7 @@ Each phase lives on a sub-branch off `fix/204-cargo-audit-advisories` to isolate ## Test Plan After each phase: + - `cargo build --workspace` - `cargo test --workspace` - `cargo clippy --workspace -- -D warnings` diff --git a/README.md b/README.md index 776d0f0..8386453 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ RUST_LOG=debug | Technology | Purpose | | ---------------------------------------------- | ------------------------------------------------------ | -| [Vitest](https://vitest.dev/) | Frontend unit tests (1488 tests) | +| [Vitest](https://vitest.dev/) | Frontend unit tests (1488 tests) | | [cargo test](https://doc.rust-lang.org/cargo/) | Rust integration tests against Docker MySQL (45 tests) | | [Docker](https://www.docker.com/) | MySQL 8, MySQL 5.7, MariaDB 11 test containers | diff --git a/scripts/issues-review.sh b/scripts/issues-review.sh new file mode 100755 index 0000000..9708af7 --- /dev/null +++ b/scripts/issues-review.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +# scripts/issues-review.sh +# +# Pull all open GitHub issues + cross-referenced PRs into a single cache file. +# Feeds the issue-reviewer agent. Pure bash + gh + jq. No LLM calls. +# +# Cache schema (.opencode/cache/issues-review.json): +# { +# "repo": "owner/name", +# "generated_at": "2026-07-19T12:34:56Z", +# "open_issues": [{number, title, body, labels, author, createdAt, updatedAt, comment_count}, ...], +# "open_prs": [{number, title, body, headRefName, author, createdAt}, ...], +# "linked_prs": [{pr, title, refs: [issue_numbers]}, ...], +# "summary": {open_count, by_label, oldest_age_days, linked_to_open_pr} +# } +# +# Usage: +# scripts/issues-review.sh # use cache if <TTL, else refresh +# scripts/issues-review.sh --fresh # force refresh +# scripts/issues-review.sh --stats # print summary only, don't refresh +# +# Exit codes: +# 0 - success (cache fresh or refreshed) +# 1 - pre-flight failure (gh/jq missing, not authed) +# 2 - fetch failed +# 3 - cache write failed + +set -euo pipefail + +REPO_DIR="${SQLPILOT_REPO_DIR:-$(pwd)}" +CACHE_DIR="${REPO_DIR}/.opencode/cache" +CACHE_FILE="${CACHE_DIR}/issues-review.json" +CACHE_TTL_SEC="${ISSUES_REVIEW_CACHE_TTL:-3600}" +LOG_PREFIX="[issues-review]" +LIMIT=500 + +log() { printf '%s %s %s\n' "$LOG_PREFIX" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"; } +err() { printf '%s ERROR: %s\n' "$LOG_PREFIX" "$*" >&2; } +die() { err "$*"; exit "${2:-1}"; } + +usage() { + sed -n '2,20p' "$0" + exit 0 +} + +# --- Parse args --- +FRESH=false +STATS_ONLY=false +for arg in "$@"; do + case "$arg" in + --fresh) FRESH=true ;; + --stats) STATS_ONLY=true ;; + --help|-h) usage ;; + -*) die "Unknown flag: $arg" 1 ;; + *) die "Unexpected arg: $arg" 1 ;; + esac +done + +# --- Pre-flight --- +[ -d "$REPO_DIR/.git" ] || die "Not a git repo: $REPO_DIR" 1 +command -v gh >/dev/null || die "gh CLI not installed" 1 +command -v jq >/dev/null || die "jq not installed (brew install jq / apt install jq)" 1 +gh auth status >/dev/null 2>&1 || die "gh not authenticated (run: gh auth login)" 1 + +mkdir -p "$CACHE_DIR" + +# --- Cache check (unless --fresh) --- +if [ "$FRESH" = false ] && [ -f "$CACHE_FILE" ]; then + MTIME=$(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE") + AGE=$(($(date +%s) - MTIME)) + if [ "$AGE" -lt "$CACHE_TTL_SEC" ]; then + log "Cache fresh (${AGE}s old, TTL ${CACHE_TTL_SEC}s). Use --fresh to force refresh." + if [ "$STATS_ONLY" = true ]; then + jq -r ' + "Open issues: \(.summary.open_count)", + "By label: \(.summary.by_label | tojson)", + "Linked to open PR: \(.summary.linked_to_open_pr)", + "Oldest issue age: \(.summary.oldest_age_days // "n/a") days" + ' "$CACHE_FILE" + else + printf '%s\n' "$CACHE_FILE" + fi + exit 0 + fi + log "Cache stale (${AGE}s > TTL ${CACHE_TTL_SEC}s). Refreshing." +fi + +# --- Fetch --- +log "Fetching open issues + PRs (limit ${LIMIT})..." +REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)" || die "gh repo view failed" 2 + +OPEN_ISSUES_JSON="$(gh issue list --state open --limit "$LIMIT" \ + --json number,title,body,labels,author,createdAt,updatedAt,comments 2>&1)" \ + || die "gh issue list failed" 2 + +OPEN_PRS_JSON="$(gh pr list --state open --limit "$LIMIT" \ + --json number,title,body,headRefName,author,createdAt 2>&1)" \ + || die "gh pr list failed" 2 + +log "Fetched $(echo "$OPEN_ISSUES_JSON" | jq 'length') issues + $(echo "$OPEN_PRS_JSON" | jq 'length') PRs." + +# --- Build linked_prs: for each open PR, extract #N refs from body, filter to open issues --- +LINKED_PRS_JSON="$(echo "$OPEN_PRS_JSON" | jq --argjson issues "$OPEN_ISSUES_JSON" ' + ($issues | map(.number)) as $nums | + map( + . as $pr | + ([((.body // "") | scan("#(\\d+)"; "g")) | .[] | .[0] | tonumber] | unique) as $refs | + ($refs | map(select(. as $n | $nums | index($n))) ) as $matches | + select($matches | length > 0) | + {pr: .number, title: .title, refs: $matches} + ) +')" + +# --- Assemble final cache --- +FINAL_JSON="$(jq -n \ + --arg repo "$REPO" \ + --arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --argjson issues "$OPEN_ISSUES_JSON" \ + --argjson prs "$OPEN_PRS_JSON" \ + --argjson linked "$LINKED_PRS_JSON" \ + ' + { + repo: $repo, + generated_at: $generated_at, + open_issues: $issues, + open_prs: $prs, + linked_prs: $linked, + open_issues: ($issues | map(. + {comment_count: (.comments | length)})), + summary: { + open_count: ($issues | length), + by_label: ( + $issues + | map(.labels[].name) + | group_by(.) + | map({name: .[0], value: length}) + | from_entries + ), + oldest_age_days: ( + if ($issues | length) > 0 + then ((now - ($issues | min_by(.createdAt).createdAt | fromdateiso8601)) / 86400 | floor) + else null + end + ), + linked_to_open_pr: ( + $linked | map(.refs[]) | unique | length + ) + } + } +')" + +# --- Write cache atomically --- +TMP="$(mktemp "${CACHE_DIR}/issues-review.XXXXXX.json")" +trap 'rm -f "$TMP"' EXIT +echo "$FINAL_JSON" > "$TMP" || die "Cache write failed: $TMP" 3 +mv "$TMP" "$CACHE_FILE" || die "Cache mv failed: $CACHE_FILE" 3 +trap - EXIT + +log "Cached → $CACHE_FILE" + +# --- Output --- +if [ "$STATS_ONLY" = true ]; then + jq -r ' + "Open issues: \(.summary.open_count)", + "By label: \(.summary.by_label | tojson)", + "Linked to open PR: \(.summary.linked_to_open_pr)", + "Oldest issue age: \(.summary.oldest_age_days // "n/a") days" + ' "$CACHE_FILE" +else + printf '%s\n' "$CACHE_FILE" +fi +exit 0