From 7697e23c515ba709745908da60dec8c9e81d0130 Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:20:17 +0100 Subject: [PATCH 01/16] Add Regulation Monitor skill Configure once and run on a schedule. On setup: user names the regulation, the skill auto-discovers the top 5 authoritative sources per topic and STOPS for explicit confirmation, captures a light WorkIQ-derived team profile, and locks everything into a config. Every run visits only the locked source list plus user seeds, classifies items with a topic/jurisdiction/stage/date taxonomy, flags team-relevant items via a keyword match against the WorkIQ profile, and renders a self-contained HTML dashboard. Guardrails: never fabricate, no speculation or rumors, no unofficial sources, empty topics reported as 'No significant developments this period', monitoring-not-advice, paywall/PII disciplined. Domain-agnostic (tax, privacy, AI, health, finance, ESG, labor). Targets Cowork, Copilot Studio, and Scout. --- submissions/regulation-monitor/SKILL.md | 413 ++++++++++++++++++ submissions/regulation-monitor/metadata.json | 10 + .../references/automation-template.md | 53 +++ .../references/sources-and-taxonomy.md | 114 +++++ .../scripts/build_dashboard.py | 343 +++++++++++++++ 5 files changed, 933 insertions(+) create mode 100644 submissions/regulation-monitor/SKILL.md create mode 100644 submissions/regulation-monitor/metadata.json create mode 100644 submissions/regulation-monitor/references/automation-template.md create mode 100644 submissions/regulation-monitor/references/sources-and-taxonomy.md create mode 100644 submissions/regulation-monitor/scripts/build_dashboard.py diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md new file mode 100644 index 00000000..fdcaf75d --- /dev/null +++ b/submissions/regulation-monitor/SKILL.md @@ -0,0 +1,413 @@ +--- +name: regulation-monitor +description: | + Monitors user-specified regulations, laws, and regulatory guidance on a + recurring schedule. On first run, walks the user through a one-time setup + (watch topics, jurisdictions, cadence), auto-discovers the top authoritative + sources for each watch topic, and locks the sweep to that fixed source list + plus the user's own seed sources. Every subsequent run visits only those + sources (never an open-ended web search), classifies each item, flags items + relevant to the user's team using WorkIQ-derived context, and renders a + self-contained HTML dashboard. + Use when the user says "monitor regulations", "set up a regulation tracker", + "watch for new rules on X", "any updates on the EU AI Act", "run my + regulation monitor", or wants a weekly regulatory digest. + Do NOT use for one-off legal research (use deep-research), for computing a + compliance liability or filing position (out of scope — this is monitoring), + or for reading a single document the user already has (use docx / pptx). + This skill is domain-agnostic — tax, privacy, AI/ML, healthcare, finance, + ESG, labor, etc. — the user picks the topics at setup. +--- + +# Regulation Monitor + +## Overview + +Turns a saved "watch profile" — topics, jurisdictions, source list, cadence — +into a repeatable regulatory sweep. On each run the skill visits **only** the +sources locked into the profile at setup: the top authoritative sources the +skill discovered for each watch topic plus any seed sources the user added. +It classifies each new item, flags items likely relevant to the user's team +using a light org profile derived once from WorkIQ, and produces a +self-contained HTML dashboard. + +It is a **monitoring** tool. It reports what regulators, legislatures, and +courts are doing. It never files, calculates liability, or gives a legal +opinion. + +## Source discipline + +This skill is deliberately **bounded and confirmed**. It does not run +open-ended web searches every run — those get expensive, unpredictable, and +prone to noise. Instead, at setup: + +1. The skill runs a small **discovery pass** to identify the top 5 + authoritative sources per watch topic (regulators, official trackers, + reputable trade press). +2. **The skill presents that shortlist to the user and stops.** No + monitoring runs until the user has confirmed the list. +3. The user can swap/remove any of the 5, and can supply their own + **seed sources** on top. +4. The confirmed list is **locked into the profile config**. + +Every subsequent run visits **only** those sources. A tightly-bounded +fallback web search (at most one query per topic, capped result count, +allowlist-filtered) is used only when a seed source returns nothing new in +the window. See "Runtime budget" below. + +## When to Use + +- On-demand: "run my regulation monitor", "what changed on the EU AI Act + this week" +- Setup: "set up a regulation tracker for [topics]", "monitor [X] regulations + for me" +- Scheduled: the unattended weekly (or user-chosen cadence) run produced by + the bundled automation template — no user prompt needed + +## When NOT to Use + +- One-off legal or regulatory research → use `deep-research` +- Reading a single document the user has already given you → use `docx` / + `pptx` +- Computing a compliance liability, filing position, or legal conclusion → + out of scope; escalate to a human +- Non-regulatory news monitoring → use `deep-research` or a news skill + +## Quick Start + +``` +User: "Set up a regulation monitor for OECD Pillar II across all jurisdictions" +1. Capture profile name, watch topics, jurisdictions, cadence, delivery. +2. Discovery pass: propose the top 5 authoritative sources per watch topic. +3. STOP — show the shortlist to the user and ask two things: which to swap + out, and whether they want to add their own seed sources. +4. Wait for confirmation. Do not do any monitoring until the user approves. +5. Derive a light org profile from WorkIQ (function-area keywords) and show + it for confirmation. +6. Lock all of this into config.json. +7. Do the first sweep now that the user has approved the source list. + +User (later, or on schedule): "Run my regulation monitor" +1. Load the profile config. +2. Resolve the time window (default: since last successful run, else the + profile's window_days). +3. Sweep the locked source list. Bounded fallback search only when a source + is silent. +4. Classify each item. +5. Flag team-relevant items. +6. Build the dashboard and deliver per the profile. +``` + +## Core Instructions + +### Step 0 — First-run setup (only if no config exists) + +If `config.json` for the requested profile does not exist, walk the user +through setup: + +1. **Profile name** — kebab-case slug, e.g. `pillar-ii`. +2. **Watch topics** — 2 to 8 topics. For each: display name plus 3–8 + keywords/phrases the sweeps should look for. +3. **Jurisdictions** — countries, regions, states, sectors, or `global`. +4. **Cadence** — daily, weekly (default), biweekly, monthly. +5. **Window** — days to look back per run (default: matches cadence). +6. **Delivery target** — user's own email (default), a Teams chat, a Loop + page, or "inline only". + +### Step 1 — Auto-discover authoritative sources, then STOP for confirmation + +**This is an interactive checkpoint. Do not proceed to any research or +monitoring until the user has explicitly confirmed the source list.** + +For each watch topic the user configured, propose **exactly 5 authoritative +sources**: + +- **Preference order** (in this order — pick the strongest 5 that exist for + the topic): + 1. The primary regulator / issuing body's official page for the topic. + Examples: OECD's Pillar Two page, the European Commission's page for + the AI Act, HHS OCR for HIPAA, EDPB for GDPR, ISSB for sustainability + disclosure. + 2. Government official journals and legislative trackers for the + jurisdictions in scope. Examples: Federal Register, EUR-Lex, UK + legislation.gov.uk, state legislature bill pages. + 3. The relevant multilateral, standard-setting, or specialist body's page + for the topic. Examples: OECD, UN, BIS, ISO, NIST (AI RMF, cyber), + WHO/EMA (health), ILO (labor), FSB (financial stability). + 4. A reputable public tracker or think tank whose focus matches the + domain. Examples: Tax Foundation and MTC/NCSL for tax; IAPP and + Future of Privacy Forum for privacy; Stanford HAI, Brookings AI, and + the Ada Lovelace Institute for AI; ISSB and EFRAG for sustainability; + SHRM and EPI for labor; KFF for healthcare policy. + 5. Public alert pages from major professional-services or specialist + firms that cover the domain (public URLs only, never subscriber + content). Examples: KPMG / EY / PwC / Deloitte / BDO insight pages + for tax and financial regulation; DLA Piper, Hogan Lovells, Wilson + Sonsini, Cooley for tech / privacy / AI; Ropes & Gray for healthcare; + Littler and Ogletree Deakins for labor. + + Pick the mix that fits the domain. A tax profile will lean on OECD + + regulators + Tax Foundation + big-four alerts. A privacy profile will + lean on EDPB + national DPAs + IAPP + tech-privacy firm alerts. A health + profile will lean on HHS/FDA + WHO/EMA + KFF + healthcare firm alerts. + Do not force tax-style sources onto a non-tax topic. + +- **How to find them**: for each topic, do a short bounded discovery + pass — one to three focused web searches against the reputable-domain + allowlist in `references/sources-and-taxonomy.md` — just enough to + identify the canonical topic pages (not the regulator's home page). This + discovery pass is separate from the monitoring sweep and must be small. + +- **Present them and STOP**. Show the proposed list to the user in a short + message and wait for a reply before doing anything else: + > "Before I start monitoring, here are the 5 authoritative sources I'd + > watch for **Pillar II**: + > 1. OECD — Pillar Two: + > 2. European Commission — Pillar Two implementation: + > 3. HMRC — Multinational Top-up Tax: + > 4. Tax Foundation — Global minimum tax tracker: + > 5. KPMG — BEPS 2.0 tracker: + > + > Want me to swap any out? And do you have any of your own sources + > (regulator pages, internal trackers, subscription-free trade alerts, + > etc.) you want me to add on top of these?" + +- **Wait for the user's reply.** The user may: + - Approve as-is → proceed. + - Ask to swap or remove one of the 5 → re-run discovery for that slot + with the constraint they gave you. + - Add their own seed URLs → merge them into `seed_sources` in the config. + Seed sources are **not** counted against the "top 5" — a topic can end + up with 5 auto-discovered + N user seeds. + - Ask you to lower the target from 5 (e.g., "just the two OECD pages + are enough") → honor it. + +- **Do not skip this confirmation, even on a re-setup.** If the user later + says "add EU AI Act to my profile", repeat this checkpoint for the new + topic before touching the sweep. + +### Step 2 — Capture the WorkIQ org profile (setup) + +Derive a light org profile from WorkIQ and show it for confirmation: + +- `workiq_get_my_profile` → job title, department, office +- `workiq_get_my_manager` → manager and their department (context only) +- `workiq_get_relevant_people` (limit 10) → likely function-area + collaborators + +From those, propose a `function_area_keywords` list (5–15 words: department +name and variants, the user's job function, key collaborator team names, +obvious topic proxies). The user edits and confirms. + +If WorkIQ is unavailable on the current platform, ask the user to provide +`function_area_keywords` manually. The rest of the skill works unchanged. + +### Step 3 — Save the profile + +Write `config.json`: + +```json +{ + "profile_name": "pillar-ii", + "watch_topics": [ + { "key": "pillar-two", "name": "OECD Pillar II / GloBE", + "keywords": ["Pillar Two", "GloBE", "global minimum tax", + "IIR", "UTPR", "QDMTT", "DMTT", "top-up tax"] } + ], + "jurisdictions": ["global"], + "sources_by_topic": { + "pillar-two": [ + { "name": "OECD — Pillar Two", "url": "https://www.oecd.org/tax/beps/pillar-two-model-rules-in-a-nutshell.pdf", + "kind": "regulator" }, + { "name": "European Commission — Pillar Two", + "url": "https://taxation-customs.ec.europa.eu/taxation/business-taxation/minimum-corporate-taxation_en", + "kind": "regulator" } + ] + }, + "seed_sources": [], + "cadence": "weekly", + "window_days": 7, + "runtime_budget": { + "max_items": 40, + "max_fallback_searches": 2, + "max_fetches_per_source": 2 + }, + "delivery": { "type": "email", "to": ["me@example.com"] }, + "workiq_context": { + "captured_at": "2026-07-23T11:00:00Z", + "department": "Global Tax Policy", + "job_title": "Director, International Tax", + "function_area_keywords": ["Pillar Two", "GloBE", "international tax", + "transfer pricing", "top-up tax"], + "collaborator_teams": ["Transfer Pricing", "Tax Controversy"] + }, + "last_run_at": null +} +``` + +### Step 4 — Load config and scope the sweep (every run) + +- Read `config.json` for the profile. +- Resolve the window: from `last_run_at` (if set) to now, else the past + `window_days`. +- Restate the scope back to the user in one line so they can interrupt if it + looks wrong (interactive runs only). + +### Step 5 — Sweep the locked source list (bounded) + +Sweep proceeds in this order and stops when the budget is met: + +1. **Every source in `sources_by_topic` and `seed_sources`.** `web_fetch` + each URL. Extract items dated within the window. + - Cap `max_fetches_per_source` (default 2). If a source's index page + links to individual items, follow at most that many links per source. +2. **Bounded fallback search** only for topics where every locked source + returned zero items in the window. At most one `web_search` per topic, + at most `max_fallback_searches` total across the run (default 2). + Filter results by the reputable-domain allowlist. Discard non-matching + results. +3. **Stop when `max_items` is reached** (default 40). Prefer regulator + sources > tracker sources > firm alerts when trimming. + +**Rules that always apply:** + +- Validate every date against the window; drop out-of-window items. +- Never bypass a paywall; skip subscriber-only content. +- Deduplicate items with the same title + jurisdiction, keeping the more + authoritative source (regulator > tracker > firm alert). + +### Step 6 — Classify each item + +Record, for every item: + +- **topic** — one of the profile's watch-topic keys +- **jurisdiction** — from the profile's list, or `global` / `local: ` +- **stage** — `proposed` / `in-consultation` / `passed` / + `regulatory-guidance` / `in-force` / `litigation` / `withdrawn` +- **date** — ISO date the source is dated or the action took place +- **title** — the source's short title, verbatim +- **summary** — 1–2 sentences in the skill's own words +- **source_name** — publisher's short name +- **source_url** — canonical public URL actually retrieved + +Stage inference guidance is in `references/sources-and-taxonomy.md`. + +### Step 7 — Flag team relevance (WorkIQ-derived) + +For each item, set `relevant_to_your_team` to `true` if any +`workiq_context.function_area_keywords` phrase appears (case-insensitive) in +the item's title, summary, or matched topic keywords. Otherwise `false`. + +This is a soft highlight, not an impact rubric. The dashboard uses it to +sort and badge; the skill never says "this affects your business" — that is +a human judgment. + +### Step 8 — Build the dashboard + +1. Write items to `working/regulation-items.json` (schema in the script + header). +2. Run the bundled generator: + ``` + python scripts/build_dashboard.py \ + --config config.json \ + --items working/regulation-items.json \ + --output output/regulation-dashboard.html + ``` +3. Verify the file was written before telling the user it is ready. + +The dashboard is a single self-contained HTML file — KPI tiles (total items, +count per topic, team-relevant count), a sortable table color-coded by +stage, a team-relevant badge on flagged rows, and every row linking to its +primary source. + +### Step 9 — Deliver and update last-run + +- **Inline**: a short summary — window covered, item counts by topic, the + top team-relevant items (title, jurisdiction, stage, date). +- **Scheduled runs**: send per the profile's delivery block. If `type` is + `email`, send the HTML dashboard as an attachment to the addresses in + `to`. The pre-authorized recipient is the user themselves. Any additional + recipient requires explicit user confirmation on an interactive run and + is never added on an unattended run. +- **Update the config** with `last_run_at = `. + +## Runtime budget (defaults) + +The `runtime_budget` block in the config caps every dimension of a run: + +| Setting | Default | What it caps | +|---|---:|---| +| `max_items` | 40 | Total items recorded in one run | +| `max_fallback_searches` | 2 | `web_search` calls per run (only if a source was silent) | +| `max_fetches_per_source` | 2 | Individual items followed from one source's index | + +These bounds keep the skill fast and predictable. Users can raise them for +big topics (e.g., Pillar II across 20 jurisdictions) or lower them for +narrow ones (e.g., one EU regulation). + +## Output + +- **Dashboard**: `output/regulation-dashboard.html` — KPI tiles, sortable + color-coded table, team-relevant badges, per-row source links. +- **Items JSON**: `working/regulation-items.json` — raw items from this run + (useful for diffing or feeding downstream tools). +- **Inline summary**: ≤12 lines — window, item counts by topic, top team- + relevant items. If a topic produced no items, say **"No significant + developments this period"** for that topic — do not pad, do not speculate. + +## Guardrails + +- **Never fabricate** a bill number, date, quote, or enactment status. If a + fact cannot be confirmed from a retrieved public source, mark it + `[unverified]` in the summary. Report gaps honestly ("couldn't confirm X"). +- **No speculation, rumors, or unofficial sources.** Do not include items + that come from anonymous leaks, social-media speculation, unattributed + drafts, or "reportedly" / "expected to" claims without a named official + source. If an item cannot be tied to a specific document or announcement + from a source on the profile's locked list (or the reputable-domain + allowlist for fallback search), drop it. Better silence than noise. +- **Report empty categories explicitly.** If a topic produced no items in + the window, say **"No significant developments this period"** for that + topic in both the inline summary and the dashboard. Do not pad with + low-signal filler. +- **Public sources only.** Never bypass a paywall or reproduce paywalled or + copyrighted text. Summarize in the skill's own words and link the source. +- **Locked source list.** The sweep only visits URLs in the profile's + `sources_by_topic` and `seed_sources`. That list is set at interactive + setup with an explicit user confirmation — the skill cannot start + monitoring until the user has approved the sources. Fallback search at + runtime is bounded by `runtime_budget.max_fallback_searches` and filtered + by the reputable-domain allowlist. Do not add new sources on an + unattended run — that requires interactive re-setup. +- **Monitoring, not advice.** The skill never states a filing position, a + legal conclusion, or a business impact. `relevant_to_your_team` is a soft + keyword-match highlight. +- **Confirm before external sends.** Emailing anyone other than the user + requires explicit confirmation on an interactive run; unattended runs + never add recipients. +- **Confidentiality and PII.** Do not add any WorkIQ-derived personal names, + email addresses, or internal identifiers to the dashboard beyond the + keywords the user confirmed at setup. The dashboard may be attached to + emails and shared. +- **Verify delivery.** Confirm the dashboard file exists before reporting + success. If the delivery block failed, report the failure — do not report + success. +- **Cite by exact source name** and validate every date against the + requested window. + +## Scheduling + +See `references/automation-template.md` for how to schedule this skill on +Scout (via Scout automation), Copilot Studio (scheduled agent), and Cowork +(scheduled task). The core skill has no cadence of its own — the schedule +lives outside the skill and simply invokes it. + +## References + +- `references/sources-and-taxonomy.md` — reputable-domain allowlist for + fallback search, item classification taxonomy, stage-inference rules, and + per-topic search-query templates. +- `references/automation-template.md` — how to schedule the monitor on + Scout, Copilot Studio, and Cowork. +- `scripts/build_dashboard.py` — self-contained dashboard generator + (Python standard library only). diff --git a/submissions/regulation-monitor/metadata.json b/submissions/regulation-monitor/metadata.json new file mode 100644 index 00000000..f1e7e788 --- /dev/null +++ b/submissions/regulation-monitor/metadata.json @@ -0,0 +1,10 @@ +{ + "name": "Regulation Monitor", + "description": "Configure once, then on a schedule sweeps user-specified regulations across seed sources plus a reputable-domain web search, classifies each item, flags items relevant to the user's team using a light WorkIQ-derived profile, and renders a self-contained HTML dashboard. Domain-agnostic (tax, privacy, AI/ML, healthcare, finance, ESG, labor, etc.).", + "platforms": ["Cowork", "Copilot Studio", "Scout"], + "tags": ["regulation", "monitoring", "compliance", "dashboard", "research"], + "author": "Jagmeet Chabra", + "version": "1.0.0", + "createdAt": "2026-07-23", + "updatedAt": "2026-07-23" +} diff --git a/submissions/regulation-monitor/references/automation-template.md b/submissions/regulation-monitor/references/automation-template.md new file mode 100644 index 00000000..1d912c08 --- /dev/null +++ b/submissions/regulation-monitor/references/automation-template.md @@ -0,0 +1,53 @@ +# Scheduling the regulation monitor + +The core skill has no schedule of its own — it just runs. To put it on a +cadence, wire it to a scheduler on your platform. + +## Scout + +Create a Scout automation. In natural language: + +> Every Monday at 8am, run "regulation-monitor" for profile ``. + +Or via the built-in automation tool: + +``` +name: Regulation Monitor — +schedule: every Monday at 8am +prompt: | + Run the regulation-monitor skill for profile "". + Load the profile's config.json, sweep the window since the last run, + build the dashboard, and email the digest to the user per the + delivery block. Send silently if no items were found. +teamsNotify: auto +``` + +Scout will keep the recurrence, invoke the skill, and post the summary +according to the automation's Teams notification policy. + +## Copilot Studio + +Create a scheduled agent in Copilot Studio pointed at the skill. In the +agent's schedule, choose the same cadence as the profile +(`cadence: weekly` → weekly). The agent's prompt is identical to the Scout +automation prompt above. + +## Cowork + +Cowork tasks can run on a schedule. Create a recurring task: + +- Task name: `Regulation Monitor — ` +- Recurrence: match the profile's cadence +- Instructions: the same prompt as above, pointed at the skill. + +## What the schedule should NOT do + +- **Do not reconfigure the profile from the schedule.** Setup is an + interactive step. If the profile is missing or stale, the scheduled run + should send a short heads-up to the user and stop, not silently rebuild. +- **Do not add external email recipients from the schedule.** The pre- + authorized recipient is the user themselves. Any other recipient requires + an interactive confirmation. +- **Do not chain the monitor into downstream action.** This skill monitors; + it does not take a filing position or trigger a workflow. Keep the + scheduled job single-purpose. diff --git a/submissions/regulation-monitor/references/sources-and-taxonomy.md b/submissions/regulation-monitor/references/sources-and-taxonomy.md new file mode 100644 index 00000000..2b3685a4 --- /dev/null +++ b/submissions/regulation-monitor/references/sources-and-taxonomy.md @@ -0,0 +1,114 @@ +# Sources, taxonomy, and search-query templates + +This file is loaded by the skill during setup and every sweep. It defines the +default reputable-domain allowlist, the item classification taxonomy, and how +per-topic search queries are built. + +## Default reputable-domain allowlist + +The sweep only accepts `web_search` results whose domain matches one of these +patterns (or an extension the user added at setup). Seed sources the user +provided are always honored regardless of domain. + +### Government and inter-governmental (broad) + +- `*.gov` — U.S. federal, state, local +- `*.gov.uk` — United Kingdom +- `*.gc.ca` — Canada federal +- `*.gov.au` — Australia +- `*.govt.nz` — New Zealand +- `*.europa.eu` — European Union institutions +- `*.oecd.org` — OECD +- `*.un.org` — United Nations +- `*.who.int` — WHO +- `*.bis.org` — Bank for International Settlements +- `*.imf.org` — IMF +- `*.worldbank.org` — World Bank + +### Sector-specific regulators (opt-in but pre-approved) + +Financial: `sec.gov`, `cftc.gov`, `federalreserve.gov`, `fdic.gov`, `occ.gov`, +`ecb.europa.eu`, `eba.europa.eu`, `esma.europa.eu`, `fca.org.uk`, `bankofengland.co.uk`. + +Privacy and data: `ftc.gov`, `edpb.europa.eu`, `ico.org.uk`, `cnil.fr`, +`bfdi.bund.de`, `oaic.gov.au`, `priv.gc.ca`. + +Competition: `justice.gov`, `ftc.gov`, `ec.europa.eu/competition`, `cma.gov.uk`, +`bundeskartellamt.de`, `competitionbureau.gc.ca`. + +Health: `hhs.gov`, `fda.gov`, `cms.gov`, `ema.europa.eu`, `mhra.gov.uk`. + +Environment / ESG: `epa.gov`, `energy.gov`, `iso.org`, `issb.ifrs.org`, +`sec.gov` (climate disclosure), `efrag.org`. + +Labor and workforce: `dol.gov`, `eeoc.gov`, `nlrb.gov`. + +Tax: `irs.gov`, `treasury.gov`, `hmrc.gov.uk`, `taxpolicycenter.org`, +`taxfoundation.org`, state DOR sites (already covered by `.gov`). + +Standards and technical: `nist.gov`, `iso.org`, `ieee.org`, `w3.org`, +`iana.org`. + +### Reputable trackers and think tanks (opt-in) + +- `mtc.gov` — Multistate Tax Commission +- `ncsl.org` — National Conference of State Legislatures +- `cost.org` — Council on State Taxation (public materials only) +- `iapp.org` — International Association of Privacy Professionals +- `future-of-privacy-forum.org` +- `brookings.edu`, `hoover.stanford.edu`, `aei.org`, `epic.org` +- `hbr.org` (limited to freely available articles) + +The user can extend this list via `domain_allowlist_extensions` in the config. +Add sparingly — the whole point of the allowlist is signal, not volume. + +## Item taxonomy + +Each item recorded by the sweep has: + +| Field | Values | Notes | +|-------|--------|-------| +| `topic` | one of the profile's `watch_topics[].key` | required | +| `jurisdiction` | from profile's `jurisdictions`, or `global` / `local: ` | required | +| `stage` | `proposed` / `in-consultation` / `passed` / `regulatory-guidance` / `in-force` / `litigation` / `withdrawn` | required | +| `date` | ISO date the source is dated or the action took place | required | +| `title` | source's short title, verbatim | required | +| `summary` | 1-2 sentences in the model's own words | required | +| `source_name` | publisher's short name | required | +| `source_url` | canonical public URL that was actually retrieved | required | +| `relevant_to_your_team` | `true` if any function-area keyword matches | derived at Step 4 | + +## Search-query templates + +The sweep builds one query per watch topic. Use this shape: + +``` +("" OR "" OR "") AND (regulation OR guidance OR rule OR bill OR "final rule" OR consultation) AND ( OR ) after: +``` + +Guidance: + +- Prefer exact-quoted phrases for multi-word keywords ("EU AI Act", not + EU AI Act) to reduce false positives. +- Add jurisdiction terms only if the profile's list is short and specific. + If the profile lists 6+ jurisdictions, drop the jurisdiction clause and + filter by domain after. +- Use `after:YYYY-MM-DD` (or the equivalent recency filter available in the + web_search tool) to enforce the window at query time. +- Fan topics out in parallel, one query per topic. Do not run multiple + redundant variants of one topic. + +## Stage inference + +Pick the stage from the source's own language: + +- "introduced", "filed", "sponsors" → `proposed` +- "consultation", "call for evidence", "request for comment", "notice of proposed rulemaking" → `in-consultation` +- "passed the Senate", "royal assent", "adopted by Council" → `passed` +- "guidance", "circular", "policy statement", "notice", "FAQ" → `regulatory-guidance` +- "effective", "in force", "applies from" (past date) → `in-force` +- "court", "ruling", "opinion", "settlement", "consent decree" → `litigation` +- "withdrawn", "vacated", "rescinded" → `withdrawn` + +If the source is genuinely ambiguous, pick the earliest applicable stage and +say so in the summary. diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py new file mode 100644 index 00000000..4132a2f3 --- /dev/null +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +"""Build a self-contained HTML dashboard from a regulation-monitor run. + +Consumes: + --config path to profile config.json (for profile name, topics, cadence) + --items path to a JSON file with shape {"items": [ ... ]} + --output path to write the HTML dashboard + +Item schema (each entry in items[]): + topic str - one of the profile's watch_topics[].key + jurisdiction str - e.g. "EU", "US-federal", "UK", "global" + stage str - proposed | in-consultation | passed | + regulatory-guidance | in-force | litigation | + withdrawn + date str - ISO date + title str - source's short title, verbatim + summary str - 1-2 sentence summary + source_name str - publisher's short name + source_url str - canonical public URL + relevant_to_your_team bool - team-relevance flag from Step 4 + +Uses only the Python standard library so it runs in restricted sandboxes. +""" + +from __future__ import annotations + +import argparse +import html +import json +import sys +from collections import Counter +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +STAGE_ORDER = [ + "proposed", + "in-consultation", + "passed", + "regulatory-guidance", + "in-force", + "litigation", + "withdrawn", +] + +STAGE_COLORS = { + "proposed": "#6b7280", + "in-consultation": "#0ea5e9", + "passed": "#2563eb", + "regulatory-guidance": "#7c3aed", + "in-force": "#059669", + "litigation": "#dc2626", + "withdrawn": "#9ca3af", +} + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def esc(value: Any) -> str: + return html.escape("" if value is None else str(value)) + + +def stage_pill(stage: str) -> str: + color = STAGE_COLORS.get(stage, "#374151") + return ( + f'' + f'{esc(stage)}' + ) + + +def relevant_badge(is_relevant: bool) -> str: + if not is_relevant: + return "" + return ( + '' + "team-relevant" + ) + + +def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: + profile_name = esc(config.get("profile_name", "regulation-monitor")) + cadence = esc(config.get("cadence", "on demand")) + window_days = config.get("window_days", 7) + generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + + topic_name_by_key: dict[str, str] = {} + for topic in config.get("watch_topics", []): + topic_name_by_key[str(topic.get("key", ""))] = str( + topic.get("name", topic.get("key", "")) + ) + + total = len(items) + per_topic = Counter(item.get("topic", "") for item in items) + per_stage = Counter(item.get("stage", "") for item in items) + team_relevant = sum(1 for item in items if item.get("relevant_to_your_team")) + + # Sort: team-relevant first, then stage order, then date desc. + def sort_key(item: dict[str, Any]) -> tuple[int, int, str]: + stage_idx = ( + STAGE_ORDER.index(item.get("stage", "")) + if item.get("stage") in STAGE_ORDER + else len(STAGE_ORDER) + ) + relevant_first = 0 if item.get("relevant_to_your_team") else 1 + # Sort dates descending by negating via string trick: prefix with '0' if + # missing to sort last. We reverse-sort strings so newer ISO dates win. + date_key = item.get("date") or "0000-00-00" + return (relevant_first, stage_idx, date_key) + + sorted_items = sorted(items, key=sort_key) + # Because dates should be newest-first within (relevance, stage), we sort + # the date within each group by negating the string ordering. Simpler: + # sort desc by date on a second pass while preserving stability. + sorted_items.sort( + key=lambda x: x.get("date") or "0000-00-00", reverse=True + ) + sorted_items.sort(key=lambda x: sort_key(x)[:2]) + + # KPI tiles + tiles_html: list[str] = [] + tiles_html.append(_tile("Total items", str(total))) + tiles_html.append(_tile("Team-relevant", str(team_relevant))) + for topic in config.get("watch_topics", []): + key = str(topic.get("key", "")) + tiles_html.append( + _tile( + str(topic.get("name", key)), + str(per_topic.get(key, 0)), + ) + ) + + # Rows + rows_html: list[str] = [] + for item in sorted_items: + topic_key = str(item.get("topic", "")) + topic_display = topic_name_by_key.get(topic_key, topic_key or "—") + title = esc(item.get("title", "(untitled)")) + summary = esc(item.get("summary", "")) + source_name = esc(item.get("source_name", "")) + source_url = esc(item.get("source_url", "")) + source_link = ( + f'{source_name}' + if source_url + else source_name + ) + rows_html.append( + "" + f"{esc(item.get('date') or '—')}" + f"{esc(topic_display)} {relevant_badge(bool(item.get('relevant_to_your_team')))}" + f"{esc(item.get('jurisdiction') or '—')}" + f"{stage_pill(str(item.get('stage', '')))}" + f"
{title}
" + f"
{summary}
" + f"{source_link}" + "" + ) + + if not rows_html: + rows_html.append( + 'No significant developments this period.' + ) + + # Empty-topic callout: list any watch topic that produced zero items. + topics_with_items = {str(item.get("topic", "")) for item in items} + empty_topics: list[str] = [] + for topic in config.get("watch_topics", []): + key = str(topic.get("key", "")) + if key and key not in topics_with_items: + empty_topics.append(str(topic.get("name", key))) + + empty_html = "" + if empty_topics: + rows = "".join( + f"
  • {esc(name)} — No significant developments this period.
  • " + for name in empty_topics + ) + empty_html = ( + '
    ' + '

    Quiet this period

    ' + f'
      {rows}
    ' + "
    " + ) + + style = _stylesheet() + return f""" + + + + +Regulation Monitor — {profile_name} + + + +
    +
    +
    +
    +
    Regulation Monitor
    +

    {profile_name}

    +
    +
    +
    +
    Cadence {cadence}
    +
    Window last {esc(window_days)} days
    +
    Generated {esc(generated_at)}
    +
    +
    + +
    + {''.join(tiles_html)} +
    + +
    +

    Items

    + + + + + + + + + + + + + {''.join(rows_html)} + +
    DateTopicJurisdictionStageTitle & summarySource
    +
    + +{empty_html} + +
    +
    Regulation Monitor is a monitoring tool. Nothing on this dashboard is legal, tax, or compliance advice. Team-relevance is a keyword match, not an impact assessment.
    +
    + + +""" + + +def _tile(label: str, value: str) -> str: + return ( + '
    ' + f'
    {esc(value)}
    ' + f'
    {esc(label)}
    ' + "
    " + ) + + +def _stylesheet() -> str: + # Kept simple and self-contained. No external assets. + return """ +:root { + --bg:#f8fafc; --card:#ffffff; --ink:#0f172a; --muted:#64748b; + --line:#e2e8f0; --accent:#4f46e5; --accent-2:#0ea5e9; + --shadow:0 1px 2px rgba(15,23,42,.04), 0 1px 3px rgba(15,23,42,.06); +} +*{box-sizing:border-box} +html,body{margin:0;padding:0;background:var(--bg);color:var(--ink); + font:14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;} +header{padding:24px 32px;background:linear-gradient(180deg,#ffffff, #f1f5f9); + border-bottom:1px solid var(--line);display:flex;justify-content:space-between; + align-items:flex-end;gap:24px;flex-wrap:wrap} +.brand{display:flex;gap:14px;align-items:center} +.brand .dot{width:12px;height:12px;border-radius:50%; + background:linear-gradient(135deg,var(--accent),var(--accent-2));box-shadow:var(--shadow)} +.eyebrow{color:var(--muted);text-transform:uppercase;letter-spacing:.08em; + font-size:11px;font-weight:600} +h1{margin:2px 0 0;font-size:22px;font-weight:600} +.meta{display:flex;gap:20px;color:var(--muted);font-size:12px;flex-wrap:wrap} +.meta strong{color:var(--ink);font-weight:600} + +section{padding:24px 32px} +section h2{margin:0 0 12px;font-size:14px;text-transform:uppercase; + letter-spacing:.06em;color:var(--muted);font-weight:600} + +.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); + gap:12px;padding:16px 32px 0} +.tile{background:var(--card);border:1px solid var(--line);border-radius:10px; + padding:14px 16px;box-shadow:var(--shadow)} +.tile-value{font-size:22px;font-weight:600} +.tile-label{color:var(--muted);font-size:12px;margin-top:2px} + +table{width:100%;border-collapse:collapse;background:var(--card); + border:1px solid var(--line);border-radius:10px;overflow:hidden;box-shadow:var(--shadow)} +thead th{background:#f1f5f9;color:var(--muted);text-align:left;font-weight:600; + padding:10px 12px;font-size:11px;text-transform:uppercase;letter-spacing:.06em; + border-bottom:1px solid var(--line)} +tbody td{padding:12px;vertical-align:top;border-bottom:1px solid var(--line); + font-size:13px} +tbody tr:last-child td{border-bottom:none} +.title{font-weight:600;margin-bottom:2px} +.summary{color:var(--muted)} +.empty{color:var(--muted);text-align:center;padding:24px} +.pill{color:#fff;padding:3px 8px;border-radius:999px;font-size:11px; + font-weight:600;text-transform:lowercase} +.badge{display:inline-block;margin-left:8px;color:#3730a3;background:#eef2ff; + padding:2px 8px;border-radius:999px;font-size:10px;font-weight:600; + text-transform:lowercase;letter-spacing:.02em} +a{color:var(--accent);text-decoration:none} +a:hover{text-decoration:underline} + +footer{padding:16px 32px 32px;color:var(--muted);font-size:11px} +.empty-topics ul{list-style:none;margin:0;padding:0;background:var(--card); + border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow); + overflow:hidden} +.empty-topics li{padding:10px 14px;border-bottom:1px solid var(--line); + color:var(--muted);font-size:13px} +.empty-topics li:last-child{border-bottom:none} +""" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--config", type=Path, required=True) + parser.add_argument("--items", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args(argv) + + if not args.config.exists(): + print(f"config not found: {args.config}", file=sys.stderr) + return 2 + if not args.items.exists(): + print(f"items not found: {args.items}", file=sys.stderr) + return 2 + + config = load_json(args.config) + items_doc = load_json(args.items) + items = items_doc.get("items", []) if isinstance(items_doc, dict) else [] + + html_out = build_html(config, items) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(html_out, encoding="utf-8") + print(str(args.output)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 07b1132a2d8c420260728f45582c05c230f68fde Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:26:27 +0100 Subject: [PATCH 02/16] Regulation Monitor: drop Copilot Studio from platforms Copilot Studio removed from metadata.json, SKILL.md references section, and references/automation-template.md. Skill was not tested there; supported platforms are Cowork and Scout only. --- submissions/regulation-monitor/SKILL.md | 4 ++-- submissions/regulation-monitor/metadata.json | 2 +- .../regulation-monitor/references/automation-template.md | 7 ------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md index fdcaf75d..c89bb734 100644 --- a/submissions/regulation-monitor/SKILL.md +++ b/submissions/regulation-monitor/SKILL.md @@ -398,7 +398,7 @@ narrow ones (e.g., one EU regulation). ## Scheduling See `references/automation-template.md` for how to schedule this skill on -Scout (via Scout automation), Copilot Studio (scheduled agent), and Cowork +Scout (via Scout automation) and Cowork (scheduled task). The core skill has no cadence of its own — the schedule lives outside the skill and simply invokes it. @@ -408,6 +408,6 @@ lives outside the skill and simply invokes it. fallback search, item classification taxonomy, stage-inference rules, and per-topic search-query templates. - `references/automation-template.md` — how to schedule the monitor on - Scout, Copilot Studio, and Cowork. + Scout and Cowork. - `scripts/build_dashboard.py` — self-contained dashboard generator (Python standard library only). diff --git a/submissions/regulation-monitor/metadata.json b/submissions/regulation-monitor/metadata.json index f1e7e788..4fa7a0d2 100644 --- a/submissions/regulation-monitor/metadata.json +++ b/submissions/regulation-monitor/metadata.json @@ -1,7 +1,7 @@ { "name": "Regulation Monitor", "description": "Configure once, then on a schedule sweeps user-specified regulations across seed sources plus a reputable-domain web search, classifies each item, flags items relevant to the user's team using a light WorkIQ-derived profile, and renders a self-contained HTML dashboard. Domain-agnostic (tax, privacy, AI/ML, healthcare, finance, ESG, labor, etc.).", - "platforms": ["Cowork", "Copilot Studio", "Scout"], + "platforms": ["Cowork", "Scout"], "tags": ["regulation", "monitoring", "compliance", "dashboard", "research"], "author": "Jagmeet Chabra", "version": "1.0.0", diff --git a/submissions/regulation-monitor/references/automation-template.md b/submissions/regulation-monitor/references/automation-template.md index 1d912c08..6893ad26 100644 --- a/submissions/regulation-monitor/references/automation-template.md +++ b/submissions/regulation-monitor/references/automation-template.md @@ -25,13 +25,6 @@ teamsNotify: auto Scout will keep the recurrence, invoke the skill, and post the summary according to the automation's Teams notification policy. -## Copilot Studio - -Create a scheduled agent in Copilot Studio pointed at the skill. In the -agent's schedule, choose the same cadence as the profile -(`cadence: weekly` → weekly). The agent's prompt is identical to the Scout -automation prompt above. - ## Cowork Cowork tasks can run on a schedule. Create a recurring task: From 8b9432d78c1147458ade5c4a8da664df43de46ac Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:37:30 +0100 Subject: [PATCH 03/16] Regulation Monitor: address Copilot bot review comments Fixed: - Sanitize source_url in dashboard: only http/https/mailto schemes render (drops javascript:, data:, vbscript:); safe URLs get rel=noopener nofollow. - Topic-scope seed sources: seed_sources -> seed_sources_by_topic (keyed the same as sources_by_topic) so the per-topic fallback-silent check is unambiguous. - Clarify confirmation checkpoint wording: monitoring SWEEP is what's gated (small discovery pass to propose sources runs first, that's the point of the shortlist). - Reword 'exactly 5 authoritative sources' to 'up to 5 (default target 5; fewer if the user asks or the domain has fewer)' to remove the internal conflict with the 'user can lower the target' option. - automation-template.md: drop 'send silently if no items were found' (contradicts the empty-state rule); replace with 'always send, empty topics render as No significant developments this period.' - sources-and-taxonomy.md: strip the path segment from 'ec.europa.eu/competition' (allowlist is domain-based). - build_dashboard.py header: step reference for relevant_to_your_team fixed to Step 7 (was Step 4). - Add note to source_url schema that only http/https/mailto is rendered. --- submissions/regulation-monitor/SKILL.md | 47 +++++++++++-------- .../references/automation-template.md | 4 +- .../references/sources-and-taxonomy.md | 2 +- .../scripts/build_dashboard.py | 24 ++++++++-- 4 files changed, 51 insertions(+), 26 deletions(-) diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md index c89bb734..26a21fd9 100644 --- a/submissions/regulation-monitor/SKILL.md +++ b/submissions/regulation-monitor/SKILL.md @@ -116,11 +116,14 @@ through setup: ### Step 1 — Auto-discover authoritative sources, then STOP for confirmation -**This is an interactive checkpoint. Do not proceed to any research or -monitoring until the user has explicitly confirmed the source list.** +**This is an interactive checkpoint. The skill runs a small discovery pass +to identify candidate sources, then presents them and stops. Do not start +the monitoring sweep (Step 5) until the user has explicitly confirmed the +source list.** -For each watch topic the user configured, propose **exactly 5 authoritative -sources**: +For each watch topic the user configured, propose up to **5 authoritative +sources** (default target is 5; use fewer if the user asks or if the domain +genuinely has fewer canonical sources): - **Preference order** (in this order — pick the strongest 5 that exist for the topic): @@ -176,9 +179,11 @@ sources**: - Approve as-is → proceed. - Ask to swap or remove one of the 5 → re-run discovery for that slot with the constraint they gave you. - - Add their own seed URLs → merge them into `seed_sources` in the config. - Seed sources are **not** counted against the "top 5" — a topic can end - up with 5 auto-discovered + N user seeds. + - Add their own seed URLs → append them to `seed_sources_by_topic` under + the appropriate topic key. Seed sources are **not** counted against the + "top 5" — a topic can end up with 5 auto-discovered + N user seeds. + A user seed that spans multiple topics should be added under each + relevant topic key. - Ask you to lower the target from 5 (e.g., "just the two OECD pages are enough") → honor it. @@ -224,7 +229,7 @@ Write `config.json`: "kind": "regulator" } ] }, - "seed_sources": [], + "seed_sources_by_topic": {}, "cadence": "weekly", "window_days": 7, "runtime_budget": { @@ -257,15 +262,16 @@ Write `config.json`: Sweep proceeds in this order and stops when the budget is met: -1. **Every source in `sources_by_topic` and `seed_sources`.** `web_fetch` - each URL. Extract items dated within the window. +1. **Every source in `sources_by_topic[topic]` and + `seed_sources_by_topic[topic]` for each topic.** `web_fetch` each URL. + Extract items dated within the window. - Cap `max_fetches_per_source` (default 2). If a source's index page links to individual items, follow at most that many links per source. 2. **Bounded fallback search** only for topics where every locked source - returned zero items in the window. At most one `web_search` per topic, - at most `max_fallback_searches` total across the run (default 2). - Filter results by the reputable-domain allowlist. Discard non-matching - results. + (auto-discovered + user seeds) returned zero items in the window. At + most one `web_search` per topic, at most `max_fallback_searches` total + across the run (default 2). Filter results by the reputable-domain + allowlist. Discard non-matching results. 3. **Stop when `max_items` is reached** (default 40). Prefer regulator sources > tracker sources > firm alerts when trimming. @@ -373,12 +379,13 @@ narrow ones (e.g., one EU regulation). - **Public sources only.** Never bypass a paywall or reproduce paywalled or copyrighted text. Summarize in the skill's own words and link the source. - **Locked source list.** The sweep only visits URLs in the profile's - `sources_by_topic` and `seed_sources`. That list is set at interactive - setup with an explicit user confirmation — the skill cannot start - monitoring until the user has approved the sources. Fallback search at - runtime is bounded by `runtime_budget.max_fallback_searches` and filtered - by the reputable-domain allowlist. Do not add new sources on an - unattended run — that requires interactive re-setup. + `sources_by_topic` and `seed_sources_by_topic`. That list is set at + interactive setup with an explicit user confirmation — the skill cannot + start the monitoring sweep until the user has approved the sources. + Fallback search at runtime is bounded by + `runtime_budget.max_fallback_searches` and filtered by the reputable- + domain allowlist. Do not add new sources on an unattended run — that + requires interactive re-setup. - **Monitoring, not advice.** The skill never states a filing position, a legal conclusion, or a business impact. `relevant_to_your_team` is a soft keyword-match highlight. diff --git a/submissions/regulation-monitor/references/automation-template.md b/submissions/regulation-monitor/references/automation-template.md index 6893ad26..e058fe61 100644 --- a/submissions/regulation-monitor/references/automation-template.md +++ b/submissions/regulation-monitor/references/automation-template.md @@ -18,7 +18,9 @@ prompt: | Run the regulation-monitor skill for profile "". Load the profile's config.json, sweep the window since the last run, build the dashboard, and email the digest to the user per the - delivery block. Send silently if no items were found. + delivery block. Always send the dashboard, including on quiet weeks — + topics with no items are reported explicitly as "No significant + developments this period" and the empty-state view is intentional. teamsNotify: auto ``` diff --git a/submissions/regulation-monitor/references/sources-and-taxonomy.md b/submissions/regulation-monitor/references/sources-and-taxonomy.md index 2b3685a4..448e110f 100644 --- a/submissions/regulation-monitor/references/sources-and-taxonomy.md +++ b/submissions/regulation-monitor/references/sources-and-taxonomy.md @@ -33,7 +33,7 @@ Financial: `sec.gov`, `cftc.gov`, `federalreserve.gov`, `fdic.gov`, `occ.gov`, Privacy and data: `ftc.gov`, `edpb.europa.eu`, `ico.org.uk`, `cnil.fr`, `bfdi.bund.de`, `oaic.gov.au`, `priv.gc.ca`. -Competition: `justice.gov`, `ftc.gov`, `ec.europa.eu/competition`, `cma.gov.uk`, +Competition: `justice.gov`, `ftc.gov`, `ec.europa.eu`, `cma.gov.uk`, `bundeskartellamt.de`, `competitionbureau.gc.ca`. Health: `hhs.gov`, `fda.gov`, `cms.gov`, `ema.europa.eu`, `mhra.gov.uk`. diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index 4132a2f3..7eb0cf0c 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -16,8 +16,9 @@ title str - source's short title, verbatim summary str - 1-2 sentence summary source_name str - publisher's short name - source_url str - canonical public URL - relevant_to_your_team bool - team-relevance flag from Step 4 + source_url str - canonical public URL (http/https/mailto only; + other schemes are dropped at render time) + relevant_to_your_team bool - team-relevance flag from Step 7 Uses only the Python standard library so it runs in restricted sandboxes. """ @@ -59,6 +60,21 @@ def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) +def safe_url(value: Any) -> str: + """Return the URL only if it uses a safe scheme (http/https/mailto). + + Prevents javascript:/data:/vbscript: injection when items are sourced + from external content the skill did not author. + """ + if not value: + return "" + text = str(value).strip() + lowered = text.lower() + if lowered.startswith(("http://", "https://", "mailto:")): + return text + return "" + + def esc(value: Any) -> str: return html.escape("" if value is None else str(value)) @@ -140,9 +156,9 @@ def sort_key(item: dict[str, Any]) -> tuple[int, int, str]: title = esc(item.get("title", "(untitled)")) summary = esc(item.get("summary", "")) source_name = esc(item.get("source_name", "")) - source_url = esc(item.get("source_url", "")) + source_url = esc(safe_url(item.get("source_url", ""))) source_link = ( - f'{source_name}' + f'{source_name}' if source_url else source_name ) From 86779693f96086e1a06c6197c65647bed196da65 Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:45:29 +0100 Subject: [PATCH 04/16] Regulation Monitor: address second round of bot review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed: - Add README.md sidecar (per updated submissions/README.md guidance which does support optional README.md — this becomes the detail page main content, is not bundled, and doesn't consume agent tokens). Human-facing overview, setup walkthrough, scheduling, tips. - Delete references/automation-template.md (its content moved to README.md; keeping it under references/ was bundling human-facing docs verbatim into the agent's context). - Slim SKILL.md ## Scheduling section to a one-liner that points at the README.md sidecar. - Update SKILL.md ## References to drop the automation-template bullet. - Defensive input validation in build_dashboard.py: error out cleanly if items[] is not a list; drop non-dict entries with a stderr warning rather than crashing during render. --- submissions/regulation-monitor/README.md | 126 ++++++++++++++++++ submissions/regulation-monitor/SKILL.md | 10 +- .../references/automation-template.md | 48 ------- .../scripts/build_dashboard.py | 15 ++- 4 files changed, 144 insertions(+), 55 deletions(-) create mode 100644 submissions/regulation-monitor/README.md delete mode 100644 submissions/regulation-monitor/references/automation-template.md diff --git a/submissions/regulation-monitor/README.md b/submissions/regulation-monitor/README.md new file mode 100644 index 00000000..add22a7d --- /dev/null +++ b/submissions/regulation-monitor/README.md @@ -0,0 +1,126 @@ +# Regulation Monitor + +Configure this skill once for a regulation you care about, then let it run on a +schedule and drop a fresh dashboard into your inbox. + +## What it does + +Turns a saved **watch profile** — topics, jurisdictions, source list, cadence — +into a repeatable regulatory sweep. On each run the skill visits only the +sources you locked into the profile at setup: the top authoritative sources +the skill proposed for each topic, plus any seed URLs you added on top. It +classifies each new item (topic, jurisdiction, stage, date), flags items that +match your team's function-area keywords using a light WorkIQ-derived profile, +and produces a self-contained HTML dashboard. + +It is a **monitoring** tool. It reports what regulators, legislatures, and +courts are doing. It never files, calculates liability, or gives a legal +opinion. + +## When to use it + +- You want a recurring digest of regulatory changes in a defined area (tax, + privacy, AI, healthcare, ESG, labor, competition, anything domain-specific). +- You already know roughly what to watch (topic + jurisdictions) and want a + bounded, predictable sweep rather than an open-ended search every week. +- You want the same view for the same profile every week so you can diff week + over week. + +## When not to use it + +- One-off legal or regulatory research → use a research skill. +- Reading a single document you already have → use `docx` / `pptx`. +- Computing a compliance liability or filing position → out of scope. Escalate + to a human. + +## Setup walkthrough + +First time you invoke the skill for a new regulation, it walks you through a +short interactive setup: + +1. **Profile name**, e.g. `pillar-ii`, `eu-ai-act`, `hipaa-sec-2`. +2. **Watch topics** — 2 to 8 topics, each with a display name and a handful + of keywords the sweep will look for. +3. **Jurisdictions** — countries, regions, sectors, or `global`. +4. **Cadence** — daily, weekly (default), biweekly, monthly. +5. **Window** — days to look back per run (default matches cadence). +6. **Delivery target** — your own email (default), a Teams chat, a Loop page, + or inline only. + +The skill then does a small **discovery pass** to propose the top 5 +authoritative sources per topic (regulator page → official journal → +multilateral body → reputable tracker → firm public alert). It **stops for +your confirmation** — you can swap any of the 5, lower the target, or add +your own seed URLs — before starting any monitoring. + +After you confirm, the skill captures a light org profile from WorkIQ +(`workiq_get_my_profile`, `workiq_get_my_manager`, `workiq_get_relevant_people`) +and proposes 5–15 function-area keywords for the team-relevance flag. You edit +and confirm those too. + +Everything is written to `config.json` next to the dashboard output. Every +subsequent run reads it and visits only the confirmed source list. + +## Scheduling + +The core skill has no schedule of its own — it just runs. Wire it to your +platform's scheduler: + +### Scout + +Create a Scout automation: + +``` +name: Regulation Monitor — +schedule: every Monday at 8am +prompt: | + Run the regulation-monitor skill for profile "". + Load the profile's config.json, sweep the window since the last run, + build the dashboard, and email the digest to the user per the + delivery block. Always send the dashboard, including on quiet weeks — + topics with no items are reported explicitly as "No significant + developments this period" and the empty-state view is intentional. +teamsNotify: auto +``` + +Scout keeps the recurrence and invokes the skill. + +### Cowork + +Create a recurring Cowork task with the same prompt above, task name +`Regulation Monitor — `, recurrence matching the profile's +cadence. + +### What the schedule should not do + +- **Do not reconfigure the profile from the schedule.** Setup is an interactive + step. If the profile is missing or stale, the scheduled run should send a + short heads-up and stop, not silently rebuild. +- **Do not add external email recipients from the schedule.** The pre-authorized + recipient is you (the user). Any other recipient requires an interactive + confirmation. +- **Do not chain the monitor into downstream action.** This skill monitors; it + does not take a filing position or trigger a workflow. Keep the scheduled + job single-purpose. + +## Output + +- **Dashboard**: `output/regulation-dashboard.html` — KPI tiles, sortable + color-coded table, team-relevant badges, per-row source links (URL scheme + sanitized — only `http`/`https`/`mailto` are rendered), and a "Quiet this + period" section listing every watch topic that produced zero items as + **"No significant developments this period"**. +- **Items JSON**: `working/regulation-items.json` — raw items from this run. +- **Inline summary**: ≤12 lines — window, item counts by topic, top team- + relevant items. Quiet topics are reported explicitly, not padded. + +## Tips + +- Keep watch topics narrow. "Pillar Two" as one topic gives you a coherent + weekly digest. "International tax" gives you noise. +- The domain-agnostic reputable-source allowlist for fallback search lives in + `references/sources-and-taxonomy.md`. Extend it via + `domain_allowlist_extensions` in the config if your domain needs sources + outside the defaults. +- Tune `runtime_budget` in the config if you find the sweep is too aggressive + (lower `max_items`) or too silent (raise `max_fallback_searches`). diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md index 26a21fd9..17eac0cb 100644 --- a/submissions/regulation-monitor/SKILL.md +++ b/submissions/regulation-monitor/SKILL.md @@ -404,17 +404,15 @@ narrow ones (e.g., one EU regulation). ## Scheduling -See `references/automation-template.md` for how to schedule this skill on -Scout (via Scout automation) and Cowork -(scheduled task). The core skill has no cadence of its own — the schedule -lives outside the skill and simply invokes it. +See the submission's `README.md` (a human-facing sidecar; not bundled into +the agent's context) for scheduling walkthroughs on Scout and Cowork. The +core skill has no cadence of its own — the schedule lives outside the skill +and simply invokes it. ## References - `references/sources-and-taxonomy.md` — reputable-domain allowlist for fallback search, item classification taxonomy, stage-inference rules, and per-topic search-query templates. -- `references/automation-template.md` — how to schedule the monitor on - Scout and Cowork. - `scripts/build_dashboard.py` — self-contained dashboard generator (Python standard library only). diff --git a/submissions/regulation-monitor/references/automation-template.md b/submissions/regulation-monitor/references/automation-template.md deleted file mode 100644 index e058fe61..00000000 --- a/submissions/regulation-monitor/references/automation-template.md +++ /dev/null @@ -1,48 +0,0 @@ -# Scheduling the regulation monitor - -The core skill has no schedule of its own — it just runs. To put it on a -cadence, wire it to a scheduler on your platform. - -## Scout - -Create a Scout automation. In natural language: - -> Every Monday at 8am, run "regulation-monitor" for profile ``. - -Or via the built-in automation tool: - -``` -name: Regulation Monitor — -schedule: every Monday at 8am -prompt: | - Run the regulation-monitor skill for profile "". - Load the profile's config.json, sweep the window since the last run, - build the dashboard, and email the digest to the user per the - delivery block. Always send the dashboard, including on quiet weeks — - topics with no items are reported explicitly as "No significant - developments this period" and the empty-state view is intentional. -teamsNotify: auto -``` - -Scout will keep the recurrence, invoke the skill, and post the summary -according to the automation's Teams notification policy. - -## Cowork - -Cowork tasks can run on a schedule. Create a recurring task: - -- Task name: `Regulation Monitor — ` -- Recurrence: match the profile's cadence -- Instructions: the same prompt as above, pointed at the skill. - -## What the schedule should NOT do - -- **Do not reconfigure the profile from the schedule.** Setup is an - interactive step. If the profile is missing or stale, the scheduled run - should send a short heads-up to the user and stop, not silently rebuild. -- **Do not add external email recipients from the schedule.** The pre- - authorized recipient is the user themselves. Any other recipient requires - an interactive confirmation. -- **Do not chain the monitor into downstream action.** This skill monitors; - it does not take a filing position or trigger a workflow. Keep the - scheduled job single-purpose. diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index 7eb0cf0c..ec30a644 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -346,7 +346,20 @@ def main(argv: list[str] | None = None) -> int: config = load_json(args.config) items_doc = load_json(args.items) - items = items_doc.get("items", []) if isinstance(items_doc, dict) else [] + raw_items = items_doc.get("items", []) if isinstance(items_doc, dict) else [] + if not isinstance(raw_items, list): + print( + f"items file must contain an 'items' list; got {type(raw_items).__name__}", + file=sys.stderr, + ) + return 2 + items = [item for item in raw_items if isinstance(item, dict)] + dropped = len(raw_items) - len(items) + if dropped: + print( + f"warning: dropped {dropped} non-dict entries from items[]", + file=sys.stderr, + ) html_out = build_html(config, items) args.output.parent.mkdir(parents=True, exist_ok=True) From 29d9dd87df62c0e65ad79b5eae963ff830d57539 Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:51:25 +0100 Subject: [PATCH 05/16] Regulation Monitor: address round 3 bot review Fixed: - metadata.json description now correctly reflects the locked-source-list behavior: auto-discovered + user-confirmed sources plus user seeds, with web_search only as a tightly-bounded fallback when a locked source is silent. Previous copy implied web_search runs every time, which conflicted with SKILL.md. - Removed unused per_stage Counter from build_dashboard.py (dead code; stages are surfaced via the pill on each row, not aggregated in a tile). --- submissions/regulation-monitor/SKILL.md | 30 +++++-------------- submissions/regulation-monitor/metadata.json | 2 +- .../scripts/build_dashboard.py | 1 - 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md index 17eac0cb..4dfd229a 100644 --- a/submissions/regulation-monitor/SKILL.md +++ b/submissions/regulation-monitor/SKILL.md @@ -1,22 +1,6 @@ --- -name: regulation-monitor -description: | - Monitors user-specified regulations, laws, and regulatory guidance on a - recurring schedule. On first run, walks the user through a one-time setup - (watch topics, jurisdictions, cadence), auto-discovers the top authoritative - sources for each watch topic, and locks the sweep to that fixed source list - plus the user's own seed sources. Every subsequent run visits only those - sources (never an open-ended web search), classifies each item, flags items - relevant to the user's team using WorkIQ-derived context, and renders a - self-contained HTML dashboard. - Use when the user says "monitor regulations", "set up a regulation tracker", - "watch for new rules on X", "any updates on the EU AI Act", "run my - regulation monitor", or wants a weekly regulatory digest. - Do NOT use for one-off legal research (use deep-research), for computing a - compliance liability or filing position (out of scope — this is monitoring), - or for reading a single document the user already has (use docx / pptx). - This skill is domain-agnostic — tax, privacy, AI/ML, healthcare, finance, - ESG, labor, etc. — the user picks the topics at setup. +name: "regulation-monitor" +description: "Monitors user-specified regulations, laws, and regulatory guidance on a" --- # Regulation Monitor @@ -404,15 +388,17 @@ narrow ones (e.g., one EU regulation). ## Scheduling -See the submission's `README.md` (a human-facing sidecar; not bundled into -the agent's context) for scheduling walkthroughs on Scout and Cowork. The -core skill has no cadence of its own — the schedule lives outside the skill -and simply invokes it. +See `references/automation-template.md` for how to schedule this skill on +Scout (via Scout automation) and Cowork +(scheduled task). The core skill has no cadence of its own — the schedule +lives outside the skill and simply invokes it. ## References - `references/sources-and-taxonomy.md` — reputable-domain allowlist for fallback search, item classification taxonomy, stage-inference rules, and per-topic search-query templates. +- `references/automation-template.md` — how to schedule the monitor on + Scout and Cowork. - `scripts/build_dashboard.py` — self-contained dashboard generator (Python standard library only). diff --git a/submissions/regulation-monitor/metadata.json b/submissions/regulation-monitor/metadata.json index 4fa7a0d2..0c6c29f5 100644 --- a/submissions/regulation-monitor/metadata.json +++ b/submissions/regulation-monitor/metadata.json @@ -1,6 +1,6 @@ { "name": "Regulation Monitor", - "description": "Configure once, then on a schedule sweeps user-specified regulations across seed sources plus a reputable-domain web search, classifies each item, flags items relevant to the user's team using a light WorkIQ-derived profile, and renders a self-contained HTML dashboard. Domain-agnostic (tax, privacy, AI/ML, healthcare, finance, ESG, labor, etc.).", + "description": "Configure once, then on a schedule sweeps a locked list of authoritative sources (auto-discovered at setup, confirmed by the user) plus any user-supplied seeds. Classifies each item, flags items relevant to the user's team using a light WorkIQ-derived profile, and renders a self-contained HTML dashboard. A tightly-bounded fallback web search is used only when a locked source is silent in the window. Domain-agnostic (tax, privacy, AI/ML, healthcare, finance, ESG, labor, etc.).", "platforms": ["Cowork", "Scout"], "tags": ["regulation", "monitoring", "compliance", "dashboard", "research"], "author": "Jagmeet Chabra", diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index ec30a644..b8455588 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -110,7 +110,6 @@ def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: total = len(items) per_topic = Counter(item.get("topic", "") for item in items) - per_stage = Counter(item.get("stage", "") for item in items) team_relevant = sum(1 for item in items if item.get("relevant_to_your_team")) # Sort: team-relevant first, then stage order, then date desc. From 8e2edf6d6d899659ce94765811171ee85a05a90c Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:03:13 +0100 Subject: [PATCH 06/16] Regulation Monitor: implement client-side sortable dashboard + repair SKILL.md The SKILL.md frontmatter description had been corrupted (truncated to 'on a') and stale references to references/automation-template.md and 'bundled automation template' remained after that file was removed in the round 2 commit. Rebuilt SKILL.md cleanly with the intended full description and pointer to README.md for scheduling. Dashboard changes: - Actually implement client-side sorting: each th is clickable, toggles asc/desc, updates aria-sort for a11y, sorts by data-sort key. Stage column sorts by taxonomy order (numeric); date sorts as ISO strings (lexicographic = chronological); text columns sort case-insensitively. Server-side pre-sort (team-relevant first, then stage, then date desc) still runs so first paint is meaningful. - Small 'click any column header to sort' hint next to the Items heading so users know it's interactive. - Updated SKILL.md and README.md copy to say 'client-side sortable' instead of just 'sortable' so the description matches behavior. --- submissions/regulation-monitor/README.md | 10 +- submissions/regulation-monitor/SKILL.md | 104 ++++++---- .../scripts/build_dashboard.py | 189 ++++++++++++------ 3 files changed, 198 insertions(+), 105 deletions(-) diff --git a/submissions/regulation-monitor/README.md b/submissions/regulation-monitor/README.md index add22a7d..f4049cd1 100644 --- a/submissions/regulation-monitor/README.md +++ b/submissions/regulation-monitor/README.md @@ -105,10 +105,12 @@ cadence. ## Output -- **Dashboard**: `output/regulation-dashboard.html` — KPI tiles, sortable - color-coded table, team-relevant badges, per-row source links (URL scheme - sanitized — only `http`/`https`/`mailto` are rendered), and a "Quiet this - period" section listing every watch topic that produced zero items as +- **Dashboard**: `output/regulation-dashboard.html` — KPI tiles, + client-side sortable color-coded table (click any column header to sort + by date, topic, jurisdiction, stage, title, or source), team-relevant + badges, per-row source links (URL scheme sanitized — only + `http`/`https`/`mailto` are rendered), and a "Quiet this period" section + listing every watch topic that produced zero items as **"No significant developments this period"**. - **Items JSON**: `working/regulation-items.json` — raw items from this run. - **Inline summary**: ≤12 lines — window, item counts by topic, top team- diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md index 4dfd229a..f6b4eed8 100644 --- a/submissions/regulation-monitor/SKILL.md +++ b/submissions/regulation-monitor/SKILL.md @@ -1,6 +1,24 @@ --- -name: "regulation-monitor" -description: "Monitors user-specified regulations, laws, and regulatory guidance on a" +name: regulation-monitor +description: | + Monitors user-specified regulations, laws, and regulatory guidance on a + recurring schedule. On first run, walks the user through a one-time setup + (watch topics, jurisdictions, cadence), auto-discovers the top authoritative + sources for each watch topic, and STOPS for explicit user confirmation + before starting any monitoring. Every subsequent run visits only the + locked source list plus user seeds (never an open-ended web search), + classifies each item, flags items relevant to the user's team using a + light WorkIQ-derived profile, and renders a self-contained HTML dashboard + with client-side sortable columns. + Use when the user says "monitor regulations", "set up a regulation + tracker", "watch for new rules on X", "any updates on the EU AI Act", + "run my regulation monitor", or wants a weekly regulatory digest. + Do NOT use for one-off legal research (use deep-research), for computing + a compliance liability or filing position (out of scope — this is + monitoring), or for reading a single document the user already has + (use docx / pptx). This skill is domain-agnostic — tax, privacy, AI/ML, + healthcare, finance, ESG, labor, etc. — the user picks the topics at + setup. --- # Regulation Monitor @@ -8,12 +26,12 @@ description: "Monitors user-specified regulations, laws, and regulatory guidance ## Overview Turns a saved "watch profile" — topics, jurisdictions, source list, cadence — -into a repeatable regulatory sweep. On each run the skill visits **only** the +into a repeatable regulatory sweep. On each run the skill visits only the sources locked into the profile at setup: the top authoritative sources the -skill discovered for each watch topic plus any seed sources the user added. +skill proposed for each watch topic plus any seed sources the user added. It classifies each new item, flags items likely relevant to the user's team using a light org profile derived once from WorkIQ, and produces a -self-contained HTML dashboard. +self-contained HTML dashboard with sortable columns. It is a **monitoring** tool. It reports what regulators, legislatures, and courts are doing. It never files, calculates liability, or gives a legal @@ -30,23 +48,24 @@ prone to noise. Instead, at setup: reputable trade press). 2. **The skill presents that shortlist to the user and stops.** No monitoring runs until the user has confirmed the list. -3. The user can swap/remove any of the 5, and can supply their own - **seed sources** on top. +3. The user can swap/remove any of the 5, lower the target, and can supply + their own **seed sources** on top. 4. The confirmed list is **locked into the profile config**. Every subsequent run visits **only** those sources. A tightly-bounded fallback web search (at most one query per topic, capped result count, -allowlist-filtered) is used only when a seed source returns nothing new in -the window. See "Runtime budget" below. +allowlist-filtered) is used only when a locked source is silent for a +topic in the window. See "Runtime budget" below. ## When to Use - On-demand: "run my regulation monitor", "what changed on the EU AI Act this week" -- Setup: "set up a regulation tracker for [topics]", "monitor [X] regulations - for me" -- Scheduled: the unattended weekly (or user-chosen cadence) run produced by - the bundled automation template — no user prompt needed +- Setup: "set up a regulation tracker for [topics]", "monitor [X] + regulations for me" +- Scheduled: unattended runs invoked by an external scheduler (Scout + automation, Cowork scheduled task, etc.) — see the submission's + README.md for platform-specific templates ## When NOT to Use @@ -62,7 +81,7 @@ the window. See "Runtime budget" below. ``` User: "Set up a regulation monitor for OECD Pillar II across all jurisdictions" 1. Capture profile name, watch topics, jurisdictions, cadence, delivery. -2. Discovery pass: propose the top 5 authoritative sources per watch topic. +2. Discovery pass: propose up to 5 authoritative sources per watch topic. 3. STOP — show the shortlist to the user and ask two things: which to swap out, and whether they want to add their own seed sources. 4. Wait for confirmation. Do not do any monitoring until the user approves. @@ -70,16 +89,6 @@ User: "Set up a regulation monitor for OECD Pillar II across all jurisdictions" it for confirmation. 6. Lock all of this into config.json. 7. Do the first sweep now that the user has approved the source list. - -User (later, or on schedule): "Run my regulation monitor" -1. Load the profile config. -2. Resolve the time window (default: since last successful run, else the - profile's window_days). -3. Sweep the locked source list. Bounded fallback search only when a source - is silent. -4. Classify each item. -5. Flag team-relevant items. -6. Build the dashboard and deliver per the profile. ``` ## Core Instructions @@ -306,9 +315,10 @@ a human judgment. 3. Verify the file was written before telling the user it is ready. The dashboard is a single self-contained HTML file — KPI tiles (total items, -count per topic, team-relevant count), a sortable table color-coded by -stage, a team-relevant badge on flagged rows, and every row linking to its -primary source. +count per topic, team-relevant count), a table color-coded by stage with +**client-side sortable columns** (click any column header to sort), a +team-relevant badge on flagged rows, and every row linking to its primary +source. ### Step 9 — Deliver and update last-run @@ -337,8 +347,9 @@ narrow ones (e.g., one EU regulation). ## Output -- **Dashboard**: `output/regulation-dashboard.html` — KPI tiles, sortable - color-coded table, team-relevant badges, per-row source links. +- **Dashboard**: `output/regulation-dashboard.html` — KPI tiles, client-side + sortable color-coded table, team-relevant badges, per-row source links + (URL scheme sanitized — only `http`/`https`/`mailto` render). - **Items JSON**: `working/regulation-items.json` — raw items from this run (useful for diffing or feeding downstream tools). - **Inline summary**: ≤12 lines — window, item counts by topic, top team- @@ -376,29 +387,38 @@ narrow ones (e.g., one EU regulation). - **Confirm before external sends.** Emailing anyone other than the user requires explicit confirmation on an interactive run; unattended runs never add recipients. -- **Confidentiality and PII.** Do not add any WorkIQ-derived personal names, - email addresses, or internal identifiers to the dashboard beyond the - keywords the user confirmed at setup. The dashboard may be attached to - emails and shared. +- **Confidentiality and sensitivity.** If slides or notes carry a + confidentiality label or sensitivity marking (for example + "Confidential", "Internal Only", "Restricted", or an enterprise + information-protection label), or contain unreleased figures, + customer or partner identifiers, or names that aren't public yet, + flag them and confirm with the user before including that content in + the spoken script. Never add PII or customer identifiers the source + material doesn't already contain. +- **Compute timings with code**, not by hand; state the pace assumption + so the presenter can recalibrate. +- **Cover exactly the requested topics** — no more, no fewer. +- **Do not reproduce third-party copyrighted text** verbatim; paraphrase + in the skill's own words. - **Verify delivery.** Confirm the dashboard file exists before reporting - success. If the delivery block failed, report the failure — do not report - success. + success. If the delivery block failed, report the failure — do not + report success. - **Cite by exact source name** and validate every date against the requested window. ## Scheduling -See `references/automation-template.md` for how to schedule this skill on -Scout (via Scout automation) and Cowork -(scheduled task). The core skill has no cadence of its own — the schedule -lives outside the skill and simply invokes it. +The core skill has no schedule of its own — it just runs. See the +submission's `README.md` (a human-facing sidecar; not bundled into the +agent's context) for platform-specific scheduling walkthroughs on Scout and +Cowork. Wire this skill to your platform's scheduler and it invokes the +skill on cadence. ## References - `references/sources-and-taxonomy.md` — reputable-domain allowlist for fallback search, item classification taxonomy, stage-inference rules, and per-topic search-query templates. -- `references/automation-template.md` — how to schedule the monitor on - Scout and Cowork. - `scripts/build_dashboard.py` — self-contained dashboard generator - (Python standard library only). + (Python standard library only; embeds a small vanilla-JS sorter for + the items table). diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index b8455588..f361a495 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -21,6 +21,8 @@ relevant_to_your_team bool - team-relevance flag from Step 7 Uses only the Python standard library so it runs in restricted sandboxes. +The generated HTML embeds a small vanilla-JS sorter that sorts by any +column when its header is clicked (no network requests, no external assets). """ from __future__ import annotations @@ -82,8 +84,7 @@ def esc(value: Any) -> str: def stage_pill(stage: str) -> str: color = STAGE_COLORS.get(stage, "#374151") return ( - f'' - f'{esc(stage)}' + f'{esc(stage)}' ) @@ -96,6 +97,30 @@ def relevant_badge(is_relevant: bool) -> str: ) +def sorted_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Initial server-side sort: team-relevant first, then stage order, + then date descending. Users can override via the client-side sorter. + """ + def key(item: dict[str, Any]) -> tuple[int, int, str]: + relevant_first = 0 if item.get("relevant_to_your_team") else 1 + stage = str(item.get("stage") or "") + stage_idx = ( + STAGE_ORDER.index(stage) if stage in STAGE_ORDER else len(STAGE_ORDER) + ) + # Negate the ISO string for descending order via reverse trick: use + # the plain string and rely on the outer sort direction. Because + # tuple sort is ascending, put a negative date proxy last and flip + # by using a value we can invert as string. Simpler: return the + # date string, then reverse-sort just the date within groups by + # returning it as the negative of the sort priority. + # Easiest approach: return date; sort the whole list, then apply + # secondary reverse-sort on date. Kept as a single ordered tuple + # for clarity — the final effect is applied below. + return (relevant_first, stage_idx, str(item.get("date") or "")) + + return sorted(items, key=key) + + def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: profile_name = esc(config.get("profile_name", "regulation-monitor")) cadence = esc(config.get("cadence", "on demand")) @@ -112,64 +137,52 @@ def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: per_topic = Counter(item.get("topic", "") for item in items) team_relevant = sum(1 for item in items if item.get("relevant_to_your_team")) - # Sort: team-relevant first, then stage order, then date desc. - def sort_key(item: dict[str, Any]) -> tuple[int, int, str]: - stage_idx = ( - STAGE_ORDER.index(item.get("stage", "")) - if item.get("stage") in STAGE_ORDER - else len(STAGE_ORDER) - ) - relevant_first = 0 if item.get("relevant_to_your_team") else 1 - # Sort dates descending by negating via string trick: prefix with '0' if - # missing to sort last. We reverse-sort strings so newer ISO dates win. - date_key = item.get("date") or "0000-00-00" - return (relevant_first, stage_idx, date_key) - - sorted_items = sorted(items, key=sort_key) - # Because dates should be newest-first within (relevance, stage), we sort - # the date within each group by negating the string ordering. Simpler: - # sort desc by date on a second pass while preserving stability. - sorted_items.sort( - key=lambda x: x.get("date") or "0000-00-00", reverse=True - ) - sorted_items.sort(key=lambda x: sort_key(x)[:2]) + ordered = sorted_items(items) # KPI tiles - tiles_html: list[str] = [] - tiles_html.append(_tile("Total items", str(total))) - tiles_html.append(_tile("Team-relevant", str(team_relevant))) + tiles_html: list[str] = [ + _tile("Total items", str(total)), + _tile("Team-relevant", str(team_relevant)), + ] for topic in config.get("watch_topics", []): key = str(topic.get("key", "")) - tiles_html.append( - _tile( - str(topic.get("name", key)), - str(per_topic.get(key, 0)), - ) - ) + tiles_html.append(_tile(str(topic.get("name", key)), str(per_topic.get(key, 0)))) - # Rows + # Rows. Each cell gets a data-sort attribute the JS sorter reads: + # numeric where useful (stage order), ISO string where lexicographic + # equals chronological (date), plain lowercased text elsewhere. rows_html: list[str] = [] - for item in sorted_items: + for item in ordered: topic_key = str(item.get("topic", "")) - topic_display = topic_name_by_key.get(topic_key, topic_key or "—") - title = esc(item.get("title", "(untitled)")) - summary = esc(item.get("summary", "")) - source_name = esc(item.get("source_name", "")) - source_url = esc(safe_url(item.get("source_url", ""))) + topic_display = topic_name_by_key.get(topic_key, topic_key or "-") + stage = str(item.get("stage") or "") + stage_sort = ( + str(STAGE_ORDER.index(stage)) + if stage in STAGE_ORDER + else str(len(STAGE_ORDER)) + ) + date_str = str(item.get("date") or "") + jurisdiction = str(item.get("jurisdiction") or "-") + title = str(item.get("title") or "(untitled)") + summary = str(item.get("summary") or "") + source_name = str(item.get("source_name") or "") + source_url = safe_url(item.get("source_url")) source_link = ( - f'{source_name}' + f'{esc(source_name)}' if source_url - else source_name + else esc(source_name) ) + rows_html.append( "" - f"{esc(item.get('date') or '—')}" - f"{esc(topic_display)} {relevant_badge(bool(item.get('relevant_to_your_team')))}" - f"{esc(item.get('jurisdiction') or '—')}" - f"{stage_pill(str(item.get('stage', '')))}" - f"
    {title}
    " - f"
    {summary}
    " - f"{source_link}" + f'{esc(date_str or "-")}' + f'{esc(topic_display)} {relevant_badge(bool(item.get("relevant_to_your_team")))}' + f'{esc(jurisdiction)}' + f'{stage_pill(stage)}' + f'' + f'
    {esc(title)}
    ' + f'
    {esc(summary)}
    ' + f'{source_link}' "" ) @@ -194,12 +207,13 @@ def sort_key(item: dict[str, Any]) -> tuple[int, int, str]: ) empty_html = ( '
    ' - '

    Quiet this period

    ' - f'
      {rows}
    ' + "

    Quiet this period

    " + f"
      {rows}
    " "
    " ) style = _stylesheet() + script = _sorter_script() return f""" @@ -229,16 +243,16 @@ def sort_key(item: dict[str, Any]) -> tuple[int, int, str]:
    -

    Items

    - +

    Items — click any column header to sort

    +
    - - - - - - + + + + + + @@ -252,6 +266,7 @@ def sort_key(item: dict[str, Any]) -> tuple[int, int, str]:
    Regulation Monitor is a monitoring tool. Nothing on this dashboard is legal, tax, or compliance advice. Team-relevance is a keyword match, not an impact assessment.
    + """ @@ -292,6 +307,8 @@ def _stylesheet() -> str: section{padding:24px 32px} section h2{margin:0 0 12px;font-size:14px;text-transform:uppercase; letter-spacing:.06em;color:var(--muted);font-weight:600} +section h2 .hint{color:var(--muted);text-transform:none;letter-spacing:0; + font-size:12px;font-weight:400;margin-left:6px} .tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px;padding:16px 32px 0} @@ -304,7 +321,13 @@ def _stylesheet() -> str: border:1px solid var(--line);border-radius:10px;overflow:hidden;box-shadow:var(--shadow)} thead th{background:#f1f5f9;color:var(--muted);text-align:left;font-weight:600; padding:10px 12px;font-size:11px;text-transform:uppercase;letter-spacing:.06em; - border-bottom:1px solid var(--line)} + border-bottom:1px solid var(--line);cursor:pointer;user-select:none; + position:relative} +thead th:hover{color:var(--ink)} +thead th::after{content:"";display:inline-block;margin-left:6px;opacity:.35; + font-size:10px;transform:translateY(-1px)} +thead th[aria-sort="ascending"]::after{content:"\\25B2";opacity:1;color:var(--accent)} +thead th[aria-sort="descending"]::after{content:"\\25BC";opacity:1;color:var(--accent)} tbody td{padding:12px;vertical-align:top;border-bottom:1px solid var(--line); font-size:13px} tbody tr:last-child td{border-bottom:none} @@ -329,6 +352,54 @@ def _stylesheet() -> str: """ +def _sorter_script() -> str: + """Vanilla-JS click-to-sort. Reads td[data-sort] for the sort key, + respects data-type on th ('number' | 'date' | 'text'), toggles + ascending / descending, and updates aria-sort for accessibility. + """ + return """ +(function(){ + var table = document.getElementById('items-table'); + if (!table) return; + var thead = table.tHead; + var tbody = table.tBodies[0]; + if (!thead || !tbody) return; + var ths = thead.rows[0].cells; + for (var i = 0; i < ths.length; i++) { + (function(colIndex){ + var th = ths[colIndex]; + th.addEventListener('click', function(){ + var type = th.getAttribute('data-type') || 'text'; + var current = th.getAttribute('aria-sort'); + var asc = current !== 'ascending'; + for (var j = 0; j < ths.length; j++) ths[j].removeAttribute('aria-sort'); + th.setAttribute('aria-sort', asc ? 'ascending' : 'descending'); + var rows = Array.prototype.slice.call(tbody.rows).filter(function(r){ + return !(r.cells.length === 1 && r.cells[0].getAttribute('colspan')); + }); + rows.sort(function(a, b){ + var av = (a.cells[colIndex] && a.cells[colIndex].getAttribute('data-sort')) || ''; + var bv = (b.cells[colIndex] && b.cells[colIndex].getAttribute('data-sort')) || ''; + var cmp; + if (type === 'number') { + cmp = (parseFloat(av) || 0) - (parseFloat(bv) || 0); + } else if (type === 'date') { + cmp = av.localeCompare(bv); + } else { + cmp = av.localeCompare(bv); + } + return asc ? cmp : -cmp; + }); + var frag = document.createDocumentFragment(); + rows.forEach(function(r){ frag.appendChild(r); }); + tbody.appendChild(frag); + }); + })(i); + } +})(); +""" + + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--config", type=Path, required=True) From 9940fee25830e30f60f770365451b2a7e84fe52b Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:09:44 +0100 Subject: [PATCH 07/16] Regulation Monitor: fix date-desc initial sort and drop stale talk-track guardrail text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs from round 5 bot review: - sorted_items() sorted ISO date strings ascending (default) despite the docstring claim of descending. Fixed with a two-pass stable sort (date desc first, then stable-sort by (relevant, stage)). Python's sort() doesn't support per-key reverse on tuple elements, so two passes is the cleanest way to get desc on one field and asc on others. Added a comment explaining why. Verified with a two-item smoke test: newer item now renders before older in the initial paint. - The Confidentiality guardrail bullet in SKILL.md was copy-pasted from the presentation-talk-track-builder skill and still talked about 'slides', 'spoken script', and 'presenter'. Rewrote it in regulation-monitor language: drop non-public sources at ingestion, never add PII/customer identifiers/non-public attribution to the dashboard. Also removed a stale 'Compute timings with code / so the presenter can recalibrate' bullet — same origin, not applicable here. --- submissions/regulation-monitor/SKILL.md | 18 +++++------- .../scripts/build_dashboard.py | 29 ++++++++++--------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md index f6b4eed8..c1c5698b 100644 --- a/submissions/regulation-monitor/SKILL.md +++ b/submissions/regulation-monitor/SKILL.md @@ -387,16 +387,14 @@ narrow ones (e.g., one EU regulation). - **Confirm before external sends.** Emailing anyone other than the user requires explicit confirmation on an interactive run; unattended runs never add recipients. -- **Confidentiality and sensitivity.** If slides or notes carry a - confidentiality label or sensitivity marking (for example - "Confidential", "Internal Only", "Restricted", or an enterprise - information-protection label), or contain unreleased figures, - customer or partner identifiers, or names that aren't public yet, - flag them and confirm with the user before including that content in - the spoken script. Never add PII or customer identifiers the source - material doesn't already contain. -- **Compute timings with code**, not by hand; state the pace assumption - so the presenter can recalibrate. +- **Confidentiality and sensitivity.** If a retrieved source carries a + confidentiality label or sensitivity marking (for example "Confidential", + "Internal Only", "Restricted", or an enterprise information-protection + label), or contains unreleased figures, customer or partner identifiers, + or names that aren't public yet, drop it — this skill uses public + sources only. Never add PII, customer identifiers, or non-public + attribution to the dashboard beyond the function-area keywords the user + confirmed at setup. - **Cover exactly the requested topics** — no more, no fewer. - **Do not reproduce third-party copyrighted text** verbatim; paraphrase in the skill's own words. diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index f361a495..c817a4d7 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -99,26 +99,27 @@ def relevant_badge(is_relevant: bool) -> str: def sorted_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]: """Initial server-side sort: team-relevant first, then stage order, - then date descending. Users can override via the client-side sorter. + then date descending within each group. Users can override any + dimension via the client-side column sorter. + + Implemented as two stable passes because Python's sort doesn't + support a per-key `reverse` for individual tuple elements: sort by + date descending first, then stable-sort by (relevant, stage) so the + date-desc order is preserved inside each stable group. """ - def key(item: dict[str, Any]) -> tuple[int, int, str]: + by_date_desc = sorted( + items, + key=lambda item: str(item.get("date") or ""), + reverse=True, + ) + def group_key(item: dict[str, Any]) -> tuple[int, int]: relevant_first = 0 if item.get("relevant_to_your_team") else 1 stage = str(item.get("stage") or "") stage_idx = ( STAGE_ORDER.index(stage) if stage in STAGE_ORDER else len(STAGE_ORDER) ) - # Negate the ISO string for descending order via reverse trick: use - # the plain string and rely on the outer sort direction. Because - # tuple sort is ascending, put a negative date proxy last and flip - # by using a value we can invert as string. Simpler: return the - # date string, then reverse-sort just the date within groups by - # returning it as the negative of the sort priority. - # Easiest approach: return date; sort the whole list, then apply - # secondary reverse-sort on date. Kept as a single ordered tuple - # for clarity — the final effect is applied below. - return (relevant_first, stage_idx, str(item.get("date") or "")) - - return sorted(items, key=key) + return (relevant_first, stage_idx) + return sorted(by_date_desc, key=group_key) def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: From c0fc72dc6fab6440c3f0e57693f5ad7605bc9ddd Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:18:47 +0100 Subject: [PATCH 08/16] Regulation Monitor: keyboard-accessible sortable columns + fix PR body file tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Column headers: added tabindex=0, role=button, aria-label per column, focus-visible outline. Sorter now responds to Enter and Space in addition to click. Bot review round 6. - (Separate) also updating the PR body file tree to drop references/automation-template.md and add README.md — the file tree in the description had gone stale after the round 2 file deletion. --- .../scripts/build_dashboard.py | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index c817a4d7..e45a3113 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -248,12 +248,12 @@ def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str:
    DateTopicJurisdictionStageTitle & summarySourceDateTopicJurisdictionStageTitle & summarySource
    - - - - - - + + + + + + @@ -323,8 +323,9 @@ def _stylesheet() -> str: thead th{background:#f1f5f9;color:var(--muted);text-align:left;font-weight:600; padding:10px 12px;font-size:11px;text-transform:uppercase;letter-spacing:.06em; border-bottom:1px solid var(--line);cursor:pointer;user-select:none; - position:relative} + position:relative;outline:none} thead th:hover{color:var(--ink)} +thead th:focus-visible{color:var(--ink);box-shadow:inset 0 -2px 0 var(--accent)} thead th::after{content:"";display:inline-block;margin-left:6px;opacity:.35; font-size:10px;transform:translateY(-1px)} thead th[aria-sort="ascending"]::after{content:"\\25B2";opacity:1;color:var(--accent)} @@ -366,34 +367,40 @@ def _sorter_script() -> str: var tbody = table.tBodies[0]; if (!thead || !tbody) return; var ths = thead.rows[0].cells; + function doSort(th, colIndex) { + var type = th.getAttribute('data-type') || 'text'; + var current = th.getAttribute('aria-sort'); + var asc = current !== 'ascending'; + for (var j = 0; j < ths.length; j++) ths[j].removeAttribute('aria-sort'); + th.setAttribute('aria-sort', asc ? 'ascending' : 'descending'); + var rows = Array.prototype.slice.call(tbody.rows).filter(function(r){ + return !(r.cells.length === 1 && r.cells[0].getAttribute('colspan')); + }); + rows.sort(function(a, b){ + var av = (a.cells[colIndex] && a.cells[colIndex].getAttribute('data-sort')) || ''; + var bv = (b.cells[colIndex] && b.cells[colIndex].getAttribute('data-sort')) || ''; + var cmp; + if (type === 'number') { + cmp = (parseFloat(av) || 0) - (parseFloat(bv) || 0); + } else { + cmp = av.localeCompare(bv); + } + return asc ? cmp : -cmp; + }); + var frag = document.createDocumentFragment(); + rows.forEach(function(r){ frag.appendChild(r); }); + tbody.appendChild(frag); + } for (var i = 0; i < ths.length; i++) { (function(colIndex){ var th = ths[colIndex]; - th.addEventListener('click', function(){ - var type = th.getAttribute('data-type') || 'text'; - var current = th.getAttribute('aria-sort'); - var asc = current !== 'ascending'; - for (var j = 0; j < ths.length; j++) ths[j].removeAttribute('aria-sort'); - th.setAttribute('aria-sort', asc ? 'ascending' : 'descending'); - var rows = Array.prototype.slice.call(tbody.rows).filter(function(r){ - return !(r.cells.length === 1 && r.cells[0].getAttribute('colspan')); - }); - rows.sort(function(a, b){ - var av = (a.cells[colIndex] && a.cells[colIndex].getAttribute('data-sort')) || ''; - var bv = (b.cells[colIndex] && b.cells[colIndex].getAttribute('data-sort')) || ''; - var cmp; - if (type === 'number') { - cmp = (parseFloat(av) || 0) - (parseFloat(bv) || 0); - } else if (type === 'date') { - cmp = av.localeCompare(bv); - } else { - cmp = av.localeCompare(bv); - } - return asc ? cmp : -cmp; - }); - var frag = document.createDocumentFragment(); - rows.forEach(function(r){ frag.appendChild(r); }); - tbody.appendChild(frag); + th.addEventListener('click', function(){ doSort(th, colIndex); }); + // Keyboard accessibility: Enter or Space triggers the same sort. + th.addEventListener('keydown', function(e){ + if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') { + e.preventDefault(); + doSort(th, colIndex); + } }); })(i); } From 8ff6320c4a8fecce4a5c4ed45871f8b09cae5d12 Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:30:40 +0100 Subject: [PATCH 09/16] Regulation Monitor: date-typed comparator + fix stale step reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Client-side sorter previously fell through to localeCompare for data-type='date' — worked for our zero-padded ISO strings but the sorter advertised date support that wasn't there. Added an explicit 'date' branch using byte-string comparison (which is stable for ISO 8601 and doesn't rely on JS Date parsing). - sources-and-taxonomy.md: the item-taxonomy table said relevant_to_your_team is 'derived at Step 4' — should be Step 7. Same class of stale step reference we fixed earlier in build_dashboard.py, missed this one. --- submissions/regulation-monitor/SKILL.md | 22 ++----------------- .../references/sources-and-taxonomy.md | 2 +- .../scripts/build_dashboard.py | 4 ++++ 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md index c1c5698b..18fa37b7 100644 --- a/submissions/regulation-monitor/SKILL.md +++ b/submissions/regulation-monitor/SKILL.md @@ -1,24 +1,6 @@ --- -name: regulation-monitor -description: | - Monitors user-specified regulations, laws, and regulatory guidance on a - recurring schedule. On first run, walks the user through a one-time setup - (watch topics, jurisdictions, cadence), auto-discovers the top authoritative - sources for each watch topic, and STOPS for explicit user confirmation - before starting any monitoring. Every subsequent run visits only the - locked source list plus user seeds (never an open-ended web search), - classifies each item, flags items relevant to the user's team using a - light WorkIQ-derived profile, and renders a self-contained HTML dashboard - with client-side sortable columns. - Use when the user says "monitor regulations", "set up a regulation - tracker", "watch for new rules on X", "any updates on the EU AI Act", - "run my regulation monitor", or wants a weekly regulatory digest. - Do NOT use for one-off legal research (use deep-research), for computing - a compliance liability or filing position (out of scope — this is - monitoring), or for reading a single document the user already has - (use docx / pptx). This skill is domain-agnostic — tax, privacy, AI/ML, - healthcare, finance, ESG, labor, etc. — the user picks the topics at - setup. +name: "regulation-monitor" +description: "Monitors user-specified regulations, laws, and regulatory guidance on a" --- # Regulation Monitor diff --git a/submissions/regulation-monitor/references/sources-and-taxonomy.md b/submissions/regulation-monitor/references/sources-and-taxonomy.md index 448e110f..c7a21cfe 100644 --- a/submissions/regulation-monitor/references/sources-and-taxonomy.md +++ b/submissions/regulation-monitor/references/sources-and-taxonomy.md @@ -76,7 +76,7 @@ Each item recorded by the sweep has: | `summary` | 1-2 sentences in the model's own words | required | | `source_name` | publisher's short name | required | | `source_url` | canonical public URL that was actually retrieved | required | -| `relevant_to_your_team` | `true` if any function-area keyword matches | derived at Step 4 | +| `relevant_to_your_team` | `true` if any function-area keyword matches | derived at Step 7 | ## Search-query templates diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index e45a3113..689dc6d6 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -382,6 +382,10 @@ def _sorter_script() -> str: var cmp; if (type === 'number') { cmp = (parseFloat(av) || 0) - (parseFloat(bv) || 0); + } else if (type === 'date') { + // ISO 8601 date strings sort correctly as byte-string comparison — + // avoid Date parsing here so malformed inputs stay deterministic. + cmp = (av < bv) ? -1 : (av > bv ? 1 : 0); } else { cmp = av.localeCompare(bv); } From 7214dddc3315389ac64521a104d60d08cb8d1087 Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:38:46 +0100 Subject: [PATCH 10/16] =?UTF-8?q?Regulation=20Monitor:=20pre-commit=20audi?= =?UTF-8?q?t=20sweep=20=E2=80=94=20concede=20on=20SKILL.md=20structure=20+?= =?UTF-8?q?=20code=20hardening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ran an independent pre-commit audit before pushing more single-issue fixes to end the review back-and-forth. Rewrote in one clean sweep: SKILL.md structure — concede fully to the recurring 'SKILL.md too human-facing' bot feedback: - Frontmatter: single-line description (previous multi-line YAML block scalar got truncated by Scout's skill loader on every re-registration, causing the 'ends with on a' regressions). - Removed sections: Overview, When to Use, When NOT to Use, Quick Start, Output, Scheduling — all restated content that lives in README.md (which is the intended human-facing sidecar per submissions/README.md, never bundled). - Kept: Source discipline (agent needs it), Instructions (Steps 0-9), Guardrails, References. - Renamed Core Instructions -> Instructions. - Folded the Runtime budget table into Step 5 as an inline bullet list. - Step 9 now carries the empty-topic rule ('No significant developments this period') directly. sources-and-taxonomy.md — allowlist cleanup: - Moved taxfoundation.org and taxpolicycenter.org from the government 'Tax' regulator subsection to 'Reputable trackers and think tanks' — they're think tanks, not regulators. - Reworded the 'Tax' subsection to remove the 'state DOR sites (already covered by .gov)' parenthetical that looked like a domain entry. build_dashboard.py — hardening: - window_days: coerce via int() with try/except and default 7 on bad input. - topic_name_by_key: skip topics with empty/whitespace key rather than colliding on ''. --- submissions/regulation-monitor/SKILL.md | 422 +++++++----------- .../references/sources-and-taxonomy.md | 5 +- .../scripts/build_dashboard.py | 12 +- 3 files changed, 184 insertions(+), 255 deletions(-) diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md index 18fa37b7..4df63963 100644 --- a/submissions/regulation-monitor/SKILL.md +++ b/submissions/regulation-monitor/SKILL.md @@ -1,79 +1,30 @@ --- -name: "regulation-monitor" -description: "Monitors user-specified regulations, laws, and regulatory guidance on a" +name: regulation-monitor +description: Use when the user asks to set up, run, or check a scheduled regulation, law, or regulatory-guidance monitor or tracker — triggers include "set up a regulation tracker for X", "monitor [regulation] for me", "run my regulation monitor", "what changed on [regulation] this week", or an unattended scheduler invoking a saved profile. On first invocation walk interactive setup (topics, jurisdictions, cadence, delivery), auto-discover the top authoritative sources per topic, and STOP for user confirmation before any monitoring. On subsequent runs visit only the locked source list plus user seeds, classify items, flag team-relevant ones via a WorkIQ-derived keyword match, and render a self-contained HTML dashboard with client-side sortable columns. Do NOT use for one-off legal research, reading a single document, computing compliance liability or filing position, or non-regulatory news monitoring. --- # Regulation Monitor -## Overview - -Turns a saved "watch profile" — topics, jurisdictions, source list, cadence — -into a repeatable regulatory sweep. On each run the skill visits only the -sources locked into the profile at setup: the top authoritative sources the -skill proposed for each watch topic plus any seed sources the user added. -It classifies each new item, flags items likely relevant to the user's team -using a light org profile derived once from WorkIQ, and produces a -self-contained HTML dashboard with sortable columns. - -It is a **monitoring** tool. It reports what regulators, legislatures, and -courts are doing. It never files, calculates liability, or gives a legal -opinion. - ## Source discipline This skill is deliberately **bounded and confirmed**. It does not run -open-ended web searches every run — those get expensive, unpredictable, and -prone to noise. Instead, at setup: - -1. The skill runs a small **discovery pass** to identify the top 5 - authoritative sources per watch topic (regulators, official trackers, - reputable trade press). -2. **The skill presents that shortlist to the user and stops.** No - monitoring runs until the user has confirmed the list. -3. The user can swap/remove any of the 5, lower the target, and can supply +open-ended web searches every run. Instead, at setup: + +1. Run a small **discovery pass** to identify the top 5 authoritative + sources per watch topic (regulators, official trackers, reputable + trade press). +2. **Present the shortlist to the user and stop.** No monitoring runs + until the user has confirmed the list. +3. The user can swap/remove any of the 5, lower the target, and supply their own **seed sources** on top. 4. The confirmed list is **locked into the profile config**. Every subsequent run visits **only** those sources. A tightly-bounded fallback web search (at most one query per topic, capped result count, allowlist-filtered) is used only when a locked source is silent for a -topic in the window. See "Runtime budget" below. - -## When to Use - -- On-demand: "run my regulation monitor", "what changed on the EU AI Act - this week" -- Setup: "set up a regulation tracker for [topics]", "monitor [X] - regulations for me" -- Scheduled: unattended runs invoked by an external scheduler (Scout - automation, Cowork scheduled task, etc.) — see the submission's - README.md for platform-specific templates - -## When NOT to Use - -- One-off legal or regulatory research → use `deep-research` -- Reading a single document the user has already given you → use `docx` / - `pptx` -- Computing a compliance liability, filing position, or legal conclusion → - out of scope; escalate to a human -- Non-regulatory news monitoring → use `deep-research` or a news skill - -## Quick Start - -``` -User: "Set up a regulation monitor for OECD Pillar II across all jurisdictions" -1. Capture profile name, watch topics, jurisdictions, cadence, delivery. -2. Discovery pass: propose up to 5 authoritative sources per watch topic. -3. STOP — show the shortlist to the user and ask two things: which to swap - out, and whether they want to add their own seed sources. -4. Wait for confirmation. Do not do any monitoring until the user approves. -5. Derive a light org profile from WorkIQ (function-area keywords) and show - it for confirmation. -6. Lock all of this into config.json. -7. Do the first sweep now that the user has approved the source list. -``` +topic in the window. -## Core Instructions +## Instructions ### Step 0 — First-run setup (only if no config exists) @@ -82,7 +33,7 @@ through setup: 1. **Profile name** — kebab-case slug, e.g. `pillar-ii`. 2. **Watch topics** — 2 to 8 topics. For each: display name plus 3–8 - keywords/phrases the sweeps should look for. + keywords the sweeps should look for. 3. **Jurisdictions** — countries, regions, states, sectors, or `global`. 4. **Cadence** — daily, weekly (default), biweekly, monthly. 5. **Window** — days to look back per run (default: matches cadence). @@ -91,80 +42,78 @@ through setup: ### Step 1 — Auto-discover authoritative sources, then STOP for confirmation -**This is an interactive checkpoint. The skill runs a small discovery pass -to identify candidate sources, then presents them and stops. Do not start -the monitoring sweep (Step 5) until the user has explicitly confirmed the -source list.** +**This is an interactive checkpoint. Run a small discovery pass to +identify candidate sources, then present them and stop. Do not start +the monitoring sweep (Step 5) until the user has explicitly confirmed +the source list.** For each watch topic the user configured, propose up to **5 authoritative -sources** (default target is 5; use fewer if the user asks or if the domain -genuinely has fewer canonical sources): - -- **Preference order** (in this order — pick the strongest 5 that exist for - the topic): - 1. The primary regulator / issuing body's official page for the topic. - Examples: OECD's Pillar Two page, the European Commission's page for - the AI Act, HHS OCR for HIPAA, EDPB for GDPR, ISSB for sustainability - disclosure. - 2. Government official journals and legislative trackers for the - jurisdictions in scope. Examples: Federal Register, EUR-Lex, UK - legislation.gov.uk, state legislature bill pages. - 3. The relevant multilateral, standard-setting, or specialist body's page - for the topic. Examples: OECD, UN, BIS, ISO, NIST (AI RMF, cyber), - WHO/EMA (health), ILO (labor), FSB (financial stability). - 4. A reputable public tracker or think tank whose focus matches the - domain. Examples: Tax Foundation and MTC/NCSL for tax; IAPP and - Future of Privacy Forum for privacy; Stanford HAI, Brookings AI, and - the Ada Lovelace Institute for AI; ISSB and EFRAG for sustainability; - SHRM and EPI for labor; KFF for healthcare policy. - 5. Public alert pages from major professional-services or specialist - firms that cover the domain (public URLs only, never subscriber - content). Examples: KPMG / EY / PwC / Deloitte / BDO insight pages - for tax and financial regulation; DLA Piper, Hogan Lovells, Wilson - Sonsini, Cooley for tech / privacy / AI; Ropes & Gray for healthcare; - Littler and Ogletree Deakins for labor. - - Pick the mix that fits the domain. A tax profile will lean on OECD + - regulators + Tax Foundation + big-four alerts. A privacy profile will - lean on EDPB + national DPAs + IAPP + tech-privacy firm alerts. A health - profile will lean on HHS/FDA + WHO/EMA + KFF + healthcare firm alerts. - Do not force tax-style sources onto a non-tax topic. - -- **How to find them**: for each topic, do a short bounded discovery - pass — one to three focused web searches against the reputable-domain - allowlist in `references/sources-and-taxonomy.md` — just enough to - identify the canonical topic pages (not the regulator's home page). This - discovery pass is separate from the monitoring sweep and must be small. - -- **Present them and STOP**. Show the proposed list to the user in a short - message and wait for a reply before doing anything else: - > "Before I start monitoring, here are the 5 authoritative sources I'd - > watch for **Pillar II**: - > 1. OECD — Pillar Two: - > 2. European Commission — Pillar Two implementation: - > 3. HMRC — Multinational Top-up Tax: - > 4. Tax Foundation — Global minimum tax tracker: - > 5. KPMG — BEPS 2.0 tracker: - > - > Want me to swap any out? And do you have any of your own sources - > (regulator pages, internal trackers, subscription-free trade alerts, - > etc.) you want me to add on top of these?" - -- **Wait for the user's reply.** The user may: - - Approve as-is → proceed. - - Ask to swap or remove one of the 5 → re-run discovery for that slot - with the constraint they gave you. - - Add their own seed URLs → append them to `seed_sources_by_topic` under - the appropriate topic key. Seed sources are **not** counted against the - "top 5" — a topic can end up with 5 auto-discovered + N user seeds. - A user seed that spans multiple topics should be added under each - relevant topic key. - - Ask you to lower the target from 5 (e.g., "just the two OECD pages - are enough") → honor it. - -- **Do not skip this confirmation, even on a re-setup.** If the user later - says "add EU AI Act to my profile", repeat this checkpoint for the new - topic before touching the sweep. +sources** (default target 5; use fewer if the user asks or if the domain +has fewer canonical sources). + +Preference order (pick the strongest 5 that exist for the topic): + +1. The primary regulator / issuing body's official page for the topic. + Examples: OECD's Pillar Two page, the European Commission's page for + the AI Act, HHS OCR for HIPAA, EDPB for GDPR, ISSB for sustainability + disclosure. +2. Government official journals and legislative trackers for the + jurisdictions in scope. Examples: Federal Register, EUR-Lex, UK + legislation.gov.uk, state legislature bill pages. +3. The relevant multilateral, standard-setting, or specialist body's + page for the topic. Examples: OECD, UN, BIS, ISO, NIST (AI RMF, + cyber), WHO/EMA (health), ILO (labor), FSB (financial stability). +4. A reputable public tracker or think tank whose focus matches the + domain. Examples: Tax Foundation and MTC/NCSL for tax; IAPP and + Future of Privacy Forum for privacy; Stanford HAI, Brookings AI, and + the Ada Lovelace Institute for AI; ISSB and EFRAG for sustainability; + SHRM and EPI for labor; KFF for healthcare policy. +5. Public alert pages from major professional-services or specialist + firms that cover the domain (public URLs only, never subscriber + content). Examples: KPMG / EY / PwC / Deloitte / BDO insight pages + for tax and financial regulation; DLA Piper, Hogan Lovells, Wilson + Sonsini, Cooley for tech / privacy / AI; Ropes & Gray for healthcare; + Littler and Ogletree Deakins for labor. + +Match the mix to the domain — do not force tax-style sources onto a +non-tax topic. + +**How to find them:** for each topic, do a short bounded discovery +pass — one to three focused web searches against the reputable-domain +allowlist in `references/sources-and-taxonomy.md` — just enough to +identify canonical topic pages (not the regulator's home page). This +discovery pass is separate from the monitoring sweep and must be small. + +**Present them and STOP.** Show the shortlist to the user and wait +before doing anything else: + +> "Before I start monitoring, here are the 5 authoritative sources I'd +> watch for **Pillar II**: +> 1. OECD — Pillar Two: +> 2. European Commission — Pillar Two implementation: +> 3. HMRC — Multinational Top-up Tax: +> 4. Tax Foundation — Global minimum tax tracker: +> 5. KPMG — BEPS 2.0 tracker: +> +> Want me to swap any out? And do you have any of your own sources +> (regulator pages, internal trackers, subscription-free trade alerts, +> etc.) you want me to add on top of these?" + +**Wait for the user's reply.** The user may: + +- Approve as-is → proceed. +- Ask to swap or remove one of the 5 → re-run discovery for that slot + with the constraint they gave. +- Add their own seed URLs → append them to `seed_sources_by_topic` + under the appropriate topic key. Seed sources are **not** counted + against the "top 5" — a topic can end up with 5 auto-discovered plus + N user seeds. A seed that spans multiple topics is added under each + relevant topic key. +- Ask you to lower the target from 5 → honor it. + +**Do not skip this confirmation, even on a re-setup.** If the user +later adds a topic, repeat this checkpoint for the new topic before +touching the sweep. ### Step 2 — Capture the WorkIQ org profile (setup) @@ -175,11 +124,11 @@ Derive a light org profile from WorkIQ and show it for confirmation: - `workiq_get_relevant_people` (limit 10) → likely function-area collaborators -From those, propose a `function_area_keywords` list (5–15 words: department -name and variants, the user's job function, key collaborator team names, -obvious topic proxies). The user edits and confirms. +Propose a `function_area_keywords` list (5–15 words: department name and +variants, the user's job function, key collaborator team names, obvious +topic proxies). The user edits and confirms. -If WorkIQ is unavailable on the current platform, ask the user to provide +If WorkIQ is unavailable on the current platform, ask the user for `function_area_keywords` manually. The rest of the skill works unchanged. ### Step 3 — Save the profile @@ -197,10 +146,8 @@ Write `config.json`: "jurisdictions": ["global"], "sources_by_topic": { "pillar-two": [ - { "name": "OECD — Pillar Two", "url": "https://www.oecd.org/tax/beps/pillar-two-model-rules-in-a-nutshell.pdf", - "kind": "regulator" }, - { "name": "European Commission — Pillar Two", - "url": "https://taxation-customs.ec.europa.eu/taxation/business-taxation/minimum-corporate-taxation_en", + { "name": "OECD — Pillar Two", + "url": "https://www.oecd.org/tax/beps/pillar-two-model-rules-in-a-nutshell.pdf", "kind": "regulator" } ] }, @@ -230,8 +177,8 @@ Write `config.json`: - Read `config.json` for the profile. - Resolve the window: from `last_run_at` (if set) to now, else the past `window_days`. -- Restate the scope back to the user in one line so they can interrupt if it - looks wrong (interactive runs only). +- Restate the scope back to the user in one line so they can interrupt + if it looks wrong (interactive runs only). ### Step 5 — Sweep the locked source list (bounded) @@ -241,52 +188,60 @@ Sweep proceeds in this order and stops when the budget is met: `seed_sources_by_topic[topic]` for each topic.** `web_fetch` each URL. Extract items dated within the window. - Cap `max_fetches_per_source` (default 2). If a source's index page - links to individual items, follow at most that many links per source. + links to individual items, follow at most that many per source. 2. **Bounded fallback search** only for topics where every locked source (auto-discovered + user seeds) returned zero items in the window. At - most one `web_search` per topic, at most `max_fallback_searches` total - across the run (default 2). Filter results by the reputable-domain - allowlist. Discard non-matching results. + most one `web_search` per topic, at most `max_fallback_searches` + total across the run (default 2). Filter results by the reputable- + domain allowlist. Discard non-matching results. 3. **Stop when `max_items` is reached** (default 40). Prefer regulator sources > tracker sources > firm alerts when trimming. -**Rules that always apply:** +Runtime budget defaults, all configurable in `config.runtime_budget`: + +- `max_items`: 40 — total items recorded per run. +- `max_fallback_searches`: 2 — `web_search` calls per run. +- `max_fetches_per_source`: 2 — items followed from one source's index. + +Rules that always apply: - Validate every date against the window; drop out-of-window items. - Never bypass a paywall; skip subscriber-only content. -- Deduplicate items with the same title + jurisdiction, keeping the more - authoritative source (regulator > tracker > firm alert). +- Deduplicate items with the same title + jurisdiction, keeping the + more authoritative source (regulator > tracker > firm alert). ### Step 6 — Classify each item Record, for every item: -- **topic** — one of the profile's watch-topic keys -- **jurisdiction** — from the profile's list, or `global` / `local: ` +- **topic** — one of the profile's watch-topic keys. +- **jurisdiction** — from the profile's list, or `global` / + `local: `. - **stage** — `proposed` / `in-consultation` / `passed` / - `regulatory-guidance` / `in-force` / `litigation` / `withdrawn` -- **date** — ISO date the source is dated or the action took place -- **title** — the source's short title, verbatim -- **summary** — 1–2 sentences in the skill's own words -- **source_name** — publisher's short name + `regulatory-guidance` / `in-force` / `litigation` / `withdrawn`. +- **date** — ISO date the source is dated or the action took place. +- **title** — the source's short title, verbatim. +- **summary** — 1–2 sentences in the skill's own words. +- **source_name** — publisher's short name. - **source_url** — canonical public URL actually retrieved + (http/https/mailto only; other schemes are dropped at render time). Stage inference guidance is in `references/sources-and-taxonomy.md`. ### Step 7 — Flag team relevance (WorkIQ-derived) For each item, set `relevant_to_your_team` to `true` if any -`workiq_context.function_area_keywords` phrase appears (case-insensitive) in -the item's title, summary, or matched topic keywords. Otherwise `false`. +`workiq_context.function_area_keywords` phrase appears (case-insensitive) +in the item's title, summary, or matched topic keywords. Otherwise +`false`. -This is a soft highlight, not an impact rubric. The dashboard uses it to -sort and badge; the skill never says "this affects your business" — that is -a human judgment. +This is a soft highlight, not an impact rubric. The dashboard uses it +to sort and badge; the skill never says "this affects your business" — +that is a human judgment. ### Step 8 — Build the dashboard -1. Write items to `working/regulation-items.json` (schema in the script - header). +1. Write items to `working/regulation-items.json`. 2. Run the bundled generator: ``` python scripts/build_dashboard.py \ @@ -296,109 +251,78 @@ a human judgment. ``` 3. Verify the file was written before telling the user it is ready. -The dashboard is a single self-contained HTML file — KPI tiles (total items, -count per topic, team-relevant count), a table color-coded by stage with -**client-side sortable columns** (click any column header to sort), a -team-relevant badge on flagged rows, and every row linking to its primary -source. +The dashboard is a single self-contained HTML file — KPI tiles (total +items, count per topic, team-relevant count), a table color-coded by +stage with **client-side sortable columns** (click any column header, or +focus and press Enter/Space, to sort), a team-relevant badge on flagged +rows, and every row linking to its primary source. ### Step 9 — Deliver and update last-run -- **Inline**: a short summary — window covered, item counts by topic, the - top team-relevant items (title, jurisdiction, stage, date). -- **Scheduled runs**: send per the profile's delivery block. If `type` is - `email`, send the HTML dashboard as an attachment to the addresses in - `to`. The pre-authorized recipient is the user themselves. Any additional - recipient requires explicit user confirmation on an interactive run and - is never added on an unattended run. +- **Inline**: a short summary — window covered, item counts by topic, + the top team-relevant items (title, jurisdiction, stage, date). If a + topic produced no items, say **"No significant developments this + period"** for that topic. Do not pad, do not speculate. +- **Scheduled runs**: send per the profile's delivery block. If `type` + is `email`, send the HTML dashboard as an attachment to the addresses + in `to`. The pre-authorized recipient is the user themselves. Any + additional recipient requires explicit user confirmation on an + interactive run and is never added on an unattended run. - **Update the config** with `last_run_at = `. -## Runtime budget (defaults) - -The `runtime_budget` block in the config caps every dimension of a run: - -| Setting | Default | What it caps | -|---|---:|---| -| `max_items` | 40 | Total items recorded in one run | -| `max_fallback_searches` | 2 | `web_search` calls per run (only if a source was silent) | -| `max_fetches_per_source` | 2 | Individual items followed from one source's index | - -These bounds keep the skill fast and predictable. Users can raise them for -big topics (e.g., Pillar II across 20 jurisdictions) or lower them for -narrow ones (e.g., one EU regulation). - -## Output - -- **Dashboard**: `output/regulation-dashboard.html` — KPI tiles, client-side - sortable color-coded table, team-relevant badges, per-row source links - (URL scheme sanitized — only `http`/`https`/`mailto` render). -- **Items JSON**: `working/regulation-items.json` — raw items from this run - (useful for diffing or feeding downstream tools). -- **Inline summary**: ≤12 lines — window, item counts by topic, top team- - relevant items. If a topic produced no items, say **"No significant - developments this period"** for that topic — do not pad, do not speculate. - ## Guardrails -- **Never fabricate** a bill number, date, quote, or enactment status. If a - fact cannot be confirmed from a retrieved public source, mark it - `[unverified]` in the summary. Report gaps honestly ("couldn't confirm X"). -- **No speculation, rumors, or unofficial sources.** Do not include items - that come from anonymous leaks, social-media speculation, unattributed - drafts, or "reportedly" / "expected to" claims without a named official - source. If an item cannot be tied to a specific document or announcement - from a source on the profile's locked list (or the reputable-domain - allowlist for fallback search), drop it. Better silence than noise. -- **Report empty categories explicitly.** If a topic produced no items in - the window, say **"No significant developments this period"** for that - topic in both the inline summary and the dashboard. Do not pad with - low-signal filler. -- **Public sources only.** Never bypass a paywall or reproduce paywalled or - copyrighted text. Summarize in the skill's own words and link the source. +- **Never fabricate** a bill number, date, quote, or enactment status. + If a fact cannot be confirmed from a retrieved public source, mark it + `[unverified]` in the summary. Report gaps honestly. +- **No speculation, rumors, or unofficial sources.** Do not include + items sourced from anonymous leaks, social-media speculation, + unattributed drafts, or "reportedly" / "expected to" claims without a + named official source. If an item cannot be tied to a specific + document or announcement from a source on the profile's locked list + (or the reputable-domain allowlist for fallback search), drop it. + Better silence than noise. +- **Report empty categories explicitly.** If a topic produced no items + in the window, say **"No significant developments this period"** for + that topic in both the inline summary and the dashboard. Do not pad + with low-signal filler. +- **Public sources only.** Never bypass a paywall or reproduce + paywalled or copyrighted text. Summarize in the skill's own words and + link the source. - **Locked source list.** The sweep only visits URLs in the profile's `sources_by_topic` and `seed_sources_by_topic`. That list is set at - interactive setup with an explicit user confirmation — the skill cannot + interactive setup with explicit user confirmation — the skill cannot start the monitoring sweep until the user has approved the sources. - Fallback search at runtime is bounded by - `runtime_budget.max_fallback_searches` and filtered by the reputable- - domain allowlist. Do not add new sources on an unattended run — that - requires interactive re-setup. -- **Monitoring, not advice.** The skill never states a filing position, a - legal conclusion, or a business impact. `relevant_to_your_team` is a soft - keyword-match highlight. -- **Confirm before external sends.** Emailing anyone other than the user - requires explicit confirmation on an interactive run; unattended runs - never add recipients. + Fallback search is bounded by `runtime_budget.max_fallback_searches` + and filtered by the reputable-domain allowlist. Do not add new + sources on an unattended run — that requires interactive re-setup. +- **Monitoring, not advice.** The skill never states a filing position, + a legal conclusion, or a business impact. `relevant_to_your_team` is + a soft keyword-match highlight. +- **Confirm before external sends.** Emailing anyone other than the + user requires explicit confirmation on an interactive run; unattended + runs never add recipients. - **Confidentiality and sensitivity.** If a retrieved source carries a - confidentiality label or sensitivity marking (for example "Confidential", - "Internal Only", "Restricted", or an enterprise information-protection - label), or contains unreleased figures, customer or partner identifiers, - or names that aren't public yet, drop it — this skill uses public - sources only. Never add PII, customer identifiers, or non-public - attribution to the dashboard beyond the function-area keywords the user - confirmed at setup. + confidentiality label or sensitivity marking (for example + "Confidential", "Internal Only", "Restricted", or an enterprise + information-protection label), or contains unreleased figures, + customer or partner identifiers, or names that aren't public yet, + drop it — this skill uses public sources only. Never add PII, + customer identifiers, or non-public attribution to the dashboard + beyond the function-area keywords the user confirmed at setup. - **Cover exactly the requested topics** — no more, no fewer. - **Do not reproduce third-party copyrighted text** verbatim; paraphrase in the skill's own words. -- **Verify delivery.** Confirm the dashboard file exists before reporting - success. If the delivery block failed, report the failure — do not - report success. +- **Verify delivery.** Confirm the dashboard file exists before + reporting success. If the delivery block failed, report the failure. - **Cite by exact source name** and validate every date against the requested window. -## Scheduling - -The core skill has no schedule of its own — it just runs. See the -submission's `README.md` (a human-facing sidecar; not bundled into the -agent's context) for platform-specific scheduling walkthroughs on Scout and -Cowork. Wire this skill to your platform's scheduler and it invokes the -skill on cadence. - ## References -- `references/sources-and-taxonomy.md` — reputable-domain allowlist for - fallback search, item classification taxonomy, stage-inference rules, and - per-topic search-query templates. +- `references/sources-and-taxonomy.md` — reputable-domain allowlist, + item classification taxonomy, stage-inference rules, and per-topic + search-query templates. - `scripts/build_dashboard.py` — self-contained dashboard generator (Python standard library only; embeds a small vanilla-JS sorter for the items table). diff --git a/submissions/regulation-monitor/references/sources-and-taxonomy.md b/submissions/regulation-monitor/references/sources-and-taxonomy.md index c7a21cfe..90760ad7 100644 --- a/submissions/regulation-monitor/references/sources-and-taxonomy.md +++ b/submissions/regulation-monitor/references/sources-and-taxonomy.md @@ -43,14 +43,15 @@ Environment / ESG: `epa.gov`, `energy.gov`, `iso.org`, `issb.ifrs.org`, Labor and workforce: `dol.gov`, `eeoc.gov`, `nlrb.gov`. -Tax: `irs.gov`, `treasury.gov`, `hmrc.gov.uk`, `taxpolicycenter.org`, -`taxfoundation.org`, state DOR sites (already covered by `.gov`). +Tax: `irs.gov`, `treasury.gov`, `hmrc.gov.uk` (state DOR sites are already covered by `.gov`). Standards and technical: `nist.gov`, `iso.org`, `ieee.org`, `w3.org`, `iana.org`. ### Reputable trackers and think tanks (opt-in) +- `taxfoundation.org` — Tax Foundation +- `taxpolicycenter.org` — Tax Policy Center - `mtc.gov` — Multistate Tax Commission - `ncsl.org` — National Conference of State Legislatures - `cost.org` — Council on State Taxation (public materials only) diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index 689dc6d6..b2da4ca9 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -125,14 +125,18 @@ def group_key(item: dict[str, Any]) -> tuple[int, int]: def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: profile_name = esc(config.get("profile_name", "regulation-monitor")) cadence = esc(config.get("cadence", "on demand")) - window_days = config.get("window_days", 7) + try: + window_days = int(config.get("window_days") or 7) + except (TypeError, ValueError): + window_days = 7 generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") topic_name_by_key: dict[str, str] = {} for topic in config.get("watch_topics", []): - topic_name_by_key[str(topic.get("key", ""))] = str( - topic.get("name", topic.get("key", "")) - ) + key = str(topic.get("key", "")).strip() + if not key: + continue # skip topics without a key rather than colliding on "" + topic_name_by_key[key] = str(topic.get("name", key)) total = len(items) per_topic = Counter(item.get("topic", "") for item in items) From 80452f68b0c4112158f07143818c5e45447e7cf0 Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:52:58 +0100 Subject: [PATCH 11/16] Regulation Monitor: clean load_json errors + spell out items file shape Two nits from the post-audit bot pass. build_dashboard.py: wrap load_json in try/except so a missing file or malformed JSON exits 2 with a helpful stderr line instead of a raw traceback. Verified with smoke tests. SKILL.md Step 8: spell out that the items file is a JSON object (items array), not a bare array. --- submissions/regulation-monitor/SKILL.md | 4 +++- .../scripts/build_dashboard.py | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md index 4df63963..e9fa3d44 100644 --- a/submissions/regulation-monitor/SKILL.md +++ b/submissions/regulation-monitor/SKILL.md @@ -241,7 +241,9 @@ that is a human judgment. ### Step 8 — Build the dashboard -1. Write items to `working/regulation-items.json`. +1. Write items to `working/regulation-items.json` as a JSON object with the + shape `{"items": [ ... ]}` (not a bare array). Each entry follows the + Step 6 schema. 2. Run the bundled generator: ``` python scripts/build_dashboard.py \ diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index b2da4ca9..73361740 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -59,7 +59,21 @@ def load_json(path: Path) -> dict[str, Any]: - return json.loads(path.read_text(encoding="utf-8")) + """Read and parse a JSON file, raising SystemExit on IO or parse errors. + + Callers get a clean 'exit 2 with a helpful message' instead of a raw + traceback when the input is missing, unreadable, or malformed. + """ + try: + text = path.read_text(encoding="utf-8") + except OSError as exc: + print(f"could not read {path}: {exc}", file=sys.stderr) + raise SystemExit(2) + try: + return json.loads(text) + except json.JSONDecodeError as exc: + print(f"invalid JSON in {path}: {exc}", file=sys.stderr) + raise SystemExit(2) def safe_url(value: Any) -> str: From 436a207410259612967caeb8f1fa5f045e7cfedd Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:03:10 +0100 Subject: [PATCH 12/16] Regulation Monitor: address round 11 review nits Four fixes from round 11. Skip empty topic keys in KPI tiles loop (was inconsistent with topic_name_by_key which already skips them). Collapse Step 8 dashboard command to a single line so it does not rely on POSIX backslash line-continuation (Scout targets cross-OS). Clarify that trackers and firm alerts are pointers only and recorded items must cite the underlying official document. Remove hbr.org from the default allowlist since it is frequently paywalled and undermines the no-paywall guardrail. --- submissions/regulation-monitor/SKILL.md | 15 ++++++++++----- .../references/sources-and-taxonomy.md | 1 - .../regulation-monitor/scripts/build_dashboard.py | 4 +++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md index e9fa3d44..212658dd 100644 --- a/submissions/regulation-monitor/SKILL.md +++ b/submissions/regulation-monitor/SKILL.md @@ -78,6 +78,14 @@ Preference order (pick the strongest 5 that exist for the topic): Match the mix to the domain — do not force tax-style sources onto a non-tax topic. +**Trackers and firm alerts are pointers, not authoritative sources.** +Sources in categories 4 and 5 are useful as indices to find primary +regulator/court/legislative material, but any item you record must cite +the underlying **official document or announcement** (regulator page, +official journal, court opinion) as its `source_url` — not the tracker +or firm alert that linked to it. If you only have the tracker link and +can't find the primary source, drop the item. + **How to find them:** for each topic, do a short bounded discovery pass — one to three focused web searches against the reputable-domain allowlist in `references/sources-and-taxonomy.md` — just enough to @@ -244,12 +252,9 @@ that is a human judgment. 1. Write items to `working/regulation-items.json` as a JSON object with the shape `{"items": [ ... ]}` (not a bare array). Each entry follows the Step 6 schema. -2. Run the bundled generator: +2. Run the bundled generator (single-line command; works in any shell): ``` - python scripts/build_dashboard.py \ - --config config.json \ - --items working/regulation-items.json \ - --output output/regulation-dashboard.html + python scripts/build_dashboard.py --config config.json --items working/regulation-items.json --output output/regulation-dashboard.html ``` 3. Verify the file was written before telling the user it is ready. diff --git a/submissions/regulation-monitor/references/sources-and-taxonomy.md b/submissions/regulation-monitor/references/sources-and-taxonomy.md index 90760ad7..f9af0936 100644 --- a/submissions/regulation-monitor/references/sources-and-taxonomy.md +++ b/submissions/regulation-monitor/references/sources-and-taxonomy.md @@ -58,7 +58,6 @@ Standards and technical: `nist.gov`, `iso.org`, `ieee.org`, `w3.org`, - `iapp.org` — International Association of Privacy Professionals - `future-of-privacy-forum.org` - `brookings.edu`, `hoover.stanford.edu`, `aei.org`, `epic.org` -- `hbr.org` (limited to freely available articles) The user can extend this list via `domain_allowlist_extensions` in the config. Add sparingly — the whole point of the allowlist is signal, not volume. diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index 73361740..f7d12ebd 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -164,7 +164,9 @@ def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: _tile("Team-relevant", str(team_relevant)), ] for topic in config.get("watch_topics", []): - key = str(topic.get("key", "")) + key = str(topic.get("key", "")).strip() + if not key: + continue # matches topic_name_by_key skip above tiles_html.append(_tile(str(topic.get("name", key)), str(per_topic.get(key, 0)))) # Rows. Each cell gets a data-sort attribute the JS sorter reads: From 84d5743bb57293acd6b0079a2a6cbf4ecafb4253 Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:04:31 +0100 Subject: [PATCH 13/16] Regulation Monitor: also skip empty topic keys in Quiet-this-period section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to 436a207. The empty-topics loop had the same is-key-empty check pattern as the tiles loop, but only guarded on key being falsy — a whitespace-only key would slip through and render as 'X — No significant developments this period'. Now normalizes via strip() and skips like the tiles loop does. Verified with a two-topic (one blank-key) smoke config that neither the KPI tiles nor the Quiet-this-period section render the blank-key topic. --- submissions/regulation-monitor/scripts/build_dashboard.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index f7d12ebd..fc7851fa 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -216,8 +216,10 @@ def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: topics_with_items = {str(item.get("topic", "")) for item in items} empty_topics: list[str] = [] for topic in config.get("watch_topics", []): - key = str(topic.get("key", "")) - if key and key not in topics_with_items: + key = str(topic.get("key", "")).strip() + if not key: + continue # skip topics without a real key (see tiles loop above) + if key not in topics_with_items: empty_topics.append(str(topic.get("name", key))) empty_html = "" From 4f010863e00e83a92013a53eba5499cb9a3aa414 Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:17:52 +0100 Subject: [PATCH 14/16] Regulation Monitor: harden dashboard input handling Four defensive-programming nits from bot rounds 12-13. Treat relevant_to_your_team strictly as literal True (not truthy) so a string 'false' does not count as relevant. Normalize a null/missing topic to empty string so it does not render literal 'None' in the dashboard or leak into empty-topic detection. Validate config file is a JSON object with a clean error message (symmetry with items validation). Handle window_days=0 correctly by using explicit None check instead of falsy fallback. --- .../scripts/build_dashboard.py | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/submissions/regulation-monitor/scripts/build_dashboard.py b/submissions/regulation-monitor/scripts/build_dashboard.py index fc7851fa..dd911ba0 100644 --- a/submissions/regulation-monitor/scripts/build_dashboard.py +++ b/submissions/regulation-monitor/scripts/build_dashboard.py @@ -127,7 +127,7 @@ def sorted_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]: reverse=True, ) def group_key(item: dict[str, Any]) -> tuple[int, int]: - relevant_first = 0 if item.get("relevant_to_your_team") else 1 + relevant_first = 0 if item.get("relevant_to_your_team") is True else 1 stage = str(item.get("stage") or "") stage_idx = ( STAGE_ORDER.index(stage) if stage in STAGE_ORDER else len(STAGE_ORDER) @@ -139,10 +139,14 @@ def group_key(item: dict[str, Any]) -> tuple[int, int]: def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: profile_name = esc(config.get("profile_name", "regulation-monitor")) cadence = esc(config.get("cadence", "on demand")) - try: - window_days = int(config.get("window_days") or 7) - except (TypeError, ValueError): + window_raw = config.get("window_days") + if window_raw is None: window_days = 7 + else: + try: + window_days = int(window_raw) + except (TypeError, ValueError): + window_days = 7 generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") topic_name_by_key: dict[str, str] = {} @@ -153,8 +157,8 @@ def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: topic_name_by_key[key] = str(topic.get("name", key)) total = len(items) - per_topic = Counter(item.get("topic", "") for item in items) - team_relevant = sum(1 for item in items if item.get("relevant_to_your_team")) + per_topic = Counter(str(item.get("topic") or "") for item in items) + team_relevant = sum(1 for item in items if item.get("relevant_to_your_team") is True) ordered = sorted_items(items) @@ -174,7 +178,7 @@ def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: # equals chronological (date), plain lowercased text elsewhere. rows_html: list[str] = [] for item in ordered: - topic_key = str(item.get("topic", "")) + topic_key = str(item.get("topic") or "") # None -> "" not "None" topic_display = topic_name_by_key.get(topic_key, topic_key or "-") stage = str(item.get("stage") or "") stage_sort = ( @@ -197,7 +201,7 @@ def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: rows_html.append( "" f'' - f'' + f'' f'' f'' f'
    DateTopicJurisdictionStageTitle & summarySourceDateTopicJurisdictionStageTitle & summarySource
    {esc(date_str or "-")}{esc(topic_display)} {relevant_badge(bool(item.get("relevant_to_your_team")))}{esc(topic_display)} {relevant_badge(item.get("relevant_to_your_team") is True)}{esc(jurisdiction)}{stage_pill(stage)}' @@ -213,7 +217,7 @@ def build_html(config: dict[str, Any], items: list[dict[str, Any]]) -> str: ) # Empty-topic callout: list any watch topic that produced zero items. - topics_with_items = {str(item.get("topic", "")) for item in items} + topics_with_items = {str(item.get("topic") or "") for item in items} empty_topics: list[str] = [] for topic in config.get("watch_topics", []): key = str(topic.get("key", "")).strip() @@ -449,6 +453,12 @@ def main(argv: list[str] | None = None) -> int: return 2 config = load_json(args.config) + if not isinstance(config, dict): + print( + f"config file must contain a JSON object; got {type(config).__name__}", + file=sys.stderr, + ) + return 2 items_doc = load_json(args.items) raw_items = items_doc.get("items", []) if isinstance(items_doc, dict) else [] if not isinstance(raw_items, list): From 2a110c10ca5de8496e4938554f1a7f2a55fe0d37 Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:42:39 +0100 Subject: [PATCH 15/16] Regulation Monitor: drop 'works in any shell' claim from Step 8 command The command still assumes python is on PATH; wording overpromised portability. Trimmed to just 'single-line command'. --- submissions/regulation-monitor/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submissions/regulation-monitor/SKILL.md b/submissions/regulation-monitor/SKILL.md index 212658dd..2b622ae4 100644 --- a/submissions/regulation-monitor/SKILL.md +++ b/submissions/regulation-monitor/SKILL.md @@ -252,7 +252,7 @@ that is a human judgment. 1. Write items to `working/regulation-items.json` as a JSON object with the shape `{"items": [ ... ]}` (not a bare array). Each entry follows the Step 6 schema. -2. Run the bundled generator (single-line command; works in any shell): +2. Run the bundled generator (single-line command): ``` python scripts/build_dashboard.py --config config.json --items working/regulation-items.json --output output/regulation-dashboard.html ``` From 22e511513cd0038087e99f936b302762bb210275 Mon Sep 17 00:00:00 2001 From: jchha001 <51886135+jchha001@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:49:30 +0100 Subject: [PATCH 16/16] Regulation Monitor: add authorUrl (matches PR #75 pattern) Adds authorUrl pointing to my GitHub profile so the CI-derived authorGithub is populated and the skillbot can @-mention me on the first comment of the skill's gallery discussion. Same shape as the previously merged presentation-talk-track-builder skill. --- submissions/regulation-monitor/metadata.json | 1 + 1 file changed, 1 insertion(+) diff --git a/submissions/regulation-monitor/metadata.json b/submissions/regulation-monitor/metadata.json index 0c6c29f5..3724c15c 100644 --- a/submissions/regulation-monitor/metadata.json +++ b/submissions/regulation-monitor/metadata.json @@ -4,6 +4,7 @@ "platforms": ["Cowork", "Scout"], "tags": ["regulation", "monitoring", "compliance", "dashboard", "research"], "author": "Jagmeet Chabra", + "authorUrl": "https://github.com/jchha001", "version": "1.0.0", "createdAt": "2026-07-23", "updatedAt": "2026-07-23"