From a37799a29cf93b66fbe71c1cd8870fabe897e94e Mon Sep 17 00:00:00 2001 From: Taylor Lucero Date: Tue, 28 Apr 2026 13:51:28 +0200 Subject: [PATCH 1/4] add command --- .github/workflows/nathan-commit-gate.yml | 115 ----------- .github/workflows/nathan-gate.yml | 216 +++++++++++++++++++++ .github/workflows/trigger-n8n-workflow.yml | 14 +- 3 files changed, 225 insertions(+), 120 deletions(-) delete mode 100644 .github/workflows/nathan-commit-gate.yml create mode 100644 .github/workflows/nathan-gate.yml diff --git a/.github/workflows/nathan-commit-gate.yml b/.github/workflows/nathan-commit-gate.yml deleted file mode 100644 index 2f631d05..00000000 --- a/.github/workflows/nathan-commit-gate.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Nathan Commit Gate - -on: - pull_request: - types: [synchronize] - -permissions: - contents: read - pull-requests: read - actions: write - -jobs: - dispatch-nathan: - # Do not allow fork PRs to auto-dispatch the secret-bearing workflow. - if: ${{ github.event.pull_request != null && github.event.pull_request.head.repo.fork == false }} - runs-on: ubuntu-latest - steps: - - name: Validate dispatcher permissions - id: perm - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - ACTOR: ${{ github.actor }} - shell: bash - run: | - set -euo pipefail - - permission="$(gh api -H "Accept: application/vnd.github+json" \ - "repos/${REPO}/collaborators/${ACTOR}/permission" \ - --jq '.permission // ""' 2>/dev/null || true)" - - case "${permission}" in - admin|maintain|write) - echo "allowed=true" >> "$GITHUB_OUTPUT" - echo "✅ ${ACTOR} has ${permission} permission." - ;; - *) - echo "allowed=false" >> "$GITHUB_OUTPUT" - echo "⏭️ ${ACTOR} does not have sufficient permission (permission='${permission:-none}'); skipping dispatch." - ;; - esac - - - name: Check latest commit message (+Nathan) - id: gate - if: ${{ steps.perm.outputs.allowed == 'true' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - shell: bash - run: | - set -euo pipefail - - commits_json="$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/commits" | jq -s 'add')" - sha="$(printf '%s' "$commits_json" | jq -r '.[-1].sha // ""')" - msg="$(printf '%s' "$commits_json" | jq -r '.[-1].commit.message // ""')" - - shopt -s nocasematch - if [[ "$msg" == *"+nathan"* ]]; then - echo "triggered=true" >> "$GITHUB_OUTPUT" - else - echo "triggered=false" >> "$GITHUB_OUTPUT" - fi - shopt -u nocasematch - - first_line="$(printf '%s\n' "$msg" | head -n1 | tr -d '\r')" - echo "Latest PR commit: ${sha:-unknown} — ${first_line:-}" - - - name: Dispatch Nathan workflow - if: ${{ steps.perm.outputs.allowed == 'true' && steps.gate.outputs.triggered == 'true' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - HEAD_REF: ${{ github.event.pull_request.head.ref }} - DISPATCH_REF: ${{ github.event.repository.default_branch }} - shell: bash - run: | - set -euo pipefail - - if [[ -z "${HEAD_REF:-}" ]]; then - echo "Missing PR head ref; cannot dispatch." - exit 1 - fi - if [[ -z "${DISPATCH_REF:-}" ]]; then - echo "Missing repository default branch; cannot dispatch." - exit 1 - fi - - echo "Dispatching trigger-n8n-workflow.yml for PR #${PR_NUMBER} (workflow ref=${DISPATCH_REF}, PR ref=${HEAD_REF})" - dispatch_output="" - if dispatch_output="$( - gh api -X POST "repos/${REPO}/actions/workflows/trigger-n8n-workflow.yml/dispatches" \ - -f ref="${DISPATCH_REF}" \ - -f inputs[pr_number]="${PR_NUMBER}" 2>&1 - )"; then - exit 0 - fi - - rc=$? - echo "$dispatch_output" - if [[ "$dispatch_output" == *"Failed to run workflow dispatch"* && "$dispatch_output" == *"\"status\":500"* ]]; then - echo "Transient GitHub 500 while dispatching workflow; retrying once..." - sleep 2 - if dispatch_output="$( - gh api -X POST "repos/${REPO}/actions/workflows/trigger-n8n-workflow.yml/dispatches" \ - -f ref="${DISPATCH_REF}" \ - -f inputs[pr_number]="${PR_NUMBER}" 2>&1 - )"; then - exit 0 - fi - echo "$dispatch_output" - fi - - exit $rc diff --git a/.github/workflows/nathan-gate.yml b/.github/workflows/nathan-gate.yml new file mode 100644 index 00000000..e64e49c1 --- /dev/null +++ b/.github/workflows/nathan-gate.yml @@ -0,0 +1,216 @@ +name: Nathan Gate + +on: + pull_request: + types: [synchronize] + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: read + actions: write + +jobs: + dispatch-nathan: + # For pull_request events: skip fork PRs to avoid exposing secrets in fork-controlled code. + # For issue_comment events: only proceed if the comment is on an open PR (not a plain issue). + # issue_comment always runs in the base-repo context regardless of fork status, so + # the maintainer permission check is the primary security gate for that event type. + if: ${{ (github.event_name == 'pull_request' && github.event.pull_request != null && github.event.pull_request.head.repo.fork == false) || (github.event_name == 'issue_comment' && github.event.issue.pull_request != null && github.event.issue.state == 'open') }} + runs-on: ubuntu-latest + # One active run per PR at a time. If a run is in progress and a new trigger arrives, + # the new one queues rather than stacking — at most 1 running + 1 pending per PR. + concurrency: + group: nathan-pr-${{ github.event.issue.number || github.event.pull_request.number }} + cancel-in-progress: false + steps: + - name: Validate dispatcher permissions (maintainers only) + id: perm + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + shell: bash + run: | + set -euo pipefail + + permission="$(gh api -H "Accept: application/vnd.github+json" \ + "repos/${REPO}/collaborators/${ACTOR}/permission" \ + --jq '.permission // ""' 2>/dev/null || true)" + + case "${permission}" in + admin|maintain|write) + echo "allowed=true" >> "$GITHUB_OUTPUT" + echo "permission=${permission}" >> "$GITHUB_OUTPUT" + echo "✅ ${ACTOR} has ${permission} permission." + ;; + *) + echo "allowed=false" >> "$GITHUB_OUTPUT" + echo "permission=${permission}" >> "$GITHUB_OUTPUT" + echo "⏭️ ${ACTOR} is not a maintainer (permission='${permission:-none}'); skipping dispatch." + ;; + esac + + - name: Cooldown check — prevent rapid re-triggers + id: cooldown + if: ${{ steps.perm.outputs.allowed == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event_name == 'issue_comment' && github.event.issue.number || github.event.pull_request.number }} + CURRENT_COMMENT_ID: ${{ github.event.comment.id }} + COOLDOWN_MINUTES: '2' + ACTOR_PERMISSION: ${{ steps.perm.outputs.permission }} + shell: bash + run: | + set -euo pipefail + + case "${ACTOR_PERMISSION:-}" in + admin|maintain|write) + echo "blocked=false" >> "$GITHUB_OUTPUT" + echo "✅ ${ACTOR_PERMISSION} permission; skipping cooldown." + exit 0 + ;; + esac + + if [[ -z "${PR_NUMBER:-}" || ! "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then + echo "blocked=false" >> "$GITHUB_OUTPUT" + echo "⚠️ No valid PR number for cooldown check; skipping." + exit 0 + fi + + cutoff="$(date -u -d "${COOLDOWN_MINUTES} minutes ago" +%Y-%m-%dT%H:%M:%SZ)" + + # Fetch only comments created within the cooldown window (since= is server-side filtering). + # Exclude the current triggering comment so a fresh +Nathan is never self-blocked. + recent="$(gh api \ + "repos/${REPO}/issues/${PR_NUMBER}/comments?since=${cutoff}&per_page=100" \ + --jq "[.[] | select(.body | ascii_downcase | contains(\"+nathan\")) | select(.id | tostring != \"${CURRENT_COMMENT_ID:-}\")] | length" \ + 2>/dev/null || echo "0")" + + if [[ "${recent}" -gt 0 ]]; then + echo "blocked=true" >> "$GITHUB_OUTPUT" + echo "⏭️ Nathan was already triggered within the last ${COOLDOWN_MINUTES} minutes; skipping to prevent duplicate reviews." + else + echo "blocked=false" >> "$GITHUB_OUTPUT" + echo "✅ No recent Nathan trigger found; proceeding." + fi + + - name: Check latest commit message (+Nathan) — commit push trigger + id: gate_commit + if: ${{ steps.perm.outputs.allowed == 'true' && steps.cooldown.outputs.blocked != 'true' && github.event_name == 'pull_request' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + shell: bash + run: | + set -euo pipefail + + commits_json="$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/commits" | jq -s 'add')" + sha="$(printf '%s' "$commits_json" | jq -r '.[-1].sha // ""')" + msg="$(printf '%s' "$commits_json" | jq -r '.[-1].commit.message // ""')" + + shopt -s nocasematch + if [[ "$msg" == *"+nathan"* ]]; then + echo "triggered=true" >> "$GITHUB_OUTPUT" + else + echo "triggered=false" >> "$GITHUB_OUTPUT" + fi + shopt -u nocasematch + + first_line="$(printf '%s\n' "$msg" | head -n1 | tr -d '\r')" + echo "Latest PR commit: ${sha:-unknown} — ${first_line:-}" + + - name: Check PR comment body (+Nathan) — comment trigger + id: gate_comment + if: ${{ steps.perm.outputs.allowed == 'true' && steps.cooldown.outputs.blocked != 'true' && github.event_name == 'issue_comment' }} + env: + COMMENT_BODY: ${{ github.event.comment.body }} + COMMENT_ID: ${{ github.event.comment.id }} + ACTOR: ${{ github.actor }} + shell: bash + run: | + set -euo pipefail + + body="$(printf '%s' "${COMMENT_BODY:-}")" + + # Reject oversized comment bodies to prevent resource exhaustion + if [[ ${#body} -gt 65536 ]]; then + echo "⏭️ Comment body exceeds size limit; skipping." + echo "triggered=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Check only for the trigger phrase — comment content is never executed + shopt -s nocasematch + if [[ "$body" == *"+nathan"* ]]; then + echo "triggered=true" >> "$GITHUB_OUTPUT" + echo "✅ Found +Nathan trigger phrase in comment #${COMMENT_ID} by ${ACTOR}." + else + echo "triggered=false" >> "$GITHUB_OUTPUT" + echo "⏭️ Comment #${COMMENT_ID} does not contain '+Nathan'; skipping dispatch." + fi + shopt -u nocasematch + + - name: Dispatch Nathan workflow + if: ${{ steps.perm.outputs.allowed == 'true' && steps.cooldown.outputs.blocked != 'true' && (steps.gate_commit.outputs.triggered == 'true' || steps.gate_comment.outputs.triggered == 'true') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event_name == 'issue_comment' && github.event.issue.number || github.event.pull_request.number }} + HEAD_REF: ${{ github.event_name == 'issue_comment' && '' || github.event.pull_request.head.ref }} + DISPATCH_REF: ${{ github.event.repository.default_branch }} + EVENT_NAME: ${{ github.event_name }} + shell: bash + run: | + set -euo pipefail + + if [[ -z "${PR_NUMBER:-}" ]]; then + echo "Missing PR number; cannot dispatch." + exit 1 + fi + if [[ ! "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then + echo "Invalid PR number '${PR_NUMBER}'; cannot dispatch." + exit 1 + fi + if [[ -z "${DISPATCH_REF:-}" ]]; then + echo "Missing repository default branch; cannot dispatch." + exit 1 + fi + + REF_INFO="${HEAD_REF:-}" + TRIGGER_SOURCE="commit" + if [[ "${EVENT_NAME}" == "issue_comment" ]]; then + TRIGGER_SOURCE="comment" + fi + echo "Dispatching trigger-n8n-workflow.yml for PR #${PR_NUMBER} (event=${EVENT_NAME:-unknown}, trigger_source=${TRIGGER_SOURCE}, workflow ref=${DISPATCH_REF}, PR ref=${REF_INFO})" + + dispatch_output="" + if dispatch_output="$( + gh api -X POST "repos/${REPO}/actions/workflows/trigger-n8n-workflow.yml/dispatches" \ + -f ref="${DISPATCH_REF}" \ + -f inputs[pr_number]="${PR_NUMBER}" \ + -f inputs[trigger_source]="${TRIGGER_SOURCE}" 2>&1 + )"; then + exit 0 + fi + + rc=$? + echo "$dispatch_output" + if [[ "$dispatch_output" == *"Failed to run workflow dispatch"* && "$dispatch_output" == *"\"status\":500"* ]]; then + echo "Transient GitHub 500 while dispatching workflow; retrying once..." + sleep 2 + if dispatch_output="$( + gh api -X POST "repos/${REPO}/actions/workflows/trigger-n8n-workflow.yml/dispatches" \ + -f ref="${DISPATCH_REF}" \ + -f inputs[pr_number]="${PR_NUMBER}" \ + -f inputs[trigger_source]="${TRIGGER_SOURCE}" 2>&1 + )"; then + exit 0 + fi + echo "$dispatch_output" + fi + + exit $rc diff --git a/.github/workflows/trigger-n8n-workflow.yml b/.github/workflows/trigger-n8n-workflow.yml index bd4c8a6b..840603f6 100644 --- a/.github/workflows/trigger-n8n-workflow.yml +++ b/.github/workflows/trigger-n8n-workflow.yml @@ -4,13 +4,17 @@ on: workflow_dispatch: inputs: pr_number: - description: PR number to process + description: PR number to process (maintainer-triggered runs) required: false type: string pr_selector: description: PR URL or branch name (used when pr_number is not provided) required: false type: string + trigger_source: + description: Source of the trigger — 'comment' or 'commit' (set by Nathan Gate; leave blank for manual runs) + required: false + type: string permissions: contents: read @@ -47,7 +51,7 @@ jobs: PR_NUMBER: ${{ github.event.inputs.pr_number }} PR_SELECTOR: ${{ github.event.inputs.pr_selector }} IS_MANUAL_RUN: ${{ github.event_name == 'workflow_dispatch' && github.actor != 'github-actions[bot]' }} - N8N_ROSE_LANGUAGES: ${{ vars.N8N_ROSE_LANGUAGES }} + TRIGGER_SOURCE: ${{ github.event.inputs.trigger_source }} steps: - name: Validate dispatcher permissions if: ${{ github.event_name == 'workflow_dispatch' && github.actor != 'github-actions[bot]' }} @@ -67,7 +71,7 @@ jobs: echo "✅ ${ACTOR} has ${PERMISSION} permission." ;; *) - echo "❌ ${ACTOR} does not have sufficient permission (permission='${PERMISSION}')." + echo "❌ ${ACTOR} is not a maintainer (permission='${PERMISSION}')." exit 1 ;; esac @@ -165,7 +169,7 @@ jobs: triggered="false" fi shopt -u nocasematch - if [[ "${IS_MANUAL_RUN}" == "true" ]]; then + if [[ "${IS_MANUAL_RUN}" == "true" || "${TRIGGER_SOURCE:-}" == "comment" ]]; then triggered="true" fi if [[ "$triggered" == "true" ]]; then @@ -359,7 +363,7 @@ jobs: fi if [[ ${IS_MANUAL_RUN} == true ]]; then - echo "ℹ️ run for PR #${PR_NUMBER}" + echo "ℹ️ Maintainer-triggered run for PR #${PR_NUMBER}" fi - name: Fetch PR metadata From 1a69012c444e69ff6798e5578d847ba214581644 Mon Sep 17 00:00:00 2001 From: Taylor Lucero Date: Wed, 29 Apr 2026 08:23:14 +0200 Subject: [PATCH 2/4] mv skill --- .claude/skills/nathan /SKILL.md | 106 ++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .claude/skills/nathan /SKILL.md diff --git a/.claude/skills/nathan /SKILL.md b/.claude/skills/nathan /SKILL.md new file mode 100644 index 00000000..a90e6c45 --- /dev/null +++ b/.claude/skills/nathan /SKILL.md @@ -0,0 +1,106 @@ +--- +name: nathan +description: Triggers the Nathan AI review workflow by posting "+Nathan" on the open GitHub PR for the current branch. Works for both direct contributors and fork contributors. Requires an open PR on the current branch and the GitHub CLI (gh) to be authenticated with write access or higher on the upstream repository. +chain-role: isolated +invocation: user +allowed-tools: Bash(git branch:*) Bash(gh repo view:*) Bash(gh api:*) Bash(gh pr list:*) Bash(gh pr comment:*) +--- + +# Nathan + +Trigger the Nathan AI review by posting `+Nathan` on the open GitHub PR for the current branch. Works transparently whether you are working on the main repo or a fork. + +## Instructions + +### Step 1: Resolve current branch +Run `git branch --show-current`. + +If output is empty (detached HEAD), stop: "Cannot determine the current branch — the repository is in detached HEAD state. Check out a branch first." + +### Step 2: Detect fork and resolve target repo +Run: +``` +gh repo view --json isFork,parent +``` + +- If `isFork` is `false`: the target repo is the current repo. Use `` as the head filter. +- If `isFork` is `true`: the target repo is `parent.nameWithOwner` (the upstream). Get the authenticated user's login to build the head filter: + ``` + gh api user --jq '.login' + ``` + Use `:` as the head filter and `` as the repo. + +### Step 3: Find open PR + +**Not a fork:** +``` +gh pr list --head "" --state open --json number,title,url --limit 1 +``` + +**Fork:** +``` +gh pr list --repo "" --head ":" --state open --json number,title,url --limit 1 +``` + +- If result is `[]`, stop: "No open PR found for branch ''. Please open a PR on GitHub before triggering a review." +- If command fails, report the error and stop. + +Parse `number`, `title`, and `url` from the result. + +### Step 4: Post +Nathan comment +Use the PR URL so the comment always targets the correct repo regardless of fork status: +```bash +gh pr comment "" --body '+Nathan' +``` + +### Step 5: Confirm +``` +✅ Nathan review triggered on PR # () +<url> +The Nathan Gate workflow will begin shortly. +``` + +## Examples + +### Example 1: Direct contributor (not a fork) +User says: `/Nathan` +Actions: +1. Gets branch → `feature/add-docs` +2. Repo is not a fork +3. Finds open PR → #23 "Add documentation" +4. Posts `+Nathan` +Result: "✅ Nathan review triggered on PR #23 (Add documentation)\nhttps://github.com/..." + +### Example 2: Fork contributor +User says: `/Nathan` +Actions: +1. Gets branch → `fix/typo` +2. Repo is a fork of `upstream-org/repo`; user login is `contributor` +3. Searches upstream for PR with head `contributor:fix/typo` → #47 "Fix typo in README" +4. Posts `+Nathan` using the PR URL +Result: "✅ Nathan review triggered on PR #47 (Fix typo in README)\nhttps://github.com/upstream-org/repo/pull/47" + +### Example 3: No open PR (edge case) +User says: `/Nathan` +Actions: +1. Gets branch → `old-branch` +2. No open PR found +Result: "No open PR found for branch 'old-branch'. Please open a PR on GitHub before triggering a review." + +## Troubleshooting + +### Error: `gh: command not found` +Cause: GitHub CLI not installed or not in PATH. +Solution: Install from https://cli.github.com and run `gh auth login`. + +### Error: HTTP 401 / auth failure +Cause: GitHub CLI is not authenticated. +Solution: Run `gh auth login` and follow the prompts. + +### Error: No open PR found +Cause: PR does not exist for this branch, or is closed/merged. +Solution: Open a PR on GitHub first, or switch to a branch with an active PR. + +### Error: Nathan review does not start +Cause: Your account does not have write access or higher on the upstream repository. +Solution: Verify your permissions — the Nathan Gate workflow requires at least write access to dispatch. From 1f3c539fa0a8a0382c4f7d1060cfe0d0636ef8b5 Mon Sep 17 00:00:00 2001 From: Taylor Lucero <Telucero@users.noreply.github.com> Date: Thu, 30 Apr 2026 08:28:32 +0200 Subject: [PATCH 3/4] resolve feedback --- .github/workflows/nathan-gate.yml | 9 --------- .github/workflows/trigger-n8n-workflow.yml | 1 + 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/nathan-gate.yml b/.github/workflows/nathan-gate.yml index e64e49c1..5f657c67 100644 --- a/.github/workflows/nathan-gate.yml +++ b/.github/workflows/nathan-gate.yml @@ -61,19 +61,10 @@ jobs: PR_NUMBER: ${{ github.event_name == 'issue_comment' && github.event.issue.number || github.event.pull_request.number }} CURRENT_COMMENT_ID: ${{ github.event.comment.id }} COOLDOWN_MINUTES: '2' - ACTOR_PERMISSION: ${{ steps.perm.outputs.permission }} shell: bash run: | set -euo pipefail - case "${ACTOR_PERMISSION:-}" in - admin|maintain|write) - echo "blocked=false" >> "$GITHUB_OUTPUT" - echo "✅ ${ACTOR_PERMISSION} permission; skipping cooldown." - exit 0 - ;; - esac - if [[ -z "${PR_NUMBER:-}" || ! "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then echo "blocked=false" >> "$GITHUB_OUTPUT" echo "⚠️ No valid PR number for cooldown check; skipping." diff --git a/.github/workflows/trigger-n8n-workflow.yml b/.github/workflows/trigger-n8n-workflow.yml index 840603f6..af92d6f0 100644 --- a/.github/workflows/trigger-n8n-workflow.yml +++ b/.github/workflows/trigger-n8n-workflow.yml @@ -52,6 +52,7 @@ jobs: PR_SELECTOR: ${{ github.event.inputs.pr_selector }} IS_MANUAL_RUN: ${{ github.event_name == 'workflow_dispatch' && github.actor != 'github-actions[bot]' }} TRIGGER_SOURCE: ${{ github.event.inputs.trigger_source }} + N8N_ROSE_LANGUAGES: ${{ vars.N8N_ROSE_LANGUAGES }} steps: - name: Validate dispatcher permissions if: ${{ github.event_name == 'workflow_dispatch' && github.actor != 'github-actions[bot]' }} From 98306d1e29630593bbebddb520b9c5f410f255c4 Mon Sep 17 00:00:00 2001 From: Taylor Lucero <Telucero@users.noreply.github.com> Date: Thu, 30 Apr 2026 08:29:15 +0200 Subject: [PATCH 4/4] feedback --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..061ecb8d45d324ae6b78f2dc52431fa0fc56ed60 100644 GIT binary patch delta 209 zcmZoMXfc=|&e%RNQEZ}~q96+c0|O%ig8)NLx?yl~es00UMD2QzAPa*YLpnnyLkUa* zD8|4yWuGImTyDOLOHxjL5>SkTv#p$C`o-gD@+o-b3o;;<g3UfD0HT0kW5!>$$p#`U jo4Gl7I2hYECVpq0%rBxS$OM#A0OAB727}FxB8Qm)=*upQ delta 71 zcmZoMXfc=|&Zs&uQEZ}~A_oHyFfuR*Y!+nv#x_}jao1*c4gn5ERUrR6^JIPzMM0n} SLjp({5P;YXn;k_CGXnr0ybZko