fix: Replace Codeberg dependency with local Forgejo instance (#611)

- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
  creates admin + bot users (dev-bot, review-bot), generates API
  tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
  FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
  FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
  FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
  →forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
  FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-23 16:57:12 +00:00
parent 39d30faf45
commit a66bd91721
58 changed files with 863 additions and 628 deletions

View file

@ -6,10 +6,10 @@ sourced as needed.
| File | What it provides | Sourced by |
|---|---|---|
| `lib/env.sh` | Loads `.env`, sets `FACTORY_ROOT`, exports project config (`CODEBERG_REPO`, `PROJECT_NAME`, etc.), defines `log()`, `codeberg_api()`, `codeberg_api_all()` (accepts optional second TOKEN parameter, defaults to `$CODEBERG_TOKEN`), `woodpecker_api()`, `wpdb()`, `matrix_send()`, `matrix_send_ctx()`. Auto-loads project TOML if `PROJECT_TOML` is set. | Every agent |
| `lib/env.sh` | Loads `.env`, sets `FACTORY_ROOT`, exports project config (`FORGE_REPO`, `PROJECT_NAME`, etc.), defines `log()`, `forge_api()`, `forge_api_all()` (accepts optional second TOKEN parameter, defaults to `$FORGE_TOKEN`), `woodpecker_api()`, `wpdb()`, `matrix_send()`, `matrix_send_ctx()`. Auto-loads project TOML if `PROJECT_TOML` is set. | Every agent |
| `lib/ci-helpers.sh` | `ci_passed()` — returns 0 if CI state is "success" (or no CI configured). `ci_required_for_pr()` — returns 0 if PR has code files (CI required), 1 if non-code only (CI not required). `is_infra_step()` — returns 0 if a single CI step failure matches infra heuristics (clone/git exit 128, any exit 137, log timeout patterns). `classify_pipeline_failure()` — returns "infra \<reason>" if any failed Woodpecker step matches infra heuristics via `is_infra_step()`, else "code". `ensure_priority_label()` — looks up (or creates) the `priority` label and returns its ID; caches in `_PRIORITY_LABEL_ID`. | dev-poll, review-poll, review-pr, supervisor-poll |
| `lib/ci-debug.sh` | CLI tool for Woodpecker CI: `list`, `status`, `logs`, `failures` subcommands. Not sourced — run directly. | Humans / dev-agent (tool access) |
| `lib/load-project.sh` | Parses a `projects/*.toml` file into env vars (`PROJECT_NAME`, `CODEBERG_REPO`, `WOODPECKER_REPO_ID`, monitoring toggles, Matrix config, etc.). | env.sh (when `PROJECT_TOML` is set), supervisor-poll (per-project iteration) |
| `lib/load-project.sh` | Parses a `projects/*.toml` file into env vars (`PROJECT_NAME`, `FORGE_REPO`, `WOODPECKER_REPO_ID`, monitoring toggles, Matrix config, etc.). | env.sh (when `PROJECT_TOML` is set), supervisor-poll (per-project iteration) |
| `lib/parse-deps.sh` | Extracts dependency issue numbers from an issue body (stdin → stdout, one number per line). Matches `## Dependencies` / `## Depends on` / `## Blocked by` sections and inline `depends on #N` / `blocked by #N` patterns. Inline scan skips fenced code blocks to prevent false positives from code examples in issue bodies. Not sourced — executed via `bash lib/parse-deps.sh`. | dev-poll, supervisor-poll |
| `lib/matrix_listener.sh` | Long-poll Matrix sync daemon. Dispatches thread replies to the correct agent via tmux session injection (dev, action, vault, review) or well-known files (`/tmp/{agent}-escalation-reply` for supervisor/gardener). Handles all agent reply routing. Run as systemd service. | Standalone daemon |
| `lib/formula-session.sh` | `acquire_cron_lock()`, `check_memory()`, `load_formula()`, `build_context_block()`, `consume_escalation_reply()`, `start_formula_session()`, `formula_phase_callback()`, `build_prompt_footer()`, `run_formula_and_monitor(AGENT [TIMEOUT] [CALLBACK])` — shared helpers for formula-driven cron agents (lock, memory guard, formula loading, prompt assembly, tmux session, monitor loop, crash recovery). `formula_phase_callback()` handles `PHASE:escalate` (unified escalation path — kills the session; callers may follow up via Matrix). `run_formula_and_monitor` accepts an optional CALLBACK (default: `formula_phase_callback`) so callers can install custom merge-through or escalation handlers. | planner-run.sh, predictor-run.sh, gardener-run.sh, supervisor-run.sh, dev-agent.sh, action-agent.sh |

View file

@ -13,7 +13,7 @@ set -euo pipefail
source "$(dirname "$0")/../lib/env.sh"
# WOODPECKER_TOKEN loaded from .env via env.sh
REPO="${CODEBERG_REPO}"
REPO="${FORGE_REPO}"
API="${WOODPECKER_SERVER}/api/repos/${WOODPECKER_REPO_ID}"
api() {

View file

@ -8,19 +8,19 @@ set -euo pipefail
# ensure_blocked_label_id — look up (or create) the "blocked" label, print its ID.
# Caches the result in _BLOCKED_LABEL_ID to avoid repeated API calls.
# Requires: CODEBERG_TOKEN, CODEBERG_API (from env.sh), codeberg_api()
# Requires: FORGE_TOKEN, FORGE_API (from env.sh), forge_api()
ensure_blocked_label_id() {
if [ -n "${_BLOCKED_LABEL_ID:-}" ]; then
printf '%s' "$_BLOCKED_LABEL_ID"
return 0
fi
_BLOCKED_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
_BLOCKED_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
| jq -r '.[] | select(.name == "blocked") | .id' 2>/dev/null || true)
if [ -z "$_BLOCKED_LABEL_ID" ]; then
_BLOCKED_LABEL_ID=$(curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${CODEBERG_API}/labels" \
"${FORGE_API}/labels" \
-d '{"name":"blocked","color":"#e11d48"}' 2>/dev/null \
| jq -r '.id // empty' 2>/dev/null || true)
fi
@ -29,19 +29,19 @@ ensure_blocked_label_id() {
# ensure_priority_label — look up (or create) the "priority" label, print its ID.
# Caches the result in _PRIORITY_LABEL_ID to avoid repeated API calls.
# Requires: CODEBERG_TOKEN, CODEBERG_API (from env.sh), codeberg_api()
# Requires: FORGE_TOKEN, FORGE_API (from env.sh), forge_api()
ensure_priority_label() {
if [ -n "${_PRIORITY_LABEL_ID:-}" ]; then
printf '%s' "$_PRIORITY_LABEL_ID"
return 0
fi
_PRIORITY_LABEL_ID=$(codeberg_api GET "/labels" 2>/dev/null \
_PRIORITY_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
| jq -r '.[] | select(.name == "priority") | .id' 2>/dev/null || true)
if [ -z "$_PRIORITY_LABEL_ID" ]; then
_PRIORITY_LABEL_ID=$(curl -sf -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${CODEBERG_API}/labels" \
"${FORGE_API}/labels" \
-d '{"name":"priority","color":"#f59e0b"}' 2>/dev/null \
| jq -r '.id // empty' 2>/dev/null || true)
fi
@ -68,7 +68,7 @@ diff_has_code_files() {
ci_required_for_pr() {
local pr_num="$1"
local files all_json
all_json=$(codeberg_api_all "/pulls/${pr_num}/files") || return 0
all_json=$(forge_api_all "/pulls/${pr_num}/files") || return 0
files=$(printf '%s' "$all_json" | jq -r '.[].filename' 2>/dev/null) || return 0
if [ -z "$files" ]; then
return 0 # empty file list — require CI as safety default
@ -113,7 +113,7 @@ ci_failed() {
is_infra_step() {
local sname="$1" ecode="$2" log_data="${3:-}"
# Clone/git step exit 128 → Codeberg connection failure / rate limit
# Clone/git step exit 128 → forge connection failure / rate limit
if { [[ "$sname" == *clone* ]] || [[ "$sname" == git* ]]; } && [ "$ecode" = "128" ]; then
echo "${sname} exit 128 (connection failure)"
return 0

View file

@ -24,17 +24,33 @@ if [ -n "${PROJECT_TOML:-}" ] && [ -f "$PROJECT_TOML" ]; then
source "${FACTORY_ROOT}/lib/load-project.sh" "$PROJECT_TOML"
fi
# Codeberg token: env var > ~/.netrc
if [ -z "${CODEBERG_TOKEN:-}" ]; then
CODEBERG_TOKEN="$(awk '/codeberg.org/{getline;getline;print $2}' ~/.netrc 2>/dev/null || true)"
# Forge token: new FORGE_TOKEN > legacy CODEBERG_TOKEN > ~/.netrc
if [ -z "${FORGE_TOKEN:-}" ]; then
FORGE_TOKEN="${CODEBERG_TOKEN:-}"
fi
export CODEBERG_TOKEN
if [ -z "${FORGE_TOKEN:-}" ]; then
FORGE_TOKEN="$(awk '/codeberg.org/{getline;getline;print $2}' ~/.netrc 2>/dev/null || true)"
fi
export FORGE_TOKEN
export CODEBERG_TOKEN="${FORGE_TOKEN}" # backwards compat
# Project config
export CODEBERG_REPO="${CODEBERG_REPO:-}"
export CODEBERG_API="${CODEBERG_API:-https://codeberg.org/api/v1/repos/${CODEBERG_REPO}}"
export CODEBERG_WEB="https://codeberg.org/${CODEBERG_REPO}"
export PROJECT_NAME="${PROJECT_NAME:-${CODEBERG_REPO##*/}}"
# Review bot token: FORGE_REVIEW_TOKEN > legacy REVIEW_BOT_TOKEN
export FORGE_REVIEW_TOKEN="${FORGE_REVIEW_TOKEN:-${REVIEW_BOT_TOKEN:-}}"
export REVIEW_BOT_TOKEN="${FORGE_REVIEW_TOKEN}" # backwards compat
# Bot usernames filter: FORGE_BOT_USERNAMES > legacy CODEBERG_BOT_USERNAMES
export FORGE_BOT_USERNAMES="${FORGE_BOT_USERNAMES:-${CODEBERG_BOT_USERNAMES:-}}"
export CODEBERG_BOT_USERNAMES="${FORGE_BOT_USERNAMES}" # backwards compat
# Project config (FORGE_* preferred, CODEBERG_* fallback)
export FORGE_REPO="${FORGE_REPO:-${CODEBERG_REPO:-}}"
export CODEBERG_REPO="${FORGE_REPO}" # backwards compat
export FORGE_URL="${FORGE_URL:-http://localhost:3000}"
export FORGE_API="${FORGE_API:-${FORGE_URL}/api/v1/repos/${FORGE_REPO}}"
export FORGE_WEB="${FORGE_WEB:-${FORGE_URL}/${FORGE_REPO}}"
export CODEBERG_API="${FORGE_API}" # backwards compat
export CODEBERG_WEB="${FORGE_WEB}" # backwards compat
export PROJECT_NAME="${PROJECT_NAME:-${FORGE_REPO##*/}}"
export PROJECT_REPO_ROOT="${PROJECT_REPO_ROOT:-/home/${USER}/${PROJECT_NAME}}"
export PRIMARY_BRANCH="${PRIMARY_BRANCH:-master}"
export WOODPECKER_REPO_ID="${WOODPECKER_REPO_ID:-}"
@ -46,23 +62,25 @@ log() {
printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*"
}
# Codeberg API helper — usage: codeberg_api GET /issues?state=open
codeberg_api() {
# Forge API helper — usage: forge_api GET /issues?state=open
forge_api() {
local method="$1" path="$2"
shift 2
curl -sf -X "$method" \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Authorization: token ${FORGE_TOKEN}" \
-H "Content-Type: application/json" \
"${CODEBERG_API}${path}" "$@"
"${FORGE_API}${path}" "$@"
}
# Backwards-compat alias
codeberg_api() { forge_api "$@"; }
# Paginate a Codeberg API GET endpoint and return all items as a merged JSON array.
# Usage: codeberg_api_all /path (no existing query params)
# codeberg_api_all /path?a=b (with existing params — appends &limit=50&page=N)
# codeberg_api_all /path TOKEN (optional second arg: token; defaults to $CODEBERG_TOKEN)
codeberg_api_all() {
# Paginate a Forge API GET endpoint and return all items as a merged JSON array.
# Usage: forge_api_all /path (no existing query params)
# forge_api_all /path?a=b (with existing params — appends &limit=50&page=N)
# forge_api_all /path TOKEN (optional second arg: token; defaults to $FORGE_TOKEN)
forge_api_all() {
local path_prefix="$1"
local CODEBERG_TOKEN="${2:-${CODEBERG_TOKEN}}"
local FORGE_TOKEN="${2:-${FORGE_TOKEN}}"
local sep page page_items count all_items="[]"
case "$path_prefix" in
*"?"*) sep="&" ;;
@ -70,7 +88,7 @@ codeberg_api_all() {
esac
page=1
while true; do
page_items=$(codeberg_api GET "${path_prefix}${sep}limit=50&page=${page}")
page_items=$(forge_api GET "${path_prefix}${sep}limit=50&page=${page}")
count=$(printf '%s' "$page_items" | jq 'length')
[ "$count" -eq 0 ] && break
all_items=$(printf '%s\n%s' "$all_items" "$page_items" | jq -s 'add')
@ -79,6 +97,8 @@ codeberg_api_all() {
done
printf '%s' "$all_items"
}
# Backwards-compat alias
codeberg_api_all() { forge_api_all "$@"; }
# Woodpecker API helper
woodpecker_api() {

View file

@ -2,7 +2,7 @@
# file-action-issue.sh — File an action issue for a formula run
#
# Usage: source this file, then call file_action_issue.
# Requires: codeberg_api() from lib/env.sh, jq, lib/secret-scan.sh
# Requires: forge_api() from lib/env.sh, jq, lib/secret-scan.sh
#
# file_action_issue <formula_name> <title> <body>
# Sets FILED_ISSUE_NUM on success.
@ -24,7 +24,7 @@ file_action_issue() {
# Dedup: skip if an open action issue for this formula already exists
local open_actions
open_actions=$(codeberg_api_all "/issues?state=open&type=issues&labels=action" 2>/dev/null || true)
open_actions=$(forge_api_all "/issues?state=open&type=issues&labels=action" 2>/dev/null || true)
if [ -n "$open_actions" ] && [ "$open_actions" != "null" ]; then
local existing
existing=$(printf '%s' "$open_actions" | \
@ -36,7 +36,7 @@ file_action_issue() {
# Fetch 'action' label ID
local action_label_id
action_label_id=$(codeberg_api GET "/labels" 2>/dev/null | \
action_label_id=$(forge_api GET "/labels" 2>/dev/null | \
jq -r '.[] | select(.name == "action") | .id' 2>/dev/null || true)
if [ -z "$action_label_id" ]; then
return 2
@ -50,7 +50,7 @@ file_action_issue() {
--argjson labels "[$action_label_id]" \
'{title: $title, body: $body, labels: $labels}')
result=$(codeberg_api POST "/issues" -d "$payload" 2>/dev/null || true)
result=$(forge_api POST "/issues" -d "$payload" 2>/dev/null || true)
FILED_ISSUE_NUM=$(printf '%s' "$result" | jq -r '.number // empty' 2>/dev/null || true)
if [ -z "$FILED_ISSUE_NUM" ]; then

View file

@ -208,21 +208,21 @@ read_scratch_context() {
# ── Prompt + monitor helpers ──────────────────────────────────────────────
# build_prompt_footer [EXTRA_API_LINES]
# Assembles the common Codeberg API reference + environment + phase protocol
# Assembles the common forge API reference + environment + phase protocol
# block for formula prompts. Sets PROMPT_FOOTER.
# Pass additional API endpoint lines (pre-formatted, newline-prefixed) via $1.
# Requires globals: CODEBERG_API, FACTORY_ROOT, PROJECT_REPO_ROOT,
# Requires globals: FORGE_API, FACTORY_ROOT, PROJECT_REPO_ROOT,
# PRIMARY_BRANCH, PHASE_FILE.
build_prompt_footer() {
local extra_api="${1:-}"
# shellcheck disable=SC2034 # consumed by the calling script's PROMPT
PROMPT_FOOTER="## Codeberg API reference
Base URL: ${CODEBERG_API}
Auth header: -H \"Authorization: token \$CODEBERG_TOKEN\"
Read issue: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/issues/{number}' | jq '.body'
Create issue: curl -sf -X POST -H \"Authorization: token \$CODEBERG_TOKEN\" -H 'Content-Type: application/json' '${CODEBERG_API}/issues' -d '{\"title\":\"...\",\"body\":\"...\",\"labels\":[LABEL_ID]}'${extra_api}
List labels: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/labels'
NEVER echo or include the actual token value in output — always reference \$CODEBERG_TOKEN.
PROMPT_FOOTER="## Forge API reference
Base URL: ${FORGE_API}
Auth header: -H \"Authorization: token \${FORGE_TOKEN}\"
Read issue: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_API}/issues/{number}' | jq '.body'
Create issue: curl -sf -X POST -H \"Authorization: token \${FORGE_TOKEN}\" -H 'Content-Type: application/json' '${FORGE_API}/issues' -d '{\"title\":\"...\",\"body\":\"...\",\"labels\":[LABEL_ID]}'${extra_api}
List labels: curl -sf -H \"Authorization: token \${FORGE_TOKEN}\" '${FORGE_API}/labels'
NEVER echo or include the actual token value in output — always reference \${FORGE_TOKEN}.
## Environment
FACTORY_ROOT=${FACTORY_ROOT}
@ -240,7 +240,7 @@ On unrecoverable error:
# run_formula_and_monitor AGENT_NAME [TIMEOUT]
# Starts the formula session, injects PROMPT, monitors phase, and logs result.
# Requires globals: SESSION_NAME, PHASE_FILE, PROJECT_REPO_ROOT, PROMPT,
# CODEBERG_REPO, CLAUDE_MODEL (exported).
# FORGE_REPO, CLAUDE_MODEL (exported).
# shellcheck disable=SC2154 # SESSION_NAME, PHASE_FILE, PROJECT_REPO_ROOT, PROMPT set by caller
run_formula_and_monitor() {
local agent_name="$1"
@ -258,7 +258,7 @@ run_formula_and_monitor() {
agent_inject_into_session "$SESSION_NAME" "$PROMPT"
log "Prompt sent to tmux session"
matrix_send "$agent_name" "${agent_name^} session started for ${CODEBERG_REPO}" 2>/dev/null || true
matrix_send "$agent_name" "${agent_name^} session started for ${FORGE_REPO}" 2>/dev/null || true
log "Monitoring phase file: ${PHASE_FILE}"
_FORMULA_CRASH_COUNT=0

View file

@ -5,7 +5,7 @@
# Blocks:
# - git push --force / -f to primary branch
# - rm -rf targeting paths outside the worktree
# - Direct Codeberg API merge calls (should go through phase protocol)
# - Direct forge API merge calls (should go through phase protocol)
# - Direct issue close calls (should go through phase protocol)
# - git checkout / git switch to primary branch (stay on feature branch)
# - FACTORY_ROOT access from worktrees (formula agents exempted)
@ -88,7 +88,7 @@ if [ -n "$worktree_path" ] \
fi
fi
# --- Guard 3: Direct Codeberg API merge calls ---
# --- Guard 3: Direct forge API merge calls ---
if printf '%s' "$command_str" | grep -qE '/pulls/[0-9]+/merge'; then
printf 'BLOCKED: Direct API merge calls must go through the phase protocol. Push your changes and write PHASE:awaiting_ci — the orchestrator handles merges.\n'
exit 2

View file

@ -5,9 +5,11 @@
# source lib/load-project.sh projects/harb.toml
#
# Exports:
# PROJECT_NAME, CODEBERG_REPO, CODEBERG_API, PROJECT_REPO_ROOT,
# PRIMARY_BRANCH, WOODPECKER_REPO_ID, PROJECT_CONTAINERS,
# CHECK_PRS, CHECK_DEV_AGENT, CHECK_PIPELINE_STALL, CI_STALE_MINUTES
# PROJECT_NAME, FORGE_REPO, FORGE_API, FORGE_WEB, FORGE_URL,
# PROJECT_REPO_ROOT, PRIMARY_BRANCH, WOODPECKER_REPO_ID,
# PROJECT_CONTAINERS, CHECK_PRS, CHECK_DEV_AGENT,
# CHECK_PIPELINE_STALL, CI_STALE_MINUTES
# (plus backwards-compat aliases: CODEBERG_REPO, CODEBERG_API, CODEBERG_WEB)
#
# If no argument given, does nothing (allows poll scripts to work with
# plain .env fallback for backwards compatibility).
@ -35,7 +37,8 @@ def emit(key, val):
# Top-level
emit('PROJECT_NAME', cfg.get('name', ''))
emit('CODEBERG_REPO', cfg.get('repo', ''))
emit('FORGE_REPO', cfg.get('repo', ''))
emit('FORGE_URL', cfg.get('forge_url', ''))
if 'repo_root' in cfg:
emit('PROJECT_REPO_ROOT', cfg['repo_root'])
@ -79,11 +82,17 @@ while IFS='=' read -r _key _val; do
export "$_key=$_val"
done <<< "$_PROJECT_VARS"
# Derive CODEBERG_API and CODEBERG_WEB if repo changed
if [ -n "$CODEBERG_REPO" ]; then
export CODEBERG_API="https://codeberg.org/api/v1/repos/${CODEBERG_REPO}"
export CODEBERG_WEB="https://codeberg.org/${CODEBERG_REPO}"
# Derive FORGE_API and FORGE_WEB from forge_url + repo
# FORGE_URL: TOML forge_url > existing FORGE_URL > default
export FORGE_URL="${FORGE_URL:-http://localhost:3000}"
if [ -n "$FORGE_REPO" ]; then
export FORGE_API="${FORGE_URL}/api/v1/repos/${FORGE_REPO}"
export FORGE_WEB="${FORGE_URL}/${FORGE_REPO}"
fi
# Backwards-compat aliases
export CODEBERG_REPO="${FORGE_REPO}"
export CODEBERG_API="${FORGE_API:-}"
export CODEBERG_WEB="${FORGE_WEB:-}"
# Derive PROJECT_REPO_ROOT if not explicitly set
if [ -z "${PROJECT_REPO_ROOT:-}" ] && [ -n "${PROJECT_NAME:-}" ]; then

View file

@ -33,8 +33,9 @@ _SAFE_PATTERNS=(
# Git SHAs in typical git contexts (commit refs, not standalone secrets)
'commit [0-9a-f]{40}'
'Merge [0-9a-f]{40}'
# Codeberg/GitHub URLs with short hex (PR refs, commit links)
# Forge/GitHub URLs with short hex (PR refs, commit links)
'codeberg\.org/[^[:space:]]+'
'localhost:3000/[^[:space:]]+'
# ShellCheck directive codes
'SC[0-9]{4}'
)