| name | s360-reporter |
| description | Generate S360 weekly reports for the Android Auth team. Fetches active action items from S360 MCP server, creates ADO work items (PBIs) for untracked items, and produces a polished Outlook-compatible HTML email report. Triggers include "S360 report", "generate S360 report", "weekly S360", "S360 status", "what are our S360 items", or any request to review, report on, or triage S360 action items for the Android Auth team. |
S360 Weekly Report Generator
Generate a polished S360 weekly report for the Android Auth team. Fetches live data from
the S360 MCP server, ensures every item has an ADO PBI, and produces an Outlook-compatible
HTML email report.
Prerequisites
- Node.js must be available (for the committed merge/reduce/report scripts:
merge-items.js, reduce-items.js, generate-report.js)
- S360 MCP Server must be running (configured in
.vscode/mcp.json as s360-breeze-mcp)
- ADO MCP Server must be running (for PBI creation and lookup)
- M365 User MCP (
m365-user) — for dynamic team member discovery via org chart
- WorkIQ MCP Server (optional) — used as fallback for pulling last week's email if the user doesn't provide it
- Read the Outlook HTML report prompt at
{{VSCODE_USER_PROMPTS_FOLDER}}/outlook-html-report.prompt.md
for HTML rendering rules before generating the report
Quick Mode
If the user says "quick S360" or "S360 status", run Steps 0–2 only and print a CLI
summary instead of generating the full report. Example output:
S360 Status — Android Auth Team (Apr 8, 2026)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15 active items: 1 🔴 Out of SLA, 2 🟠 Approaching, 12 🟢 In SLA
7 items missing ETA
🔴 GDPR Streams Left to Review — moghosh — Due Mar 22 (17 days overdue)
🟠 Disable local auth for container registries — moghosh — Due Apr 12
🟠 Threat Model Review — zhangrichard — Due May 5
Skip PBI creation, report generation, and email drafting in quick mode.
Target Services
| Service | Service Tree ID |
|---|
| AuthN SDK - MSAL Android | 8d0d308e-cd5c-44a3-9518-43eeeb424b57 |
| Microsoft Authenticator - Android | 0b97f26e-fcfc-4ed1-95e9-1dca3a2fde3b |
Decommissioned Scope
These service trees were removed and are no longer monitored. They must never be
re-added to the fetch scope — even if they still appear in a pasted "last week's report"
and even if their items are still active in S360:
| Service (removed) | Service Tree ID |
|---|
| AuthN SDK - ADAL Android | 937cdc57-1253-4b55-878e-5854368926a2 |
merge-items.js enforces this as a hard denylist: items targeting these GUIDs are
always dropped (from both the service and person queries), and the GUID is stripped even
if passed via a team.json serviceIds override. Do not attempt to work around it by
adding the owning manager's alias or the GUID to "recover" such items — see Step 1e's
decommissioned-scope guard.
KPIs Where ETA Is Not Applicable
Some S360 KPIs do not have an ETA column in the portal. For items belonging to these KPIs,
show "N/A" in the ETA field instead of "Missing ETA ⚠". Do NOT count them in the
"Missing ETA" summary count.
| KPI ID | KPI Name |
|---|
d573888d-4c6f-81cc-7992-50dc17c87d83 | [Compl-CC1.3] Data Type Classification (GDPR) |
Maintaining this list: If the user reports that an item shows "Missing ETA ⚠" but
the S360 portal has no ETA column for it, add the KPI ID to this table.
Workflow
Step 0: Discover Team Members via Graph
Some S360 items (e.g., on-call readiness) are person-targeted (TargetType: "Person",
TargetId: "alias") rather than service-targeted. Searching by service tree IDs alone
misses these. To capture them, dynamically discover team member aliases from the org chart.
-
Get current user's manager:
m365-user-GetManagerDetails(
select: "id,displayName,userPrincipalName"
)
Extract the manager's id (GUID) and userPrincipalName.
-
Get all direct reports of the manager (= your teammates):
m365-user-GetDirectReportsDetails(
userId: "<manager UPN from step 1>",
select: "id,displayName,userPrincipalName,jobTitle,accountEnabled"
)
-
Filter and extract aliases:
- Exclude accounts where
userPrincipalName starts with SC- or sc- (non-EA service accounts)
- Exclude accounts where
accountEnabled is false
- Extract alias from
userPrincipalName by stripping @microsoft.com
- Store the list of aliases and a display-name map for later use in the report
Fallback: If the M365 User MCP is unavailable, fall back to the ADO Teams API:
- Call
mcp_ado_core_list_project_teams(project: "Engineering", mine: true)
- Find the team named "Auth Client - Android" and note its
id
- Call the ADO REST API via terminal:
Invoke-RestMethod -Uri "https://identitydivision.visualstudio.com/_apis/projects/Engineering/teams/{teamId}/members?api-version=7.1"
- Extract
uniqueName values, strip @microsoft.com to get aliases
Step 0b: Collect Last Week's Report
Before fetching S360 data, ask the user if they have last week's S360 report available.
This is the primary method for determining "new this week" items, resolved items,
and pre-existing PBI assignments. Use the ask_user tool:
question: "Do you have last week's S360 report to paste? This helps detect new/resolved items and avoid duplicate PBIs. You can paste the report text, or skip and I'll try to find it automatically."
choices: ["I'll paste it now", "Skip — find it automatically"]
If the user pastes the report:
- Parse the pasted text for:
- Item titles with owners — each row in the report table
- AB# references — extract numeric ADO work item IDs (e.g.,
AB#12345, Product Backlog Item 12345, or Bug 12345)
- ADO work item URLs — links like
dev.azure.com/.../workitems/12345
- SLA states — Missed SLA, Near SLA, In SLA
- Build a previous report map: title → { pbi, owner, slaState }.
Exclude any items belonging to a decommissioned service (see "Decommissioned
Scope", e.g. ADAL Android). They are out of scope this week and must not enter the
week-over-week diff — do not list them as resolved and do not use them to expand the
fetch scope.
- Store this map for use in:
- Step 1e (resolved items = items in last week's map but NOT in current active set)
- Step 3 (existing PBIs = AB# numbers from the map)
- Step 5 (new items = items in current active set but NOT in last week's map)
If the user skips or doesn't respond:
Fall back to automatic discovery in Step 3a (WorkIQ → Mail Search → proceed without).
Step 1: Fetch S360 Data
Fetch items from two sources and merge them:
1a: Service-targeted items
Call mcp_s360-breeze-m_search_active_s360_kpi_action_items with both service tree IDs:
request: {
"pageSize": 50,
"targetIds": [
"8d0d308e-cd5c-44a3-9518-43eeeb424b57",
"0b97f26e-fcfc-4ed1-95e9-1dca3a2fde3b"
]
}
If more than 50 items, paginate using the nextCursor field.
1b: Person-targeted items
Using the aliases discovered in Step 0, call mcp_s360-breeze-m_search_active_s360_kpi_action_items
with assignedTo:
request: {
"pageSize": 50,
"assignedTo": ["alias1", "alias2", ...all team aliases from Step 0...]
}
This captures person-targeted items like on-call readiness checklists and certifications
that are tied to individuals rather than service tree IDs.
Important: The assignedTo search returns ALL items for those aliases across Microsoft,
including items from other team memberships. After fetching, filter results to only include
items where one of these conditions is met:
TargetType is "Person" AND TargetId exactly matches one of the team aliases
TargetId matches one of our two service tree IDs
CustomDimensions.TenantName contains "Auth Client", "MSAL", or "Authenticator"
Critical — do NOT expand group items: Each S360 item has exactly one AssignedTo
and one TargetId. Treat each item as-is — one row per KpiActionItemId. Never split
a single item into multiple rows by parsing names from the title or description. If
multiple team members share the same on-call KPI, S360 creates separate items for
each person (each with its own KpiActionItemId and AssignedTo). If an alias has no
matching item in the API response, that person simply has no action item — do not
fabricate one.
1c: Merge results
Use the committed merge-items.js script to filter person items to team-relevance
and deduplicate by KpiActionItemId. The script encodes the filter rules from 1b and
the dedup rule below, so they cannot be forgotten or re-implemented inconsistently
across weekly runs.
# Write the team config (aliases + nameMap from Step 0) to a JSON file:
$team = @{ aliases = @('alias1','alias2',...); nameMap = @{ alias1='Name1'; ... } } |
ConvertTo-Json -Depth 3
$team | Out-File -Encoding utf8 "$env:TEMP\s360-team.json"
# Run the merger (writes warnings/counts to stderr, merged JSON to --output):
node .github/skills/s360-reporter/merge-items.js `
--service "$env:TEMP\s360-service.json" `
--person "$env:TEMP\s360-person.json" `
--team "$env:TEMP\s360-team.json" `
--output "$env:TEMP\s360-merged.json"
The script accepts MCP envelopes ({ result: { resources: [...] } }), trimmed
envelopes ({ resources: [...] }), or bare arrays for the --service / --person
inputs.
Filter logic (enforced by the script — do not duplicate ad-hoc):
TargetType == "Person" AND TargetId is a team alias → keep
TargetId is one of the two service tree GUIDs → keep
CustomDimensions.TenantName matches an Auth-team pattern → keep
AssignedTo alone is NOT sufficient — the person query already filters by
assignedTo, so every returned item has a team-alias AssignedTo but many are
for OTHER teams the person also belongs to. Items that match only AssignedTo
with no direct relevance signal are dropped (the script logs sample drops to
stderr for sanity checking).
Dedup: by KpiActionItemId. Cross-source duplicates (same item appearing in
both service and person queries) are removed.
Fallback: If Node.js is unavailable, apply the filter + dedup logic manually
following the rules above. Do not invent new merging rules — anything that
collapses items into fewer rows MUST honor the per-finding exception in Step 2.
1d: Fetch KPI Metadata (for Program Names)
After merging, collect the unique KpiId values from all items and fetch metadata
for each one:
mcp_s360-breeze-m_get_s360_kpi_metadata_by_kpi_id(kpiId: "<each unique KPI ID>")
Extract the displayName field from each response and build a KpiId → displayName
map. These display names are the authoritative program/category labels (e.g.,
[SFI-ES4.2.4] Network Isolation for CFS endpoints, [Compl-CC1.3] Data Type Classification, Individual On-Call Readiness).
Cache results — multiple items share the same KpiId, so you only need one lookup
per unique KPI, not per item.
Important: Validate that the KpiId is a proper GUID (8-4-4-4-12 hex format) before
calling the API. Some raw data may have malformed IDs — if invalid, log a warning and
fall back to the item Title as the program name.
1e: Detect Resolved Items (Week-over-Week)
To populate the "Resolved Since Last Week" section, compare the current S360 item set
against last week's report:
-
Pull last week's S360 items via one of these sources (in priority order):
a. User-provided report (from Step 0b) — if the user pasted last week's report,
use the parsed previous report map. This is the most reliable source.
b. Call mcp_s360-breeze-m_search_resolved_s360_kpi_action_items with the same
targetIds and assignedTo used in 1a/1b. Cross-reference results against the
user-provided report if available — only include items that appear in BOTH sources.
c. If no user-provided report and the resolved search tool is unavailable, parse
last week's email (from Step 3a) and extract the item titles + AB# numbers.
d. If none of the above are available, skip this step.
-
Identify resolved items: Items that appeared in last week's report but are NOT
in the current active set (from 1c) are considered resolved.
Decommissioned-scope guard (critical): If an item from last week's report belongs
to a decommissioned service (e.g. ADAL Android — see "Decommissioned Scope"), it is
out of scope, not resolved. Exclude it from the diff entirely: do not report it
as resolved, and do not "recover" it by re-adding the service tree GUID or the
owning manager's alias to the fetch (in Step 1a/1b or via team.json). These items are
intentionally no longer monitored even though they may still be active in S360;
merge-items.js will drop them regardless. A missing decommissioned item is expected —
it is not a false-resolved to be corrected.
-
For each resolved item, look up its ADO PBI state:
- If the PBI is
Done or Removed, mark as resolved with its AB# and assignee.
- If the PBI is still open, the S360 item may have been resolved but the PBI wasn't
closed — still include it in the resolved list but note the PBI state.
-
Store the resolved items list for use in Step 5 (report generation).
Each resolved item should have: Title, AB#, Assignee, PBI State.
Fallback: If no previous report data is available, show a note in the report:
"No resolved items were detected this week. If items were resolved manually, they may
not appear here."
Step 2: Parse and Deduplicate Items
Use the committed reduce-items.js script to turn the merged items from Step 1c
into logical report rows. The script encodes every dedup rule below — most
importantly the CRITICAL per-finding exception — so they cannot be skipped or
reinterpreted on each weekly run.
# Build a KpiId → displayName map from the metadata you fetched in Step 1d:
$kpiMap = @{
'a0f0ce42-3063-5d3b-3b47-1ff3143abdc9' = '[SFI-PS3.1] Security Code Bugs'
'2d6597da-8e08-4495-a4e1-954f7697a4a8' = 'SDL Annual Assessment'
# ... (one entry per unique KpiId)
}
$kpiMap | ConvertTo-Json | Out-File -Encoding utf8 "$env:TEMP\s360-kpi.json"
# Run the reducer (warnings to stderr, reduced rows to --output):
node .github/skills/s360-reporter/reduce-items.js `
--input "$env:TEMP\s360-merged.json" `
--kpi-metadata "$env:TEMP\s360-kpi.json" `
--output "$env:TEMP\s360-reduced.json"
What the reducer guarantees (the rules below are now enforced in code):
- Extracts a per-item ADO work-item ID from
URL (regex _workitems/edit/(\d+)),
falling back to S360Dimensions.ADOWorkItemHTMLUrl. Warns on conflicts.
- Resolves
ETA by walking the item object for any key containing eta
(case-insensitive) that parses as a strict ISO date. Picks the most recent.
- Groups items into rows using:
wi:<ADOWorkItemId> if the item has a non-null, non-reused ADO ID → one
row per work item (the per-finding case).
pf:<KpiActionItemId> if the item's KPI is in the script's
PER_FINDING_KPIS set but URL is missing → still one row per item, so a
temporary URL outage cannot silently umbrella-merge.
nowi:<KpiId>|<baseTitle>|<TargetId> otherwise → umbrella merge (CFS
multi-endpoint case).
- Detects reused work-item IDs (same ADO ID referenced by multiple distinct
KpiId|baseTitle|TargetId tuples) and refuses to use them as grouping
authority — prevents a shared template work item from collapsing unrelated
items into one row.
- Picks the worst-SLA + earliest-due item as the group representative
(deterministic tiebreak on
KpiActionItemId).
- Sorts output rows deterministically by SLA, due date, program, title — same
input always produces identical output.
- Logs URL-coverage warnings for known per-finding KPIs when not every item
has an ADO link.
Maintaining the PER_FINDING_KPIS set: If a new KPI is discovered where each
S360 item maps to its own ADO Bug (e.g. accessibility per-issue, BinSkim per-rule),
add its KpiId to the PER_FINDING_KPIS set near the top of reduce-items.js.
The script will still do the right thing if you forget (URL-based grouping handles
it as long as URLs are populated) — the set is a defense-in-depth fallback for
missing URLs.
For each field the reducer extracts (and the rest of the workflow consumes):
| Field | JSON Path | Notes |