postmerge Skill
<!-- postmerge-harness-protocol:start -->
Harness adapter (Claude vs Grok Build)
Invoke via Claude: soleur:postmerge <PR> | Grok: /postmerge <PR>.
Polling CI / health checks without asking the operator:
- Claude Code: Monitor tool for Phase 2 main CI and Phase 3 health retries.
- Grok Build: AwaitShell with
pattern(completed success,postmerge verification complete) or Shell withblock_until_ms.
Canonical: plugins/soleur/lib/harness.ts → pollInstructions(). Parent skills (ship Step 3.8, one-shot Step 8) MUST invoke postmerge before <promise>DONE</promise>. <!-- postmerge-harness-protocol:end -->
Purpose: Enforce post-merge verification so bugs that only appear in production context are caught immediately after merge -- not days later. This closes the "last mile" gap where QA passes locally but production diverges (stale files, unapplied migrations, CSP violations from injected scripts).
CRITICAL: No command substitution. Never use $() in Bash commands. When a step says "get value X, then use it in command Y", run them as two separate Bash tool calls -- first get the value, then use it literally in the next call.
Arguments
$ARGUMENTS should contain the PR number. If omitted, detect from the most recently merged PR on the current branch.
Phase 1: Verify PR is Merged
Confirm the PR reached MERGED state:
gh pr view <number> --json state,mergeCommit,headRefName --jq '{state, mergeCommit: .mergeCommit.oid, branch: .headRefName}'
If state is not MERGED, stop:
STOPPED: PR #<number> is not merged (state: <state>). Run /soleur:merge-pr first.
Record the merge commit SHA for later verification.
Phase 2: Wait for CI on Main
Check the latest CI run on main triggered by the merge:
gh run list --branch main --limit 3 --json databaseId,status,conclusion,headSha
Find the run matching the merge commit SHA. If no matching run yet, poll with the harness adapter (max 5 minutes) — Claude: Monitor tool; Grok: AwaitShell/Shell per pollInstructions(). Do NOT ask the operator to watch CI. Do NOT use Bash run_in_background:
for i in $(seq 1 20); do
result=$(gh run view <run-id> --json status,conclusion --jq '{status, conclusion}')
echo "$(date +%H:%M:%S) $result"
echo "$result" | grep -q '"completed"' && break
sleep 15
done
React to the final status from the Monitor output.
If CI passes: Proceed to Phase 3.
If CI fails: Report the failure with details:
gh run view <run-id> --log-failed 2>&1 | tail -50
Stop:
STOPPED: CI failed on main after merge.
Run ID: <run-id>
Conclusion: <conclusion>
Failed log tail:
<last 50 lines>
Investigate before proceeding. The merge is complete but production may not deploy.
Phase 3: Verify Production Deployment
Check if a health endpoint is configured. Look for deployment URLs in environment or project config:
# Check for DEPLOY_URL or PRODUCTION_URL in environment
echo "${DEPLOY_URL:-not_set}"
echo "${PRODUCTION_URL:-not_set}"
If a production URL is available, verify the deployment:
curl -sf --max-time 10 "<production-url>/health" | jq .
Use /health (the public, middleware-/CSP-bypassed health route returning {"status":"ok","version","build_sha","supabase","sentry",...}), NOT /api/health — the latter is an authenticated API route that 307-redirects an unauthenticated probe to /login, so curl -sf fails and HEALTH_VERIFIED is left false even when production is healthy. The build_sha field also confirms the merge commit is the live build.
If health check succeeds: Record the response, set HEALTH_VERIFIED=true, and proceed.
If health check fails or no URL configured: set HEALTH_VERIFIED=false, warn, and proceed (not all PRs trigger deployments):
WARNING: No production health check available. Skipping deployment verification.
HEALTH_VERIFIED is the explicit signal Phase 3.8 gates the feature-tweet draft on — a ship tweet must only be drafted for a feature confirmed live. Track it as a literal true/false; do NOT infer "verified" from "reached this line" (the warn-and-proceed branch also falls through to the next phase).
Phase 3.5: Sentry Cron Monitor Health
Verify scheduled functions are healthy post-deploy by querying Sentry cron monitors.
Prerequisites: SENTRY_AUTH_TOKEN (or SENTRY_API_TOKEN fallback) must be available. If missing, warn and skip:
WARNING: SENTRY_AUTH_TOKEN not set. Skipping Sentry health verification.
Query cron monitors:
SENTRY_TOKEN=$(doppler secrets get SENTRY_AUTH_TOKEN -p soleur -c prd --plain 2>/dev/null || \
doppler secrets get SENTRY_API_TOKEN -p soleur -c prd --plain)
SENTRY_ORG=$(doppler secrets get SENTRY_ORG -p soleur -c prd --plain 2>/dev/null || echo "jikigai")
API_HOST="${SENTRY_ORG}.sentry.io"
curl -sfS -H "Authorization: Bearer ${SENTRY_TOKEN}" \
"https://${API_HOST}/api/0/organizations/${SENTRY_ORG}/monitors/?per_page=100" \
| jq '[.[] | {slug: .slug, status: .status}] | map(select(.status != "ok" and .status != "active"))'
- If all monitors report
okoractive: "Sentry cron monitors: all healthy" - If any monitor reports
errorormissed: flag with monitor name and status. This is a WARNING, not a blocker — the monitor may have been unhealthy before this deploy. - If Sentry API is unreachable or returns non-200: warn and skip (do not block on Sentry outages).
Graceful degradation: This check is advisory. A Sentry API failure does not block the postmerge pipeline.
Inngest liveness awareness (#6374)
Surface any OPEN inngest-down alarm so the operator is never told "prod is healthy" while the durable trigger layer (armed reminders + all server/inngest/functions/ crons) is dark. The external watchdog (scheduled-inngest-health.yml) files [ci/inngest-down] on a confirmed down; this is a cheap read that needs no Sentry token:
gh issue list --label ci/inngest-down --state open \
--json number,title,createdAt --jq '.[] | "#\(.number) \(.title) (opened \(.createdAt))"'
- If it prints an open issue: surface a one-line advisory — "ADVISORY: inngest reports down (
#<n>) — armed reminders and scheduled crons may not be firing; the external watchdog is auto-restarting / escalating. Do not assume scheduled work ran." Include it prominently in the Phase 7 report. - If empty: silent (no line needed).
Advisory only — never hard-block the turn. inngest-down does not block all work; the operator retains agency. (An optional deeper probe — curl the /hooks/inngest-liveness HMAC+CF-Access hook — is available for a live verdict but is not required here; the open-issue read is the cheap default.)
Phase 3.6: Sentry Error-Count Delta (Fix Efficacy)
A merged-and-deployed fix can pass every gate above and still not work — the deploy is healthy, monitors are alive, files are fresh, but the error keeps firing because the fix addressed the wrong root cause (the KB-sync / oauth-probe failure class). Phase 3.5 proves the monitor is alive; this phase asks the harder question: did the error this PR claims to fix actually stop?
Run only when the PR body or linked issue names a specific Sentry issue (a *.sentry.io/issues/<id> URL, a SENTRY-<SHORTID>, or a Closes #N whose issue references one). If no Sentry issue is identified, skip silently — there is no error to measure.
Prerequisites: same SENTRY_AUTH_TOKEN resolution as Phase 3.5 for the aggregate Discover count. The single-issue GET below, however, requires the write-scoped SENTRY_ISSUE_RW_TOKEN — the /organizations/<org>/issues/<id>/ endpoint returns 403 on the read-only SENTRY_AUTH_TOKEN/SENTRY_API_TOKEN (they carry Discover/ingest scope, not event:read on the issue resource). Using the read token here makes the curl -sfS GET exit non-zero, leaving `ISSUE_J