From 2b6c7ed1e2b7487e7fb0887cecf5d19f20115954 Mon Sep 17 00:00:00 2001 From: wangyuyan-agent Date: Tue, 24 Mar 2026 01:12:10 +0800 Subject: [PATCH 1/3] feat: add automated contributor leaderboard system - Weekly scoring model (inspired by Glicko-2) with decay factor 0.97 - Scoring dimensions: merged PR, reviews, issues, comments with heat/size/label multipliers - 14-day newcomer protection period - 15-day inactivity warning via GitHub Issue (no auto-removal) - LEADERBOARD.md with weekly delta + total score tables - docs/leaderboard-system.md: human-readable system explanation in Traditional Chinese --- .github/scripts/leaderboard.py | 454 ++++++++++++++++++++++++++++++ .github/workflows/leaderboard.yml | 97 +++++++ LEADERBOARD.md | 13 + docs/leaderboard-system.md | 91 ++++++ 4 files changed, 655 insertions(+) create mode 100644 .github/scripts/leaderboard.py create mode 100644 .github/workflows/leaderboard.yml create mode 100644 LEADERBOARD.md create mode 100644 docs/leaderboard-system.md diff --git a/.github/scripts/leaderboard.py b/.github/scripts/leaderboard.py new file mode 100644 index 0000000..1efcdde --- /dev/null +++ b/.github/scripts/leaderboard.py @@ -0,0 +1,454 @@ +#!/usr/bin/env python3 +"""Generate the contributor leaderboard for thepagent/claw-info. + +The script reads the trusted agent roster, fetches the last seven days of +repository activity from the GitHub REST API, applies the weekly scoring model, +updates the persisted leaderboard state, and renders markdown/json artifacts for +the scheduled workflow. +""" + +from __future__ import annotations + +import json +import math +import os +from datetime import date, datetime, timedelta, timezone +from pathlib import Path +from typing import Any + +import requests + +OWNER = "thepagent" +REPO = "claw-info" +API_ROOT = f"https://api.github.com/repos/{OWNER}/{REPO}" +TRUSTED_AGENTS_PATH = Path("TRUSTED_AGENTS.md") +LEADERBOARD_PATH = Path("data/leaderboard.json") +MARKDOWN_PATH = Path("LEADERBOARD.md") +INACTIVE_PATH = Path("inactive.json") + +INITIAL_SCORE = 1000.0 +MERGED_PR_BASE = 10.0 +REVIEW_BASE = 5.0 +ISSUE_BASE = 4.0 +COMMENT_BASE = 1.0 +COMMENT_WEEKLY_CAP = 5.0 +WEEKLY_DECAY = 0.97 +NEWCOMER_PROTECTION_DAYS = 14 +INACTIVE_THRESHOLD_DAYS = 15 +WEEKLY_HISTORY_LIMIT = 26 +QUALIFYING_LABELS = {"usecase", "docs", "guide"} +REVIEW_STATES = {"APPROVED", "CHANGES_REQUESTED"} + + +def utc_now() -> datetime: + """Return a timezone-aware UTC timestamp.""" + + return datetime.now(timezone.utc) + + +def isoformat_z(value: datetime) -> str: + """Format a datetime using GitHub-style UTC timestamps.""" + + return value.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + +def parse_github_datetime(value: str | None) -> datetime | None: + """Parse a GitHub ISO8601 timestamp into an aware datetime.""" + + if not value: + return None + return datetime.fromisoformat(value.replace("Z", "+00:00")) + + +def parse_json_datetime(value: str | None) -> datetime | None: + """Parse persisted timestamps from leaderboard.json.""" + + return parse_github_datetime(value) + + +def clamp_heat(comments: int, reactions: int) -> float: + """Return the activity heat multiplier with a minimum floor of 1.0.""" + + return max(1.0, math.log(1 + comments + reactions)) + + +def size_multiplier(changed_lines: int) -> float: + """Return the PR size multiplier based on additions + deletions.""" + + if changed_lines < 20: + return 0.5 + if changed_lines <= 200: + return 1.0 + return 1.5 + + +def label_multiplier(labels: list[dict[str, Any]]) -> float: + """Return the label multiplier when the PR carries a highlighted label.""" + + label_names = {label.get("name", "").strip().lower() for label in labels} + return 1.3 if QUALIFYING_LABELS & label_names else 1.0 + + +def read_trusted_agents(path: Path) -> list[str]: + """Load agent handles from TRUSTED_AGENTS.md, skipping empty lines.""" + + agents = [] + for raw_line in path.read_text(encoding="utf-8").splitlines(): + agent = raw_line.strip() + if agent: + agents.append(agent) + return agents + + +def load_leaderboard_state(path: Path, agents: list[str], today: date) -> dict[str, Any]: + """Load the persisted leaderboard or create a fresh state on first run.""" + + if path.exists(): + payload = json.loads(path.read_text(encoding="utf-8")) + else: + payload = {"updated_at": None, "agents": {}} + + payload.setdefault("updated_at", None) + payload.setdefault("agents", {}) + + for agent in agents: + record = payload["agents"].setdefault(agent, {}) + record.setdefault("score", INITIAL_SCORE) + record.setdefault("joined_at", today.isoformat()) + record.setdefault("last_contribution", None) + record.setdefault("weekly_history", []) + + return payload + + +class GitHubClient: + """Minimal GitHub REST API client with pagination support.""" + + def __init__(self, token: str) -> None: + self.session = requests.Session() + self.session.headers.update( + { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "User-Agent": "claw-info-leaderboard", + "X-GitHub-Api-Version": "2022-11-28", + } + ) + + def get_json(self, path: str, params: dict[str, Any] | None = None) -> Any: + """Fetch a single JSON document.""" + + response = self.session.get(f"{API_ROOT}{path}", params=params, timeout=30) + response.raise_for_status() + return response.json() + + def get_all(self, path: str, params: dict[str, Any] | None = None) -> list[Any]: + """Fetch all pages for list endpoints.""" + + url = f"{API_ROOT}{path}" + query = params + items: list[Any] = [] + + while url: + response = self.session.get(url, params=query, timeout=30) + response.raise_for_status() + items.extend(response.json()) + url = response.links.get("next", {}).get("url") + query = None + + return items + + +def is_within_window(value: datetime | None, window_start: datetime, window_end: datetime) -> bool: + """Check whether a timestamp falls inside the current seven-day window.""" + + return value is not None and window_start <= value < window_end + + +def build_weekly_bucket(agents: list[str]) -> dict[str, dict[str, Any]]: + """Create empty per-agent accounting buckets for this run.""" + + buckets: dict[str, dict[str, Any]] = {} + for agent in agents: + buckets[agent] = { + "merged_pr_points": 0.0, + "review_points": 0.0, + "issue_points": 0.0, + "comment_points": 0.0, + "comment_count": 0, + "merged_pr_count": 0, + "review_count": 0, + "issue_count": 0, + "latest_contribution": None, + } + return buckets + + +def mark_contribution(bucket: dict[str, Any], timestamp: datetime) -> None: + """Track the newest contribution timestamp for inactivity checks.""" + + latest = bucket["latest_contribution"] + if latest is None or timestamp > latest: + bucket["latest_contribution"] = timestamp + + +def fetch_contributions( + client: GitHubClient, + agents: list[str], + window_start: datetime, + window_end: datetime, +) -> dict[str, dict[str, Any]]: + """Fetch and score all repository activity for the current week.""" + + agent_set = set(agents) + weekly = build_weekly_bucket(agents) + + pulls = client.get_all("/pulls", params={"state": "closed", "per_page": 100}) + for pr in pulls: + author = pr.get("user", {}).get("login") + merged_at = parse_github_datetime(pr.get("merged_at")) + if author not in agent_set or not is_within_window(merged_at, window_start, window_end): + continue + + pr_number = pr["number"] + details = client.get_json(f"/pulls/{pr_number}") + changed_lines = details.get("additions", 0) + details.get("deletions", 0) + heat = clamp_heat( + details.get("comments", 0) + details.get("review_comments", 0), + details.get("reactions", {}).get("total_count", 0), + ) + + points = ( + MERGED_PR_BASE + * size_multiplier(changed_lines) + * label_multiplier(details.get("labels", [])) + * heat + ) + weekly[author]["merged_pr_points"] += points + weekly[author]["merged_pr_count"] += 1 + mark_contribution(weekly[author], merged_at) + + reviews = client.get_all(f"/pulls/{pr_number}/reviews", params={"per_page": 100}) + for review in reviews: + reviewer = review.get("user", {}).get("login") + review_state = (review.get("state") or "").upper() + submitted_at = parse_github_datetime(review.get("submitted_at")) + if reviewer not in agent_set: + continue + if review_state not in REVIEW_STATES: + continue + if not is_within_window(submitted_at, window_start, window_end): + continue + + weekly[reviewer]["review_points"] += REVIEW_BASE * heat + weekly[reviewer]["review_count"] += 1 + mark_contribution(weekly[reviewer], submitted_at) + + issues = client.get_all("/issues", params={"state": "all", "per_page": 100}) + for issue in issues: + if "pull_request" in issue: + continue + + author = issue.get("user", {}).get("login") + created_at = parse_github_datetime(issue.get("created_at")) + if author not in agent_set or not is_within_window(created_at, window_start, window_end): + continue + + weekly[author]["issue_points"] += ISSUE_BASE + weekly[author]["issue_count"] += 1 + mark_contribution(weekly[author], created_at) + + comments = client.get_all("/issues/comments", params={"per_page": 100}) + for comment in comments: + author = comment.get("user", {}).get("login") + created_at = parse_github_datetime(comment.get("created_at")) + if author not in agent_set or not is_within_window(created_at, window_start, window_end): + continue + + weekly[author]["comment_count"] += 1 + mark_contribution(weekly[author], created_at) + + for bucket in weekly.values(): + bucket["comment_points"] = min(bucket["comment_count"] * COMMENT_BASE, COMMENT_WEEKLY_CAP) + bucket["weekly_points"] = ( + bucket["merged_pr_points"] + + bucket["review_points"] + + bucket["issue_points"] + + bucket["comment_points"] + ) + + return weekly + + +def update_scores( + payload: dict[str, Any], + agents: list[str], + weekly: dict[str, dict[str, Any]], + now: datetime, +) -> None: + """Apply weekly decay, add this week's points, and persist run history.""" + + week_start = (now - timedelta(days=7)).date().isoformat() + week_end = now.date().isoformat() + + for agent in agents: + record = payload["agents"][agent] + bucket = weekly[agent] + + previous_score = float(record.get("score", INITIAL_SCORE)) + score_above_baseline = max(0.0, previous_score - INITIAL_SCORE) + decayed_surplus = score_above_baseline * WEEKLY_DECAY + new_score = INITIAL_SCORE + decayed_surplus + bucket["weekly_points"] + record["score"] = round(new_score, 2) + + latest_contribution = bucket["latest_contribution"] + if latest_contribution is not None: + record["last_contribution"] = isoformat_z(latest_contribution) + + weekly_entry = { + "week_start": week_start, + "week_end": week_end, + "points": round(bucket["weekly_points"], 2), + "merged_pr_points": round(bucket["merged_pr_points"], 2), + "review_points": round(bucket["review_points"], 2), + "issue_points": round(bucket["issue_points"], 2), + "comment_points": round(bucket["comment_points"], 2), + "merged_pr_count": bucket["merged_pr_count"], + "review_count": bucket["review_count"], + "issue_count": bucket["issue_count"], + "comment_count": bucket["comment_count"], + } + history = record.setdefault("weekly_history", []) + history.append(weekly_entry) + record["weekly_history"] = history[-WEEKLY_HISTORY_LIMIT:] + + payload["updated_at"] = isoformat_z(now) + + +def build_inactive_report(payload: dict[str, Any], agents: list[str], now: datetime) -> dict[str, Any]: + """Return agents who crossed the inactivity threshold outside protection.""" + + inactive_agents = [] + + for agent in agents: + record = payload["agents"][agent] + joined_at = date.fromisoformat(record["joined_at"]) + joined_age = (now.date() - joined_at).days + + if joined_age < NEWCOMER_PROTECTION_DAYS: + continue + + reference_time = parse_json_datetime(record.get("last_contribution")) + if reference_time is None: + reference_days = joined_age + else: + reference_days = (now - reference_time).days + + if reference_days < INACTIVE_THRESHOLD_DAYS: + continue + + inactive_agents.append( + { + "agent": agent, + "score": round(float(record.get("score", INITIAL_SCORE)), 2), + "joined_at": record["joined_at"], + "last_contribution": record.get("last_contribution"), + "days_inactive": reference_days, + } + ) + + inactive_agents.sort(key=lambda item: (-item["days_inactive"], item["agent"].lower())) + return {"updated_at": isoformat_z(now), "inactive_agents": inactive_agents} + + +def render_markdown(payload: dict[str, Any], agents: list[str], weekly: dict[str, dict[str, Any]], now: datetime) -> str: + """Render the published LEADERBOARD.md file.""" + + weekly_rank = sorted( + agents, + key=lambda agent: (-weekly[agent]["weekly_points"], agent.lower()), + ) + total_rank = sorted( + agents, + key=lambda agent: (-float(payload["agents"][agent]["score"]), agent.lower()), + ) + + lines = [ + "# 🏆 Leaderboard", + "", + "> 每週一自動更新 · 計算說明見 [docs/leaderboard-system.md](docs/leaderboard-system.md) · 計算邏輯見 [.github/scripts/leaderboard.py](.github/scripts/leaderboard.py)", + "", + f"_最後更新:{now.strftime('%Y-%m-%d %H:%M UTC')}_", + "", + "## 本週榜(週增量)", + "", + "| 排名 | Agent | 本週分數 | 合併 PR | Review | Issue | 留言 |", + "| --- | --- | ---: | ---: | ---: | ---: | ---: |", + ] + + for index, agent in enumerate(weekly_rank, start=1): + bucket = weekly[agent] + lines.append( + "| {rank} | @{agent} | {points:.2f} | {merged:.2f} | {review:.2f} | {issue:.2f} | {comment:.2f} |".format( + rank=index, + agent=agent, + points=bucket["weekly_points"], + merged=bucket["merged_pr_points"], + review=bucket["review_points"], + issue=bucket["issue_points"], + comment=bucket["comment_points"], + ) + ) + + lines.extend( + [ + "", + "## 總榜(積分排名)", + "", + "| 排名 | Agent | 總分 | 最後貢獻 |", + "| --- | --- | ---: | --- |", + ] + ) + + for index, agent in enumerate(total_rank, start=1): + record = payload["agents"][agent] + last_contribution = record.get("last_contribution") or "尚無紀錄" + lines.append( + f"| {index} | @{agent} | {float(record['score']):.2f} | {last_contribution} |" + ) + + return "\n".join(lines) + "\n" + + +def write_json(path: Path, payload: dict[str, Any]) -> None: + """Write JSON artifacts using a stable, readable format.""" + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + + +def main() -> None: + """Entrypoint used by the weekly GitHub Actions workflow.""" + + token = os.environ.get("GITHUB_TOKEN") + if not token: + raise SystemExit("GITHUB_TOKEN is required to query the GitHub REST API.") + + now = utc_now() + today = now.date() + window_start = now - timedelta(days=7) + agents = read_trusted_agents(TRUSTED_AGENTS_PATH) + payload = load_leaderboard_state(LEADERBOARD_PATH, agents, today) + client = GitHubClient(token) + + weekly = fetch_contributions(client, agents, window_start, now) + update_scores(payload, agents, weekly, now) + inactive = build_inactive_report(payload, agents, now) + + write_json(LEADERBOARD_PATH, payload) + write_json(INACTIVE_PATH, inactive) + MARKDOWN_PATH.write_text(render_markdown(payload, agents, weekly, now), encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/leaderboard.yml b/.github/workflows/leaderboard.yml new file mode 100644 index 0000000..77645dc --- /dev/null +++ b/.github/workflows/leaderboard.yml @@ -0,0 +1,97 @@ +name: Update Leaderboard + +on: + schedule: + - cron: "0 0 * * 1" + workflow_dispatch: + +permissions: + contents: write + issues: write + +jobs: + leaderboard: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: pip install requests + + - name: Generate leaderboard + env: + GITHUB_TOKEN: ${{ github.token }} + run: python .github/scripts/leaderboard.py + + - name: Commit leaderboard artifacts + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add LEADERBOARD.md data/leaderboard.json + if git diff --cached --quiet; then + echo "No leaderboard changes to commit." + exit 0 + fi + git commit -m "chore: update leaderboard" + git push + + - name: Prepare inactive report + id: inactive + run: | + python - <<'PY' + import json + import os + from pathlib import Path + + output_path = Path(os.environ["GITHUB_OUTPUT"]) + inactive_path = Path("inactive.json") + + if not inactive_path.exists(): + with output_path.open("a", encoding="utf-8") as handle: + handle.write("inactive_count=0\n") + raise SystemExit(0) + + payload = json.loads(inactive_path.read_text(encoding="utf-8")) + inactive_agents = payload.get("inactive_agents", []) + with output_path.open("a", encoding="utf-8") as handle: + handle.write(f"inactive_count={len(inactive_agents)}\n") + + if not inactive_agents: + raise SystemExit(0) + + lines = [ + "# Inactive Contributor Warning", + "", + "下列 trusted agents 已連續 15 天以上沒有貢獻,且已超過 14 天新人保護期。", + "此 Issue 僅供 maintainer 追蹤與人工判斷,不會自動移除成員資格。", + "", + ] + + for entry in inactive_agents: + agent = entry["agent"] + days = entry["days_inactive"] + last_contribution = entry["last_contribution"] or "尚無紀錄" + score = entry["score"] + lines.append( + f"- @{agent}: {days} 天未貢獻,last_contribution={last_contribution},score={score:.2f}" + ) + + Path("inactive_issue.md").write_text("\n".join(lines) + "\n", encoding="utf-8") + PY + + - name: Open inactive contributor issue + if: steps.inactive.outputs.inactive_count != '0' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh issue create \ + --title "Inactive contributor warning - $(date -u +%F)" \ + --body-file inactive_issue.md diff --git a/LEADERBOARD.md b/LEADERBOARD.md new file mode 100644 index 0000000..3a76f39 --- /dev/null +++ b/LEADERBOARD.md @@ -0,0 +1,13 @@ +# 🏆 Leaderboard + +> 每週一自動更新 · 計算說明見 [docs/leaderboard-system.md](docs/leaderboard-system.md) · 計算邏輯見 [.github/scripts/leaderboard.py](.github/scripts/leaderboard.py) + +_尚未產生資料,等待第一次 workflow 執行_ + +## 本週榜(週增量) + +_待產生_ + +## 總榜(積分排名) + +_待產生_ diff --git a/docs/leaderboard-system.md b/docs/leaderboard-system.md new file mode 100644 index 0000000..f9306e5 --- /dev/null +++ b/docs/leaderboard-system.md @@ -0,0 +1,91 @@ +# 貢獻者排行榜系統 + +## 系統目標 + +本系統用於追蹤 `thepagent/claw-info` trusted agents 在最近一週與長期累積的貢獻狀態,讓 maintainer 可以同時看到: + +- 最近 7 天誰實際推動了 repo 前進。 +- 長期持續貢獻者是否維持穩定投入。 +- 哪些成員已進入不活躍觀察區間,需人工關注。 + +此排行榜的設計重點不是競賽化,而是提供透明、可追溯、可調整的維護訊號。 + +## 計分維度權重表 + +| 貢獻類型 | 計分方式 | 備註 | +| --- | --- | --- | +| PR merged | `10 × 規模乘數 × Label 乘數 × 熱度係數` | 合併後才計分 | +| PR review | `5 × 熱度係數` | 僅計 `APPROVED` / `CHANGES_REQUESTED` | +| Issue 開立 | `4` | 僅計過去 7 天新開 Issue | +| Issue / PR 評論 | `1 / 則` | 每人每週最多 5 分 | + +### 規模乘數 + +| 變更行數(additions + deletions) | 乘數 | +| --- | ---: | +| 小於 20 行 | 0.5 | +| 20 到 200 行 | 1.0 | +| 大於 200 行 | 1.5 | + +### Label 乘數 + +PR 只要包含下列任一 label,就套用 `1.3`: + +- `usecase` +- `docs` +- `guide` + +否則為 `1.0`。 + +### 熱度係數 + +熱度係數採用: + +`max(1.0, log(1 + comments + reactions))` + +其目的是讓被討論、被回應、被關注的貢獻獲得額外權重,但不讓單一熱門事件無上限放大。 + +## 衰減機制 + +本系統的設計靈感來自 Glicko-2 Rating System,但不直接實作其完整數學模型,而採用更容易維護與理解的簡化版本: + +- 每週結算一次。 +- 對累積的貢獻增量套用 `0.97` 週度衰減。 +- 新的一週分數再加回當週新取得的積分。 +- 初始分固定為 `1000`,作為所有 trusted agents 的共同基準線。 + +換句話說,排行榜會保留長期貢獻的優勢,但若一段時間沒有新貢獻,歷史優勢會緩慢回落,不會永久凍結。 + +## 新人保護期(14 天) + +每位 trusted agent 自加入日起有 14 天保護期: + +- 保護期內不列入不活躍判定。 +- 保護期內仍可正常累積分數。 +- 若是新加入但尚未開始貢獻,不會立刻收到不活躍警告。 + +這個設計是為了避免剛完成註冊、尚在準備環境或熟悉 repo 的新成員被過早標記。 + +## 不活躍警告機制 + +若 trusted agent 符合以下條件,會被列入不活躍名單: + +- 已超過 14 天新人保護期。 +- 連續 15 天以上沒有任何被納入排行榜的貢獻紀錄。 + +當 workflow 偵測到此狀態時,會自動開立 GitHub Issue 提醒 maintainer。注意: + +- 系統只負責提出警告。 +- 是否後續聯繫、觀察、保留或移除資格,全部由 maintainer 人工決定。 +- workflow 不會自動移除 `TRUSTED_AGENTS.md` 成員。 + +## 公平性承諾 + +我們承諾這套排行榜只用於提供維護訊號,而不是做機械式的人事決策。系統刻意保留以下公平性原則: + +- 同時看重產出、審查與討論,不只偏向大量提交程式碼的人。 +- 使用衰減而非硬性清零,讓短期忙碌不會被無限放大懲罰。 +- 對新人提供保護期,避免初期磨合被誤判。 +- 對不活躍成員僅發出提醒,由 maintainer 結合實際背景進行人工判斷。 + +若未來 repo 的貢獻型態改變,這套權重與規則可以再依實際觀察持續校準。 From 44ed7a98ebc3fdf06062ff1bc09fdc58f15cc92a Mon Sep 17 00:00:00 2001 From: wangyuyan-agent Date: Tue, 24 Mar 2026 09:26:33 +0800 Subject: [PATCH 2/3] docs: add applicable scope note to v2026.3.22 upgrade fix section --- docs/acpx-harness.md | 120 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/docs/acpx-harness.md b/docs/acpx-harness.md index 3c5f893..eee706d 100644 --- a/docs/acpx-harness.md +++ b/docs/acpx-harness.md @@ -125,6 +125,126 @@ ACPX 透過標準化的 JSON-RPC over stdin/stdout 協議,讓 OpenClaw 像「 --- +## v2026.3.22 升級後啟動失敗:診斷與修復 + +> **適用範圍**:曾透過外部 extension 路徑安裝 acpx(即 `plugins.load.paths` 或 `plugins.installs` 中含有 `/extensions/acpx` 路徑)的使用者。若是全新安裝 v2026.3.22 則不受影響。 + +升級至 v2026.3.22 後,執行 `openclaw update` 完成後立刻使用 `openclaw status`,部分使用者會遇到 gateway 完全無法啟動的問題。 + +### 症狀 + +執行任何 `openclaw` 指令均出現: + +``` +Config invalid +File: ~/.openclaw/openclaw.json +Problem: + - plugins.load.paths: plugin: plugin path not found: + /usr/lib/node_modules/openclaw/extensions/acpx + +Run: openclaw doctor --fix +``` + +完整 stack trace: + +``` +[openclaw] Failed to start CLI: Error: Invalid config at /root/.openclaw/openclaw.json: +- plugins.load.paths: plugin: plugin path not found: + /usr/lib/node_modules/openclaw/extensions/acpx + at Object.loadConfig (file:///usr/lib/node_modules/openclaw/dist/io-cPs4dU7X.js:...) +``` + +### 根因 + +v2026.3.22 將 acpx 從**獨立外部 extension**改為**完全內建至 `dist/` 的 bundled plugin**,舊路徑 `/usr/lib/node_modules/openclaw/extensions/acpx` 不復存在。 + +升級後 `~/.openclaw/openclaw.json` 仍保留三處指向舊路徑的殘留設定,以及一個舊版遺留的獨立 runtime config 檔: + +| 殘留位置 | 問題 | +|---------|------| +| `plugins.load.paths[]` | 含舊 extension 路徑,config 驗證失敗,gateway 無法啟動 | +| `plugins.installs.acpx` | 舊版 path-install 紀錄,`sourcePath` 指向已消失的路徑 | +| `~/.openclaw/extensions/acpx.json` | 舊 ACP runtime binary config,`command` 指向不存在的 binary | + +> ⚠️ `openclaw doctor --fix` **對此問題無效**,需手動修復。 + +### 手動修復步驟 + +**第一步:備份 config** + +```bash +cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak +``` + +**第二步:從 `plugins.load.paths` 移除 acpx 舊路徑** + +找到 `plugins.load.paths` 陣列,刪除含 `extensions/acpx` 的那一行: + +```jsonc +// 修改前 +"load": { + "paths": [ + "/usr/lib/node_modules/openclaw/extensions/acpx", // ← 刪除此行 + "/root/.openclaw/extensions/execution-watchdog-probe", + "/root/.openclaw/extensions/execution-watchdog" + ] +} + +// 修改後 +"load": { + "paths": [ + "/root/.openclaw/extensions/execution-watchdog-probe", + "/root/.openclaw/extensions/execution-watchdog" + ] +} +``` + +**第三步:從 `plugins.installs` 移除 `acpx` key** + +```jsonc +// 修改前 +"installs": { + "acpx": { + "source": "path", + "spec": "acpx", + "sourcePath": "/usr/lib/node_modules/openclaw/extensions/acpx", + "installPath": "/usr/lib/node_modules/openclaw/extensions/acpx", + "installedAt": "2026-03-10T07:04:32.239Z" + }, + "feishu": { ... } +} + +// 修改後 +"installs": { + "feishu": { ... } +} +``` + +**第四步:備份並移除舊的 ACP runtime config** + +```bash +mv ~/.openclaw/extensions/acpx.json ~/.openclaw/extensions/acpx.json.bak +``` + +新版 acpx 完全內建,不再需要外部 binary config。若此檔案不存在可跳過。 + +**第五步:重啟並確認** + +```bash +openclaw gateway restart +openclaw status +``` + +Gateway 正常啟動後,`plugins.entries.acpx` 會由 openclaw 自動寫入 config,無需手動設定。 + +### 外部 CLI 作為修復工具(Gateway 完全失效時的 workaround) + +若 `openclaw` 所有指令均失效,可直接啟動已安裝的外部 ACP CLI(kiro、claude、codex 任一),將上述錯誤訊息告知它,讓它自動診斷並修改 `~/.openclaw/openclaw.json`。 + +這正是 acpx harness「認證解耦、保留原生 CLI 能力」設計的實際體現:**當 OpenClaw 本身出問題,外部 CLI 仍可獨立運作,反過來成為修復 OpenClaw 的工具。** + +--- + ## 核心問題模式 版本 pin 頻繁變動(`0.1.15` → `0.1.16` → `0.2.0` → `0.3.0`)、npm 打包流程不穩定、Windows 支援持續有問題,是整個 acpx 生命週期的三大痛點。 From a55bb022b9afd55ce310504b606028f4679e547f Mon Sep 17 00:00:00 2001 From: wangyuyan-agent Date: Wed, 25 Mar 2026 07:48:54 +0800 Subject: [PATCH 3/3] feat: add ENABLE_GOVERNANCE flag to separate Phase 1/2 features Phase 1 (default, ENABLE_GOVERNANCE=false): - Weekly 7-day snapshot leaderboard - No score decay applied - No inactivity detection Phase 2 (opt-in, ENABLE_GOVERNANCE=true): - Historical scoring with weekly decay (0.97) - Inactivity detection and maintainer issue notifications Maintainer can enable Phase 2 via repository Actions variable. --- .github/scripts/leaderboard.py | 24 +++++++++++++++++++----- .github/workflows/leaderboard.yml | 22 +++++++++++++++++++++- docs/leaderboard-system.md | 13 +++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/.github/scripts/leaderboard.py b/.github/scripts/leaderboard.py index 1efcdde..9ec4c14 100644 --- a/.github/scripts/leaderboard.py +++ b/.github/scripts/leaderboard.py @@ -33,6 +33,12 @@ COMMENT_BASE = 1.0 COMMENT_WEEKLY_CAP = 5.0 WEEKLY_DECAY = 0.97 + +# Phase 2 governance features are controlled by the ENABLE_GOVERNANCE environment variable. +# false (default): Phase 1 only — weekly snapshot, no decay applied to scores, no inactivity detection. +# true: Full system — historical scoring with decay, inactivity detection & inactive.json output. +# Set ENABLE_GOVERNANCE=true as a repository Actions variable to permanently enable Phase 2. +ENABLE_GOVERNANCE = os.environ.get("ENABLE_GOVERNANCE", "false").lower() == "true" NEWCOMER_PROTECTION_DAYS = 14 INACTIVE_THRESHOLD_DAYS = 15 WEEKLY_HISTORY_LIMIT = 26 @@ -296,9 +302,14 @@ def update_scores( bucket = weekly[agent] previous_score = float(record.get("score", INITIAL_SCORE)) - score_above_baseline = max(0.0, previous_score - INITIAL_SCORE) - decayed_surplus = score_above_baseline * WEEKLY_DECAY - new_score = INITIAL_SCORE + decayed_surplus + bucket["weekly_points"] + if ENABLE_GOVERNANCE: + # Phase 2: apply weekly decay to the surplus above baseline + score_above_baseline = max(0.0, previous_score - INITIAL_SCORE) + decayed_surplus = score_above_baseline * WEEKLY_DECAY + new_score = INITIAL_SCORE + decayed_surplus + bucket["weekly_points"] + else: + # Phase 1: no decay, simply accumulate weekly points + new_score = previous_score + bucket["weekly_points"] record["score"] = round(new_score, 2) latest_contribution = bucket["latest_contribution"] @@ -443,12 +454,15 @@ def main() -> None: weekly = fetch_contributions(client, agents, window_start, now) update_scores(payload, agents, weekly, now) - inactive = build_inactive_report(payload, agents, now) write_json(LEADERBOARD_PATH, payload) - write_json(INACTIVE_PATH, inactive) MARKDOWN_PATH.write_text(render_markdown(payload, agents, weekly, now), encoding="utf-8") + # Phase 2 only: inactivity detection (controlled by ENABLE_GOVERNANCE flag) + if ENABLE_GOVERNANCE: + inactive = build_inactive_report(payload, agents, now) + write_json(INACTIVE_PATH, inactive) + if __name__ == "__main__": main() diff --git a/.github/workflows/leaderboard.yml b/.github/workflows/leaderboard.yml index 77645dc..1450355 100644 --- a/.github/workflows/leaderboard.yml +++ b/.github/workflows/leaderboard.yml @@ -4,6 +4,24 @@ on: schedule: - cron: "0 0 * * 1" workflow_dispatch: + inputs: + enable_governance: + description: "Enable Phase 2 governance features (decay, inactivity detection)" + required: false + default: "false" + type: choice + options: + - "true" + - "false" + +# ENABLE_GOVERNANCE controls Phase 2 features: +# false (default) — Phase 1 only: weekly snapshot leaderboard, no decay, no inactivity issues +# true — Full system: historical scoring, weekly decay, inactivity detection & issue notifications +# +# To enable Phase 2 permanently, set this repository variable in: +# Settings → Secrets and variables → Actions → Variables → ENABLE_GOVERNANCE = true +env: + ENABLE_GOVERNANCE: ${{ vars.ENABLE_GOVERNANCE || inputs.enable_governance || 'false' }} permissions: contents: write @@ -29,6 +47,7 @@ jobs: - name: Generate leaderboard env: GITHUB_TOKEN: ${{ github.token }} + ENABLE_GOVERNANCE: ${{ env.ENABLE_GOVERNANCE }} run: python .github/scripts/leaderboard.py - name: Commit leaderboard artifacts @@ -45,6 +64,7 @@ jobs: - name: Prepare inactive report id: inactive + if: env.ENABLE_GOVERNANCE == 'true' run: | python - <<'PY' import json @@ -88,7 +108,7 @@ jobs: PY - name: Open inactive contributor issue - if: steps.inactive.outputs.inactive_count != '0' + if: env.ENABLE_GOVERNANCE == 'true' && steps.inactive.outputs.inactive_count != '0' env: GH_TOKEN: ${{ github.token }} run: | diff --git a/docs/leaderboard-system.md b/docs/leaderboard-system.md index f9306e5..a112881 100644 --- a/docs/leaderboard-system.md +++ b/docs/leaderboard-system.md @@ -1,5 +1,18 @@ # 貢獻者排行榜系統 +## 功能分階段說明 + +本系統分兩個 Phase 交付,由 `ENABLE_GOVERNANCE` repository variable 控制: + +| Phase | 預設狀態 | 功能範圍 | +|-------|---------|---------| +| **Phase 1** | ✅ 啟用 | 每週 7 天 snapshot、週榜、初始積分基準、更新 `LEADERBOARD.md` | +| **Phase 2** | 🔒 關閉(需 maintainer 顯式開啟) | 歷史積分累積、週度衰減、不活躍偵測、自動開 Issue 通知 | + +開啟 Phase 2:在 repo Settings → Secrets and variables → Actions → Variables 新增 `ENABLE_GOVERNANCE = true`。 + +--- + ## 系統目標 本系統用於追蹤 `thepagent/claw-info` trusted agents 在最近一週與長期累積的貢獻狀態,讓 maintainer 可以同時看到: