From 6bffc1b29e2d70cad03b5a2a72432629420b7e96 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 1 May 2026 07:12:55 +0100 Subject: [PATCH] feat: add clawsweeper dashboard renderer --- .github/workflows/dashboard.yml | 58 +++++++ .gitignore | 2 + README.md | 249 +++++++++++++++++++++++++++++ package.json | 15 ++ pnpm-lock.yaml | 9 ++ scripts/markdown.mjs | 44 +++++ scripts/render.mjs | 40 +++++ scripts/repair-dashboard.mjs | 216 +++++++++++++++++++++++++ scripts/source.mjs | 91 +++++++++++ scripts/sweep-dashboard.mjs | 273 ++++++++++++++++++++++++++++++++ test/render.test.mjs | 10 ++ 11 files changed, 1007 insertions(+) create mode 100644 .github/workflows/dashboard.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 scripts/markdown.mjs create mode 100644 scripts/render.mjs create mode 100644 scripts/repair-dashboard.mjs create mode 100644 scripts/source.mjs create mode 100644 scripts/sweep-dashboard.mjs create mode 100644 test/render.test.mjs diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml new file mode 100644 index 0000000000..8b90d153c7 --- /dev/null +++ b/.github/workflows/dashboard.yml @@ -0,0 +1,58 @@ +name: dashboard + +on: + workflow_dispatch: + repository_dispatch: + types: [clawsweeper_dashboard_refresh] + schedule: + - cron: "*/15 * * * *" + +permissions: + contents: write + +concurrency: + group: clawsweeper-dashboard + cancel-in-progress: true + +jobs: + render: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + path: dashboard + fetch-depth: 0 + + - uses: actions/checkout@v6 + with: + repository: openclaw/clawsweeper + path: clawsweeper + filter: blob:none + fetch-depth: 1 + + - uses: actions/setup-node@v5 + with: + node-version: 24 + + - name: Enable pnpm + run: corepack enable + + - name: Render dashboard + working-directory: dashboard + run: | + pnpm install --frozen-lockfile + pnpm run check + pnpm run render -- --source ../clawsweeper + + - name: Commit dashboard + working-directory: dashboard + run: | + if [ -z "$(git status --porcelain -- README.md)" ]; then + echo "Dashboard unchanged." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add README.md + git commit -m "chore: update dashboard" + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..2752eb92e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000000..800de90246 --- /dev/null +++ b/README.md @@ -0,0 +1,249 @@ +# ClawSweeper Dashboard + +Generated dashboard for [openclaw/clawsweeper](https://github.com/openclaw/clawsweeper). + +## Sweep Dashboard + +Last source update: May 1, 2026, 06:11 UTC + +### Fleet + +| Metric | Count | +| --- | ---: | +| Covered repositories | 3 | +| Open review records | 7651 | +| Archived closed records | 15505 | +| Fresh reviews, 7d | 7606 | +| Proposed closes awaiting apply | 6 | +| Work candidates awaiting promotion | 2413 | +| Failed or stale reviews | 21 | + +### Current Runs + +| Repository | State | Updated | Run | +| --- | --- | --- | --- | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | Idle | unknown | _none_ | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | Idle | unknown | _none_ | +| [openclaw/clawsweeper](https://github.com/openclaw/clawsweeper) | Idle | unknown | _none_ | + +### Repositories + +| Repository | Open records | Archived | Fresh | Proposed closes | Work candidates | Failed/stale | Last review | Last close | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | --- | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | 6722 | 15479 | 6677 | 6 | 2367 | 9 | May 1, 2026, 06:11 UTC | May 1, 2026, 06:05 UTC | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | 924 | 26 | 924 | 0 | 45 | 12 | Apr 29, 2026, 22:22 UTC | Apr 29, 2026, 17:17 UTC | +| [openclaw/clawsweeper](https://github.com/openclaw/clawsweeper) | 5 | 0 | 5 | 0 | 1 | 0 | May 1, 2026, 02:03 UTC | unknown | + +### Work Candidates + +| Repository | Item | Title | Priority | Reviewed | Report | +| --- | --- | --- | --- | --- | --- | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75183](https://github.com/openclaw/openclaw/pull/75183) | fix: simplify bundled runtime dependency repair | high | May 1, 2026, 06:08 UTC | [records/openclaw-openclaw/items/75183.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75183.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#72025](https://github.com/openclaw/openclaw/pull/72025) | fix(signal): enable inbound status reactions | high | May 1, 2026, 06:08 UTC | [records/openclaw-openclaw/items/72025.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/72025.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#72677](https://github.com/openclaw/openclaw/pull/72677) | fix(cron): warn on main heartbeat handoff ghost runs | high | May 1, 2026, 06:05 UTC | [records/openclaw-openclaw/items/72677.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/72677.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#74377](https://github.com/openclaw/openclaw/issues/74377) | [Bug]: tools array empty at Anthropic provider despite 17 tools computed in attempt.ts (Telegram channel) | high | May 1, 2026, 06:04 UTC | [records/openclaw-openclaw/items/74377.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/74377.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#73243](https://github.com/openclaw/openclaw/pull/73243) | fix(diagnostics): abort stuck sessions | high | May 1, 2026, 06:04 UTC | [records/openclaw-openclaw/items/73243.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/73243.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75155](https://github.com/openclaw/openclaw/issues/75155) | Talk Mode: user voice transcripts don't render in WebChat/Control UI thread | high | May 1, 2026, 06:04 UTC | [records/openclaw-openclaw/items/75155.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75155.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75206](https://github.com/openclaw/openclaw/issues/75206) | Auto-update leaves stale chunk reference: conversation-runtime imports missing inbound.runtime-*.js → Teleg... | high | May 1, 2026, 06:04 UTC | [records/openclaw-openclaw/items/75206.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75206.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75153](https://github.com/openclaw/openclaw/issues/75153) | [Feature]: expose channels.start / stop / restart via CLI so a wedged channel can recover without container... | high | May 1, 2026, 06:04 UTC | [records/openclaw-openclaw/items/75153.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75153.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75456](https://github.com/openclaw/openclaw/issues/75456) | security audit --json can hang while staging plugin runtime deps and scanning backup extension dirs | high | May 1, 2026, 06:03 UTC | [records/openclaw-openclaw/items/75456.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75456.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75171](https://github.com/openclaw/openclaw/issues/75171) | [Bug]: TypeError: (0, _pluginSdk.createReplyPrefixContext) is not a function after update | high | May 1, 2026, 06:03 UTC | [records/openclaw-openclaw/items/75171.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75171.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75131](https://github.com/openclaw/openclaw/issues/75131) | [Bug]: delivery-queue: send-retry creates fresh UUIDs (not idempotent); recovery should fail-permanent on T... | high | May 1, 2026, 06:03 UTC | [records/openclaw-openclaw/items/75131.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75131.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75151](https://github.com/openclaw/openclaw/issues/75151) | [Bug]: Context overflow reset can map sessionFile to nonexistent transcript, orphaning real session history | high | May 1, 2026, 06:03 UTC | [records/openclaw-openclaw/items/75151.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75151.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75180](https://github.com/openclaw/openclaw/pull/75180) | fix(reply): delivery-aware media tracking in block reply pipeline (#75156) | high | May 1, 2026, 06:00 UTC | [records/openclaw-openclaw/items/75180.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75180.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75452](https://github.com/openclaw/openclaw/issues/75452) | Heartbeat per-turn model override persists after turn completes (2026.4.29) | high | May 1, 2026, 05:57 UTC | [records/openclaw-openclaw/items/75452.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75452.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75425](https://github.com/openclaw/openclaw/pull/75425) | fix(media): fall back to qrcode/lib subpath when bare specifier fails to resolve | high | May 1, 2026, 05:57 UTC | [records/openclaw-openclaw/items/75425.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75425.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#71324](https://github.com/openclaw/openclaw/pull/71324) | fix(webchat): hide legacy runtime transcript wrappers | high | May 1, 2026, 05:56 UTC | [records/openclaw-openclaw/items/71324.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/71324.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#58683](https://github.com/openclaw/openclaw/pull/58683) | feat(heartbeat): add time-of-day schedule for variable intervals | high | May 1, 2026, 05:56 UTC | [records/openclaw-openclaw/items/58683.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/58683.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#58650](https://github.com/openclaw/openclaw/pull/58650) | fix(reply-runtime): preserve final answer in /reasoning on with block replies | high | May 1, 2026, 05:56 UTC | [records/openclaw-openclaw/items/58650.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/58650.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#58608](https://github.com/openclaw/openclaw/issues/58608) | Session corruption from orphaned tool_result blocks survives restart (overload/timeout interruption) | high | May 1, 2026, 05:55 UTC | [records/openclaw-openclaw/items/58608.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/58608.md) | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75447](https://github.com/openclaw/openclaw/issues/75447) | [Bug]: openclaw update installls old whatsapp extension - update should not install extensions that are old... | high | May 1, 2026, 05:49 UTC | [records/openclaw-openclaw/items/75447.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-openclaw/items/75447.md) | + +### Recently Closed + +| Repository | Item | Title | Reason | Closed | Report | +| --- | --- | --- | --- | --- | --- | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1434](https://github.com/openclaw/clawhub/issues/1434) | Sonarbay-skill incorrectly flaged. | closed externally after review | Apr 29, 2026, 17:17 UTC | [records/openclaw-clawhub/closed/1434.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1434.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1376](https://github.com/openclaw/clawhub/issues/1376) | False positive: openclaw-workspace-sync flagged as suspicious | closed externally after review | Apr 29, 2026, 17:06 UTC | [records/openclaw-clawhub/closed/1376.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1376.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1812](https://github.com/openclaw/clawhub/issues/1812) | Skill be flagged as suspicious and being marked hidden | closed externally after review | Apr 29, 2026, 13:48 UTC | [records/openclaw-clawhub/closed/1812.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1812.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1861](https://github.com/openclaw/clawhub/pull/1861) | feat: add owner rescan security surfaces | closed externally after review | Apr 28, 2026, 23:32 UTC | [records/openclaw-clawhub/closed/1861.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1861.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1870](https://github.com/openclaw/clawhub/pull/1870) | fix: hide admin CLI help from non-admins | none | Apr 28, 2026, 23:24 UTC | [records/openclaw-clawhub/closed/1870.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1870.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1869](https://github.com/openclaw/clawhub/pull/1869) | [codex] Use GitHub App auth for publish gate lookups | closed externally after review | Apr 28, 2026, 22:18 UTC | [records/openclaw-clawhub/closed/1869.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1869.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1841](https://github.com/openclaw/clawhub/pull/1841) | fix: calibrate VT Code Insight moderation | closed externally after review | Apr 28, 2026, 08:18 UTC | [records/openclaw-clawhub/closed/1841.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1841.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1830](https://github.com/openclaw/clawhub/issues/1830) | False positive: skill-factory incorrectly flagged as suspicious | closed externally after review | Apr 28, 2026, 08:18 UTC | [records/openclaw-clawhub/closed/1830.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1830.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1517](https://github.com/openclaw/clawhub/issues/1517) | [Appeal] Skill Wrongly Flagged: abu-shotai/ai-video-remix | closed externally after review | Apr 28, 2026, 07:43 UTC | [records/openclaw-clawhub/closed/1517.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1517.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#125](https://github.com/openclaw/clawhub/issues/125) | New Provider Plugin: ClawRouter — 30+ models, smart routing, x402 payments | closed externally after review | Apr 28, 2026, 06:41 UTC | [records/openclaw-clawhub/closed/125.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/125.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1699](https://github.com/openclaw/clawhub/issues/1699) | Plugin search returns 500, and plugin catalog breaks after page 2 | closed externally after review | Apr 28, 2026, 05:46 UTC | [records/openclaw-clawhub/closed/1699.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1699.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1736](https://github.com/openclaw/clawhub/pull/1736) | Align hover stats with denormalized counters and fix seed digest stat drift | implemented_on_main | Apr 28, 2026, 05:18 UTC | [records/openclaw-clawhub/closed/1736.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1736.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1324](https://github.com/openclaw/clawhub/pull/1324) | feat: add --dry-run flag to package publish command | implemented_on_main | Apr 28, 2026, 05:18 UTC | [records/openclaw-clawhub/closed/1324.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1324.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1240](https://github.com/openclaw/clawhub/pull/1240) | fix: use esbuild minification for safari builds | implemented_on_main | Apr 28, 2026, 05:18 UTC | [records/openclaw-clawhub/closed/1240.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1240.md) | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | [#1842](https://github.com/openclaw/clawhub/pull/1842) | fix: constrain plugin catalog queries | closed externally after review | Apr 28, 2026, 05:05 UTC | [records/openclaw-clawhub/closed/1842.md](https://github.com/openclaw/clawsweeper/blob/main/records/openclaw-clawhub/closed/1842.md) | + +
+Recently Reviewed + +| Repository | Item | Title | Outcome | Status | Reviewed | +| --- | --- | --- | --- | --- | --- | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75448](https://github.com/openclaw/openclaw/pull/75448) | feat(ui): replace textarea with CodeMirror 6 JSON editor in raw config view | keep_open / kept_open | complete | May 1, 2026, 06:11 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75183](https://github.com/openclaw/openclaw/pull/75183) | fix: simplify bundled runtime dependency repair | keep_open / kept_open | complete | May 1, 2026, 06:08 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#72025](https://github.com/openclaw/openclaw/pull/72025) | fix(signal): enable inbound status reactions | keep_open / kept_open | complete | May 1, 2026, 06:08 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75459](https://github.com/openclaw/openclaw/pull/75459) | fix: voice-call CLI gateway delegation path actionable regressions | keep_open / kept_open | complete | May 1, 2026, 06:08 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75454](https://github.com/openclaw/openclaw/issues/75454) | 1M context window not applied for github-copilot/claude-opus-4.x-1m models (capped at 272k) | keep_open / kept_open | complete | May 1, 2026, 06:06 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75423](https://github.com/openclaw/openclaw/pull/75423) | fix(gateway): refresh stale channel health cache | keep_open / kept_open | complete | May 1, 2026, 06:06 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75327](https://github.com/openclaw/openclaw/pull/75327) | fix: deliver group/channel replies automatically when bot is @mentioned | keep_open / kept_open | complete | May 1, 2026, 06:06 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75419](https://github.com/openclaw/openclaw/issues/75419) | [Bug]: [2026.4.29] openclaw cannot accept for any input | keep_open / kept_open | complete | May 1, 2026, 06:05 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#72677](https://github.com/openclaw/openclaw/pull/72677) | fix(cron): warn on main heartbeat handoff ghost runs | keep_open / kept_open | complete | May 1, 2026, 06:05 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#74377](https://github.com/openclaw/openclaw/issues/74377) | [Bug]: tools array empty at Anthropic provider despite 17 tools computed in attempt.ts (Telegram channel) | keep_open / kept_open | complete | May 1, 2026, 06:04 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75406](https://github.com/openclaw/openclaw/issues/75406) | v2026.4.29 - synchronous WASM tokenizer init blocks event loop 20-79s on first LLM call (system-prompt / ru... | keep_open / kept_open | complete | May 1, 2026, 06:04 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#73320](https://github.com/openclaw/openclaw/pull/73320) | Limit internal task completion payloads | keep_open / kept_open | complete | May 1, 2026, 06:04 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#58482](https://github.com/openclaw/openclaw/pull/58482) | fix(memory-host): add AbortSignal support to batch polling (OpenAI, Voyage, Gemini) | keep_open / kept_open | complete | May 1, 2026, 06:04 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75279](https://github.com/openclaw/openclaw/issues/75279) | [Performance]: 3-7 minute agent response latency on Windows — 66 plugins loaded, 49 disabled, 48 skills mis... | keep_open / kept_open | complete | May 1, 2026, 06:04 UTC | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | [#75298](https://github.com/openclaw/openclaw/issues/75298) | Webhook plugin re-registers repeatedly whenever event loop saturates (dreaming, cron catchup, Control UI po... | keep_open / kept_open | complete | May 1, 2026, 06:04 UTC | + +
+ +### Audit Health + +| Repository | Status | Last audit | Missing eligible | Stale records | Protected proposed | Scan complete | +| --- | --- | --- | ---: | ---: | ---: | --- | +| [openclaw/openclaw](https://github.com/openclaw/openclaw) | _unknown_ | unknown | 0 | 0 | 0 | unknown | +| [openclaw/clawhub](https://github.com/openclaw/clawhub) | _unknown_ | unknown | 0 | 0 | 0 | unknown | +| [openclaw/clawsweeper](https://github.com/openclaw/clawsweeper) | _unknown_ | unknown | 0 | 0 | 0 | unknown | + + +## Repair Dashboard + +Last source update: May 1, 2026, 06:03 UTC + +State: Failed clusters need inspection + +| Metric | Count | Rate | +| --- | ---: | ---: | +| Latest clusters reviewed | 362 | 100% | +| Run attempts archived | 631 | audit | +| Latest successful clusters | 332 | 91.7% | +| Latest failed clusters | 7 | 1.9% | +| Latest cancelled clusters | 2 | 0.6% | +| Needs-human clusters | 42 | 11.6% | +| Fix actions failed | 26 | 11.8% | +| Fix actions blocked | 56 | 25.5% | +| Completed close actions | 30 | 5.2% | +| Completed merge actions | 23 | 4.0% | +| Blocked mutation attempts | 269 | 46.2% | +| Skipped mutation attempts | 260 | 44.7% | + +### Clusters Needing Inspection + +| Cluster | State | Reason | Report | Run | +| --- | --- | --- | --- | --- | +| [clawsweeper-commit-openclaw-openclaw-464e57360262](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-464e57360262.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-464e57360262](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-464e57360262.md) | [25204309027](https://github.com/openclaw/clawsweeper/actions/runs/25204309027) | +| [clawsweeper-commit-openclaw-openclaw-e8f9c3e6dedc](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-e8f9c3e6dedc.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-e8f9c3e6dedc](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-e8f9c3e6dedc.md) | [25204170897](https://github.com/openclaw/clawsweeper/actions/runs/25204170897) | +| [clawsweeper-commit-openclaw-openclaw-214b3d333676](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-214b3d333676.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-214b3d333676](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-214b3d333676.md) | [25203815918](https://github.com/openclaw/clawsweeper/actions/runs/25203815918) | +| [clawsweeper-commit-openclaw-openclaw-76930da7ebc7](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-76930da7ebc7.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-76930da7ebc7](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-76930da7ebc7.md) | [25203495459](https://github.com/openclaw/clawsweeper/actions/runs/25203495459) | +| [automerge-openclaw-openclaw-75382](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75382.md) | merge_canonical blocked | job does not allow merge | [automerge-openclaw-openclaw-75382](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75382.md) | [25202542275](https://github.com/openclaw/clawsweeper/actions/runs/25202542275) | +| [clawsweeper-commit-openclaw-openclaw-5d1ba08e3c97](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-5d1ba08e3c97.md) | execute_fix blocked | fix artifact is too broad for autonomous execution; split into narrower jobs or explicitly set CLAWSWEEPER_ALLOW_BROAD_FIX_ARTIFACTS=1 | [clawsweeper-commit-openclaw-openclaw-5d1ba08e3c97](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-5d1ba08e3c97.md) | [25202885744](https://github.com/openclaw/clawsweeper/actions/runs/25202885744) | +| [automerge-openclaw-openclaw-75363](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75363.md) | fix failed | Codex /review did not pass after 2 attempt(s): No security-sensitive issue found in the diff. The artifact’s unrelated `.agents` files are absent,... | [automerge-openclaw-openclaw-75363](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75363.md) | [25202543309](https://github.com/openclaw/clawsweeper/actions/runs/25202543309) | +| [clawsweeper-commit-openclaw-openclaw-8989ceee50ab](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-8989ceee50ab.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-8989ceee50ab](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-8989ceee50ab.md) | [25202541355](https://github.com/openclaw/clawsweeper/actions/runs/25202541355) | +| [automerge-openclaw-openclaw-74506](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74506.md) | merge_canonical blocked | job does not allow merge | [automerge-openclaw-openclaw-74506](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74506.md) | [25202544672](https://github.com/openclaw/clawsweeper/actions/runs/25202544672) | +| [clawsweeper-commit-openclaw-openclaw-3e67ee63b4e0](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-3e67ee63b4e0.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-3e67ee63b4e0](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-3e67ee63b4e0.md) | [25200994696](https://github.com/openclaw/clawsweeper/actions/runs/25200994696) | +| [clawsweeper-commit-openclaw-openclaw-45b86450795d](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-45b86450795d.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-45b86450795d](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-45b86450795d.md) | [25200785766](https://github.com/openclaw/clawsweeper/actions/runs/25200785766) | +| [clawsweeper-commit-openclaw-openclaw-60bdb96f2c4c](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-60bdb96f2c4c.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-60bdb96f2c4c](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-60bdb96f2c4c.md) | [25200786346](https://github.com/openclaw/clawsweeper/actions/runs/25200786346) | +| [clawsweeper-commit-openclaw-openclaw-354084b1b320](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-354084b1b320.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-354084b1b320](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-354084b1b320.md) | [25200785151](https://github.com/openclaw/clawsweeper/actions/runs/25200785151) | +| [clawsweeper-commit-openclaw-openclaw-e0fe02fb0970](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-e0fe02fb0970.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-e0fe02fb0970](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-e0fe02fb0970.md) | [25200786950](https://github.com/openclaw/clawsweeper/actions/runs/25200786950) | +| [clawsweeper-commit-openclaw-openclaw-df0ee092f017](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-df0ee092f017.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-df0ee092f017](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-df0ee092f017.md) | [25200373309](https://github.com/openclaw/clawsweeper/actions/runs/25200373309) | +| [automerge-openclaw-openclaw-75326](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75326.md) | merge_canonical blocked | job does not allow merge | [automerge-openclaw-openclaw-75326](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75326.md) | [25198747424](https://github.com/openclaw/clawsweeper/actions/runs/25198747424) | +| [clawsweeper-commit-openclaw-openclaw-b277ae3f4c40](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-b277ae3f4c40.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-b277ae3f4c40](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-b277ae3f4c40.md) | [25198053576](https://github.com/openclaw/clawsweeper/actions/runs/25198053576) | +| [clawsweeper-commit-openclaw-openclaw-a102f4dede6a](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-a102f4dede6a.md) | execute_fix blocked | validation command failed (pnpm check:changed): [check:changed] lanes=core, coreTests [check:changed] src/agents/agent-command.ts: core production... | [clawsweeper-commit-openclaw-openclaw-a102f4dede6a](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-a102f4dede6a.md) | [25197023690](https://github.com/openclaw/clawsweeper/actions/runs/25197023690) | +| [automerge-openclaw-openclaw-75302](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75302.md) | merge_canonical blocked | job does not allow merge | [automerge-openclaw-openclaw-75302](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75302.md) | [25196881228](https://github.com/openclaw/clawsweeper/actions/runs/25196881228) | +| [clawsweeper-commit-openclaw-openclaw-52bf20b07d6e](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-52bf20b07d6e.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-52bf20b07d6e](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-52bf20b07d6e.md) | [25195746048](https://github.com/openclaw/clawsweeper/actions/runs/25195746048) | +| [clawsweeper-commit-openclaw-openclaw-ef799fd57a77](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-ef799fd57a77.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-ef799fd57a77](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-ef799fd57a77.md) | [25195271001](https://github.com/openclaw/clawsweeper/actions/runs/25195271001) | +| [clawsweeper-commit-openclaw-openclaw-af5a1fbddb14](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-af5a1fbddb14.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-af5a1fbddb14](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-af5a1fbddb14.md) | [25192772436](https://github.com/openclaw/clawsweeper/actions/runs/25192772436) | +| [automerge-openclaw-openclaw-74716](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74716.md) | fix failed | validation command failed (pnpm check:changed): [check:changed] lanes=core, coreTests, extensions, extensionTests, apps, docs [check:changed] exten... | [automerge-openclaw-openclaw-74716](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74716.md) | [25192389369](https://github.com/openclaw/clawsweeper/actions/runs/25192389369) | +| [clawsweeper-commit-openclaw-openclaw-1d74ecd71f0f](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-1d74ecd71f0f.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-1d74ecd71f0f](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-1d74ecd71f0f.md) | [25191267120](https://github.com/openclaw/clawsweeper/actions/runs/25191267120) | +| [clawsweeper-commit-openclaw-openclaw-027ea5f08bd9](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-027ea5f08bd9.md) | execute_fix blocked | Codex /review did not pass after 2 attempt(s): Merge is blocked by one incomplete repair. The branch is narrow and `pnpm check:changed` plus `git d... | [clawsweeper-commit-openclaw-openclaw-027ea5f08bd9](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-027ea5f08bd9.md) | [25187047448](https://github.com/openclaw/clawsweeper/actions/runs/25187047448) | +| [automerge-openclaw-openclaw-75209](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75209.md) | merge_canonical blocked | job does not allow merge | [automerge-openclaw-openclaw-75209](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75209.md) | [25186718052](https://github.com/openclaw/clawsweeper/actions/runs/25186718052) | +| [clawsweeper-commit-openclaw-openclaw-581fbea1d653](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-581fbea1d653.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-581fbea1d653](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-581fbea1d653.md) | [25183870132](https://github.com/openclaw/clawsweeper/actions/runs/25183870132) | +| [clawsweeper-commit-openclaw-openclaw-54e6e3d7daf5](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-54e6e3d7daf5.md) | merge_canonical blocked | job does not allow merge | [clawsweeper-commit-openclaw-openclaw-54e6e3d7daf5](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-54e6e3d7daf5.md) | [25183184702](https://github.com/openclaw/clawsweeper/actions/runs/25183184702) | +| [automerge-openclaw-openclaw-74472](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74472.md) | merge_canonical blocked | job does not allow merge | [automerge-openclaw-openclaw-74472](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74472.md) | [25182390249](https://github.com/openclaw/clawsweeper/actions/runs/25182390249) | +| [clawsweeper-commit-openclaw-openclaw-82ca6ecdde80](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-82ca6ecdde80.md) | execute_fix blocked | fix artifact is too broad for autonomous execution; split into narrower jobs or explicitly set CLAWSWEEPER_ALLOW_BROAD_FIX_ARTIFACTS=1 | [clawsweeper-commit-openclaw-openclaw-82ca6ecdde80](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-82ca6ecdde80.md) | [25176627885](https://github.com/openclaw/clawsweeper/actions/runs/25176627885) | + +### Fix Failure Queue + +| Cluster | Status | Target | Branch/PR | Reason | Run | +| --- | --- | --- | --- | --- | --- | +| [clawsweeper-commit-openclaw-openclaw-5d1ba08e3c97](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-5d1ba08e3c97.md) | blocked | | | fix artifact is too broad for autonomous execution; split into narrower jobs or explicitly set CLAWSWEEPER_ALLOW_BROAD_FIX_ARTIFACTS=1 | [25202885744](https://github.com/openclaw/clawsweeper/actions/runs/25202885744) | +| [automerge-openclaw-openclaw-75363](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75363.md) | failed | | | Codex /review did not pass after 2 attempt(s): No security-sensitive issue found in the diff. The artifact’s unrelated `.agents` files are absent,... | [25202543309](https://github.com/openclaw/clawsweeper/actions/runs/25202543309) | +| [automerge-openclaw-openclaw-75363](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-75363.md) | blocked | | | Codex /review did not pass after 2 attempt(s): No security-sensitive issue found in the diff. The artifact’s unrelated `.agents` files are absent,... | [25202543309](https://github.com/openclaw/clawsweeper/actions/runs/25202543309) | +| [clawsweeper-commit-openclaw-openclaw-a102f4dede6a](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-a102f4dede6a.md) | blocked | | | validation command failed (pnpm check:changed): [check:changed] lanes=core, coreTests [check:changed] src/agents/agent-command.ts: core production... | [25197023690](https://github.com/openclaw/clawsweeper/actions/runs/25197023690) | +| [automerge-openclaw-openclaw-74716](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74716.md) | failed | | | validation command failed (pnpm check:changed): [check:changed] lanes=core, coreTests, extensions, extensionTests, apps, docs [check:changed] exten... | [25192389369](https://github.com/openclaw/clawsweeper/actions/runs/25192389369) | +| [automerge-openclaw-openclaw-74716](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74716.md) | blocked | | | validation command failed (pnpm check:changed): [check:changed] lanes=core, coreTests, extensions, extensionTests, apps, docs [check:changed] exten... | [25192389369](https://github.com/openclaw/clawsweeper/actions/runs/25192389369) | +| [clawsweeper-commit-openclaw-openclaw-027ea5f08bd9](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-027ea5f08bd9.md) | blocked | | | Codex /review did not pass after 2 attempt(s): Merge is blocked by one incomplete repair. The branch is narrow and `pnpm check:changed` plus `git d... | [25187047448](https://github.com/openclaw/clawsweeper/actions/runs/25187047448) | +| [clawsweeper-commit-openclaw-openclaw-82ca6ecdde80](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-82ca6ecdde80.md) | blocked | | | fix artifact is too broad for autonomous execution; split into narrower jobs or explicitly set CLAWSWEEPER_ALLOW_BROAD_FIX_ARTIFACTS=1 | [25176627885](https://github.com/openclaw/clawsweeper/actions/runs/25176627885) | +| [clawsweeper-commit-openclaw-openclaw-ac599c9e539f](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-ac599c9e539f.md) | blocked | | | validation command failed (pnpm check:changed): [check:changed] lanes=core, coreTests [check:changed] src/plugins/bundled-runtime-deps-selection.ts... | [25171874076](https://github.com/openclaw/clawsweeper/actions/runs/25171874076) | +| [clawsweeper-commit-openclaw-openclaw-3c9437ae547a](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-3c9437ae547a.md) | blocked | | | fix artifact is too broad for autonomous execution; split into narrower jobs or explicitly set CLAWSWEEPER_ALLOW_BROAD_FIX_ARTIFACTS=1 | [25149929229](https://github.com/openclaw/clawsweeper/actions/runs/25149929229) | +| [clawsweeper-commit-openclaw-openclaw-0142c791232e](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-0142c791232e.md) | blocked | | | fix artifact is too broad for autonomous execution; split into narrower jobs or explicitly set CLAWSWEEPER_ALLOW_BROAD_FIX_ARTIFACTS=1 | [25147836943](https://github.com/openclaw/clawsweeper/actions/runs/25147836943) | +| [clawsweeper-commit-openclaw-openclaw-d7396d4ffa2f](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-d7396d4ffa2f.md) | blocked | | | fix artifact is too broad for autonomous execution; split into narrower jobs or explicitly set CLAWSWEEPER_ALLOW_BROAD_FIX_ARTIFACTS=1 | [25147023250](https://github.com/openclaw/clawsweeper/actions/runs/25147023250) | +| [automerge-openclaw-openclaw-74638](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74638.md) | failed | | | validation command failed (pnpm check:changed): [check:changed] lanes=core, coreTests, docs [check:changed] src/config/config.schema-regressions.te... | [25146762143](https://github.com/openclaw/clawsweeper/actions/runs/25146762143) | +| [automerge-openclaw-openclaw-74638](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74638.md) | blocked | | | validation command failed (pnpm check:changed): [check:changed] lanes=core, coreTests, docs [check:changed] src/config/config.schema-regressions.te... | [25146762143](https://github.com/openclaw/clawsweeper/actions/runs/25146762143) | +| [clawsweeper-commit-openclaw-openclaw-25d2e9bdace3](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-25d2e9bdace3.md) | blocked | | | validation command failed (pnpm check:changed): [check:changed] lanes=apps [check:changed] apps/macos/Sources/OpenClaw/GatewayLaunchAgentManager.sw... | [25145494152](https://github.com/openclaw/clawsweeper/actions/runs/25145494152) | +| [clawsweeper-commit-openclaw-openclaw-58153d38af57](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-58153d38af57.md) | blocked | | | validation command failed (pnpm check:changed): [check:changed] lanes=core, coreTests, extensions, extensionTests [check:changed] extensions/codex/... | [25143847817](https://github.com/openclaw/clawsweeper/actions/runs/25143847817) | +| [clawsweeper-commit-openclaw-openclaw-0b59964ec945](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-0b59964ec945.md) | blocked | | | Codex /review failed: structured output was not written to replacement-codex-review-1.json; stdout={"type":"thread.started","thread_id":"019ddbc8-0... | [25140762037](https://github.com/openclaw/clawsweeper/actions/runs/25140762037) | +| [automerge-openclaw-openclaw-74528](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74528.md) | blocked | | | Codex /review failed: structured output was not written to replacement-codex-review-1.json; stdout={"type":"thread.started","thread_id":"019ddba8-1... | [25139586999](https://github.com/openclaw/clawsweeper/actions/runs/25139586999) | +| [clawsweeper-commit-openclaw-openclaw-db6951088a19](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-db6951088a19.md) | blocked | | | validation command failed (pnpm check:changed): [check:changed] lanes=core, coreTests, extensions, extensionTests [check:changed] extensions/telegr... | [25128072554](https://github.com/openclaw/clawsweeper/actions/runs/25128072554) | +| [clawsweeper-commit-openclaw-openclaw-6a4c866b6a8b](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-6a4c866b6a8b.md) | blocked | | | fix artifact is too broad for autonomous execution; split into narrower jobs or explicitly set CLAWSWEEPER_REPAIR_ALLOW_BROAD_FIX_ARTIFACTS=1 | [25094690632](https://github.com/openclaw/clawsweeper/actions/runs/25094690632) | +| [ghcrawl-166004-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-166004-agentic-merge.md) | blocked | | | Codex /review did not pass after 2 attempt(s): Cannot perform the review without inspecting the repository diff and validation state. | [25087637821](https://github.com/openclaw/clawsweeper/actions/runs/25087637821) | +| [ghcrawl-156717-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156717-autonomous-smoke.md) | blocked | | | validation command failed (pnpm check:changed): [check:changed] lanes=core, coreTests, docs [check:changed] ui/src/styles/chat/layout.css: core pro... | [25085937628](https://github.com/openclaw/clawsweeper/actions/runs/25085937628) | +| [ghcrawl-156593-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156593-autonomous-smoke.md) | failed | | | To https://github.com/LiaoyuanNing/openclaw.git ! [remote rejected] HEAD -> fix/feishu-p2p-thread-reply (refusing to allow a GitHub App to create o... | [25084314903](https://github.com/openclaw/clawsweeper/actions/runs/25084314903) | +| [ghcrawl-156593-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156593-autonomous-smoke.md) | blocked | | | Codex /review did not pass after 2 attempt(s): Blocked. The diff is narrow and has no visible secret, dependency, workflow, install, or security-se... | [25084314903](https://github.com/openclaw/clawsweeper/actions/runs/25084314903) | +| [ghcrawl-156627-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156627-autonomous-smoke.md) | blocked | | | fix artifact is too broad for autonomous execution; split into narrower jobs or explicitly set CLAWSWEEPER_REPAIR_ALLOW_BROAD_FIX_ARTIFACTS=1 | [25070489790](https://github.com/openclaw/clawsweeper/actions/runs/25070489790) | + +### Top Blocked Reasons + +| Reason | Latest count | Example cluster | +| --- | ---: | --- | +| job does not allow merge | 90 | [clawsweeper-commit-openclaw-openclaw-464e57360262](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/clawsweeper-commit-openclaw-openclaw-464e57360262.md) | +| action status is blocked | 89 | [ghcrawl-156636-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156636-autonomous-smoke.md) | +| close requires ClawSweeper Repair fix PR opened/pushed or merge executed first | 13 | [ghcrawl-156593-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156593-autonomous-smoke.md) | +| merge requires CLAWSWEEPER_REPAIR_ALLOW_MERGE=1; labeled for human review | 10 | [ghcrawl-156679-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156679-autonomous-smoke.md) | +| merge state status is UNSTABLE | 10 | [ghcrawl-156647-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156647-autonomous-smoke.md) | +| target changed since worker review | 9 | [ghcrawl-156624-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156624-autonomous-smoke.md) | +| mergeable state is CONFLICTING | 5 | [ghcrawl-156651-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156651-autonomous-smoke.md) | +| target is not listed in job candidates | 4 | [ghcrawl-156682-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156682-autonomous-smoke.md) | +| Fix-first policy blocks superseded closeout until #49430 is repaired and merged. | 2 | [ghcrawl-156585-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156585-autonomous-smoke.md) | +| Clearly superseded by the canonical PR path, but closure is blocked until the canonical fix lands. | 2 | [ghcrawl-156879-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156879-autonomous-smoke.md) | +| require_fix_before_close blocks superseded PR closeout until the canonical fix path is landed or opened as a concrete fix PR. | 2 | [ghcrawl-156789-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156789-autonomous-smoke.md) | +| canonical is not listed in job refs | 2 | [ghcrawl-156658-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156658-autonomous-smoke.md) | +| maintainer issue comment blocks low-signal auto-close | 2 | [low-signal-pr-sweep-20260425T2346-01](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/low-signal-pr-sweep-20260425t2346-01.md) | +| Blocked on the canonical fix path landing; job policy does not allow unmerged fix closeout. | 1 | [automerge-openclaw-openclaw-74666](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74666.md) | +| Superseded closeout is clear, but closure is blocked by this job's policy and requires human approval. | 1 | [automerge-openclaw-openclaw-74134](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/automerge-openclaw-openclaw-74134.md) | + +### Latest Repair Closures + +| Target | Action | Title | Closed | Cluster | Report | Run | +| --- | --- | --- | --- | --- | --- | --- | +| [#59439](#59439) | close_superseded | | Apr 28, 2026, 05:25 UTC | [ghcrawl-207050-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-207050-agentic-merge.md) | [ghcrawl-207050-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-207050-agentic-merge.md) | [25035228706](https://github.com/openclaw/clawsweeper/actions/runs/25035228706) | +| [#48388](#48388) | close_fixed_by_candidate | | Apr 28, 2026, 05:23 UTC | [ghcrawl-199239-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-199239-agentic-merge.md) | [ghcrawl-199239-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-199239-agentic-merge.md) | [25035231796](https://github.com/openclaw/clawsweeper/actions/runs/25035231796) | +| [#50435](#50435) | close_superseded | | Apr 28, 2026, 05:23 UTC | [ghcrawl-199239-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-199239-agentic-merge.md) | [ghcrawl-199239-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-199239-agentic-merge.md) | [25035231796](https://github.com/openclaw/clawsweeper/actions/runs/25035231796) | +| [#59409](#59409) | close_fixed_by_candidate | | Apr 28, 2026, 05:23 UTC | [ghcrawl-199239-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-199239-agentic-merge.md) | [ghcrawl-199239-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-199239-agentic-merge.md) | [25035231796](https://github.com/openclaw/clawsweeper/actions/runs/25035231796) | +| [#59431](#59431) | close_superseded | | Apr 28, 2026, 05:23 UTC | [ghcrawl-199239-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-199239-agentic-merge.md) | [ghcrawl-199239-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-199239-agentic-merge.md) | [25035231796](https://github.com/openclaw/clawsweeper/actions/runs/25035231796) | +| [#61016](#61016) | close_superseded | | Apr 28, 2026, 05:00 UTC | [ghcrawl-156640-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156640-autonomous-smoke.md) | [ghcrawl-156640-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156640-autonomous-smoke.md) | [25034019917](https://github.com/openclaw/clawsweeper/actions/runs/25034019917) | +| [#49957](#49957) | close_duplicate | | Apr 28, 2026, 04:59 UTC | [ghcrawl-156664-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156664-autonomous-smoke.md) | [ghcrawl-156664-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156664-autonomous-smoke.md) | [25033552424](https://github.com/openclaw/clawsweeper/actions/runs/25033552424) | +| [#49961](#49961) | close_superseded | | Apr 28, 2026, 04:59 UTC | [ghcrawl-156664-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156664-autonomous-smoke.md) | [ghcrawl-156664-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-156664-autonomous-smoke.md) | [25033552424](https://github.com/openclaw/clawsweeper/actions/runs/25033552424) | +| [#54429](#54429) | close_duplicate | | Apr 26, 2026, 03:04 UTC | [ghcrawl-166002-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-166002-agentic-merge.md) | [ghcrawl-166002-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-166002-agentic-merge.md) | [24946559138](https://github.com/openclaw/clawsweeper-repair/actions/runs/24946559138) | +| [#41992](#41992) | close_duplicate | | Apr 26, 2026, 03:04 UTC | [ghcrawl-165992-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-165992-agentic-merge.md) | [ghcrawl-165992-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-165992-agentic-merge.md) | [24946558493](https://github.com/openclaw/clawsweeper-repair/actions/runs/24946558493) | +| [#43242](#43242) | close_duplicate | | Apr 26, 2026, 03:04 UTC | [ghcrawl-165992-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-165992-agentic-merge.md) | [ghcrawl-165992-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-165992-agentic-merge.md) | [24946558493](https://github.com/openclaw/clawsweeper-repair/actions/runs/24946558493) | +| [#56298](#56298) | close_duplicate | | Apr 26, 2026, 03:04 UTC | [ghcrawl-165992-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-165992-agentic-merge.md) | [ghcrawl-165992-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-165992-agentic-merge.md) | [24946558493](https://github.com/openclaw/clawsweeper-repair/actions/runs/24946558493) | +| [#63094](#63094) | close_duplicate | | Apr 26, 2026, 03:04 UTC | [ghcrawl-165992-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-165992-agentic-merge.md) | [ghcrawl-165992-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-165992-agentic-merge.md) | [24946558493](https://github.com/openclaw/clawsweeper-repair/actions/runs/24946558493) | +| [#67622](#67622) | close_duplicate | | Apr 26, 2026, 03:04 UTC | [ghcrawl-165992-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-165992-agentic-merge.md) | [ghcrawl-165992-agentic-merge](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-165992-agentic-merge.md) | [24946558493](https://github.com/openclaw/clawsweeper-repair/actions/runs/24946558493) | +| [#67029](#67029) | close_duplicate | | Apr 26, 2026, 01:36 UTC | [ghcrawl-143819-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143819-autonomous-smoke.md) | [ghcrawl-143819-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143819-autonomous-smoke.md) | [24939011554](https://github.com/openclaw/clawsweeper-repair/actions/runs/24939011554) | +| [#69886](#69886) | close_duplicate | | Apr 26, 2026, 01:36 UTC | [ghcrawl-143819-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143819-autonomous-smoke.md) | [ghcrawl-143819-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143819-autonomous-smoke.md) | [24939011554](https://github.com/openclaw/clawsweeper-repair/actions/runs/24939011554) | +| [#70353](#70353) | close_duplicate | | Apr 26, 2026, 01:36 UTC | [ghcrawl-143819-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143819-autonomous-smoke.md) | [ghcrawl-143819-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143819-autonomous-smoke.md) | [24939011554](https://github.com/openclaw/clawsweeper-repair/actions/runs/24939011554) | +| [#70395](#70395) | close_duplicate | | Apr 26, 2026, 01:36 UTC | [ghcrawl-143819-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143819-autonomous-smoke.md) | [ghcrawl-143819-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143819-autonomous-smoke.md) | [24939011554](https://github.com/openclaw/clawsweeper-repair/actions/runs/24939011554) | +| [#71133](#71133) | close_duplicate | | Apr 26, 2026, 01:36 UTC | [ghcrawl-143819-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143819-autonomous-smoke.md) | [ghcrawl-143819-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143819-autonomous-smoke.md) | [24939011554](https://github.com/openclaw/clawsweeper-repair/actions/runs/24939011554) | +| [#70180](#70180) | close_duplicate | | Apr 26, 2026, 01:36 UTC | [ghcrawl-143816-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143816-autonomous-smoke.md) | [ghcrawl-143816-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143816-autonomous-smoke.md) | [24939009401](https://github.com/openclaw/clawsweeper-repair/actions/runs/24939009401) | +| [#65832](#65832) | close_duplicate | | Apr 26, 2026, 01:36 UTC | [ghcrawl-143816-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143816-autonomous-smoke.md) | [ghcrawl-143816-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143816-autonomous-smoke.md) | [24939009401](https://github.com/openclaw/clawsweeper-repair/actions/runs/24939009401) | +| [#66758](#66758) | close_duplicate | | Apr 26, 2026, 01:36 UTC | [ghcrawl-143816-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143816-autonomous-smoke.md) | [ghcrawl-143816-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143816-autonomous-smoke.md) | [24939009401](https://github.com/openclaw/clawsweeper-repair/actions/runs/24939009401) | +| [#67406](#67406) | close_duplicate | | Apr 26, 2026, 01:36 UTC | [ghcrawl-143816-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143816-autonomous-smoke.md) | [ghcrawl-143816-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143816-autonomous-smoke.md) | [24939009401](https://github.com/openclaw/clawsweeper-repair/actions/runs/24939009401) | +| [#50541](#50541) | close_duplicate | | Apr 26, 2026, 01:36 UTC | [ghcrawl-143815-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143815-autonomous-smoke.md) | [ghcrawl-143815-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143815-autonomous-smoke.md) | [24939008778](https://github.com/openclaw/clawsweeper-repair/actions/runs/24939008778) | +| [#50691](#50691) | close_duplicate | | Apr 26, 2026, 01:36 UTC | [ghcrawl-143815-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143815-autonomous-smoke.md) | [ghcrawl-143815-autonomous-smoke](https://github.com/openclaw/clawsweeper/blob/main/results/openclaw/ghcrawl-143815-autonomous-smoke.md) | [24939008778](https://github.com/openclaw/clawsweeper-repair/actions/runs/24939008778) | + diff --git a/package.json b/package.json new file mode 100644 index 0000000000..0b61ec36e8 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "@openclaw/clawsweeper-dashboard", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "render": "node scripts/render.mjs", + "check": "node --check scripts/*.mjs && node --test test/*.test.mjs" + }, + "engines": { + "node": ">=24" + }, + "packageManager": "pnpm@10.33.2" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000000..9b60ae1782 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,9 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} diff --git a/scripts/markdown.mjs b/scripts/markdown.mjs new file mode 100644 index 0000000000..62bbddba70 --- /dev/null +++ b/scripts/markdown.mjs @@ -0,0 +1,44 @@ +export function tableCell(value) { + return String(value ?? "").replaceAll("|", "\\|").replace(/\s+/g, " ").trim(); +} + +export function link(label, url) { + return `[${tableCell(label)}](${url})`; +} + +export function githubPath(path) { + return String(path) + .split("/") + .map((segment) => encodeURIComponent(segment)) + .join("/"); +} + +export function formatTimestamp(value) { + if (!value) return "unknown"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return String(value); + return new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false, + timeZone: "UTC", + timeZoneName: "short", + }).format(date); +} + +export function percent(value, total) { + return total > 0 ? `${((value / total) * 100).toFixed(1)}%` : "0.0%"; +} + +export function truncate(value, max = 110) { + const text = tableCell(value); + if (text.length <= max) return text; + return `${text.slice(0, Math.max(0, max - 3)).trimEnd()}...`; +} + +export function rowsOrNone(rows, columns) { + return rows.length ? rows.join("\n") : `| _None_ |${" |".repeat(columns - 1)}`; +} diff --git a/scripts/render.mjs b/scripts/render.mjs new file mode 100644 index 0000000000..539d52ce73 --- /dev/null +++ b/scripts/render.mjs @@ -0,0 +1,40 @@ +#!/usr/bin/env node +import path from "node:path"; +import { renderRepairDashboard } from "./repair-dashboard.mjs"; +import { writeText } from "./source.mjs"; +import { renderSweepDashboard } from "./sweep-dashboard.mjs"; + +const args = parseArgs(process.argv.slice(2)); +const sourceRoot = path.resolve(args.source ?? process.env.CLAWSWEEPER_SOURCE ?? "../clawsweeper"); +const output = path.resolve(args.output ?? "README.md"); + +const body = `# ClawSweeper Dashboard + +Generated dashboard for [` + + `openclaw/clawsweeper](https://github.com/openclaw/clawsweeper). + +${renderSweepDashboard(sourceRoot)} + +${renderRepairDashboard(sourceRoot)} +`; + +writeText(output, body); +console.log(JSON.stringify({ output, source: sourceRoot })); + +function parseArgs(argv) { + const parsed = {}; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--") continue; + if (arg === "--source") parsed.source = requiredValue(argv, ++index, arg); + else if (arg === "--output") parsed.output = requiredValue(argv, ++index, arg); + else throw new Error(`Unknown argument: ${arg}`); + } + return parsed; +} + +function requiredValue(argv, index, flag) { + const value = argv[index]; + if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value`); + return value; +} diff --git a/scripts/repair-dashboard.mjs b/scripts/repair-dashboard.mjs new file mode 100644 index 0000000000..2d3ddc4bf2 --- /dev/null +++ b/scripts/repair-dashboard.mjs @@ -0,0 +1,216 @@ +import path from "node:path"; +import { formatTimestamp, link, percent, rowsOrNone, tableCell, truncate } from "./markdown.mjs"; +import { jsonFiles, newestTimestamp, readJson } from "./source.mjs"; + +const REPORT_REPO = "https://github.com/openclaw/clawsweeper"; +const CLOSE_ACTIONS = new Set([ + "close", + "close_duplicate", + "close_superseded", + "close_fixed_by_candidate", + "close_low_signal", + "post_merge_close", +]); +const MERGE_ACTIONS = new Set(["merge_candidate", "merge_canonical"]); + +export function renderRepairDashboard(root) { + const records = jsonFiles(path.join(root, "results", "runs")) + .map((file) => readJson(file)) + .filter(Boolean); + const archivedRaw = readJson(path.join(root, "results", "archived-clusters.json"), []); + const archived = new Set( + Array.isArray(archivedRaw) + ? archivedRaw + : (archivedRaw.archived_clusters ?? []).map((record) => record.cluster_id), + ); + const latest = latestByCluster(records).filter((record) => !archived.has(record.cluster_id)); + const allApplyRows = records.flatMap((record) => + (record.apply_actions ?? []).map((action) => ({ record, action })), + ); + const latestApplyRows = latest.flatMap((record) => + (record.apply_actions ?? []).map((action) => ({ record, action })), + ); + const latestFixRows = latest.flatMap((record) => + (record.fix_actions ?? []).map((action) => ({ record, action })), + ); + const mutationRows = allApplyRows.filter((row) => + ["executed", "blocked", "skipped"].includes(String(row.action.status ?? "")), + ); + const closedRows = allApplyRows + .filter((row) => row.action.status === "executed" && CLOSE_ACTIONS.has(String(row.action.action))) + .sort(newestActionFirst); + const mergedRows = allApplyRows.filter( + (row) => row.action.status === "executed" && MERGE_ACTIONS.has(String(row.action.action)), + ); + const blockedRows = latestApplyRows.filter((row) => + ["blocked", "skipped"].includes(String(row.action.status ?? "")), + ); + const failedFixRows = latestFixRows.filter((row) => + ["blocked", "failed"].includes(String(row.action.status ?? "")), + ); + const inspectionRows = inspectionQueue(latest, failedFixRows, blockedRows); + const totals = { + latest: latest.length, + runs: records.length, + success: latest.filter((record) => record.workflow_conclusion === "success").length, + failure: latest.filter((record) => record.workflow_conclusion === "failure").length, + cancelled: latest.filter((record) => record.workflow_conclusion === "cancelled").length, + needsHuman: latest.filter((record) => (record.needs_human ?? []).length > 0).length, + closed: closedRows.length, + merged: mergedRows.length, + blocked: allApplyRows.filter((row) => row.action.status === "blocked").length, + skipped: allApplyRows.filter((row) => row.action.status === "skipped").length, + fixFailed: latestFixRows.filter((row) => row.action.status === "failed").length, + fixBlocked: latestFixRows.filter((row) => row.action.status === "blocked").length, + mutationAttempts: mutationRows.length, + }; + const lastSourceUpdate = newestTimestamp(...records.map((record) => record.published_at)); + + return `## Repair Dashboard + +Last source update: ${formatTimestamp(lastSourceUpdate)} + +State: ${repairState(totals, inspectionRows)} + +| Metric | Count | Rate | +| --- | ---: | ---: | +| Latest clusters reviewed | ${totals.latest} | 100% | +| Run attempts archived | ${totals.runs} | audit | +| Latest successful clusters | ${totals.success} | ${percent(totals.success, totals.latest)} | +| Latest failed clusters | ${totals.failure} | ${percent(totals.failure, totals.latest)} | +| Latest cancelled clusters | ${totals.cancelled} | ${percent(totals.cancelled, totals.latest)} | +| Needs-human clusters | ${totals.needsHuman} | ${percent(totals.needsHuman, totals.latest)} | +| Fix actions failed | ${totals.fixFailed} | ${percent(totals.fixFailed, latestFixRows.length)} | +| Fix actions blocked | ${totals.fixBlocked} | ${percent(totals.fixBlocked, latestFixRows.length)} | +| Completed close actions | ${totals.closed} | ${percent(totals.closed, totals.mutationAttempts)} | +| Completed merge actions | ${totals.merged} | ${percent(totals.merged, totals.mutationAttempts)} | +| Blocked mutation attempts | ${totals.blocked} | ${percent(totals.blocked, totals.mutationAttempts)} | +| Skipped mutation attempts | ${totals.skipped} | ${percent(totals.skipped, totals.mutationAttempts)} | + +### Clusters Needing Inspection + +| Cluster | State | Reason | Report | Run | +| --- | --- | --- | --- | --- | +${rowsOrNone(inspectionRows.slice(0, 30).map(inspectionRow), 5)} + +### Fix Failure Queue + +| Cluster | Status | Target | Branch/PR | Reason | Run | +| --- | --- | --- | --- | --- | --- | +${rowsOrNone(failedFixRows.slice(0, 25).map(fixRow), 6)} + +### Top Blocked Reasons + +| Reason | Latest count | Example cluster | +| --- | ---: | --- | +${rowsOrNone(blockedReasonRows(blockedRows), 3)} + +### Latest Repair Closures + +| Target | Action | Title | Closed | Cluster | Report | Run | +| --- | --- | --- | --- | --- | --- | --- | +${rowsOrNone(closedRows.slice(0, 25).map(closeRow), 7)} +`; +} + +function latestByCluster(records) { + const byCluster = new Map(); + for (const record of records) { + const key = String(record.cluster_id ?? ""); + const previous = byCluster.get(key); + if (!previous || Date.parse(record.published_at ?? "") > Date.parse(previous.published_at ?? "")) { + byCluster.set(key, record); + } + } + return [...byCluster.values()].sort( + (a, b) => Date.parse(b.published_at ?? "") - Date.parse(a.published_at ?? ""), + ); +} + +function repairState(totals, inspectionRows) { + if (totals.failure > 0) return "Failed clusters need inspection"; + if (totals.fixFailed + totals.fixBlocked > 0) return "Fix execution needs repair"; + if (inspectionRows.length > 0) return "Inspection needed"; + return "Clean"; +} + +function inspectionQueue(latest, failedFixRows, blockedRows) { + const rows = []; + const seen = new Set(); + for (const row of [...failedFixRows, ...blockedRows]) { + const key = String(row.record.cluster_id ?? ""); + if (seen.has(key)) continue; + seen.add(key); + rows.push({ + record: row.record, + state: row.action.status === "failed" ? "fix failed" : `${row.action.action ?? "action"} ${row.action.status}`, + reason: row.action.reason ?? row.record.summary ?? "", + }); + } + for (const record of latest) { + const key = String(record.cluster_id ?? ""); + if (seen.has(key) || (record.needs_human ?? []).length === 0) continue; + seen.add(key); + rows.push({ + record, + state: "needs human", + reason: (record.needs_human ?? []).join("; "), + }); + } + return rows.sort( + (a, b) => Date.parse(b.record.published_at ?? "") - Date.parse(a.record.published_at ?? ""), + ); +} + +function blockedReasonRows(rows) { + const byReason = new Map(); + for (const row of rows) { + const reason = truncate(row.action.reason ?? "unspecified", 140); + const current = byReason.get(reason) ?? { count: 0, row }; + current.count += 1; + byReason.set(reason, current); + } + return [...byReason.entries()] + .sort((a, b) => b[1].count - a[1].count) + .slice(0, 15) + .map(([reason, value]) => `| ${reason} | ${value.count} | ${clusterLink(value.row.record)} |`); +} + +function newestActionFirst(left, right) { + const leftTime = Date.parse(left.action.closed_at ?? left.action.merged_at ?? left.record.published_at ?? ""); + const rightTime = Date.parse(right.action.closed_at ?? right.action.merged_at ?? right.record.published_at ?? ""); + return rightTime - leftTime; +} + + +function clusterLink(record) { + const owner = String(record.repo ?? "unknown/unknown").split("/")[0] || "unknown"; + const cluster = String(record.cluster_id ?? ""); + const slug = cluster.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); + return link(cluster || "unknown", `${REPORT_REPO}/blob/main/results/${owner}/${slug}.md`); +} + +function runLink(record) { + return record.run_url ? link(record.run_id ?? "run", record.run_url) : "_none_"; +} + +function targetLink(action) { + const target = String(action.target ?? ""); + const match = target.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(issues|pull)\/(\d+)/); + if (match) return link(`#${match[3]}`, target); + return target ? link(target, target) : ""; +} + +function inspectionRow(row) { + return `| ${clusterLink(row.record)} | ${tableCell(row.state)} | ${truncate(row.reason, 150)} | ${clusterLink(row.record)} | ${runLink(row.record)} |`; +} + +function fixRow(row) { + const action = row.action; + return `| ${clusterLink(row.record)} | ${tableCell(action.status)} | ${targetLink(action)} | ${tableCell(action.branch ?? action.pr ?? "")} | ${truncate(action.reason, 150)} | ${runLink(row.record)} |`; +} + +function closeRow(row) { + const action = row.action; + return `| ${targetLink(action)} | ${tableCell(action.action)} | ${truncate(action.title ?? "")} | ${formatTimestamp(action.closed_at ?? action.merged_at ?? row.record.published_at)} | ${clusterLink(row.record)} | ${clusterLink(row.record)} | ${runLink(row.record)} |`; +} diff --git a/scripts/source.mjs b/scripts/source.mjs new file mode 100644 index 0000000000..62e8f5b497 --- /dev/null +++ b/scripts/source.mjs @@ -0,0 +1,91 @@ +import fs from "node:fs"; +import path from "node:path"; + +export const PROFILES = [ + { + slug: "openclaw-openclaw", + displayName: "OpenClaw", + repo: "openclaw/openclaw", + }, + { + slug: "openclaw-clawhub", + displayName: "ClawHub", + repo: "openclaw/clawhub", + }, + { + slug: "openclaw-clawsweeper", + displayName: "ClawSweeper", + repo: "openclaw/clawsweeper", + }, +]; + +export function profileForSlug(slug) { + return PROFILES.find((profile) => profile.slug === slug); +} + +export function readText(file) { + return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : ""; +} + +export function readJson(file, fallback = null) { + if (!fs.existsSync(file)) return fallback; + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +export function writeText(file, text) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, text, "utf8"); +} + +export function markdownFiles(dir) { + if (!fs.existsSync(dir)) return []; + return fs + .readdirSync(dir) + .filter((name) => name.endsWith(".md")) + .sort() + .map((name) => path.join(dir, name)); +} + +export function jsonFiles(dir) { + if (!fs.existsSync(dir)) return []; + return fs + .readdirSync(dir) + .filter((name) => name.endsWith(".json")) + .sort() + .map((name) => path.join(dir, name)); +} + +export function parseFrontMatter(markdown) { + const match = markdown.match(/^---\n([\s\S]*?)\n---/); + const body = {}; + if (!match?.[1]) return body; + for (const line of match[1].split(/\r?\n/)) { + const pair = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); + if (!pair) continue; + body[pair[1]] = parseFrontMatterScalar(pair[2]); + } + return body; +} + +function parseFrontMatterScalar(value) { + const trimmed = String(value ?? "").trim(); + if (!trimmed) return ""; + try { + return JSON.parse(trimmed); + } catch { + return trimmed.replace(/^"|"$/g, ""); + } +} + +export function relativePath(root, file) { + return path.relative(root, file).split(path.sep).join("/"); +} + +export function numberFromFile(file) { + const match = path.basename(file).match(/(\d+)\.md$/); + return match ? Number(match[1]) : 0; +} + +export function newestTimestamp(...values) { + return values.filter(Boolean).sort((a, b) => Date.parse(b) - Date.parse(a))[0] ?? ""; +} diff --git a/scripts/sweep-dashboard.mjs b/scripts/sweep-dashboard.mjs new file mode 100644 index 0000000000..ba2bbac507 --- /dev/null +++ b/scripts/sweep-dashboard.mjs @@ -0,0 +1,273 @@ +import path from "node:path"; +import { formatTimestamp, link, percent, rowsOrNone, tableCell, truncate } from "./markdown.mjs"; +import { + PROFILES, + markdownFiles, + newestTimestamp, + numberFromFile, + parseFrontMatter, + profileForSlug, + readJson, + readText, + relativePath, +} from "./source.mjs"; + +const FRESH_DAYS = 7; +const DAY_MS = 24 * 60 * 60 * 1000; +const REPORT_REPO = "https://github.com/openclaw/clawsweeper"; + +export function renderSweepDashboard(root) { + const snapshots = PROFILES.map((profile) => sweepSnapshot(root, profile)); + const totals = snapshots.reduce( + (acc, snapshot) => { + acc.openRecords += snapshot.openRecords; + acc.closedRecords += snapshot.closedRecords; + acc.fresh += snapshot.fresh; + acc.proposedClose += snapshot.proposedClose; + acc.workCandidates += snapshot.workCandidates; + acc.failedOrStale += snapshot.failedOrStale; + return acc; + }, + { + openRecords: 0, + closedRecords: 0, + fresh: 0, + proposedClose: 0, + workCandidates: 0, + failedOrStale: 0, + }, + ); + const recent = snapshots + .flatMap((snapshot) => snapshot.recent) + .sort((a, b) => Date.parse(b.reviewedAt ?? "") - Date.parse(a.reviewedAt ?? "")) + .slice(0, 15); + const closed = snapshots + .flatMap((snapshot) => snapshot.closed) + .sort((a, b) => Date.parse(b.closedAt ?? "") - Date.parse(a.closedAt ?? "")) + .slice(0, 15); + const work = snapshots + .flatMap((snapshot) => snapshot.work) + .sort( + (a, b) => + priorityScore(b.workPriority) - priorityScore(a.workPriority) || + Date.parse(b.reviewedAt ?? "") - Date.parse(a.reviewedAt ?? ""), + ) + .slice(0, 20); + const lastSourceUpdate = newestTimestamp( + ...snapshots.flatMap((snapshot) => [ + snapshot.status?.updated_at, + snapshot.audit?.generatedAt, + snapshot.lastReviewAt, + snapshot.lastCloseAt, + ]), + ); + + return `## Sweep Dashboard + +Last source update: ${formatTimestamp(lastSourceUpdate)} + +### Fleet + +| Metric | Count | +| --- | ---: | +| Covered repositories | ${snapshots.length} | +| Open review records | ${totals.openRecords} | +| Archived closed records | ${totals.closedRecords} | +| Fresh reviews, ${FRESH_DAYS}d | ${totals.fresh} | +| Proposed closes awaiting apply | ${totals.proposedClose} | +| Work candidates awaiting promotion | ${totals.workCandidates} | +| Failed or stale reviews | ${totals.failedOrStale} | + +### Current Runs + +| Repository | State | Updated | Run | +| --- | --- | --- | --- | +${rowsOrNone(snapshots.map(statusRow), 4)} + +### Repositories + +| Repository | Open records | Archived | Fresh | Proposed closes | Work candidates | Failed/stale | Last review | Last close | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | --- | +${rowsOrNone(snapshots.map(repositoryRow), 9)} + +### Work Candidates + +| Repository | Item | Title | Priority | Reviewed | Report | +| --- | --- | --- | --- | --- | --- | +${rowsOrNone(work.map(workRow), 6)} + +### Recently Closed + +| Repository | Item | Title | Reason | Closed | Report | +| --- | --- | --- | --- | --- | --- | +${rowsOrNone(closed.map(closedRow), 6)} + +
+Recently Reviewed + +| Repository | Item | Title | Outcome | Status | Reviewed | +| --- | --- | --- | --- | --- | --- | +${rowsOrNone(recent.map(recentRow), 6)} + +
+ +### Audit Health + +| Repository | Status | Last audit | Missing eligible | Stale records | Protected proposed | Scan complete | +| --- | --- | --- | ---: | ---: | ---: | --- | +${rowsOrNone(snapshots.map(auditRow), 7)} +`; +} + +function sweepSnapshot(root, profile) { + const itemsDir = path.join(root, "records", profile.slug, "items"); + const closedDir = path.join(root, "records", profile.slug, "closed"); + const status = readJson(path.join(root, "results", "sweep-status", `${profile.slug}.json`), { + state: "Idle", + detail: "No workflow status has been published yet.", + updated_at: "", + run_url: null, + }); + const audit = readJson(path.join(root, "results", "audit", `${profile.slug}.json`), null); + const items = markdownFiles(itemsDir).map((file) => recordFromFile(root, file, profile, false)); + const closedItems = markdownFiles(closedDir).map((file) => + recordFromFile(root, file, profile, true), + ); + const fresh = items.filter(isFresh).length; + const proposedClose = items.filter( + (item) => isFresh(item) && item.decision === "close" && item.action === "proposed_close", + ).length; + const work = items.filter( + (item) => + isFresh(item) && + item.workCandidate === "queue_fix_pr" && + item.workStatus === "candidate", + ); + return { + profile, + status, + audit, + openRecords: items.length, + closedRecords: closedItems.length, + fresh, + proposedClose, + workCandidates: work.length, + failedOrStale: items.filter( + (item) => item.reviewStatus === "failed" || item.reviewStatus.startsWith("stale_"), + ).length, + lastReviewAt: newestTimestamp(...items.map((item) => item.reviewedAt)), + lastCloseAt: newestTimestamp(...closedItems.map((item) => item.closedAt)), + recent: items + .filter((item) => item.reviewedAt) + .sort((a, b) => Date.parse(b.reviewedAt) - Date.parse(a.reviewedAt)) + .slice(0, 15), + closed: closedItems.filter((item) => item.closedAt).slice(0, 15), + work, + }; +} + +function recordFromFile(root, file, fallbackProfile, archived) { + const frontMatter = parseFrontMatter(readText(file)); + const profile = profileForSlug(path.basename(path.dirname(path.dirname(file)))) ?? fallbackProfile; + const repo = String(frontMatter.repository ?? profile.repo); + const kind = String(frontMatter.type ?? "issue"); + const action = String(frontMatter.action_taken ?? "unknown"); + const currentState = String(frontMatter.current_state ?? ""); + return { + profile, + repo, + number: numberFromFile(file), + kind, + title: String(frontMatter.title ?? ""), + reviewedAt: String(frontMatter.reviewed_at ?? ""), + decision: String(frontMatter.decision ?? "unknown"), + action, + reviewStatus: String(frontMatter.review_status ?? ""), + workCandidate: String(frontMatter.work_candidate ?? "none"), + workPriority: String(frontMatter.work_priority ?? "low"), + workStatus: String(frontMatter.work_status ?? "none"), + closeReason: closeReason(frontMatter, action, currentState), + closedAt: dashboardClosedAt(frontMatter, action, currentState), + reportPath: relativePath(root, file), + archived, + }; +} + +function isFresh(item) { + const reviewedAt = Date.parse(item.reviewedAt); + return Number.isFinite(reviewedAt) && Date.now() - reviewedAt < FRESH_DAYS * DAY_MS; +} + +function dashboardClosedAt(frontMatter, action, currentState) { + if (frontMatter.applied_at) return String(frontMatter.applied_at); + if (frontMatter.current_item_closed_at) return String(frontMatter.current_item_closed_at); + if (currentState === "closed") return String(frontMatter.reconciled_at ?? ""); + if (action === "skipped_already_closed") return String(frontMatter.apply_checked_at ?? ""); + return ""; +} + +function closeReason(frontMatter, action, currentState) { + if (action === "closed") return String(frontMatter.close_reason ?? "closed"); + if (action === "skipped_already_closed") return "already closed before apply"; + if (currentState === "closed") return "closed externally after review"; + return String(frontMatter.close_reason ?? ""); +} + +function priorityScore(priority) { + if (priority === "high") return 3; + if (priority === "medium") return 2; + if (priority === "low") return 1; + return 0; +} + +function repoLink(repo) { + return link(repo, `https://github.com/${repo}`); +} + +function itemLink(item) { + const segment = item.kind === "pull_request" ? "pull" : "issues"; + return link(`#${item.number}`, `https://github.com/${item.repo}/${segment}/${item.number}`); +} + +function reportLink(item) { + return link(item.reportPath, `${REPORT_REPO}/blob/main/${item.reportPath}`); +} + +function statusRow(snapshot) { + const status = snapshot.status ?? {}; + const run = status.run_url ? link("run", status.run_url) : "_none_"; + return `| ${repoLink(snapshot.profile.repo)} | ${tableCell(status.state ?? "Idle")} | ${formatTimestamp(status.updated_at)} | ${run} |`; +} + +function repositoryRow(snapshot) { + return `| ${repoLink(snapshot.profile.repo)} | ${snapshot.openRecords} | ${snapshot.closedRecords} | ${snapshot.fresh} | ${snapshot.proposedClose} | ${snapshot.workCandidates} | ${snapshot.failedOrStale} | ${formatTimestamp(snapshot.lastReviewAt)} | ${formatTimestamp(snapshot.lastCloseAt)} |`; +} + +function workRow(item) { + return `| ${repoLink(item.repo)} | ${itemLink(item)} | ${truncate(item.title)} | ${item.workPriority} | ${formatTimestamp(item.reviewedAt)} | ${reportLink(item)} |`; +} + +function closedRow(item) { + return `| ${repoLink(item.repo)} | ${itemLink(item)} | ${truncate(item.title)} | ${tableCell(item.closeReason)} | ${formatTimestamp(item.closedAt)} | ${reportLink(item)} |`; +} + +function recentRow(item) { + return `| ${repoLink(item.repo)} | ${itemLink(item)} | ${truncate(item.title)} | ${item.decision} / ${item.action} | ${item.reviewStatus} | ${formatTimestamp(item.reviewedAt)} |`; +} + +function auditRow(snapshot) { + const audit = snapshot.audit; + if (!audit) return `| ${repoLink(snapshot.profile.repo)} | _unknown_ | unknown | 0 | 0 | 0 | unknown |`; + const counts = audit.counts ?? {}; + return `| ${repoLink(snapshot.profile.repo)} | ${tableCell(auditStatus(audit))} | ${formatTimestamp(audit.generatedAt)} | ${counts.missingEligibleOpen ?? 0} | ${counts.staleItemRecords ?? 0} | ${counts.protectedProposed ?? 0} | ${audit.scan?.complete ? "yes" : "no"} |`; +} + +function auditStatus(audit) { + const counts = audit.counts ?? {}; + if (!audit.scan?.complete) return "scan incomplete"; + if ((counts.protectedProposed ?? 0) > 0) return "protected proposed closes"; + if ((counts.duplicateRecords ?? 0) > 0) return "duplicate records"; + if ((counts.openArchived ?? 0) > 0) return "open archived records"; + if ((counts.missingEligibleOpen ?? 0) > 0) return "missing records"; + return "clean"; +} diff --git a/test/render.test.mjs b/test/render.test.mjs new file mode 100644 index 0000000000..3a438648d7 --- /dev/null +++ b/test/render.test.mjs @@ -0,0 +1,10 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { formatTimestamp, percent, tableCell } from "../scripts/markdown.mjs"; + +test("markdown helpers keep dashboard tables stable", () => { + assert.equal(tableCell("a | b\nc"), "a \\| b c"); + assert.equal(percent(1, 4), "25.0%"); + assert.equal(percent(1, 0), "0.0%"); + assert.match(formatTimestamp("2026-05-01T05:37:00.000Z"), /May 1, 2026/); +});