Spaces:
Running
Running
Commit Β·
cf2f7da
1
Parent(s): 93f60f8
Add agents file and skills
Browse files- .agents/skills/sync-nowai-leaderboard/SKILL.md +65 -0
- .agents/skills/sync-nowai-leaderboard/lib/teams.mjs +107 -0
- .agents/skills/sync-nowai-leaderboard/lib/transform.mjs +164 -0
- .agents/skills/sync-nowai-leaderboard/package.json +8 -0
- .agents/skills/sync-nowai-leaderboard/sync.mjs +266 -0
- .agents/skills/sync-nowai-leaderboard/test/transform.test.mjs +178 -0
- AGENTS.md +135 -0
.agents/skills/sync-nowai-leaderboard/SKILL.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: sync-nowai-leaderboard
|
| 3 |
+
description: Use when syncing the NowAI-Bench landing-page leaderboards from upstream EOG + EVA sources, or when a scheduled run fires to check for new results. Pulls both sources, re-derives data/leaderboard.json, and pushes to staging only on sane changes.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Sync NowAI-Bench Leaderboard
|
| 7 |
+
|
| 8 |
+
Pulls the two upstream leaderboard sources, re-derives `data/leaderboard.json`,
|
| 9 |
+
and (when run with `--push`) deploys the change to the **staging** HF Space only.
|
| 10 |
+
|
| 11 |
+
## Sources
|
| 12 |
+
- EOG: `https://raw.githubusercontent.com/EnterpriseOps-Gym/EnterpriseOps-Gym.github.io/main/script.js` (`LEADERBOARD_DATA` array)
|
| 13 |
+
- EVA: `https://raw.githubusercontent.com/ServiceNow/eva/main/website/src/data/leaderboardStats.json` (`systems[]`)
|
| 14 |
+
|
| 15 |
+
## How to run
|
| 16 |
+
|
| 17 |
+
From the repo root:
|
| 18 |
+
|
| 19 |
+
1. **Preview (always do this first):**
|
| 20 |
+
`node .Codex/skills/sync-nowai-leaderboard/sync.mjs --check`
|
| 21 |
+
Prints the diff vs the committed `data/leaderboard.json`. No writes.
|
| 22 |
+
|
| 23 |
+
2. **Inspect the printed diff.** If it looks sane (no garbled names, scores in
|
| 24 |
+
range, no wild swings), proceed. If sanity gates already flagged issues, the
|
| 25 |
+
script aborts β investigate upstream before forcing anything.
|
| 26 |
+
|
| 27 |
+
3. **Apply + deploy to staging:**
|
| 28 |
+
`node .Codex/skills/sync-nowai-leaderboard/sync.mjs --push`
|
| 29 |
+
Writes the file, commits, and pushes to `origin` (staging). Posts an Adaptive
|
| 30 |
+
Card to Teams (success / failure / abort) if `TEAMS_WEBHOOK_URL` is set.
|
| 31 |
+
|
| 32 |
+
To commit locally without deploying, use `--apply` instead of `--push`.
|
| 33 |
+
|
| 34 |
+
## Guardrails (NON-NEGOTIABLE)
|
| 35 |
+
- **Push to `origin` (staging) ONLY. NEVER push to `prod`.** Promotion to prod is
|
| 36 |
+
a human decision made after reviewing staging. The script refuses to push
|
| 37 |
+
unless `origin` resolves to the staging Space (`NowAI-Bench-Staging`).
|
| 38 |
+
- The script exits non-zero and notifies (does not commit) if: a fetch fails, a
|
| 39 |
+
source fails to parse, any board has <3 rows, any score is out of range
|
| 40 |
+
(EOG 0β100, EVA 0β1), or the top score swings >25 (EOG) / >0.25 (EVA) vs the
|
| 41 |
+
committed data. Do not override these by editing the JSON by hand to bypass.
|
| 42 |
+
|
| 43 |
+
## Tests
|
| 44 |
+
`cd .Codex/skills/sync-nowai-leaderboard && node --test`
|
| 45 |
+
|
| 46 |
+
## Teams notifications
|
| 47 |
+
- Set `TEAMS_WEBHOOK_URL` (secret/env var, never committed β keep it in the
|
| 48 |
+
gitignored `.env`) to enable. The URL is a Power Automate "Workflows" webhook
|
| 49 |
+
(`*.powerplatform.com`), so `lib/teams.mjs` posts an **Adaptive Card** in the
|
| 50 |
+
`message/attachments` envelope that template expects β not a raw MessageCard.
|
| 51 |
+
- Cards: green β
success (with "Open staging" button), red β failure, amber β οΈ
|
| 52 |
+
sanity-gate abort, blue π Friday heartbeat (no changes detected β shows last
|
| 53 |
+
date `data/leaderboard.json` actually changed). No card on "no changes" MonβThu.
|
| 54 |
+
- The daily 14:00 launchd job (`~/.nowai-sync/`) loads `TEAMS_WEBHOOK_URL` from
|
| 55 |
+
`.env` and also fires a local macOS desktop notification for every outcome.
|
| 56 |
+
|
| 57 |
+
## Logs
|
| 58 |
+
|
| 59 |
+
Every launchd run appends to `~/Library/Logs/nowai-sync.log`. Each entry includes
|
| 60 |
+
timestamp, `sync exit=<code>`, full sync output, and a `RESULT:` summary line.
|
| 61 |
+
Tail it to confirm the job is running during quiet weeks with no upstream changes:
|
| 62 |
+
|
| 63 |
+
```
|
| 64 |
+
tail -50 ~/Library/Logs/nowai-sync.log
|
| 65 |
+
```
|
.agents/skills/sync-nowai-leaderboard/lib/teams.mjs
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Posts an Adaptive Card to a Microsoft Teams channel via a Power Automate
|
| 2 |
+
// "Workflows" webhook (the *.powerplatform.com / *.logic.azure.com replacement
|
| 3 |
+
// for the retired webhook.office.com Office 365 connector).
|
| 4 |
+
//
|
| 5 |
+
// The default "Post to a channel when a webhook request is received" workflow
|
| 6 |
+
// template expects an Adaptive Card wrapped in a message/attachments envelope β
|
| 7 |
+
// NOT a raw MessageCard. The card below mirrors the look of Plato chat's Teams
|
| 8 |
+
// MessageCard (bold title, colored accent, subtitle, fact table, optional link).
|
| 9 |
+
//
|
| 10 |
+
// Secret comes from TEAMS_WEBHOOK_URL in the environment (never committed).
|
| 11 |
+
|
| 12 |
+
// Adaptive Card TextBlock colors map to outcome semantics.
|
| 13 |
+
const PALETTE = {
|
| 14 |
+
success: 'Good', // green
|
| 15 |
+
info: 'Accent', // blue
|
| 16 |
+
warning: 'Warning', // amber
|
| 17 |
+
error: 'Attention', // red
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
export function buildCard({ kind = 'info', title, subtitle, facts = [], openUrl, openLabel = 'Open staging' }) {
|
| 21 |
+
// Header band: shaded `emphasis` container that bleeds to the card edges, so the
|
| 22 |
+
// colored title + subtitle read as a distinct zone above the fact table (the
|
| 23 |
+
// visual hierarchy of a Teams connector card / Plato chat alert).
|
| 24 |
+
const header = {
|
| 25 |
+
type: 'Container',
|
| 26 |
+
style: 'emphasis',
|
| 27 |
+
bleed: true,
|
| 28 |
+
items: [
|
| 29 |
+
{
|
| 30 |
+
type: 'TextBlock',
|
| 31 |
+
text: title,
|
| 32 |
+
weight: 'Bolder',
|
| 33 |
+
size: 'Large',
|
| 34 |
+
color: PALETTE[kind] ?? 'Default',
|
| 35 |
+
wrap: true,
|
| 36 |
+
spacing: 'None',
|
| 37 |
+
},
|
| 38 |
+
],
|
| 39 |
+
};
|
| 40 |
+
if (subtitle) {
|
| 41 |
+
header.items.push({ type: 'TextBlock', text: subtitle, isSubtle: true, spacing: 'Small', wrap: true });
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
const body = [header];
|
| 45 |
+
if (facts.length) {
|
| 46 |
+
body.push({
|
| 47 |
+
type: 'FactSet',
|
| 48 |
+
spacing: 'Medium',
|
| 49 |
+
separator: true,
|
| 50 |
+
facts: facts.map(([title, value]) => ({ title, value: String(value) })),
|
| 51 |
+
});
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const card = {
|
| 55 |
+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
| 56 |
+
type: 'AdaptiveCard',
|
| 57 |
+
version: '1.4',
|
| 58 |
+
body,
|
| 59 |
+
msteams: { width: 'Full' },
|
| 60 |
+
};
|
| 61 |
+
if (openUrl) {
|
| 62 |
+
card.actions = [{ type: 'Action.OpenUrl', title: openLabel, url: openUrl }];
|
| 63 |
+
}
|
| 64 |
+
return {
|
| 65 |
+
type: 'message',
|
| 66 |
+
attachments: [{ contentType: 'application/vnd.microsoft.card.adaptive', content: card }],
|
| 67 |
+
};
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Returns true on a 2xx post, false otherwise (never throws β notification
|
| 71 |
+
// failure must not mask the underlying sync result).
|
| 72 |
+
export async function postTeams(opts) {
|
| 73 |
+
const url = process.env.TEAMS_WEBHOOK_URL;
|
| 74 |
+
if (!url) {
|
| 75 |
+
console.log('[teams] no TEAMS_WEBHOOK_URL set; skipping card.');
|
| 76 |
+
return false;
|
| 77 |
+
}
|
| 78 |
+
try {
|
| 79 |
+
const res = await fetch(url, {
|
| 80 |
+
method: 'POST',
|
| 81 |
+
headers: { 'Content-Type': 'application/json' },
|
| 82 |
+
body: JSON.stringify(buildCard(opts)),
|
| 83 |
+
});
|
| 84 |
+
if (!res.ok) {
|
| 85 |
+
console.error(`[teams] HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
| 86 |
+
return false;
|
| 87 |
+
}
|
| 88 |
+
return true;
|
| 89 |
+
} catch (e) {
|
| 90 |
+
console.error('[teams] post failed: ' + e.message);
|
| 91 |
+
return false;
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// CLI (manual testing only): node teams.mjs <kind> <title> <subtitle> [name=value ...] [--open URL]
|
| 96 |
+
const isMain = import.meta.url === `file://${process.argv[1]}`;
|
| 97 |
+
if (isMain) {
|
| 98 |
+
const argv = process.argv.slice(2);
|
| 99 |
+
let openUrl;
|
| 100 |
+
const openIdx = argv.indexOf('--open');
|
| 101 |
+
if (openIdx !== -1) { openUrl = argv[openIdx + 1]; argv.splice(openIdx, 2); }
|
| 102 |
+
const [kind, title, subtitle, ...rest] = argv;
|
| 103 |
+
const facts = rest.map((s) => { const i = s.indexOf('='); return [s.slice(0, i), s.slice(i + 1)]; });
|
| 104 |
+
const ok = await postTeams({ kind, title, subtitle, facts, openUrl });
|
| 105 |
+
console.log(ok ? '[teams] posted.' : '[teams] not posted.');
|
| 106 |
+
process.exit(ok ? 0 : 1);
|
| 107 |
+
}
|
.agents/skills/sync-nowai-leaderboard/lib/transform.mjs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Pure transforms: parse upstream sources and derive our leaderboard schema.
|
| 2 |
+
// No network or filesystem access lives here.
|
| 3 |
+
|
| 4 |
+
export function parseEogData(scriptText) {
|
| 5 |
+
const start = scriptText.indexOf('LEADERBOARD_DATA');
|
| 6 |
+
if (start === -1) throw new Error('LEADERBOARD_DATA not found in EOG script');
|
| 7 |
+
const open = scriptText.indexOf('[', start);
|
| 8 |
+
if (open === -1) throw new Error('EOG array open bracket not found');
|
| 9 |
+
|
| 10 |
+
// Match brackets to find the array literal end. Model names contain no [ or ].
|
| 11 |
+
let depth = 0;
|
| 12 |
+
let end = -1;
|
| 13 |
+
for (let i = open; i < scriptText.length; i++) {
|
| 14 |
+
const c = scriptText[i];
|
| 15 |
+
if (c === '[') depth++;
|
| 16 |
+
else if (c === ']') {
|
| 17 |
+
depth--;
|
| 18 |
+
if (depth === 0) { end = i; break; }
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
if (end === -1) throw new Error('EOG array close bracket not found');
|
| 22 |
+
|
| 23 |
+
let body = scriptText.slice(open, end + 1);
|
| 24 |
+
body = body.replace(/\/\/[^\n]*/g, ''); // strip line comments
|
| 25 |
+
body = body.replace(/([{,]\s*)([A-Za-z_]\w*)\s*:/g, '$1"$2":'); // quote keys
|
| 26 |
+
body = body.replace(/,(\s*[}\]])/g, '$1'); // strip trailing commas
|
| 27 |
+
|
| 28 |
+
try {
|
| 29 |
+
return JSON.parse(body);
|
| 30 |
+
} catch (e) {
|
| 31 |
+
throw new Error('EOG array parse failed: ' + e.message);
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
const round1 = (n) => Math.round(n * 10) / 10;
|
| 36 |
+
|
| 37 |
+
const VENDOR_MAP = [
|
| 38 |
+
[/gemini/i, 'Google'],
|
| 39 |
+
[/gpt|openai|\brealtime\b/i, 'OpenAI'],
|
| 40 |
+
[/whisper/i, 'OpenAI'],
|
| 41 |
+
[/claude/i, 'Anthropic'],
|
| 42 |
+
[/nova/i, 'Amazon'],
|
| 43 |
+
[/qwen/i, 'Alibaba'],
|
| 44 |
+
[/deepseek/i, 'DeepSeek'],
|
| 45 |
+
[/llama/i, 'Meta'],
|
| 46 |
+
];
|
| 47 |
+
|
| 48 |
+
export function vendorFromName(name) {
|
| 49 |
+
for (const [re, vendor] of VENDOR_MAP) if (re.test(name)) return vendor;
|
| 50 |
+
return name.split(/[\s+]/)[0];
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export function deriveEog(eogArray) {
|
| 54 |
+
const sorted = [...eogArray].sort((a, b) => b.avg - a.avg).slice(0, 5);
|
| 55 |
+
return {
|
| 56 |
+
metricLabel: 'Task Success Rate Β· Oracle mode',
|
| 57 |
+
rows: sorted.map((m, i) => ({
|
| 58 |
+
rank: i + 1,
|
| 59 |
+
model: m.model,
|
| 60 |
+
org: vendorFromName(m.model),
|
| 61 |
+
type: m.type,
|
| 62 |
+
score: round1(m.avg),
|
| 63 |
+
// bar = absolute % of max. EOG score is already a 0β100 success rate.
|
| 64 |
+
bar: Math.round(m.avg),
|
| 65 |
+
})),
|
| 66 |
+
};
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const round2 = (n) => Math.round(n * 100) / 100;
|
| 70 |
+
|
| 71 |
+
export function parseEvaData(jsonText) {
|
| 72 |
+
const data = JSON.parse(jsonText);
|
| 73 |
+
if (!Array.isArray(data.systems)) throw new Error('EVA data missing systems[] array');
|
| 74 |
+
return data;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
export function deriveEvaBoard(systems, metricKey) {
|
| 78 |
+
const scored = systems
|
| 79 |
+
.map((s) => ({ s, pooled: s.clean?.[metricKey]?.pooled }))
|
| 80 |
+
.filter((x) => x.pooled && typeof x.pooled.point === 'number')
|
| 81 |
+
.sort((a, b) => b.pooled.point - a.pooled.point)
|
| 82 |
+
.slice(0, 3);
|
| 83 |
+
return {
|
| 84 |
+
metricLabel: 'Pass@1',
|
| 85 |
+
rows: scored.map(({ s, pooled }, i) => ({
|
| 86 |
+
rank: i + 1,
|
| 87 |
+
name: s.name,
|
| 88 |
+
subtitle:
|
| 89 |
+
s.type === 'cascade'
|
| 90 |
+
? 'Mixed Models Β· Cascade'
|
| 91 |
+
: `${vendorFromName(s.name)} Β· Speech-to-Speech`,
|
| 92 |
+
score: round2(pooled.point),
|
| 93 |
+
ciLower: round2(pooled.ci_lower),
|
| 94 |
+
ciUpper: round2(pooled.ci_upper),
|
| 95 |
+
// bar = absolute % of max. EVA Pass@1 is a 0β1 proportion.
|
| 96 |
+
bar: Math.round(pooled.point * 100),
|
| 97 |
+
})),
|
| 98 |
+
};
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
export function buildLeaderboard({ eogScript, evaJson, sources, now }) {
|
| 102 |
+
const eogArray = parseEogData(eogScript);
|
| 103 |
+
const evaData = parseEvaData(evaJson);
|
| 104 |
+
return {
|
| 105 |
+
updated: now,
|
| 106 |
+
sources,
|
| 107 |
+
eog: deriveEog(eogArray),
|
| 108 |
+
evaAccuracy: deriveEvaBoard(evaData.systems, 'EVA-A_pass'),
|
| 109 |
+
evaExperience: deriveEvaBoard(evaData.systems, 'EVA-X_pass'),
|
| 110 |
+
};
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
const BOARD_KEYS = ['eog', 'evaAccuracy', 'evaExperience'];
|
| 114 |
+
|
| 115 |
+
// Compare only the parts we render (ignore `updated`/`sources` meta).
|
| 116 |
+
export function hasChanges(oldData, newData) {
|
| 117 |
+
if (!oldData) return true;
|
| 118 |
+
const pick = (d) => JSON.stringify(BOARD_KEYS.map((k) => d[k]));
|
| 119 |
+
return pick(oldData) !== pick(newData);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
export function diffBoards(oldData, newData) {
|
| 123 |
+
const label = (r) => r.model ?? r.name;
|
| 124 |
+
const lines = [];
|
| 125 |
+
for (const key of BOARD_KEYS) {
|
| 126 |
+
const oldRows = oldData?.[key]?.rows ?? [];
|
| 127 |
+
const newRows = newData[key].rows;
|
| 128 |
+
const oldStr = oldRows.map((r) => `${label(r)}=${r.score}`).join(', ');
|
| 129 |
+
const newStr = newRows.map((r) => `${label(r)}=${r.score}`).join(', ');
|
| 130 |
+
if (oldStr !== newStr) {
|
| 131 |
+
lines.push(`${key}:\n old: ${oldStr || '(none)'}\n new: ${newStr}`);
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
return lines;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Returns an array of human-readable issue strings; empty array = clean.
|
| 138 |
+
export function validate(newData, oldData) {
|
| 139 |
+
const issues = [];
|
| 140 |
+
const specs = [
|
| 141 |
+
['eog', newData.eog, 0, 100, 25, oldData?.eog],
|
| 142 |
+
['evaAccuracy', newData.evaAccuracy, 0, 1, 0.25, oldData?.evaAccuracy],
|
| 143 |
+
['evaExperience', newData.evaExperience, 0, 1, 0.25, oldData?.evaExperience],
|
| 144 |
+
];
|
| 145 |
+
for (const [name, board, min, max, guard, oldBoard] of specs) {
|
| 146 |
+
if (!board || !Array.isArray(board.rows)) {
|
| 147 |
+
issues.push(`${name}: missing rows`);
|
| 148 |
+
continue;
|
| 149 |
+
}
|
| 150 |
+
if (board.rows.length < 3) issues.push(`${name}: only ${board.rows.length} rows (<3)`);
|
| 151 |
+
for (const r of board.rows) {
|
| 152 |
+
if (r.score < min || r.score > max) {
|
| 153 |
+
issues.push(`${name}: score ${r.score} out of [${min},${max}]`);
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
const oldTop = oldBoard?.rows?.[0]?.score;
|
| 157 |
+
const newTop = board.rows[0]?.score;
|
| 158 |
+
if (typeof oldTop === 'number' && typeof newTop === 'number') {
|
| 159 |
+
const delta = Math.abs(newTop - oldTop);
|
| 160 |
+
if (delta > guard) issues.push(`${name}: top score swing ${delta.toFixed(2)} > ${guard}`);
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
return issues;
|
| 164 |
+
}
|
.agents/skills/sync-nowai-leaderboard/package.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "sync-nowai-leaderboard",
|
| 3 |
+
"private": true,
|
| 4 |
+
"type": "module",
|
| 5 |
+
"scripts": {
|
| 6 |
+
"test": "node --test"
|
| 7 |
+
}
|
| 8 |
+
}
|
.agents/skills/sync-nowai-leaderboard/sync.mjs
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
// Orchestrator for the leaderboard sync. Side effects only; pure logic in lib/transform.mjs.
|
| 3 |
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
| 4 |
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
| 5 |
+
import { createHash } from 'node:crypto';
|
| 6 |
+
import { execFileSync } from 'node:child_process';
|
| 7 |
+
import { join, dirname } from 'node:path';
|
| 8 |
+
import { buildLeaderboard, hasChanges, diffBoards, validate } from './lib/transform.mjs';
|
| 9 |
+
import { postTeams } from './lib/teams.mjs';
|
| 10 |
+
|
| 11 |
+
const STAGING_URL = 'https://huggingface.co/spaces/ServiceNow-AI/NowAI-Bench-Staging';
|
| 12 |
+
|
| 13 |
+
const EOG_URL = 'https://raw.githubusercontent.com/EnterpriseOps-Gym/EnterpriseOps-Gym.github.io/main/script.js';
|
| 14 |
+
const EVA_URL = 'https://raw.githubusercontent.com/ServiceNow/eva/main/website/src/data/leaderboardStats.json';
|
| 15 |
+
|
| 16 |
+
// Guard: `origin` must point at the staging Space. Pushing only ever targets a
|
| 17 |
+
// remote whose URL contains this slug, so a misconfigured `origin` can never
|
| 18 |
+
// deploy to the prod Space (ServiceNow-AI/NowAI-Bench).
|
| 19 |
+
const STAGING_SLUG = 'NowAI-Bench-Staging';
|
| 20 |
+
const FETCH_TIMEOUT_MS = 30_000;
|
| 21 |
+
|
| 22 |
+
function repoRoot() {
|
| 23 |
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim();
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function sha256(text) {
|
| 27 |
+
return 'sha256:' + createHash('sha256').update(text).digest('hex');
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
async function fetchText(url, timeoutMs = FETCH_TIMEOUT_MS) {
|
| 31 |
+
const controller = new AbortController();
|
| 32 |
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
| 33 |
+
try {
|
| 34 |
+
const res = await fetch(url, { signal: controller.signal });
|
| 35 |
+
if (!res.ok) throw new Error(`fetch ${url} β HTTP ${res.status}`);
|
| 36 |
+
return await res.text();
|
| 37 |
+
} finally {
|
| 38 |
+
clearTimeout(timer);
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Shared subtitle line for every Teams card: "NowAI-Bench Sync Β· 2026-06-12 14:00 UTC".
|
| 43 |
+
function subtitle() {
|
| 44 |
+
return 'NowAI-Bench Sync Β· ' + new Date().toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Convenience: an error/abort card with a consistent title.
|
| 48 |
+
function notifyFailure(facts, kind = 'error') {
|
| 49 |
+
return postTeams({
|
| 50 |
+
kind,
|
| 51 |
+
title: kind === 'warning' ? 'β οΈ Leaderboard sync aborted' : 'β Leaderboard sync failed',
|
| 52 |
+
subtitle: subtitle(),
|
| 53 |
+
facts,
|
| 54 |
+
});
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
function git(root, args) {
|
| 58 |
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8' });
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Quarter helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 62 |
+
|
| 63 |
+
function quarterForDate(d) {
|
| 64 |
+
return `${d.getUTCFullYear()}Q${Math.floor(d.getUTCMonth() / 3) + 1}`;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Read index.html, extract the year+quarter label, compare to today's quarter.
|
| 68 |
+
// Returns { needsUpdate, current, expected, html, htmlPath }.
|
| 69 |
+
function detectQuarterLabel(root) {
|
| 70 |
+
const htmlPath = join(root, 'index.html');
|
| 71 |
+
const html = readFileSync(htmlPath, 'utf8');
|
| 72 |
+
const RE = /(\d{4})<span class="text-on-surface-variant text-base font-medium ml-1">(Q[1-4])<\/span>/;
|
| 73 |
+
const m = html.match(RE);
|
| 74 |
+
if (!m) return { needsUpdate: false };
|
| 75 |
+
const current = `${m[1]}${m[2]}`;
|
| 76 |
+
const expected = quarterForDate(new Date());
|
| 77 |
+
return { needsUpdate: current !== expected, current, expected, html, htmlPath };
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function applyQuarterLabel({ html, htmlPath, expected }) {
|
| 81 |
+
const year = expected.slice(0, 4);
|
| 82 |
+
const q = expected.slice(4);
|
| 83 |
+
const updated = html.replace(
|
| 84 |
+
/(\d{4})<span class="text-on-surface-variant text-base font-medium ml-1">(Q[1-4])<\/span>/,
|
| 85 |
+
`${year}<span class="text-on-surface-variant text-base font-medium ml-1">${q}</span>`,
|
| 86 |
+
);
|
| 87 |
+
writeFileSync(htmlPath, updated);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
async function main() {
|
| 91 |
+
const args = new Set(process.argv.slice(2));
|
| 92 |
+
const mode = args.has('--push') ? 'push' : args.has('--apply') ? 'apply' : 'check';
|
| 93 |
+
|
| 94 |
+
const root = repoRoot();
|
| 95 |
+
const dataPath = join(root, 'data', 'leaderboard.json');
|
| 96 |
+
|
| 97 |
+
// 1. Fetch upstream.
|
| 98 |
+
let eogScript, evaJson;
|
| 99 |
+
try {
|
| 100 |
+
[eogScript, evaJson] = await Promise.all([fetchText(EOG_URL), fetchText(EVA_URL)]);
|
| 101 |
+
} catch (e) {
|
| 102 |
+
console.error('[sync] fetch failed: ' + e.message);
|
| 103 |
+
await notifyFailure([['Stage', 'upstream fetch'], ['Error', e.message]]);
|
| 104 |
+
process.exit(1);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// 2. Build new data.
|
| 108 |
+
let newData;
|
| 109 |
+
try {
|
| 110 |
+
newData = buildLeaderboard({
|
| 111 |
+
eogScript,
|
| 112 |
+
evaJson,
|
| 113 |
+
sources: {
|
| 114 |
+
eog: { url: EOG_URL, hash: sha256(eogScript), fetchedAt: new Date().toISOString() },
|
| 115 |
+
eva: { url: EVA_URL, hash: sha256(evaJson), fetchedAt: new Date().toISOString() },
|
| 116 |
+
},
|
| 117 |
+
now: new Date().toISOString(),
|
| 118 |
+
});
|
| 119 |
+
} catch (e) {
|
| 120 |
+
console.error('[sync] parse/derive failed: ' + e.message);
|
| 121 |
+
await notifyFailure([['Stage', 'parse / derive (schema drift?)'], ['Error', e.message]]);
|
| 122 |
+
process.exit(1);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// 3. Load existing.
|
| 126 |
+
let oldData = null;
|
| 127 |
+
if (existsSync(dataPath)) {
|
| 128 |
+
try {
|
| 129 |
+
oldData = JSON.parse(await readFile(dataPath, 'utf8'));
|
| 130 |
+
} catch (e) {
|
| 131 |
+
console.error('[sync] existing leaderboard.json is unreadable: ' + e.message);
|
| 132 |
+
await notifyFailure([['Stage', 'read existing leaderboard.json'], ['Error', e.message]]);
|
| 133 |
+
process.exit(1);
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// 3.5. Detect quarter label mismatch in index.html.
|
| 138 |
+
const quarterInfo = detectQuarterLabel(root);
|
| 139 |
+
if (quarterInfo.needsUpdate) {
|
| 140 |
+
console.log(`[sync] quarter label: ${quarterInfo.current} β ${quarterInfo.expected}`);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// 4. Change detection.
|
| 144 |
+
const leaderboardChanged = hasChanges(oldData, newData);
|
| 145 |
+
|
| 146 |
+
if (!leaderboardChanged && !quarterInfo.needsUpdate) {
|
| 147 |
+
console.log('[sync] no display changes. Done.');
|
| 148 |
+
// Friday heartbeat: post a card so the Teams channel confirms the job is alive
|
| 149 |
+
// even during quiet weeks with no ranking changes.
|
| 150 |
+
if (new Date().getDay() === 5) {
|
| 151 |
+
let lastChanged = 'unknown';
|
| 152 |
+
try {
|
| 153 |
+
lastChanged = git(root, ['log', '-1', '--format=%as', '--', 'data/leaderboard.json']).trim() || 'unknown';
|
| 154 |
+
} catch (_) {}
|
| 155 |
+
await postTeams({
|
| 156 |
+
kind: 'info',
|
| 157 |
+
title: 'π Leaderboard sync β no changes this week',
|
| 158 |
+
subtitle: subtitle(),
|
| 159 |
+
facts: [
|
| 160 |
+
['Status', 'Up to date β no ranking changes detected'],
|
| 161 |
+
['Last changed', lastChanged],
|
| 162 |
+
],
|
| 163 |
+
openUrl: STAGING_URL,
|
| 164 |
+
openLabel: 'Open staging',
|
| 165 |
+
});
|
| 166 |
+
}
|
| 167 |
+
return;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
let changeLines = [];
|
| 171 |
+
if (leaderboardChanged) {
|
| 172 |
+
changeLines = diffBoards(oldData, newData);
|
| 173 |
+
console.log('[sync] changes detected:\n' + changeLines.join('\n'));
|
| 174 |
+
|
| 175 |
+
// 5. Sanity gates (only when leaderboard data changes).
|
| 176 |
+
const issues = validate(newData, oldData);
|
| 177 |
+
if (issues.length) {
|
| 178 |
+
console.error('[sync] sanity gates failed:\n - ' + issues.join('\n - '));
|
| 179 |
+
await notifyFailure(
|
| 180 |
+
[['Reason', 'sanity gates failed'], ...issues.map((s, i) => [`Issue ${i + 1}`, s])],
|
| 181 |
+
'warning',
|
| 182 |
+
);
|
| 183 |
+
process.exit(2);
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
if (mode === 'check') {
|
| 188 |
+
if (quarterInfo.needsUpdate) {
|
| 189 |
+
console.log(`[sync] header quarter: ${quarterInfo.current} β ${quarterInfo.expected}`);
|
| 190 |
+
}
|
| 191 |
+
console.log('[sync] --check: no writes. Re-run with --apply (commit) or --push (commit + staging).');
|
| 192 |
+
return;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// 6. Write + commit (apply and push only; never in check mode).
|
| 196 |
+
if (mode !== 'apply' && mode !== 'push') return;
|
| 197 |
+
|
| 198 |
+
// For push: verify `origin` is staging BEFORE writing/committing, so a push
|
| 199 |
+
// from a wrong checkout can't leave a dangling local commit to roll back.
|
| 200 |
+
if (mode === 'push') {
|
| 201 |
+
const originUrl = git(root, ['remote', 'get-url', 'origin']).trim();
|
| 202 |
+
if (!originUrl.includes(STAGING_SLUG)) {
|
| 203 |
+
console.error(`[sync] refusing to push: origin is not the staging remote (${originUrl}). Nothing written.`);
|
| 204 |
+
await notifyFailure([['Reason', 'push refused β origin is not staging'], ['origin', originUrl]]);
|
| 205 |
+
process.exit(4);
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
if (leaderboardChanged) {
|
| 210 |
+
await mkdir(dirname(dataPath), { recursive: true });
|
| 211 |
+
await writeFile(dataPath, JSON.stringify(newData, null, 2) + '\n');
|
| 212 |
+
git(root, ['add', dataPath]);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
if (quarterInfo.needsUpdate) {
|
| 216 |
+
applyQuarterLabel(quarterInfo);
|
| 217 |
+
git(root, ['add', join(root, 'index.html')]);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
const today = new Date().toISOString().slice(0, 10);
|
| 221 |
+
const parts = [];
|
| 222 |
+
if (leaderboardChanged) parts.push('EOG + EVA');
|
| 223 |
+
if (quarterInfo.needsUpdate) parts.push(`header β ${quarterInfo.expected}`);
|
| 224 |
+
const commitDetail = leaderboardChanged ? '\n\n' + changeLines.join('\n') : '';
|
| 225 |
+
const msg = `Sync leaderboard: ${parts.join(', ')} ${today}${commitDetail}`;
|
| 226 |
+
git(root, ['commit', '-m', msg]);
|
| 227 |
+
console.log('[sync] committed.');
|
| 228 |
+
|
| 229 |
+
// 7. Push (staging only) if requested. Origin already verified as staging above.
|
| 230 |
+
if (mode === 'push') {
|
| 231 |
+
try {
|
| 232 |
+
git(root, ['push', 'origin', 'HEAD:main']);
|
| 233 |
+
console.log('[sync] pushed to origin (staging).');
|
| 234 |
+
const noticeFacts = [];
|
| 235 |
+
if (leaderboardChanged) {
|
| 236 |
+
noticeFacts.push(['Boards changed', String(changeLines.length)]);
|
| 237 |
+
noticeFacts.push(['Detail', changeLines.join(' Β· ')]);
|
| 238 |
+
}
|
| 239 |
+
if (quarterInfo.needsUpdate) {
|
| 240 |
+
noticeFacts.push(['Header updated', `${quarterInfo.current} β ${quarterInfo.expected}`]);
|
| 241 |
+
}
|
| 242 |
+
if (!leaderboardChanged) {
|
| 243 |
+
noticeFacts.push(['Note', 'Quarter label only β no ranking changes']);
|
| 244 |
+
}
|
| 245 |
+
noticeFacts.push(['Next', 'Review staging, then promote to prod']);
|
| 246 |
+
await postTeams({
|
| 247 |
+
kind: 'success',
|
| 248 |
+
title: leaderboardChanged ? 'β
Staging ready for review' : 'π
Header updated β staging ready',
|
| 249 |
+
subtitle: subtitle(),
|
| 250 |
+
facts: noticeFacts,
|
| 251 |
+
openUrl: STAGING_URL,
|
| 252 |
+
});
|
| 253 |
+
} catch (e) {
|
| 254 |
+
console.error('[sync] push failed: ' + e.message);
|
| 255 |
+
await notifyFailure([['Stage', 'git push to staging'], ['Note', 'committed locally OK'], ['Error', e.message]]);
|
| 256 |
+
process.exit(3);
|
| 257 |
+
}
|
| 258 |
+
} else {
|
| 259 |
+
console.log('[sync] --apply: committed locally, not pushed. Use --push to deploy to staging.');
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
main().catch((e) => {
|
| 264 |
+
console.error('[sync] unexpected error: ' + e.message);
|
| 265 |
+
process.exit(1);
|
| 266 |
+
});
|
.agents/skills/sync-nowai-leaderboard/test/transform.test.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { test } from 'node:test';
|
| 2 |
+
import assert from 'node:assert/strict';
|
| 3 |
+
import { parseEogData } from '../lib/transform.mjs';
|
| 4 |
+
|
| 5 |
+
const EOG_SAMPLE = `
|
| 6 |
+
// ββ Leaderboard Data ββ
|
| 7 |
+
const LEADERBOARD_DATA = [
|
| 8 |
+
// Closed Source
|
| 9 |
+
{ model: "Gemini 3.5 Flash (High)", type: "closed", teams: 48.0, csm: 47.9, email: 55.8, itsm: 42.5, calendar: 42.3, hr: 40.8, drive: 55.2, hybrid: 35.3, avg: 46.0 },
|
| 10 |
+
{ model: "Claude Opus 4.5", type: "closed", teams: 50.0, csm: 34.2, email: 51.9, itsm: 23.8, calendar: 43.2, hr: 32.1, drive: 49.5, hybrid: 30.7, avg: 37.0 },
|
| 11 |
+
];
|
| 12 |
+
function renderLeaderboard() {}
|
| 13 |
+
`;
|
| 14 |
+
|
| 15 |
+
test('parseEogData extracts the array of model objects', () => {
|
| 16 |
+
const arr = parseEogData(EOG_SAMPLE);
|
| 17 |
+
assert.equal(arr.length, 2);
|
| 18 |
+
assert.equal(arr[0].model, 'Gemini 3.5 Flash (High)');
|
| 19 |
+
assert.equal(arr[0].type, 'closed');
|
| 20 |
+
assert.equal(arr[0].avg, 46.0);
|
| 21 |
+
assert.equal(arr[1].model, 'Claude Opus 4.5');
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
test('parseEogData throws when marker missing', () => {
|
| 25 |
+
assert.throws(() => parseEogData('const X = 1;'), /LEADERBOARD_DATA not found/);
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
import { deriveEog, vendorFromName } from '../lib/transform.mjs';
|
| 29 |
+
|
| 30 |
+
test('vendorFromName maps known model families', () => {
|
| 31 |
+
assert.equal(vendorFromName('Gemini 3.5 Flash'), 'Google');
|
| 32 |
+
assert.equal(vendorFromName('GPT-5.4'), 'OpenAI');
|
| 33 |
+
assert.equal(vendorFromName('Claude Opus 4.5'), 'Anthropic');
|
| 34 |
+
assert.equal(vendorFromName('Mystery-7B'), 'Mystery-7B'); // fallback: first token
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
test('deriveEog sorts by avg desc, takes top 5, computes bars', () => {
|
| 38 |
+
const arr = [
|
| 39 |
+
{ model: 'A', type: 'closed', avg: 20 },
|
| 40 |
+
{ model: 'B', type: 'open', avg: 40 },
|
| 41 |
+
{ model: 'C', type: 'closed', avg: 30 },
|
| 42 |
+
];
|
| 43 |
+
const out = deriveEog(arr);
|
| 44 |
+
assert.equal(out.metricLabel, 'Task Success Rate Β· Oracle mode');
|
| 45 |
+
assert.deepEqual(out.rows.map(r => r.model), ['B', 'C', 'A']);
|
| 46 |
+
assert.equal(out.rows[0].rank, 1);
|
| 47 |
+
assert.equal(out.rows[0].score, 40);
|
| 48 |
+
assert.equal(out.rows[0].bar, 40); // absolute: avg 40 of max 100
|
| 49 |
+
assert.equal(out.rows[1].bar, 30); // avg 30
|
| 50 |
+
assert.equal(out.rows[2].bar, 20); // avg 20
|
| 51 |
+
assert.equal(out.rows[0].type, 'open');
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
import { parseEvaData, deriveEvaBoard } from '../lib/transform.mjs';
|
| 55 |
+
|
| 56 |
+
const EVA_SAMPLE = JSON.stringify({
|
| 57 |
+
systems: [
|
| 58 |
+
{
|
| 59 |
+
id: 'a', name: 'Gemini Live', type: 's2s', stt: '-', llm: 'gemini', tts: '-',
|
| 60 |
+
clean: {
|
| 61 |
+
'EVA-A_pass': { pooled: { point: 0.30, ci_lower: 0.28, ci_upper: 0.32 } },
|
| 62 |
+
'EVA-X_pass': { pooled: { point: 0.49, ci_lower: 0.47, ci_upper: 0.51 } },
|
| 63 |
+
},
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
id: 'b', name: 'Nova + GPT + Sonic', type: 'cascade', stt: 'Nova', llm: 'GPT', tts: 'Sonic',
|
| 67 |
+
clean: {
|
| 68 |
+
'EVA-A_pass': { pooled: { point: 0.41, ci_lower: 0.39, ci_upper: 0.43 } },
|
| 69 |
+
'EVA-X_pass': { pooled: { point: 0.20, ci_lower: 0.18, ci_upper: 0.22 } },
|
| 70 |
+
},
|
| 71 |
+
},
|
| 72 |
+
],
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
test('parseEvaData returns parsed object with systems array', () => {
|
| 76 |
+
const data = parseEvaData(EVA_SAMPLE);
|
| 77 |
+
assert.equal(data.systems.length, 2);
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
test('deriveEvaBoard ranks by accuracy point, builds subtitle + ci + bars', () => {
|
| 81 |
+
const data = parseEvaData(EVA_SAMPLE);
|
| 82 |
+
const out = deriveEvaBoard(data.systems, 'EVA-A_pass');
|
| 83 |
+
assert.equal(out.metricLabel, 'Pass@1');
|
| 84 |
+
assert.deepEqual(out.rows.map(r => r.name), ['Nova + GPT + Sonic', 'Gemini Live']);
|
| 85 |
+
assert.equal(out.rows[0].score, 0.41);
|
| 86 |
+
assert.equal(out.rows[0].subtitle, 'Mixed Models Β· Cascade');
|
| 87 |
+
assert.equal(out.rows[0].bar, 41); // absolute: 0.41 Γ 100
|
| 88 |
+
assert.equal(out.rows[0].ciLower, 0.39);
|
| 89 |
+
assert.equal(out.rows[0].ciUpper, 0.43);
|
| 90 |
+
assert.equal(out.rows[1].subtitle, 'Google Β· Speech-to-Speech');
|
| 91 |
+
assert.equal(out.rows[1].bar, 30); // 0.30 Γ 100
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
test('deriveEvaBoard ranks experience board independently', () => {
|
| 95 |
+
const data = parseEvaData(EVA_SAMPLE);
|
| 96 |
+
const out = deriveEvaBoard(data.systems, 'EVA-X_pass');
|
| 97 |
+
assert.deepEqual(out.rows.map(r => r.name), ['Gemini Live', 'Nova + GPT + Sonic']);
|
| 98 |
+
assert.equal(out.rows[0].score, 0.49);
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
+
import { validate } from '../lib/transform.mjs';
|
| 102 |
+
|
| 103 |
+
function boardWith(rows) { return { metricLabel: 'x', rows }; }
|
| 104 |
+
function dataWith(eogRows, accRows, expRows) {
|
| 105 |
+
return { eog: boardWith(eogRows), evaAccuracy: boardWith(accRows), evaExperience: boardWith(expRows) };
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
test('validate passes clean data', () => {
|
| 109 |
+
const good = dataWith(
|
| 110 |
+
[{ rank: 1, model: 'A', score: 46 }, { rank: 2, model: 'B', score: 40 }, { rank: 3, model: 'C', score: 30 }],
|
| 111 |
+
[{ rank: 1, name: 'X', score: 0.4 }, { rank: 2, name: 'Y', score: 0.3 }, { rank: 3, name: 'Z', score: 0.2 }],
|
| 112 |
+
[{ rank: 1, name: 'X', score: 0.5 }, { rank: 2, name: 'Y', score: 0.4 }, { rank: 3, name: 'Z', score: 0.3 }],
|
| 113 |
+
);
|
| 114 |
+
assert.deepEqual(validate(good, null), []);
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
test('validate flags too-few rows', () => {
|
| 118 |
+
const bad = dataWith([{ rank: 1, model: 'A', score: 46 }], [], []);
|
| 119 |
+
const issues = validate(bad, null);
|
| 120 |
+
assert.ok(issues.some((i) => i.includes('eog') && i.includes('<3')));
|
| 121 |
+
});
|
| 122 |
+
|
| 123 |
+
test('validate flags out-of-range scores', () => {
|
| 124 |
+
const bad = dataWith(
|
| 125 |
+
[{ rank: 1, model: 'A', score: 150 }, { rank: 2, model: 'B', score: 40 }, { rank: 3, model: 'C', score: 30 }],
|
| 126 |
+
[{ rank: 1, name: 'X', score: 0.4 }, { rank: 2, name: 'Y', score: 0.3 }, { rank: 3, name: 'Z', score: 0.2 }],
|
| 127 |
+
[{ rank: 1, name: 'X', score: 0.5 }, { rank: 2, name: 'Y', score: 0.4 }, { rank: 3, name: 'Z', score: 0.3 }],
|
| 128 |
+
);
|
| 129 |
+
assert.ok(validate(bad, null).some((i) => i.includes('out of')));
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
test('validate flags suspicious top-score swing vs previous', () => {
|
| 133 |
+
const prev = dataWith(
|
| 134 |
+
[{ rank: 1, model: 'A', score: 46 }, { rank: 2, model: 'B', score: 40 }, { rank: 3, model: 'C', score: 30 }],
|
| 135 |
+
[{ rank: 1, name: 'X', score: 0.4 }, { rank: 2, name: 'Y', score: 0.3 }, { rank: 3, name: 'Z', score: 0.2 }],
|
| 136 |
+
[{ rank: 1, name: 'X', score: 0.5 }, { rank: 2, name: 'Y', score: 0.4 }, { rank: 3, name: 'Z', score: 0.3 }],
|
| 137 |
+
);
|
| 138 |
+
const now = dataWith(
|
| 139 |
+
[{ rank: 1, model: 'A', score: 10 }, { rank: 2, model: 'B', score: 8 }, { rank: 3, model: 'C', score: 6 }],
|
| 140 |
+
[{ rank: 1, name: 'X', score: 0.4 }, { rank: 2, name: 'Y', score: 0.3 }, { rank: 3, name: 'Z', score: 0.2 }],
|
| 141 |
+
[{ rank: 1, name: 'X', score: 0.5 }, { rank: 2, name: 'Y', score: 0.4 }, { rank: 3, name: 'Z', score: 0.3 }],
|
| 142 |
+
);
|
| 143 |
+
assert.ok(validate(now, prev).some((i) => i.includes('swing')));
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
import { buildLeaderboard, hasChanges, diffBoards } from '../lib/transform.mjs';
|
| 147 |
+
|
| 148 |
+
test('buildLeaderboard assembles all three boards plus meta', () => {
|
| 149 |
+
const out = buildLeaderboard({
|
| 150 |
+
eogScript: EOG_SAMPLE,
|
| 151 |
+
evaJson: EVA_SAMPLE,
|
| 152 |
+
sources: { eog: { url: 'u1', hash: 'h1' }, eva: { url: 'u2', hash: 'h2' } },
|
| 153 |
+
now: '2026-06-02T00:00:00Z',
|
| 154 |
+
});
|
| 155 |
+
assert.equal(out.updated, '2026-06-02T00:00:00Z');
|
| 156 |
+
assert.equal(out.sources.eog.hash, 'h1');
|
| 157 |
+
assert.equal(out.eog.rows[0].model, 'Gemini 3.5 Flash (High)');
|
| 158 |
+
assert.equal(out.evaAccuracy.rows[0].name, 'Nova + GPT + Sonic');
|
| 159 |
+
assert.equal(out.evaExperience.rows[0].name, 'Gemini Live');
|
| 160 |
+
});
|
| 161 |
+
|
| 162 |
+
test('hasChanges ignores meta, detects board changes', () => {
|
| 163 |
+
const base = buildLeaderboard({ eogScript: EOG_SAMPLE, evaJson: EVA_SAMPLE, sources: {}, now: 'A' });
|
| 164 |
+
const sameDataNewMeta = buildLeaderboard({ eogScript: EOG_SAMPLE, evaJson: EVA_SAMPLE, sources: { eog: { hash: 'x' } }, now: 'B' });
|
| 165 |
+
assert.equal(hasChanges(base, sameDataNewMeta), false);
|
| 166 |
+
|
| 167 |
+
const changedEog = EOG_SAMPLE.replace('avg: 46.0', 'avg: 99.0');
|
| 168 |
+
const changed = buildLeaderboard({ eogScript: changedEog, evaJson: EVA_SAMPLE, sources: {}, now: 'A' });
|
| 169 |
+
assert.equal(hasChanges(base, changed), true);
|
| 170 |
+
});
|
| 171 |
+
|
| 172 |
+
test('diffBoards produces human-readable change lines', () => {
|
| 173 |
+
const base = buildLeaderboard({ eogScript: EOG_SAMPLE, evaJson: EVA_SAMPLE, sources: {}, now: 'A' });
|
| 174 |
+
const changedEog = EOG_SAMPLE.replace('avg: 46.0', 'avg: 41.0');
|
| 175 |
+
const changed = buildLeaderboard({ eogScript: changedEog, evaJson: EVA_SAMPLE, sources: {}, now: 'A' });
|
| 176 |
+
const lines = diffBoards(base, changed);
|
| 177 |
+
assert.ok(lines.some((l) => l.startsWith('eog:')));
|
| 178 |
+
});
|
AGENTS.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AGENTS.md
|
| 2 |
+
|
| 3 |
+
## Purpose
|
| 4 |
+
|
| 5 |
+
This repo powers the NowAI-Bench static site and its leaderboard sync workflow.
|
| 6 |
+
The main site files are `index.html`, `style.css`, and `data/leaderboard.json`.
|
| 7 |
+
|
| 8 |
+
## Instruction Order
|
| 9 |
+
|
| 10 |
+
Read this file first.
|
| 11 |
+
|
| 12 |
+
If a root `CLAUDE.md` exists, read it next and treat it as additional
|
| 13 |
+
repo-specific user guidance.
|
| 14 |
+
|
| 15 |
+
When the task is about leaderboard syncing or leaderboard data, also read:
|
| 16 |
+
|
| 17 |
+
- `.claude/skills/sync-nowai-leaderboard/SKILL.md`
|
| 18 |
+
- `docs/superpowers/specs/2026-06-02-leaderboard-sync-design.md`
|
| 19 |
+
- `docs/superpowers/plans/2026-06-02-leaderboard-sync.md`
|
| 20 |
+
|
| 21 |
+
## Leaderboard Sync Support
|
| 22 |
+
|
| 23 |
+
This repo includes a repo-local skill/workflow at
|
| 24 |
+
`.claude/skills/sync-nowai-leaderboard/`. Codex should support that workflow
|
| 25 |
+
directly even if skill auto-discovery is unavailable.
|
| 26 |
+
|
| 27 |
+
Use this run order:
|
| 28 |
+
|
| 29 |
+
1. `node .claude/skills/sync-nowai-leaderboard/sync.mjs --check`
|
| 30 |
+
2. Inspect the diff and sanity-gate output.
|
| 31 |
+
3. Only if the result is sane, run
|
| 32 |
+
`node .claude/skills/sync-nowai-leaderboard/sync.mjs --push`
|
| 33 |
+
|
| 34 |
+
## Deployment Guardrails
|
| 35 |
+
|
| 36 |
+
- `origin` is staging: `ServiceNow-AI/NowAI-Bench-Staging`
|
| 37 |
+
- `prod` is production: `ServiceNow-AI/NowAI-Bench`
|
| 38 |
+
- Never push to `prod` as part of automated or routine sync work.
|
| 39 |
+
- For sync tasks, prefer the scripted guardrails in `sync.mjs` over manual edits.
|
| 40 |
+
|
| 41 |
+
## Verification
|
| 42 |
+
|
| 43 |
+
If you change the sync workflow or transform logic, run:
|
| 44 |
+
|
| 45 |
+
- `cd .claude/skills/sync-nowai-leaderboard && node --test`
|
| 46 |
+
|
| 47 |
+
If you change site rendering, verify the static page still loads cleanly and
|
| 48 |
+
that leaderboard data remains consistent with `data/leaderboard.json`.
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
<claude-mem-context>
|
| 52 |
+
# Memory Context
|
| 53 |
+
|
| 54 |
+
# [NowAI-Bench] recent context, 2026-06-30 12:47pm PDT
|
| 55 |
+
|
| 56 |
+
Legend: π―session π΄bugfix π£feature πrefactor β
change π΅discovery βοΈdecision π¨security_alert πsecurity_note
|
| 57 |
+
Format: ID TIME TYPE TITLE
|
| 58 |
+
Fetch details: get_observations([IDs]) | Search: mem-search skill
|
| 59 |
+
|
| 60 |
+
Stats: 50 obs (14,815t read) | 128,192t work | 88% savings
|
| 61 |
+
|
| 62 |
+
### Jun 12, 2026
|
| 63 |
+
1761 3:07p β
Created project memory link for sync automation system
|
| 64 |
+
1762 " β
Added sync automation entry to project memory index
|
| 65 |
+
1764 5:32p β
Teams webhook integration configured in environment
|
| 66 |
+
1765 5:33p π΅ Teams MessageCard webhook format identified from plato chat pattern
|
| 67 |
+
1766 5:34p π΅ Teams webhook URL configured securely in NowAI-Bench
|
| 68 |
+
1767 5:35p π£ Teams Adaptive Card webhook module implemented for NowAI-Bench
|
| 69 |
+
1768 5:36p π΅ Teams Adaptive Card successfully posted to Power Automate webhook
|
| 70 |
+
1769 " β
Teams module integrated into leaderboard sync orchestrator
|
| 71 |
+
1770 " π Notification system refactored to use Adaptive Cards
|
| 72 |
+
1771 5:37p β
Error handling migrated to structured Teams notifications
|
| 73 |
+
1772 " β
All sync error paths migrated to structured Teams notifications
|
| 74 |
+
1773 5:38p π£ Success notification implemented for staging deployment
|
| 75 |
+
1774 " β
TEAMS_WEBHOOK_URL integrated into cron sync script
|
| 76 |
+
1775 " π΅ Comprehensive validation confirms Teams notification integration working
|
| 77 |
+
1776 5:39p π΅ Error Adaptive Card successfully posted to Teams
|
| 78 |
+
1777 " β
Teams notification feature documented as complete and deployed
|
| 79 |
+
1778 " β
Teams notification feature integrated into automation design documentation
|
| 80 |
+
1779 " β
Teams notification support integrated into project memory
|
| 81 |
+
1780 5:43p π Adaptive Card layout refined with header band styling for better visual hierarchy
|
| 82 |
+
1781 " π΅ Refined Adaptive Card layout validated with both success and error styles
|
| 83 |
+
1782 5:54p π£ Teams webhook integration for leaderboard sync notifications
|
| 84 |
+
1783 " β
Git artifact exclusion: nowai-bench-v0.zip
|
| 85 |
+
1784 " β
Documented Teams webhook configuration in .env.example
|
| 86 |
+
1785 " π£ Teams Adaptive Card notifications committed to leaderboard sync
|
| 87 |
+
1786 " β
Teams Adaptive Card feature deployed to staging
|
| 88 |
+
1787 5:55p β
Launchd job restricted to weekdays (Mon-Fri) at 14:00
|
| 89 |
+
1788 " π΅ Launchd weekday scheduling verified and activated
|
| 90 |
+
1789 " β
Updated DESIGN.md to reflect weekday-only scheduling
|
| 91 |
+
1790 " β
Documented launchd weekday scheduling implementation detail
|
| 92 |
+
1791 5:56p β
Updated project memory with weekday scheduling detail
|
| 93 |
+
### Jun 23, 2026
|
| 94 |
+
S1124 Diagnose why NowAI leaderboard sync hasn't been running; implement observability improvement during quiet data periods (Jun 23 at 10:23 AM)
|
| 95 |
+
S1125 Investigate sync inactivity concern and improve observability for background automation running during quiet data periods (Jun 23 at 10:27 AM)
|
| 96 |
+
S1126 Diagnose why NowAI leaderboard sync hasn't been running for a week; implement observability improvement to confirm automation health during stable-data periods (Jun 23 at 10:29 AM)
|
| 97 |
+
S1129 Confirmed logging mechanism for sync checks β verifying that automated sync runs are being captured and persisted (Jun 23 at 10:30 AM)
|
| 98 |
+
S1130 Update leaderboard sync skill documentation β clarify notification behavior (Friday heartbeat) and add logging guidance (Jun 23 at 2:03 PM)
|
| 99 |
+
2219 2:04p π΅ Leaderboard Auto-Sync Architecture β Approved Design Specification
|
| 100 |
+
2220 " π΅ Leaderboard Sync Skill β Operational Implementation & Guardrails
|
| 101 |
+
S1131 Understand and improve leaderboard sync logging and operational tooling β document behavior and create convenience shortcuts (Jun 23 at 2:04 PM)
|
| 102 |
+
2221 2:05p β
Makefile for Leaderboard Sync β Convenience Targets Added
|
| 103 |
+
S1132 Commit and push Makefile and SKILL.md documentation updates to staging and production (Jun 23 at 2:05 PM)
|
| 104 |
+
2222 2:14p β
Makefile and SKILL.md committed with launchd sync documentation
|
| 105 |
+
2223 " β
Makefile and SKILL.md deployed to staging and production
|
| 106 |
+
S1154 Extend NowAI-Bench quarterly header sync: add "Last Updated" text detection and update logic to the leaderboard sync process, with Teams notification indicating if that's the only change (Jun 23 at 2:15 PM)
|
| 107 |
+
### Jun 24, 2026
|
| 108 |
+
2264 12:02p π΅ Located "Last Updated" Header in NowAI-Bench UI and Identified Sync Script Gap
|
| 109 |
+
2265 " π΅ Located Quarter Indicator and "Last Updated" Label in index.html
|
| 110 |
+
2266 " π΅ Identified Data Source and Existing Scripts in Project Structure
|
| 111 |
+
2267 12:03p π΅ Located Existing NowAI Sync Infrastructure and Teams Notification Module
|
| 112 |
+
2268 " π΅ Existing Sync Workflow: 7-Step Process with Change Detection and Teams Notifications
|
| 113 |
+
2269 " π΅ Located Quarterly Header Markup and Transformation Module
|
| 114 |
+
S1172 Implement quarter label detection and application in sync.mjs with distinct handling for quarter-only vs. leaderboard-only changes (Jun 24 at 12:05 PM)
|
| 115 |
+
2273 2:28p βοΈ Quarter label detection implementation approach for sync.mjs
|
| 116 |
+
2274 " β
Added synchronous file I/O imports to sync.mjs
|
| 117 |
+
2275 2:29p π£ Implemented quarter label detection and application helpers
|
| 118 |
+
2276 " π Restructured main() to support independent quarter label and leaderboard syncs
|
| 119 |
+
2277 2:30p π΅ sync.mjs syntax and runtime validation successful
|
| 120 |
+
2278 " π΅ Quarter label detection implementation task completed
|
| 121 |
+
2279 2:33p β
Quarter label detection feature committed to repository
|
| 122 |
+
2280 2:35p β
Quarter label detection feature deployed to staging HuggingFace Space
|
| 123 |
+
2281 " β
Quarter label detection feature deployed to production HuggingFace Space
|
| 124 |
+
S1173 Implement quarter label detection and auto-update in NowAI-Bench leaderboard sync with distinct notifications for quarter-only vs. leaderboard-only changes (Jun 24 at 2:36 PM)
|
| 125 |
+
**Investigated**: Examined sync.mjs orchestration flow (fetch β build β validate β commit β push), identified quarter label location in index.html regex pattern, reviewed staging/prod deployment architecture with two separate HF Space remotes
|
| 126 |
+
|
| 127 |
+
**Learned**: Sync workflow has three independent concerns that must be orchestrated separately: upstream data fetching, leaderboard validation gates, and UI header updates. Sanity validation gates should apply only to leaderboard data changes, allowing quarter-only updates to bypass validation. Teams notifications must signal what changed (rankings, header, or both) to enable appropriate reviewer workflows. The staging/prod split uses force-push and metadata patching on staging to isolate metadata changes from code.
|
| 128 |
+
|
| 129 |
+
**Completed**: Added synchronous file I/O imports (readFileSync, writeFileSync); implemented three helper functions (quarterForDate, detectQuarterLabel, applyQuarterLabel) with regex-based quarter label extraction and in-place HTML patching; restructured main() orchestration with step 3.5 quarter detection, independent change flags (leaderboardChanged and quarterInfo.needsUpdate), conditional validation/writes/commits; implemented dynamic commit messages and differentiated Teams notification titles (π
for quarter-only, β
for rankings); validated via syntax check and smoke test; committed to main (SHA 5661ed6); deployed to both staging and production HuggingFace Spaces
|
| 130 |
+
|
| 131 |
+
**Next Steps**: Feature is complete and live in both environments. The quarter label auto-update will activate automatically next time the leaderboard sync script runs after Q3 begins (July 1). No further development work planned in this session.
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
Access 128k tokens of past work via get_observations([IDs]) or mem-search skill.
|
| 135 |
+
</claude-mem-context>
|