bradsansnow commited on
Commit
cf2f7da
Β·
1 Parent(s): 93f60f8

Add agents file and skills

Browse files
.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>