Skill: zero2ai-hub/skill-github-daily-ops (version 1.0.1)

--- name: skill-github-daily-ops version: 1.0.0 description: Daily GitHub repo health check + safe Dependabot auto-merge. Outputs markdown report. metadata: openclaw: requires: { bins: ["node"] } --- # skill-github-daily-ops Daily GitHub ops: health report per repo + safe auto-merge of Dependabot PRs (medium/low CVEs only). ## Usage ```bash # Health report for all org repos node scripts/daily-ops.js --org Zero2Ai-hub --report # Auto-merge safe Dependabot PRs node scripts/daily-ops.js --org Zero2Ai-hub --merge-dependabot # Both — specific repos node scripts/daily-ops.js --org Zero2Ai-hub --repos "repo1,repo2" --report --merge-dependabot ``` ## Arguments | Arg | Default | Description | |---|---|---| | `--org` | `$GITHUB_ORG` env | GitHub organization | | `--repos` | all | Comma-separated repo names | | `--report` | false | Output markdown health report | | `--merge-dependabot` | false | Auto-merge safe Dependabot PRs | ## Environment Variables | Var | Description | |---|---| | `GITHUB_TOKEN` | GitHub PAT (or reads from `~/.github_token`) | | `GITHUB_ORG` | Default org | ## Auto-Merge Rules - ✅ Merges: Dependabot PRs where severity is LOW or MEDIUM **and** CI passes - ⛔ Skips: HIGH or CRITICAL CVE PRs (require human review) - ⏳ Skips: PRs with failing or in-progress CI ## Report Output Markdown table per repo with: open PRs, open issues, last commit date, Dependabot PR count. ## Cron Example ```bash # Daily at 08:00 Dubai time (04:00 UTC) 0 4 * * * cd /path/to/workspace && node skills/skill-github-daily-ops/scripts/daily-ops.js --org Zero2Ai-hub --report --merge-dependabot >> /var/log/github-daily-ops.log 2>&1 ```

path: _meta.json
content: name: skill-github-daily-ops
version: 1.0.0
description: Daily GitHub repo health check + safe Dependabot auto-merge. Outputs markdown report.
author: Zero2Ai-hub
license: MIT

path: scripts/auto-merge.sh
content: #!/usr/bin/env bash
# auto-merge.sh — Find open Dependabot PRs and merge if CI is green
# Usage: bash auto-merge.sh [repo1 repo2 ...]

set -euo pipefail

export GH_TOKEN="${GH_TOKEN:-$(cat ~/.github_token 2>/dev/null || echo '')}"
if [[ -z "$GH_TOKEN" ]]; then
  echo "❌ GH_TOKEN not set and ~/.github_token not found" >&2
  exit 1
fi

REPOS=("${@:-Zero2Ai-hub/Jarvis-Ops Zero2Ai-hub/Zeerotoai.com Zero2Ai-hub/openclaw-skills}")
if [[ $# -eq 0 ]]; then
  REPOS=(
    "Zero2Ai-hub/Jarvis-Ops"
    "Zero2Ai-hub/Zeerotoai.com"
    "Zero2Ai-hub/openclaw-skills"
  )
fi

MERGED=()
SKIPPED=()
FAILED=()

check_and_merge() {
  local repo="$1"
  local pr_number="$2"
  local pr_title="$3"
  local head_sha="$4"

  echo "  🔍 Checking CI for PR #${pr_number}: ${pr_title}"

  # Get check runs for the head SHA
  local checks
  checks=$(gh api "repos/${repo}/commits/${head_sha}/check-runs" \
    --jq '.check_runs[] | {name: .name, status: .status, conclusion: .conclusion}' 2>/dev/null || echo "")

  if [[ -z "$checks" ]]; then
    echo "  ⚠️  No CI checks found for PR #${pr_number} — skipping"
    SKIPPED+=("${repo}#${pr_number} (no CI checks)")
    return
  fi

  # Check if all completed checks have passed
  local total incomplete failed_count
  total=$(echo "$checks" | grep -c '"status"' || true)
  incomplete=$(echo "$checks" | grep '"status": "in_progress"\|"status": "queued"' | wc -l || echo 0)
  failed_count=$(echo "$checks" | grep '"conclusion": "failure"\|"conclusion": "cancelled"\|"conclusion": "timed_out"' | wc -l || echo 0)

  if [[ "$incomplete" -gt 0 ]]; then
    echo "  ⏳ PR #${pr_number} — CI still running (${incomplete} pending) — skipping"
    SKIPPED+=("${repo}#${pr_number} (CI pending)")
    return
  fi

  if [[ "$failed_count" -gt 0 ]]; then
    echo "  ❌ PR #${pr_number} — CI failed (${failed_count} failure(s)) — skipping"
    SKIPPED+=("${repo}#${pr_number} (CI failed)")
    return
  fi

  # All checks passed — merge
  echo "  ✅ CI green — merging PR #${pr_number}..."
  if gh pr merge "${pr_number}" --repo "${repo}" --squash --auto 2>/dev/null || \
     gh pr merge "${pr_number}" --repo "${repo}" --merge 2>/dev/null; then
    echo "  🎉 Merged PR #${pr_number}: ${pr_title}"
    MERGED+=("${repo}#${pr_number}: ${pr_title}")
  else
    echo "  ⚠️  Could not merge PR #${pr_number} (may need approval or branch protection)"
    FAILED+=("${repo}#${pr_number}: ${pr_title}")
  fi
}

for REPO in "${REPOS[@]}"; do
  echo ""
  echo "🔎 Checking Dependabot PRs in ${REPO}..."

  # Fetch open Dependabot PRs
  prs_json=$(gh pr list \
    --repo "${REPO}" \
    --author "app/dependabot" \
    --state open \
    --json number,title,headRefOid,headRefName \
    2>/dev/null || echo "[]")

  pr_count=$(echo "$prs_json" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)

  if [[ "$pr_count" -eq 0 ]]; then
    echo "  ✓ No open Dependabot PRs"
    continue
  fi

  echo "  Found ${pr_count} Dependabot PR(s)"

  # Process each PR
  echo "$prs_json" | python3 -c "
import sys, json
prs = json.load(sys.stdin)
for pr in prs:
    print(f\"{pr['number']}|{pr['title']}|{pr['headRefOid']}\")
" | while IFS='|' read -r number title sha; do
    check_and_merge "${REPO}" "${number}" "${title}" "${sha}"
  done
done

echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📊 Auto-merge Summary"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Merged: ${#MERGED[@]}"
for m in "${MERGED[@]:-}"; do [[ -n "$m" ]] && echo "   • $m"; done
echo "⏭️  Skipped: ${#SKIPPED[@]}"
for s in "${SKIPPED[@]:-}"; do [[ -n "$s" ]] && echo "   • $s"; done
echo "⚠️  Could not merge: ${#FAILED[@]}"
for f in "${FAILED[@]:-}"; do [[ -n "$f" ]] && echo "   • $f"; done

# Export results for daily-ops.sh to consume
export AUTOMERGE_MERGED="${MERGED[*]:-}"
export AUTOMERGE_SKIPPED="${SKIPPED[*]:-}"
export AUTOMERGE_FAILED="${FAILED[*]:-}"


path: scripts/daily-ops.js
content: #!/usr/bin/env node
/**
 * daily-ops.js — GitHub Daily Health Check + Dependabot Auto-Merge
 *
 * Usage:
 *   node daily-ops.js --org Zero2Ai-hub --report
 *   node daily-ops.js --org Zero2Ai-hub --merge-dependabot
 *   node daily-ops.js --repos "repo1,repo2" --report --merge-dependabot
 *
 * Env vars:
 *   GITHUB_TOKEN  — GitHub PAT (or reads from ~/.github_token)
 *   GITHUB_ORG    — default org if --org not passed
 */

const https = require('https');
const fs = require('fs');
const os = require('os');
const path = require('path');

// ── Config ───────────────────────────────────────────────────────────────────
function getToken() {
  if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
  const tokenFile = path.join(os.homedir(), '.github_token');
  if (fs.existsSync(tokenFile)) return fs.readFileSync(tokenFile, 'utf8').trim();
  throw new Error('No GitHub token. Set GITHUB_TOKEN env or create ~/.github_token');
}

function parseArgs() {
  const args = process.argv.slice(2);
  const opts = { repos: null, mergeDependabot: false, report: false };
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--org') opts.org = args[++i];
    else if (args[i] === '--repos') opts.repos = args[++i].split(',').map(r => r.trim());
    else if (args[i] === '--merge-dependabot') opts.mergeDependabot = true;
    else if (args[i] === '--report') opts.report = true;
  }
  opts.org = opts.org || process.env.GITHUB_ORG;
  return opts;
}

// ── GitHub API ────────────────────────────────────────────────────────────────
function ghRequest(token, path, method = 'GET', body = null) {
  return new Promise((resolve, reject) => {
    const options = {
      hostname: 'api.github.com',
      path,
      method,
      headers: {
        Authorization: `Bearer ${token}`,
        Accept: 'application/vnd.github+json',
        'X-GitHub-Api-Version': '2022-11-28',
        'User-Agent': 'jarvis-daily-ops/1.0',
        'Content-Type': 'application/json',
      },
    };
    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', c => data += c);
      res.on('end', () => {
        try {
          resolve({ status: res.statusCode, body: data ? JSON.parse(data) : {} });
        } catch {
          resolve({ status: res.statusCode, body: data });
        }
      });
    });
    req.on('error', reject);
    if (body) req.write(JSON.stringify(body));
    req.end();
  });
}

async function listRepos(token, org) {
  const all = [];
  let page = 1;
  while (true) {
    const { body } = await ghRequest(token, `/orgs/${org}/repos?per_page=100&page=${page}`);
    if (!Array.isArray(body) || body.length === 0) break;
    all.push(...body.map(r => r.name));
    page++;
  }
  return all;
}

async function getRepoInfo(token, org, repo) {
  const [prs, issues, commits, alerts] = await Promise.all([
    ghRequest(token, `/repos/${org}/${repo}/pulls?state=open&per_page=100`),
    ghRequest(token, `/repos/${org}/${repo}/issues?state=open&per_page=100&filter=all`),
    ghRequest(token, `/repos/${org}/${repo}/commits?per_page=1`),
    ghRequest(token, `/repos/${org}/${repo}/vulnerability-alerts`).catch(() => ({ status: 404 })),
  ]);

  const openPRs = Array.isArray(prs.body) ? prs.body : [];
  const openIssues = Array.isArray(issues.body)
    ? issues.body.filter(i => !i.pull_request)
    : [];
  const lastCommit = Array.isArray(commits.body) && commits.body[0]
    ? commits.body[0].commit?.committer?.date || 'unknown'
    : 'unknown';

  const dependabotPRs = openPRs.filter(pr =>
    pr.user?.login === 'dependabot[bot]' || pr.user?.login === 'dependabot'
  );

  return { openPRs, openIssues, lastCommit, dependabotPRs };
}

async function getPRChecks(token, org, repo, pr) {
  const ref = pr.head?.sha;
  if (!ref) return { passing: false };
  const { body } = await ghRequest(token, `/repos/${org}/${repo}/commits/${ref}/check-runs`);
  const runs = body?.check_runs || [];
  if (runs.length === 0) return { passing: true }; // No CI = assume ok
  const passing = runs.every(r => r.conclusion === 'success' || r.conclusion === 'skipped' || r.status === 'in_progress');
  return { passing };
}

function getSeverityFromTitle(title) {
  const low = /low/i.test(title);
  const medium = /moderate|medium/i.test(title);
  const high = /high|critical/i.test(title);
  if (high) return 'HIGH';
  if (medium) return 'MEDIUM';
  if (low) return 'LOW';
  return 'UNKNOWN';
}

async function mergePR(token, org, repo, pr) {
  const { status } = await ghRequest(token,
    `/repos/${org}/${repo}/pulls/${pr.number}/merge`,
    'PUT',
    { merge_method: 'squash', commit_title: `chore: ${pr.title} (auto-merge)` }
  );
  return status === 200;
}

// ── Main ─────────────────────────────────────────────────────────────────────
async function main() {
  const opts = parseArgs();
  const [REDACTED_SECRET]);

  if (!opts.org && !opts.repos) {
    console.error('❌ Provide --org or --repos');
    process.exit(1);
  }

  let repos = opts.repos;
  if (!repos) {
    console.log(`📋 Fetching repos for org: ${opts.org} ...`);
    repos = await listRepos(token, opts.org);
    console.log(`   Found ${repos.length} repos\n`);
  }

  const org = opts.org || 'unknown';
  const results = [];
  let totalMerged = 0, totalSkipped = 0;

  for (const repo of repos) {
    process.stdout.write(`🔍 ${repo} ... `);
    try {
      const info = await getRepoInfo(token, org, repo);
      process.stdout.write(`✅\n`);
      results.push({ repo, ...info });

      if (opts.mergeDependabot && info.dependabotPRs.length > 0) {
        for (const pr of info.dependabotPRs) {
          const severity = getSeverityFromTitle(pr.title);
          if (severity === 'HIGH') {
            console.log(`  ⛔ Skip HIGH CVE PR #${pr.number}: ${pr.title} (human review required)`);
            totalSkipped++;
            continue;
          }
          const { passing } = await getPRChecks(token, org, repo, pr);
          if (!passing) {
            console.log(`  ⏳ Skip PR #${pr.number}: CI not passing yet`);
            totalSkipped++;
            continue;
          }
          console.log(`  🔀 Merging PR #${pr.number}: ${pr.title}`);
          const merged = await mergePR(token, org, repo, pr);
          if (merged) {
            console.log(`     ✅ Merged`);
            totalMerged++;
          } else {
            console.log(`     ❌ Merge failed`);
            totalSkipped++;
          }
        }
      }
    } catch (e) {
      process.stdout.write(`❌ ${e.message}\n`);
      results.push({ repo, error: e.message });
    }
  }

  // ── Report ─────────────────────────────────────────────────────────────────
  if (opts.report) {
    const now = new Date().toISOString().slice(0, 10);
    let md = `# GitHub Daily Ops Report — ${now}\n\n`;
    md += `**Org:** ${org} | **Repos:** ${repos.length}\n\n`;
    md += `| Repo | Open PRs | Open Issues | Last Commit | Dependabot PRs |\n`;
    md += `|---|---|---|---|---|\n`;

    for (const r of results) {
      if (r.error) {
        md += `| ${r.repo} | ❌ error | — | — | — |\n`;
      } else {
        const lastCommit = r.lastCommit?.slice(0, 10) || '—';
        md += `| ${r.repo} | ${r.openPRs.length} | ${r.openIssues.length} | ${lastCommit} | ${r.dependabotPRs.length} |\n`;
      }
    }

    if (opts.mergeDependabot) {
      md += `\n## Dependabot Auto-Merge\n`;
      md += `- Merged: ${totalMerged}\n`;
      md += `- Skipped (HIGH/CI): ${totalSkipped}\n`;
    }

    console.log('\n' + md);
  }

  if (opts.mergeDependabot) {
    console.log(`\n✅ Dependabot: merged ${totalMerged}, skipped ${totalSkipped}`);
  }
}

main().catch(err => {
  console.error(`FATAL: ${err.message}`);
  process.exit(1);
});


path: scripts/daily-ops.sh
content: #!/usr/bin/env bash
# daily-ops.sh — GitHub daily health check for Zero2Ai-hub
# Runs: Dependabot auto-merge, CI failure check, open PR/issue review, workspace push
# Output: /tmp/github-daily-ops-report.md

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPORT="/tmp/github-daily-ops-report.md"
WORKSPACE="${WORKSPACE:-$HOME/.openclaw/workspace}"
TODAY=$(date -u +"%Y-%m-%d %H:%M UTC")

REPOS=(
  "Zero2Ai-hub/Jarvis-Ops"
  "Zero2Ai-hub/Zeerotoai.com"
  "Zero2Ai-hub/openclaw-skills"
)

# ── Auth ──────────────────────────────────────────────────────────────────────
export GH_TOKEN="${GH_TOKEN:-$(cat ~/.github_token 2>/dev/null || echo '')}"
if [[ -z "$GH_TOKEN" ]]; then
  echo "❌ GH_TOKEN not set and ~/.github_token not found" >&2
  exit 1
fi

echo "⚡ Jarvis GitHub Daily Ops — ${TODAY}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

# ── Report init ───────────────────────────────────────────────────────────────
cat > "$REPORT" <<EOF
# ⚡ GitHub Daily Ops Report
**Generated:** ${TODAY}
**Repos:** ${REPOS[*]}

---
EOF

# ── Section helpers ───────────────────────────────────────────────────────────
report_section() {
  echo "" >> "$REPORT"
  echo "## $1" >> "$REPORT"
}

report_line() {
  echo "$1" >> "$REPORT"
}

# ── 1. Dependabot Auto-merge ──────────────────────────────────────────────────
echo ""
echo "🤖 STEP 1: Dependabot Auto-merge"
echo "─────────────────────────────────"
report_section "🤖 Dependabot Auto-merge"

MERGED_PRS=()
SKIPPED_PRS=()
FAILED_MERGE=()

for REPO in "${REPOS[@]}"; do
  echo "  📦 ${REPO}"
  report_line ""
  report_line "### ${REPO}"

  prs_json=$(gh pr list \
    --repo "${REPO}" \
    --author "app/dependabot" \
    --state open \
    --json number,title,headRefOid,url \
    2>/dev/null || echo "[]")

  pr_count=$(echo "$prs_json" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)

  if [[ "$pr_count" -eq 0 ]]; then
    echo "  ✓ No open Dependabot PRs"
    report_line "✓ No open Dependabot PRs"
    continue
  fi

  echo "  Found ${pr_count} Dependabot PR(s)"

  while IFS='|' read -r number title sha url; do
    echo "  🔍 PR #${number}: ${title}"

    # Get check runs
    checks_json=$(gh api "repos/${REPO}/commits/${sha}/check-runs" 2>/dev/null || echo '{"check_runs":[]}')

    total=$(echo "$checks_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('check_runs',[])))")
    pending=$(echo "$checks_json" | python3 -c "
import sys,json
d=json.load(sys.stdin)
print(sum(1 for r in d.get('check_runs',[]) if r['status'] in ('in_progress','queued')))
")
    failed=$(echo "$checks_json" | python3 -c "
import sys,json
d=json.load(sys.stdin)
print(sum(1 for r in d.get('check_runs',[]) if r.get('conclusion') in ('failure','cancelled','timed_out')))
")

    if [[ "$total" -eq 0 ]]; then
      echo "    ⚠️  No CI checks — skipping"
      report_line "- ⚠️ PR #${number} [${title}](${url}) — no CI checks, skipped"
      SKIPPED_PRS+=("${REPO}#${number}")
    elif [[ "$pending" -gt 0 ]]; then
      echo "    ⏳ CI pending (${pending}/${total}) — skipping"
      report_line "- ⏳ PR #${number} [${title}](${url}) — CI pending (${pending}/${total})"
      SKIPPED_PRS+=("${REPO}#${number}")
    elif [[ "$failed" -gt 0 ]]; then
      echo "    ❌ CI failed (${failed} failure(s)) — skipping"
      report_line "- ❌ PR #${number} [${title}](${url}) — CI failed, skipped"
      SKIPPED_PRS+=("${REPO}#${number}")
    else
      echo "    ✅ CI green — merging..."
      if gh pr merge "${number}" --repo "${REPO}" --squash --delete-branch 2>/dev/null; then
        echo "    🎉 Merged!"
        report_line "- ✅ PR #${number} [${title}](${url}) — **MERGED** 🎉"
        MERGED_PRS+=("${REPO}#${number}: ${title}")
      else
        echo "    ⚠️  Merge failed (branch protection?)"
        report_line "- ⚠️ PR #${number} [${title}](${url}) — merge failed (branch protection?)"
        FAILED_MERGE+=("${REPO}#${number}: ${title}")
      fi
    fi
  done < <(echo "$prs_json" | python3 -c "
import sys, json
for pr in json.load(sys.stdin):
    print(f\"{pr['number']}|{pr['title']}|{pr['headRefOid']}|{pr['url']}\")
")
done

# ── 2. Failed CI Runs ─────────────────────────────────────────────────────────
echo ""
echo "🚨 STEP 2: Failed CI Runs (last 24h)"
echo "─────────────────────────────────────"
report_section "🚨 Failed CI Runs (last 24h)"

FAILED_CI=()

for REPO in "${REPOS[@]}"; do
  echo "  📦 ${REPO}"
  report_line ""
  report_line "### ${REPO}"

  # Get workflow runs that failed in last 24h
  since=$(date -u -d '24 hours ago' +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v-24H +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")

  failed_runs=$(gh run list \
    --repo "${REPO}" \
    --status failure \
    --limit 10 \
    --json databaseId,name,headBranch,createdAt,url \
    2>/dev/null || echo "[]")

  run_count=$(echo "$failed_runs" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)

  if [[ "$run_count" -eq 0 ]]; then
    echo "  ✓ No failed runs"
    report_line "✓ No failed CI runs"
  else
    echo "  ⚠️  ${run_count} failed run(s):"
    echo "$failed_runs" | python3 -c "
import sys, json
for r in json.load(sys.stdin):
    print(f\"    ❌ [{r['name']}] branch: {r['headBranch']} — {r['url']}\")
" | while read -r line; do
      echo "$line"
      report_line "- $line"
      FAILED_CI+=("$line")
    done
  fi
done

# ── 3. Open Issues & PRs needing review ───────────────────────────────────────
echo ""
echo "📋 STEP 3: Open Issues & PRs Needing Review"
echo "─────────────────────────────────────────────"
report_section "📋 Open Issues & PRs Needing Review"

for REPO in "${REPOS[@]}"; do
  echo "  📦 ${REPO}"
  report_line ""
  report_line "### ${REPO}"

  # Open PRs (non-Dependabot)
  open_prs=$(gh pr list \
    --repo "${REPO}" \
    --state open \
    --json number,title,author,url,reviewDecision,isDraft \
    2>/dev/null || echo "[]")

  human_prs=$(echo "$open_prs" | python3 -c "
import sys, json
prs = [p for p in json.load(sys.stdin) if p['author']['login'] != 'dependabot[bot]' and not p.get('isDraft')]
for p in prs:
    decision = p.get('reviewDecision') or 'NEEDS_REVIEW'
    print(f\"PR #{p['number']}: {p['title']} [{decision}] — {p['url']}\")
print(f'__COUNT__{len(prs)}')
" 2>/dev/null || echo "__COUNT__0")

  pr_count=$(echo "$human_prs" | grep '__COUNT__' | sed 's/__COUNT__//')
  pr_lines=$(echo "$human_prs" | grep -v '__COUNT__')

  if [[ "$pr_count" -gt 0 ]]; then
    echo "  📝 ${pr_count} open PR(s):"
    while IFS= read -r line; do
      [[ -z "$line" ]] && continue
      echo "    • ${line}"
      report_line "- 📝 ${line}"
    done <<< "$pr_lines"
  else
    report_line "✓ No open human PRs"
  fi

  # Open Issues
  open_issues=$(gh issue list \
    --repo "${REPO}" \
    --state open \
    --limit 10 \
    --json number,title,author,url,labels \
    2>/dev/null || echo "[]")

  issue_count=$(echo "$open_issues" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)

  if [[ "$issue_count" -gt 0 ]]; then
    echo "  🐛 ${issue_count} open issue(s):"
    echo "$open_issues" | python3 -c "
import sys, json
for i in json.load(sys.stdin):
    labels = ', '.join(l['name'] for l in i.get('labels', []))
    label_str = f' [{labels}]' if labels else ''
    print(f\"    • Issue #{i['number']}: {i['title']}{label_str} — {i['url']}\")
" | while IFS= read -r line; do
      echo "$line"
      report_line "- 🐛 ${line}"
    done
  else
    report_line "✓ No open issues"
  fi

  if [[ "$pr_count" -eq 0 ]] && [[ "$issue_count" -eq 0 ]]; then
    echo "  ✓ All clear"
  fi
done

# ── 4. Push workspace commits ─────────────────────────────────────────────────
echo ""
echo "📤 STEP 4: Push Workspace Commits"
echo "──────────────────────────────────"
report_section "📤 Workspace Push"

if [[ -d "${WORKSPACE}/.git" ]]; then
  cd "${WORKSPACE}"

  # Check for unpushed commits
  unpushed=$(git log @{u}..HEAD --oneline 2>/dev/null || echo "")
  uncommitted=$(git status --porcelain 2>/dev/null || echo "")

  if [[ -n "$uncommitted" ]]; then
    echo "  ⚠️  Uncommitted changes in workspace (not auto-committing)"
    report_line "⚠️ Uncommitted changes present — manual commit required"
    git status --short
  fi

  if [[ -n "$unpushed" ]]; then
    commit_count=$(echo "$unpushed" | wc -l)
    echo "  📤 Pushing ${commit_count} unpushed commit(s)..."
    echo "$unpushed" | while read -r line; do echo "    • $line"; done

    if git push 2>&1; then
      echo "  ✅ Push successful"
      report_line "✅ Pushed ${commit_count} commit(s):"
      echo "$unpushed" | while IFS= read -r line; do
        report_line "- \`${line}\`"
      done
    else
      echo "  ❌ Push failed"
      report_line "❌ Push failed — check remote/auth"
    fi
  else
    echo "  ✓ Workspace is up to date"
    report_line "✓ No unpushed commits"
  fi
else
  echo "  ⚠️  Workspace is not a git repo"
  report_line "⚠️ Workspace at ${WORKSPACE} is not a git repo"
fi

# ── 5. Summary ────────────────────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📊 DAILY OPS SUMMARY"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Dependabot PRs merged : ${#MERGED_PRS[@]}"
echo "⏭️  PRs skipped           : ${#SKIPPED_PRS[@]}"
echo "⚠️  Merge failures        : ${#FAILED_MERGE[@]}"
echo "🚨 Failed CI runs        : ${#FAILED_CI[@]}"
echo ""
echo "📄 Full report: ${REPORT}"

report_section "📊 Summary"
report_line ""
report_line "| Metric | Count |"
report_line "|--------|-------|"
report_line "| Dependabot PRs merged | ${#MERGED_PRS[@]} |"
report_line "| PRs skipped (CI not ready) | ${#SKIPPED_PRS[@]} |"
report_line "| Merge failures | ${#FAILED_MERGE[@]} |"
report_line "| Failed CI runs | ${#FAILED_CI[@]} |"
report_line ""
report_line "---"
report_line "*Generated by skill-github-daily-ops · Jarvis ⚡*"

echo ""
cat "$REPORT"


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)