bradsansnow commited on
Commit
dce9e9f
Β·
1 Parent(s): 93824b1

docs: add blocked sync Teams warning plan

Browse files
docs/superpowers/plans/2026-07-01-blocked-sync-teams-warnings.md ADDED
@@ -0,0 +1,522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Blocked Sync Teams Warnings Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Send a Teams warning whenever the local NowAI-Bench sync wrapper blocks a run before `sync.mjs` starts.
6
+
7
+ **Architecture:** Keep preflight ownership in `~/.nowai-sync/nowai-sync-cron.sh`, but move blocked-warning message formatting into repo-owned Node code that reuses `lib/teams.mjs`. The wrapper stays thin: it detects blocked states, passes a normalized reason plus facts to the repo notifier, and exits without running `sync.mjs`.
8
+
9
+ **Tech Stack:** Node 20 ESM, `node:test`, bash, git, Power Automate Teams webhook
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ - Create: `.claude/skills/sync-nowai-leaderboard/lib/blocked-notify.mjs` β€” pure helpers for blocked reason labels, timestamp subtitle, and CLI argument parsing.
16
+ - Create: `.claude/skills/sync-nowai-leaderboard/notify-blocked.mjs` β€” CLI entrypoint the wrapper calls to post a blocked warning card.
17
+ - Create: `.claude/skills/sync-nowai-leaderboard/test/blocked-notify.test.mjs` β€” unit tests for reason mapping and CLI argument parsing.
18
+ - Create: `.claude/skills/sync-nowai-leaderboard/test/notify-blocked-cli.test.mjs` β€” smoke test for the CLI entrypoint without a webhook configured.
19
+ - Modify: `.claude/skills/sync-nowai-leaderboard/SKILL.md` β€” document blocked preflight Teams warnings.
20
+ - Modify: `~/.nowai-sync/nowai-sync-cron.sh` β€” add dirty-worktree precheck and call the repo notifier for every blocked state.
21
+ - Modify: `~/.nowai-sync/DESIGN.md` β€” document the new blocked-warning flow and the safe clone-based smoke tests.
22
+ - Existing: `docs/superpowers/specs/2026-07-01-blocked-sync-teams-warnings-design.md` β€” approved design record.
23
+ - Create: `docs/superpowers/plans/2026-07-01-blocked-sync-teams-warnings.md` β€” this implementation handoff.
24
+
25
+ ### Task 1: Add a tested blocked-notification builder
26
+
27
+ **Files:**
28
+ - Create: `.claude/skills/sync-nowai-leaderboard/lib/blocked-notify.mjs`
29
+ - Create: `.claude/skills/sync-nowai-leaderboard/test/blocked-notify.test.mjs`
30
+
31
+ - [ ] **Step 1: Write the failing unit test**
32
+
33
+ Create `.claude/skills/sync-nowai-leaderboard/test/blocked-notify.test.mjs`:
34
+
35
+ ```js
36
+ import { test } from 'node:test';
37
+ import assert from 'node:assert/strict';
38
+ import {
39
+ buildBlockedNotification,
40
+ parseBlockedCliArgs,
41
+ } from '../lib/blocked-notify.mjs';
42
+
43
+ test('buildBlockedNotification maps dirty-worktree to a warning card payload', () => {
44
+ const out = buildBlockedNotification({
45
+ reason: 'dirty-worktree',
46
+ facts: [['Branch', 'main'], ['Files', '3']],
47
+ now: new Date('2026-07-01T21:00:00Z'),
48
+ });
49
+
50
+ assert.equal(out.kind, 'warning');
51
+ assert.equal(out.title, '⚠️ Leaderboard sync blocked');
52
+ assert.equal(out.subtitle, 'NowAI-Bench Sync Β· 2026-07-01 21:00 UTC');
53
+ assert.deepEqual(out.facts, [
54
+ ['Reason', 'Worktree has uncommitted files'],
55
+ ['Branch', 'main'],
56
+ ['Files', '3'],
57
+ ]);
58
+ });
59
+
60
+ test('parseBlockedCliArgs accepts repeated --fact pairs', () => {
61
+ const out = parseBlockedCliArgs([
62
+ '--reason', 'behind-or-diverged',
63
+ '--fact', 'Branch=main',
64
+ '--fact', 'RemoteAhead=2',
65
+ '--fact', 'LocalAhead=0',
66
+ ]);
67
+
68
+ assert.equal(out.reason, 'behind-or-diverged');
69
+ assert.deepEqual(out.facts, [
70
+ ['Branch', 'main'],
71
+ ['RemoteAhead', '2'],
72
+ ['LocalAhead', '0'],
73
+ ]);
74
+ });
75
+
76
+ test('parseBlockedCliArgs rejects unknown blocked reasons', () => {
77
+ assert.throws(
78
+ () => parseBlockedCliArgs(['--reason', 'mystery-reason']),
79
+ /Unknown blocked reason: mystery-reason/,
80
+ );
81
+ });
82
+
83
+ test('parseBlockedCliArgs rejects malformed fact entries', () => {
84
+ assert.throws(
85
+ () => parseBlockedCliArgs(['--reason', 'wrong-branch', '--fact', 'Branch']),
86
+ /Invalid --fact, expected key=value: Branch/,
87
+ );
88
+ });
89
+ ```
90
+
91
+ - [ ] **Step 2: Run the new test and confirm it fails**
92
+
93
+ Run:
94
+
95
+ ```bash
96
+ cd .claude/skills/sync-nowai-leaderboard
97
+ node --test test/blocked-notify.test.mjs
98
+ ```
99
+
100
+ Expected: FAIL with `Cannot find module '../lib/blocked-notify.mjs'`.
101
+
102
+ - [ ] **Step 3: Write the minimal implementation**
103
+
104
+ Create `.claude/skills/sync-nowai-leaderboard/lib/blocked-notify.mjs`:
105
+
106
+ ```js
107
+ const REASON_LABELS = {
108
+ 'dirty-worktree': 'Worktree has uncommitted files',
109
+ 'wrong-branch': 'Current branch is not main',
110
+ 'fetch-failed': 'git fetch origin main failed',
111
+ 'behind-or-diverged': 'Local main is behind or diverged from origin/main',
112
+ };
113
+
114
+ export function blockedSubtitle(now = new Date()) {
115
+ return 'NowAI-Bench Sync Β· ' + now.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
116
+ }
117
+
118
+ export function buildBlockedNotification({ reason, facts = [], now = new Date() }) {
119
+ const label = REASON_LABELS[reason];
120
+ if (!label) throw new Error(`Unknown blocked reason: ${reason}`);
121
+
122
+ return {
123
+ kind: 'warning',
124
+ title: '⚠️ Leaderboard sync blocked',
125
+ subtitle: blockedSubtitle(now),
126
+ facts: [['Reason', label], ...facts],
127
+ };
128
+ }
129
+
130
+ export function parseBlockedCliArgs(argv) {
131
+ const facts = [];
132
+ let reason;
133
+
134
+ for (let i = 0; i < argv.length; i++) {
135
+ const token = argv[i];
136
+
137
+ if (token === '--reason') {
138
+ reason = argv[++i];
139
+ continue;
140
+ }
141
+
142
+ if (token === '--fact') {
143
+ const raw = argv[++i] ?? '';
144
+ const eq = raw.indexOf('=');
145
+ if (eq <= 0) throw new Error(`Invalid --fact, expected key=value: ${raw}`);
146
+ facts.push([raw.slice(0, eq), raw.slice(eq + 1)]);
147
+ continue;
148
+ }
149
+
150
+ throw new Error(`Unknown argument: ${token}`);
151
+ }
152
+
153
+ if (!reason) throw new Error('Missing required --reason');
154
+
155
+ // Reuse payload validation so the CLI parser and notifier stay in lockstep.
156
+ buildBlockedNotification({ reason, facts, now: new Date(0) });
157
+
158
+ return { reason, facts };
159
+ }
160
+ ```
161
+
162
+ - [ ] **Step 4: Run the test again and confirm it passes**
163
+
164
+ Run:
165
+
166
+ ```bash
167
+ cd .claude/skills/sync-nowai-leaderboard
168
+ node --test test/blocked-notify.test.mjs
169
+ ```
170
+
171
+ Expected: PASS with 4 passing tests.
172
+
173
+ - [ ] **Step 5: Commit the repo-tracked change**
174
+
175
+ ```bash
176
+ git add \
177
+ .claude/skills/sync-nowai-leaderboard/lib/blocked-notify.mjs \
178
+ .claude/skills/sync-nowai-leaderboard/test/blocked-notify.test.mjs
179
+ git commit -m "feat: add blocked sync notification builder"
180
+ ```
181
+
182
+ ### Task 2: Add the CLI entrypoint the wrapper will call
183
+
184
+ **Files:**
185
+ - Create: `.claude/skills/sync-nowai-leaderboard/notify-blocked.mjs`
186
+ - Create: `.claude/skills/sync-nowai-leaderboard/test/notify-blocked-cli.test.mjs`
187
+
188
+ - [ ] **Step 1: Write the failing CLI smoke test**
189
+
190
+ Create `.claude/skills/sync-nowai-leaderboard/test/notify-blocked-cli.test.mjs`:
191
+
192
+ ```js
193
+ import { test } from 'node:test';
194
+ import assert from 'node:assert/strict';
195
+ import { spawnSync } from 'node:child_process';
196
+ import { fileURLToPath } from 'node:url';
197
+
198
+ const ENTRY = fileURLToPath(new URL('../notify-blocked.mjs', import.meta.url));
199
+
200
+ test('notify-blocked CLI exits 1 when no Teams webhook is configured', () => {
201
+ const res = spawnSync(
202
+ process.execPath,
203
+ [ENTRY, '--reason', 'wrong-branch', '--fact', 'Branch=feature/demo'],
204
+ {
205
+ env: { ...process.env, TEAMS_WEBHOOK_URL: '' },
206
+ encoding: 'utf8',
207
+ },
208
+ );
209
+
210
+ assert.equal(res.status, 1);
211
+ assert.match(res.stdout, /no TEAMS_WEBHOOK_URL set; skipping card/);
212
+ assert.match(res.stdout, /\[teams\] not posted\./);
213
+ assert.equal(res.stderr, '');
214
+ });
215
+ ```
216
+
217
+ - [ ] **Step 2: Run the CLI smoke test and confirm it fails**
218
+
219
+ Run:
220
+
221
+ ```bash
222
+ cd .claude/skills/sync-nowai-leaderboard
223
+ node --test test/notify-blocked-cli.test.mjs
224
+ ```
225
+
226
+ Expected: FAIL because `notify-blocked.mjs` does not exist yet.
227
+
228
+ - [ ] **Step 3: Implement the CLI entrypoint**
229
+
230
+ Create `.claude/skills/sync-nowai-leaderboard/notify-blocked.mjs`:
231
+
232
+ ```js
233
+ #!/usr/bin/env node
234
+ import { postTeams } from './lib/teams.mjs';
235
+ import {
236
+ buildBlockedNotification,
237
+ parseBlockedCliArgs,
238
+ } from './lib/blocked-notify.mjs';
239
+
240
+ const isMain = import.meta.url === `file://${process.argv[1]}`;
241
+
242
+ if (isMain) {
243
+ try {
244
+ const { reason, facts } = parseBlockedCliArgs(process.argv.slice(2));
245
+ const ok = await postTeams(buildBlockedNotification({ reason, facts }));
246
+ console.log(ok ? '[teams] posted.' : '[teams] not posted.');
247
+ process.exit(ok ? 0 : 1);
248
+ } catch (e) {
249
+ console.error('[teams] invalid blocked notification args: ' + e.message);
250
+ process.exit(2);
251
+ }
252
+ }
253
+ ```
254
+
255
+ - [ ] **Step 4: Run the targeted tests and confirm both pass**
256
+
257
+ Run:
258
+
259
+ ```bash
260
+ cd .claude/skills/sync-nowai-leaderboard
261
+ node --test test/blocked-notify.test.mjs test/notify-blocked-cli.test.mjs
262
+ ```
263
+
264
+ Expected: PASS with both test files green.
265
+
266
+ - [ ] **Step 5: Commit the repo-tracked change**
267
+
268
+ ```bash
269
+ git add \
270
+ .claude/skills/sync-nowai-leaderboard/notify-blocked.mjs \
271
+ .claude/skills/sync-nowai-leaderboard/test/notify-blocked-cli.test.mjs
272
+ git commit -m "feat: add blocked sync Teams notifier cli"
273
+ ```
274
+
275
+ ### Task 3: Wire blocked preflight notifications into the local wrapper
276
+
277
+ **Files:**
278
+ - Modify: `~/.nowai-sync/nowai-sync-cron.sh`
279
+
280
+ - [ ] **Step 1: Make the wrapper repo path overridable and add a blocked-notify helper**
281
+
282
+ Update the top of `~/.nowai-sync/nowai-sync-cron.sh` so clone-based smoke tests can point at a temporary repo without editing the script:
283
+
284
+ ```bash
285
+ REPO="${REPO:-/Users/bradley.cleveland/git/hf/NowAI-Bench}"
286
+ SYNC="$REPO/.claude/skills/sync-nowai-leaderboard/sync.mjs"
287
+ BLOCKED_NOTIFY="$REPO/.claude/skills/sync-nowai-leaderboard/notify-blocked.mjs"
288
+ STAGING_URL="https://huggingface.co/spaces/ServiceNow-AI/NowAI-Bench-Staging"
289
+ LOG="$HOME/Library/Logs/nowai-sync.log"
290
+ NOTIFIER="/opt/homebrew/bin/terminal-notifier"
291
+ TITLE="NowAI-Bench Sync"
292
+ ```
293
+
294
+ Add this helper below `fail()`:
295
+
296
+ ```bash
297
+ blocked() {
298
+ local reason="$1" message="$2"
299
+ shift 2
300
+
301
+ log "BLOCKED: $reason - $message"
302
+ notify "Skipped" "$message" "Basso"
303
+
304
+ if [ -f "$BLOCKED_NOTIFY" ]; then
305
+ if ! node "$BLOCKED_NOTIFY" --reason "$reason" "$@" >>"$LOG" 2>&1; then
306
+ log "WARN: Teams blocked-notify failed for $reason"
307
+ fi
308
+ else
309
+ log "WARN: blocked notifier missing: $BLOCKED_NOTIFY"
310
+ fi
311
+
312
+ exit 0
313
+ }
314
+ ```
315
+
316
+ - [ ] **Step 2: Add the dirty-worktree precheck before fetch**
317
+
318
+ Insert this after the branch lookup:
319
+
320
+ ```bash
321
+ branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
322
+ if [ "$branch" != "main" ]; then
323
+ blocked "wrong-branch" \
324
+ "On branch '$branch', not main. No sync run." \
325
+ --fact "Branch=$branch" \
326
+ --fact "Expected=main"
327
+ fi
328
+
329
+ dirty_count="$(git status --porcelain | wc -l | tr -d ' ')"
330
+ if [ "$dirty_count" != "0" ]; then
331
+ blocked "dirty-worktree" \
332
+ "Worktree has uncommitted files. Sync did not run." \
333
+ --fact "Branch=$branch" \
334
+ --fact "Files=$dirty_count"
335
+ fi
336
+ ```
337
+
338
+ - [ ] **Step 3: Convert fetch and behind/diverged skips to blocked Teams warnings**
339
+
340
+ Replace the current `git fetch` / `merge-base` section with:
341
+
342
+ ```bash
343
+ if ! fetch_out="$(git fetch origin main 2>&1)"; then
344
+ fetch_tail="$(printf '%s' "$fetch_out" | tail -n1)"
345
+ blocked "fetch-failed" \
346
+ "git fetch origin main failed. Sync did not run." \
347
+ --fact "Remote=origin/main" \
348
+ --fact "Error=${fetch_tail:-unknown}"
349
+ fi
350
+
351
+ if ! git merge-base --is-ancestor origin/main HEAD 2>/dev/null; then
352
+ counts="$(git rev-list --left-right --count origin/main...HEAD 2>/dev/null || printf 'unknown unknown')"
353
+ remote_ahead="${counts%% *}"
354
+ local_ahead="${counts##* }"
355
+ blocked "behind-or-diverged" \
356
+ "Local main is behind or diverged from staging. Reconcile manually, then it resumes." \
357
+ --fact "Branch=$branch" \
358
+ --fact "RemoteAhead=$remote_ahead" \
359
+ --fact "LocalAhead=$local_ahead"
360
+ fi
361
+ ```
362
+
363
+ - [ ] **Step 4: Verify the dirty-worktree blocked path with a temporary clone**
364
+
365
+ Run:
366
+
367
+ ```bash
368
+ rm -rf /tmp/nowai-bench-dirty
369
+ git clone . /tmp/nowai-bench-dirty
370
+ touch /tmp/nowai-bench-dirty/.blocked-sync-smoke
371
+ REPO=/tmp/nowai-bench-dirty /bin/bash ~/.nowai-sync/nowai-sync-cron.sh
372
+ rm -rf /tmp/nowai-bench-dirty
373
+ ```
374
+
375
+ Expected:
376
+ - desktop toast says the run was skipped
377
+ - `~/Library/Logs/nowai-sync.log` gains a `BLOCKED: dirty-worktree` line
378
+ - Teams receives `⚠️ Leaderboard sync blocked` with `Reason = Worktree has uncommitted files`
379
+
380
+ - [ ] **Step 5: Verify the wrong-branch blocked path with a temporary clone**
381
+
382
+ Run:
383
+
384
+ ```bash
385
+ rm -rf /tmp/nowai-bench-wrong-branch
386
+ git clone . /tmp/nowai-bench-wrong-branch
387
+ git -C /tmp/nowai-bench-wrong-branch switch -c blocked-sync-smoke
388
+ REPO=/tmp/nowai-bench-wrong-branch /bin/bash ~/.nowai-sync/nowai-sync-cron.sh
389
+ rm -rf /tmp/nowai-bench-wrong-branch
390
+ ```
391
+
392
+ Expected:
393
+ - log shows `BLOCKED: wrong-branch`
394
+ - Teams card includes `Branch = blocked-sync-smoke`
395
+
396
+ - [ ] **Step 6: Verify the behind/diverged blocked path with a temporary clone**
397
+
398
+ Run:
399
+
400
+ ```bash
401
+ rm -rf /tmp/nowai-bench-behind
402
+ git clone . /tmp/nowai-bench-behind
403
+ git -C /tmp/nowai-bench-behind checkout -B main origin/main~1
404
+ REPO=/tmp/nowai-bench-behind /bin/bash ~/.nowai-sync/nowai-sync-cron.sh
405
+ rm -rf /tmp/nowai-bench-behind
406
+ ```
407
+
408
+ Expected:
409
+ - log shows `BLOCKED: behind-or-diverged`
410
+ - Teams card includes `RemoteAhead` of at least `1`
411
+
412
+ - [ ] **Step 7: Verify the fetch-failed blocked path with a temporary clone**
413
+
414
+ Run:
415
+
416
+ ```bash
417
+ rm -rf /tmp/nowai-bench-fetch
418
+ git clone . /tmp/nowai-bench-fetch
419
+ git -C /tmp/nowai-bench-fetch remote set-url origin https://127.0.0.1.invalid/ServiceNow-AI/NowAI-Bench-Staging
420
+ REPO=/tmp/nowai-bench-fetch /bin/bash ~/.nowai-sync/nowai-sync-cron.sh
421
+ rm -rf /tmp/nowai-bench-fetch
422
+ ```
423
+
424
+ Expected:
425
+ - log shows `BLOCKED: fetch-failed`
426
+ - Teams card includes `Remote = origin/main`
427
+ - Teams card includes the fetch error text in the `Error` fact
428
+
429
+ ### Task 4: Update docs and run the final repo verification
430
+
431
+ **Files:**
432
+ - Modify: `.claude/skills/sync-nowai-leaderboard/SKILL.md`
433
+ - Modify: `~/.nowai-sync/DESIGN.md`
434
+
435
+ - [ ] **Step 1: Update the repo skill doc to mention blocked preflight Teams warnings**
436
+
437
+ Change the `Teams notifications` section in `.claude/skills/sync-nowai-leaderboard/SKILL.md` to:
438
+
439
+ ```md
440
+ ## Teams notifications
441
+ - Set `TEAMS_WEBHOOK_URL` (secret/env var, never committed β€” keep it in the
442
+ gitignored `.env`) to enable. The URL is a Power Automate "Workflows" webhook
443
+ (`*.powerplatform.com`), so `lib/teams.mjs` posts an **Adaptive Card** in the
444
+ `message/attachments` envelope that template expects β€” not a raw MessageCard.
445
+ - Cards: green βœ… success (with "Open staging" button), red ❌ failure, amber ⚠️
446
+ sanity-gate abort, amber ⚠️ blocked preflight skip (dirty worktree / wrong
447
+ branch / fetch failure / behind-diverged), blue πŸ“‹ Friday heartbeat (no
448
+ changes detected β€” shows last date `data/leaderboard.json` actually changed).
449
+ - The daily 14:00 launchd job (`~/.nowai-sync/`) loads `TEAMS_WEBHOOK_URL` from
450
+ `.env`, posts blocked-preflight warnings through `notify-blocked.mjs`, and
451
+ also fires a local macOS desktop notification for every outcome.
452
+ ```
453
+
454
+ - [ ] **Step 2: Update the local automation design doc**
455
+
456
+ In `~/.nowai-sync/DESIGN.md`, make these three edits:
457
+
458
+ 1. Change wrapper flow step 3 to:
459
+
460
+ ```md
461
+ 3. **Precheck (abort + notify on fail):** branch must be `main`; worktree must
462
+ be clean; `git fetch origin main`; local must not be behind/diverged from
463
+ `origin/main`. Every blocked precheck also posts a Teams warning card.
464
+ ```
465
+
466
+ 2. Change the Teams-notification table to:
467
+
468
+ ```md
469
+ | Outcome | Card | Color |
470
+ |---|---|---|
471
+ | Updated + pushed | "βœ… Staging ready for review" + Open-staging button + change facts | Good (green) |
472
+ | Sanity-gate abort | "⚠️ Leaderboard sync aborted" + issues | Warning (amber) |
473
+ | Blocked precheck | "⚠️ Leaderboard sync blocked" + reason/facts | Warning (amber) |
474
+ | Any failure | "❌ Leaderboard sync failed" + stage/error facts | Attention (red) |
475
+ | No changes | *(no Teams card β€” desktop only)* | β€” |
476
+ ```
477
+
478
+ 3. Add this smoke-test section under `Install / manage`:
479
+
480
+ ~~~md
481
+ ### Smoke-test blocked warnings
482
+
483
+ Use a temporary clone so the live repo is not disturbed:
484
+
485
+ ```sh
486
+ rm -rf /tmp/nowai-bench-dirty
487
+ git clone /Users/bradley.cleveland/git/hf/NowAI-Bench /tmp/nowai-bench-dirty
488
+ touch /tmp/nowai-bench-dirty/.blocked-sync-smoke
489
+ REPO=/tmp/nowai-bench-dirty /bin/bash ~/.nowai-sync/nowai-sync-cron.sh
490
+ rm -rf /tmp/nowai-bench-dirty
491
+ ```
492
+ ~~~
493
+
494
+ - [ ] **Step 3: Run the full repo test suite**
495
+
496
+ Run:
497
+
498
+ ```bash
499
+ cd .claude/skills/sync-nowai-leaderboard
500
+ node --test
501
+ ```
502
+
503
+ Expected: PASS, including the existing transform tests and the new blocked-notify tests.
504
+
505
+ - [ ] **Step 4: Commit the repo-tracked changes**
506
+
507
+ ```bash
508
+ git add \
509
+ .claude/skills/sync-nowai-leaderboard/lib/blocked-notify.mjs \
510
+ .claude/skills/sync-nowai-leaderboard/notify-blocked.mjs \
511
+ .claude/skills/sync-nowai-leaderboard/test/blocked-notify.test.mjs \
512
+ .claude/skills/sync-nowai-leaderboard/test/notify-blocked-cli.test.mjs \
513
+ .claude/skills/sync-nowai-leaderboard/SKILL.md
514
+ git commit -m "feat: send Teams warnings for blocked sync runs"
515
+ ```
516
+
517
+ - [ ] **Step 5: Leave the local wrapper and local DESIGN doc in place**
518
+
519
+ There is no git commit for `~/.nowai-sync/nowai-sync-cron.sh` or
520
+ `~/.nowai-sync/DESIGN.md` because those files intentionally live outside the
521
+ repo. The implementation is not complete until those local files have been
522
+ edited and the smoke tests above have posted real Teams warnings.