Skip to content

feat(telemetry): 1DS skill-run telemetry infra - #176

Merged
amitjoshi438 merged 66 commits into
mainfrom
users/amitjosh/1ds-feature
Jun 11, 2026
Merged

feat(telemetry): 1DS skill-run telemetry infra#176
amitjoshi438 merged 66 commits into
mainfrom
users/amitjosh/1ds-feature

Conversation

@amitjoshi438

@amitjoshi438 amitjoshi438 commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds 1DS skill-run telemetry to the repo, with power-pages as the
first adopter. Telemetry is default-on and opt-out via
POWER_PLATFORM_SKILLS_TELEMETRY=0, with a repo-side disabled kill switch in
ikey.json that ships true until the tenant-side Kusto stream is provisioned.

What's included

  • Shared library (shared/telemetry/) — zero-dependency, Node stdlib only.
    Strict FIELD_TYPES allowlist (events.js), detached fire-and-forget
    dispatcher (emit-spawn.js / emit-dispatcher.js), CS4.0 envelope, PAC-auth
    • AI-agent detection, per-process session IDs, started↔completed correlation,
      and a local dev-log fallback when no real iKey is configured.
  • Power Pages adoption — synced copy under scripts/lib/telemetry/, three
    hooks (PreToolUse / PostToolUse / UserPromptSubmit) emitting
    skill_started . The PostToolUse hook keeps running each
    skill's validator; telemetry is layered on, fail-closed, and never changes
    exit codes.
  • Tests — 126 shared + the power-pages script suite, all hermetic
    (injectable seams, no real network / PAC shellouts).
  • Docsshared/telemetry/README.md integration guide, plus telemetry
    sections in AGENTS.md, README.md, and plugins/power-pages/AGENTS.md.

Scope

The diff is telemetry-only (48 files, +4367/−22). Validators and standalone
scripts are unchanged from main — the branch carries no incidental refactors.

Privacy posture

Anonymous only. No file paths, cwd, env vars, site names, Dataverse URLs, stack
traces, err.message, skill args, prompt text, usernames, or hostnames.
errorClass is the constructor name; errorDescription is err.code only. A
defense-in-depth allowlist filter runs in the dispatcher before serialization.

amitjoshi438 and others added 29 commits May 13, 2026 15:56
…ders (#142)

* docs: add design spec for 1DS telemetry infra

Design for a shared telemetry library at shared/telemetry/ consumed
by the power-pages plugin first, with interactive first-run consent,
strict-allowlist payloads, and fail-closed emission via the existing
PreToolUse/PostToolUse:Skill hook surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(telemetry): add shared 1DS telemetry library

Introduces the shared 1DS telemetry infrastructure used by all
adopting plugins. Anonymous, default-on, env-var opt-out only
(POWER_PLATFORM_SKILLS_TELEMETRY=0). No persistent consent file,
no first-run prompt.

Library (shared/telemetry/lib/):
- events.js — type-aware allowlist builders (PowerPagesPluginEvent
  envelope, top-level columns, per-event field set, durationMs as int)
- emit-dispatcher.js — detached child that POSTs to OneCollector
  with the Common Schema 4.0 envelope, exits silently on any failure
- emit-spawn.js — fire-and-forget spawn helper with restricted child env
- emit-from-prompt.js — orchestrator for slash-command skill_started
  events (reads ikey.json, calls pac auth + agent info, builds event)
- pac-auth.js — shells out to `pac auth who` for orgId/tenantId
- agent-info.js — detects host AI agent (Claude Code, Copilot CLI)
  + pac CLI version via env override → auto-detect → empty
- prompt-detector.js — matches /plugin:skill from user prompt text
- local-log.js — placeholder-iKey JSONL fallback at
  ~/.power-platform-skills/events.jsonl
- with-telemetry.js — runInstrumented() wrapper for script-level signals
- correlation.js, session.js, scrubber.js — utilities

Config: shared/telemetry/ikey.json carries the iKey, collector URL,
per-plugin event_stream_name, and a 'disabled' kill switch. The flag
ships true so infrastructure can land before tenant-side annotation
+ Kusto provisioning is complete; a single PR flips it false later.

Sync: shared/telemetry/sync-to-plugin.js copies lib/ + ikey.json
into <plugin>/scripts/lib/telemetry/. Adopting plugins never
hand-edit the synced copy.

Tests: full node:test coverage for every module plus the dispatcher
and spawn integration (109 tests, all hermetic; envelope-shape probe
via POWER_PLATFORM_SKILLS_FAKE_HTTPS).

Docs: rebuild design spec (2026-05-04) + plugin adoption guide
(2026-04-29) under docs/superpowers/specs/. Older draft spec
(2026-04-20) removed.

* docs(telemetry): align README with code — camelCase fields, correct PAC posture

shared/telemetry/README.md drifted from the rebuild work:

- Field names listed in snake_case (plugin_name, session_id, ...) but
  the code uses camelCase (pluginName, sessionId, ...). Names match
  Kusto column names directly.
- "What is NEVER sent" claimed tenant IDs are not sent — but
  pac-auth.js explicitly emits orgId and tenantId GUIDs from the
  active PAC profile. Moved them to "What is sent" with the rest of
  the PAC + agent fields.
- Missing from "What is sent": pluginVersion, osName/osVersion (was
  os_family), pacCliVersion, aiAgentName, aiAgentVersion, eventInfo,
  errorDescription.
- Added a note about errorDescription being a 500-char-truncated
  Error.message — callers must keep PII out of exception messages.
- Documented the ikey.json "disabled" repo-side kill switch under
  Privacy posture.

* refactor(telemetry): address review feedback (PR #142)

Four fixes from PR review:

1. **iKey full form in envelope** — emit-dispatcher.js now sends the
   complete iKey value in the Common Schema `iKey` field
   (`"o:" + IKEY`) instead of truncating to just the first
   hyphen-separated segment. Aligns with the powerplatform-vscode
   pattern. Multi-cloud key restructuring deferred to a follow-up PR.

2. **Drop POWER_PLATFORM_SKILLS_BYPASS_KILL_SWITCH** — production code
   no longer carries a test-only escape hatch. Tests now override the
   ikey.json path via POWER_PLATFORM_SKILLS_IKEY_JSON (parallels the
   existing IKEY/COLLECTOR/CONFIG_DIR/FAKE_HTTPS env-var pattern) and
   supply their own `disabled: false` config when they need to
   exercise emission paths. emit-spawn.js forwards the new var to the
   detached child.

3. **Defense-in-depth FIELD_TYPES enforcement in dispatcher** — events.js
   now exports `pick`. The dispatcher imports both `FIELD_TYPES` and
   `pick`, then re-runs the allowlist filter on `event.data` before
   building the envelope. Any field that bypasses the builders is
   dropped before it reaches the wire. The three reserved meta fields
   (eventName, eventType, severity) survive via an explicit allow list.

4. **errorDescription emits err.code only, never err.message** —
   with-telemetry.js no longer reads `err.message` (which can contain
   file paths, GUIDs, tokens). The field now captures `err.code` —
   short non-PII metadata like `ENOENT` or `ERR_INVALID_ARG_TYPE`.
   Test "errorDescription truncated to 500 chars" replaced with one
   that asserts a PII-bearing message is NOT emitted anywhere when
   `err.code` is set. README updated accordingly.

New dispatcher test: `dispatcher strips unknown fields from event.data`
hand-crafts an event with filePath/stackTrace/rawPrompt/tokenValue
and asserts none survive the FIELD_TYPES filter.

Tests: 110/110.

* refactor(telemetry): rename ikey.json key — ikey → instrumentationKey

Per PR #142 comment 1: the JSON property is now spelled in its full
form (`instrumentationKey`) to match the powerplatform-vscode
constants pattern instead of the abbreviated `ikey`. The envelope
`iKey` Common Schema field stays at the standard tenant-routing
short form (`o:<32-char-tenant>`); the previous attempt to put the
full hyphenated key in the envelope is reverted because the full
form already lives in the `x-apikey` header.

- shared/telemetry/ikey.json: `"ikey"` → `"instrumentationKey"`.
- shared/telemetry/lib/emit-from-prompt.js: read
  `cfg.instrumentationKey` (other JSON keys —
  `collector_url`, `event_stream_name`, `disabled` — unchanged).
- shared/telemetry/lib/emit-dispatcher.js: envelope reverted to
  `iKey: "o:" + IKEY.split("-")[0]`.
- Test fixtures updated across emit-dispatcher / emit-spawn /
  emit-from-prompt tests.

Multi-cloud iKey restructuring (the second half of PR #142
comment 1) remains deferred to a follow-up PR.

Tests: 110/110.

---------

Co-authored-by: Amit Joshi <amitjoshi@microsoft.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#153)

* feat(power-pages): adopt 1DS telemetry — synced library, hooks, validators

Wires the shared 1DS telemetry library into the power-pages plugin.
Synced copy of the library lives under scripts/lib/telemetry/ and runs
at user time. PreToolUse:Skill emits skill_started; PostToolUse:Skill
emits skill_completed wrapped around the existing validator chain.

Plugin-side additions:
- scripts/lib/telemetry/ — synced from shared/telemetry/ via
  sync-to-plugin.js (12 lib modules + ikey.json carrying the
  PowerPagesPluginEvent stream name + disabled:true kill switch).
- scripts/lib/telemetry-runner.js — runInstrumented() shim plus the
  PowerPagesPluginEvent envelope name and tracked-script registry.
- hooks/run-skill-pretool-telemetry.js, hooks/run-skill-posttool-validation.js
  — fire-and-forget telemetry around the Skill tool, preserving the
  existing validator's behavior and exit code.
- hooks/hooks.json — registers the two Skill hooks. (UserPromptSubmit
  is registered separately by the slash-command branch.)
- Validators (validate-activation/seo/site/webroles/audit/auth/datamodel/
  cloudflow/serverlogic/webapi-integration) and four other plugin
  scripts (check-activation-status, clear-site-cache, render-audit-report,
  verify-dataverse-access) wrapped with runInstrumented() so each
  script run produces script_started/script_completed signals.
- scripts/tests/{telemetry-hook-pretool,telemetry-hook-posttool,telemetry-runner}.test.js
  — hermetic node:test coverage for the new wiring.

Repo updates:
- AGENTS.md (plugin) gains the Telemetry section pointing at
  shared/telemetry/ as the canonical source of truth.
- README.md (root) mentions anonymous telemetry + env opt-out.
- docs/superpowers/handoff/PowerPagesPluginEvent-annotation.xml
  is the tenant-team handoff describing the EventStreamingAnnotation
  for the per-plugin Kusto stream.

Privacy posture: default-on, env-var opt-out only
(POWER_PLATFORM_SKILLS_TELEMETRY=0). No first-run prompt, no
consent file. Telemetry stays gated by ikey.json's disabled:true
flag until the tenant-side annotation + Kusto table are provisioned.

* fix(power-pages): telemetry-runner — pass envelopeName, read instrumentationKey

Two regressions caught during E2E verification of the local-log path:

1. **envelopeName was never forwarded** — `runInstrumented()` called
   `withTelemetry()` without an `envelopeName`, so events shipped with
   `name: ""`. In production this means the tenant's
   `EventStreamingAnnotation name="^PowerPagesPluginEvent$"` matcher
   silently drops every event (`acc:1` from the collector but zero rows
   in Kusto). Now reads `event_stream_name` from `ikey.json` and passes
   it through.

2. **iKey read the old JSON property** — `deps.ikeyCfg.ikey` was
   undefined after the `ikey` → `instrumentationKey` rename in the
   foundation library (PR #142). The dispatcher saw an empty
   `POWER_PLATFORM_SKILLS_IKEY` and always took the placeholder /
   local-log path, even with a real iKey provisioned. Now reads
   `deps.ikeyCfg.instrumentationKey`.

Adds an opt-in `_overrides` test seam on `runInstrumented()` so unit
tests can inject a fake `withTelemetry` and assert opts are propagated
correctly. New tests:
- `runInstrumented forwards envelopeName from ikey.json event_stream_name`
- `runInstrumented reads iKey from instrumentationKey property (not legacy 'ikey')`

Verified end-to-end by piping a synthetic event directly to the
dispatcher: envelope `name` is now `"PowerPagesPluginEvent"`,
`scriptName` is correctly set, event lands in the local JSONL.

Tests: 4/4 telemetry-runner, full plugin suite still green.

* telemetry: detect GitHub Copilot CLI as ai agent host

readAiAgent now recognizes COPILOT_CLI=1 and reads COPILOT_CLI_BINARY_VERSION, in addition to Claude Code. Priority remains: explicit AI_AGENT_NAME env > Claude Code > Copilot CLI. Adds test coverage and re-syncs into plugins/power-pages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(telemetry): drop power-pages testing iKey from shared/telemetry/ikey.json

`shared/telemetry/ikey.json` is the dev-time placeholder copied by
`sync-to-plugin.js`. It was committed with power-pages's real testing
iKey and INT collector URL, which meant any future adopter
(canvas-apps, code-apps, model-apps) running the sync would inherit
those values and — if they ever flipped `disabled: false` without
replacing the iKey — silently route their telemetry to power-pages's
Kusto stream.

This commit replaces both with explicit placeholders so every plugin
is forced to provision its own cluster config before going live. The
plugin's own synced copy at `plugins/power-pages/scripts/lib/telemetry/ikey.json`
keeps its real plugin-specific values; only the shared template is sanitized.

- instrumentationKey: <pp's value> → "PLACEHOLDER_REPLACE_BEFORE_SHIPPING"
- collector_url:      <INT URL>    → ""

`event_stream_name` ("PluginEventStreamPlaceholder") and `disabled: true`
were already plugin-agnostic and stay unchanged.

Tests: 110/110 shared (no behavior change — the placeholder iKey
constant matches the dispatcher's existing keyMissing check).

* fix(power-pages): address PR #153 review — await, dedup, hook fast-path

Three review fixes from priyanshu92 on PR #153:

1. **telemetry-runner.js: `return await asyncFn()`** — added the `await`
   on the no-telemetry-deps fallback path so stack traces include the
   `runInstrumented` frame. Behavioural no-op; debug-friendliness only.

2. **validate-auth.js: drop duplicate `runInstrumented` call** — the
   file invoked `runInstrumented('validate-setup-auth', main)` twice:
   once unconditionally at module top-level (line 69-72), and again
   inside the standard `if (require.main === module)` guard (line
   137-142). When executed as a script (the actual PostToolUse-hook
   use case), `main()` ran twice — duplicating validator I/O and
   emitting two `script_started`/`script_completed` pairs per run.
   When required as a module (tests), the top-level call still fired,
   breaking import semantics. Deleted the top-level invocation, kept
   the require.main guard.

3. **Skill hooks: fast-path opt-out / kill switch BEFORE pac shell-outs**
   — `run-skill-pretool-telemetry.js` and `run-skill-posttool-validation.js`
   ran `pac auth who` (3s timeout) and `pac --version` (2s timeout)
   unconditionally on every tracked Skill invocation, even when the
   user had opted out (`POWER_PLATFORM_SKILLS_TELEMETRY=0`) or the
   shipped kill switch was on (`ikey.json` `disabled: true`). Worst
   case ~10s added latency per Skill tool use, currently hit by every
   user since `disabled: true` is the shipped default.

   Both hooks now check, in this order, BEFORE any pac shell-out:
     a) env opt-out (`POWER_PLATFORM_SKILLS_TELEMETRY === "0"`)
     b) repo-side kill switch (`ikey.json` `disabled === true`)
     c) unconfigured (`!ikey.instrumentationKey`)

   Any of those → exit early. The posttool hook still preserves the
   validator's exit code regardless of telemetry state.

   `readIkey()` in the pretool hook now also exposes the `disabled`
   flag so the gate can read it without re-parsing.

Comment #3 (extend agent-info detection to Codex / OpenCode) is
pending reviewer confirmation of those hosts' env-var conventions —
not implemented here. Any host can already self-identify via the
AI_AGENT_NAME / AI_AGENT_VERSION override.

Tests: 10/10 plugin telemetry hooks + runner. Shared suite unchanged.

---------

Co-authored-by: Amit Joshi <amitjoshi@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…168)

Adds telemetry for skills invoked via slash command (e.g.
/power-pages:add-seo) at the user-prompt boundary, which the
existing PreToolUse:Skill hook does not cover (that fires only for
programmatic Skill tool calls).

- hooks/run-user-prompt-telemetry.js: UserPromptSubmit hook that
  matches /<plugin>:<skill> via shared/telemetry/lib/prompt-detector.js
  and calls emitSkillStartedFromPrompt() with PAC auth + agent info
  enrichment. Fire-and-forget; never blocks prompt submission.
- hooks/hooks.json: registers the new UserPromptSubmit hook alongside
  the existing PreToolUse / PostToolUse:Skill hooks.
- scripts/tests/run-user-prompt-telemetry.test.js: hermetic coverage
  for the hook (tracked-skill match, no-match no-op, malformed input,
  env opt-out).
- docs/superpowers/specs/2026-04-23-slash-command-telemetry-design.md:
  design doc covering why this hook exists and how it composes with
  the Skill-tool path.

skill_completed remains the responsibility of the PostToolUse:Skill
hook for skills that flow through the Skill tool. For slash-only
skills, completion is tracked indirectly via script-level
runInstrumented() spans.

Co-authored-by: Amit Joshi <amitjoshi@microsoft.com>
The 1DS telemetry foundation has landed and is in active use. The design
specs (slash-command-telemetry-design, telemetry-rebuild-design, plugin
adoption guide) and the PowerPagesPluginEvent annotation XML were
implementation-time artifacts — the live integration is now documented
in shared/telemetry/README.md, which covers what is sent, the privacy
posture, and how to adopt the library in a new plugin.

Updates plugins/power-pages/AGENTS.md to point readers at the README
instead of the deleted spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + script paths

The pretool and posttool hooks already short-circuit on cfg.disabled
before invoking pac auth who (~3s) and pac --version (~2s). The two
remaining entry points — emit-from-prompt (used by the UserPromptSubmit
hook) and telemetry-runner (used by scripts wrapped in runInstrumented)
— were calling PAC and spawning the detached dispatcher regardless;
the dispatcher caught disabled later but the user had already paid the
shellout cost.

Add three fast-path gates at both entry points, in this order:
- POWER_PLATFORM_SKILLS_TELEMETRY=0 (env opt-out)
- cfg.disabled === true (repo-side kill switch)
- empty instrumentationKey (unprovisioned)

Any of these returns immediately and runs the user's original async fn
(telemetry-runner) or a no-op result (emit-from-prompt). Zero PAC, zero
agent-info, zero spawn.

Also fix a pre-existing destructuring bug in
emit-from-prompt.test.js's mkTelemetryDir helper: callers passed
'instrumentationKey' but the helper destructured 'ikey', so every test
ran with an empty iKey field by accident. Fix the helper and add
test coverage for all three new fast paths in both modules.

Synced shared/telemetry/lib/emit-from-prompt.js into the power-pages
plugin copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reorganize around the questions a plugin author actually asks:
- What does the library do? (data flow diagram)
- What goes on the wire? (fields + privacy posture)
- Where do the modules live? (lib/ layout)
- How do I adopt this in my plugin? (5 numbered steps)
- How do I update the shared lib without clobbering my ikey.json?
- What test seams exist?

Drop the dangling reference to the deleted design spec. The new
"Adopting in a new plugin" section walks through sync, ikey.json
config, hook registration, optional script wrapping, and local
verification — covering everything a new plugin needs without
requiring the implementation history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two real bugs causing every emitted event to silently drop orgId,
tenantId, and pacCliVersion:

1) pac-auth.js TIMEOUT_MS was 3000, but `pac auth who` cold-start on
   Windows consistently takes 3.5-4s (.NET runtime + cached-token
   validation). Every call timed out, the catch handler swallowed it,
   readPacAuth returned null, and the hooks emitted events without
   the PAC identity fields. Bumped to 8000 — still well under the
   30s hook budget.

2) agent-info.js readPacCliVersion ran `pac --version`, but PAC 2.x
   treats `--version` as an unknown command: it prints the version
   banner ("Version: 2.7.4+g06bb2eb ...") to stdout but exits with
   status 1. execFileSync throws on non-zero exit, the old catch
   discarded everything, and the version was lost. Now we read
   err.stdout from the thrown error and parse the banner. Tightened
   the regex from a bare semver match to one anchored on the
   "Version:" prefix so we don't accidentally pick up the .NET
   Framework version from the same line. Bumped its timeout to 8s
   too.

Verified on this machine: cold-start total of both reads is ~5.7s,
returning real orgId/tenantId/cloud and pacCliVersion "2.7.4".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…de Code session

session.getSessionId() cached a random UUID in module-level state, but
each hook spawns as a fresh Node process — the cache reset every
invocation, so every event in a Claude Code session carried a different
sessionId. That broke session-scoped joins in Kusto.

Claude Code sends session_id in the hook stdin payload. Wire it
through:

- session.js: getSessionId(override?) — if a truthy string is passed,
  prime the cache with it. Subsequent calls (with or without override)
  return the primed value. _resetCache() exported for tests.
- pretool / posttool / user-prompt hooks: extract parsed.session_id
  from the hook payload and pass it to getSessionId / through
  emitSkillStartedFromPrompt opts.
- emit-from-prompt.js: accept opts.sessionId and forward to
  getSessionId.

Synced into the plugin copy. New session tests cover override winning
over a previously-cached UUID, multi-process stability when callers
pass the same primed value, and falsy-override safety.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion cleanup

Records the audit findings and approved design for collapsing the
telemetry event vocabulary to skill_started + skill_completed only.
Removes the script-wrapping path (with-telemetry, telemetry-runner,
runInstrumented) and adds a TTL sweep to correlation.js to bound
leaks when PostToolUse never fires. Ratifies the just-shipped
sessionId fix that sources Claude Code's session_id from hook stdin.

Finding E (missing skill_completed in local trace) deferred to a
separate investigation after this cleanup lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… Copilot CLI)

Per review feedback: sessionId should come from whichever host agent
the user is running under. Add extractSessionIdFromPayload helper that
checks both session_id (Claude Code convention) and sessionId
(camelCase fallback for hosts that use that). aiAgentName already
detects both hosts via agent-info.js; this brings sessionId into the
same multi-host pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…opilot CLI

Copilot CLI exposes its session ID via the env var COPILOT_SESSION_ID,
not through the hook stdin payload. Rename the helper to
resolveHostSessionId and have it consult, in precedence order:
payload.session_id, payload.sessionId, env.COPILOT_SESSION_ID, empty.
Each hook stays a single line; new hosts can be added by extending
the resolver without touching hook code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…load

Per user feedback: GitHub Copilot CLI uses the same hook stdin payload
mechanism as Claude Code to surface its session id. Drop the
COPILOT_SESSION_ID env var fallback; resolveHostSessionId now only
inspects payload.session_id and payload.sessionId. Simpler resolver,
one fewer test, no env coupling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…correlation cleanup

Bite-sized TDD plan covering: resolveHostSessionId helper, correlation
TTL sweep, removal of buildScript builders, deletion of with-telemetry
+ telemetry-runner, unwrap runInstrumented from 10 validators + 4
standalone scripts, switch 3 hooks to resolveHostSessionId, sync,
README update, manual verification, push. 12 tasks total. Spec
reference: docs/superpowers/specs/2026-05-28-session-correlation-cleanup-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The const input was scoped to the first try block while the telemetry
block (second try) also references it for resolveHostSessionId. The
ReferenceError was silently swallowed by the outer telemetry catch,
preventing all skill_completed events from emitting. This is the root
cause of Finding E (9 skill_started, 0 skill_completed). Hoist to outer
async scope so both blocks see the parsed payload.
…AGENT_NAME is set

If the host sets AI_AGENT_NAME (e.g., via .claude/settings.local.json)
but omits AI_AGENT_VERSION, the previous code returned aiAgentVersion=""
and the hook silently dropped the field. Now: when AI_AGENT_NAME is
explicit but the version is empty, fall through to the same built-in
detection used when AI_AGENT_NAME isn't set at all — read the version
from CLAUDE_CODE_EXECPATH/../package.json for Claude Code, or
COPILOT_CLI_BINARY_VERSION for Copilot CLI.

Explicit AI_AGENT_VERSION still wins over the backfill when present.
Extracted readClaudeCodeVersion(env) helper to share the package.json
read path between the explicit-name and built-in detection branches.

Synced into the plugin copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflect actual agent-info logic: Claude Code version is read from the
installed package.json via CLAUDE_CODE_EXECPATH, Copilot CLI version from
COPILOT_CLI_BINARY_VERSION, and an explicit AI_AGENT_NAME backfills its
version from the matching built-in detector when AI_AGENT_VERSION is empty.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the telemetry feature branch up to date with main (canvas-apps,
model-apps, Power Pages ALM, security skills, multi-provider setup-auth).

Resolved conflict in setup-auth/scripts/validate-auth.js: kept the
branch's async main() wrapper + module.exports/require.main guard and
adopted main's new validation logic (marker gate, AUTH_PROVIDERS check,
refined authz-utils check) with `return approve()`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The session/correlation cleanup work has shipped; drop the superpowers
design spec and implementation plan that tracked it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These 10 validators and 4 standalone scripts diverged from main only by
the leftover runInstrumented-unwrap shape (async main() wrapper +
require.main guard + module.exports). That structure is a vestige of an
earlier per-script telemetry approach that was dropped in favor of
hook-based skill telemetry, so it's no longer needed. Restore main's
versions to keep the telemetry PR scoped to telemetry-only changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 29, 2026 12:03
Copilot AI review requested due to automatic review settings June 8, 2026 10:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 60 out of 60 changed files in this pull request and generated 2 comments.

Comment thread plugins/power-pages/scripts/lib/powerpages-hook-utils.js
Comment thread plugins/power-pages/scripts/tests/telemetry-hook-posttool.test.js
Copilot AI review requested due to automatic review settings June 10, 2026 11:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 58 out of 58 changed files in this pull request and generated 2 comments.

Comment thread plugins/power-pages/scripts/tests/telemetry-hook-posttool.test.js
Comment thread shared/telemetry/tests/emit-spawn.test.js
Switch the telemetry skill from a repo-root-relative workflow reference
(${CLAUDE_PLUGIN_ROOT}/../../shared/...) to the bundled-path pattern used
by report-issue: SKILL.md points at ${CLAUDE_PLUGIN_ROOT}/skills/telemetry/
telemetry-workflow.md, backed by a per-plugin symlink to the shared source.

Marketplace installs copy only the plugin directory, so the old ../../shared
path did not resolve at user time. The symlink keeps a single source of truth
in-repo while letting installers dereference it into the plugin cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@amitjoshi438
amitjoshi438 force-pushed the users/amitjosh/1ds-feature branch from ae1c39f to dc61be8 Compare June 10, 2026 11:56
Amit Joshi and others added 2 commits June 10, 2026 17:37
…try-symlink

Brings the 18 commits of telemetry work (per-plugin opt-out CLI + config,
removal of the POWER_PLATFORM_SKILLS_TELEMETRY env var, skill wrapper, README
docs) plus the model-apps eval suite, report-issue bundling, and main merges
into the symlink-experiment branch.

Conflict resolution (architecture decision: keep the symlink experiment):

- plugins/power-pages/scripts/lib/telemetry/lib: kept as a symlink to
  ../../../../../shared/telemetry/lib (ours). Discarded the real synced copies
  1ds-feature reintroduced; the new shared modules (telemetry-config.js,
  user-config.js) are now exposed through the link.

- shared/telemetry/lib/emit-spawn.js: dropped the POWER_PLATFORM_SKILLS_TELEMETRY
  forwarding (1ds-feature removed that env var and its reader entirely), kept the
  opts.ikeyJsonPath plumbing from b2b791b, which is load-bearing for the symlink
  architecture: it lets the dispatcher read the plugin's real ikey.json instead
  of shared/'s placeholder when lib/ is reached via the symlink.

Verified: shared telemetry suite 151/151 pass; power-pages telemetry hook suite
23/23 pass (incl. the ikey.json override-seam tests that drive the plumbing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…update docs)

Make the symlink the single source of truth for the shared telemetry lib,
replacing the synced-copy mechanism:

- remove shared/telemetry/sync-to-plugin.js and its test (superseded by the
  per-plugin lib symlink; the script would have copied lib/ onto itself)
- rewrite the telemetry docs to the symlink model across root AGENTS.md,
  plugins/power-pages/AGENTS.md, and shared/telemetry/README.md, including the
  symlink adoption recipe and the ikey.json posture rule (committed stays
  disabled:true; working-tree disabled:false is a local experiment only)
- gitignore .omc/ scratch directories

Verified: shared telemetry suite 147/147, power-pages telemetry hooks 23/23.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 10, 2026 13:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 46 out of 47 changed files in this pull request and generated 5 comments.

Comment thread shared/telemetry/lib/user-config.js
Comment thread shared/telemetry/lib/telemetry-config.js
Comment thread plugins/power-pages/scripts/lib/powerpages-hook-utils.js
Comment thread plugins/power-pages/scripts/tests/telemetry-hook-posttool.test.js
Comment thread shared/telemetry/tests/events.test.js
…est comment)

- user-config.readConfig: ignore an array config.json. typeof [] === "object"
  passed the old guard, so setTelemetryChoice set .telemetry on the array and
  JSON.stringify silently dropped it — returning true while persisting nothing.
  Now guarded with !Array.isArray + regression test.
- powerpages-hook-utils: build TRACKED_SKILLS with Object.create(null). Membership
  is tested via bracket access, so a plain {} let inherited keys (toString,
  constructor, __proto__) test truthy and emit bogus skill names. Added a test.
- events.test.js: fix the misleading "drop guarded by clamp" comment — boolean
  true is kept and coerced to 1 (Number(true)), not dropped.

Addresses copilot review comments on PR #176. Shared telemetry 148/148,
power-pages telemetry hooks 24/24.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@amitjoshi438
amitjoshi438 merged commit 98430e9 into main Jun 11, 2026
7 checks passed
@amitjoshi438
amitjoshi438 deleted the users/amitjosh/1ds-feature branch June 11, 2026 08:11
amitjoshi438 pushed a commit that referenced this pull request Jun 15, 2026
Reconcile the two telemetry designs that diverged after PR #176:
- Keep region routing (region-resolver/region-cache/artemis-service): the
  dispatcher resolves the iKey/collector per org geo + cloud stamp.
- Adopt main's per-plugin opt-out (config.json via user-config/telemetry-config
  + the `telemetry` skill); remove the POWER_PLATFORM_SKILLS_TELEMETRY env
  kill switch everywhere. Dispatcher now writes the local mirror BEFORE the
  opt-out gate and fails closed on a missing/unreadable ikey.json.
- Adopt main's symlink for the shared lib: plugins/power-pages/scripts/lib/
  telemetry/lib -> shared/telemetry/lib (mode 120000); delete sync-to-plugin.js.
  The pretool hook + emit-spawn now pass the plugin's ikey.json via
  ikeyJsonPath so the symlinked dispatcher reads the plugin config, not
  shared/'s placeholder.
- Plugin ikey.json + shared ikey.json keep the region-routing structure.
- Take main's powerpages-hook-utils test (all skill folders are tracked).
- Docs (root + plugin AGENTS.md, READMEs) describe symlink + config.json
  opt-out + region routing; drop the duplicated/stale sync sections.

All shared/telemetry (190) and power-pages plugin (1105) tests pass;
alm-lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
amitjoshi438 pushed a commit that referenced this pull request Jun 15, 2026
…y the merge

A bulk `--ours` resolution during the earlier main→branch merge kept the branch's
older (pre-#176-squash) versions of two NON-region files, reverting main's work:

- prompt-detector.js: restore bare-form slash-command detection (`/add-seo` as
  well as `/power-pages:add-seo`). The branch's namespaced-only regex would have
  reverted main's enhancement and silently dropped telemetry for bare commands.
- agent-info.js: restore the full AGENT_DETECTORS framework (Codex/OpenCode/
  Hermes/OpenClaw in addition to Claude Code + Copilot). main is a strict
  superset (it already includes the branch's AI_AGENT-version + backfill fixes);
  keeping the branch's hand-rolled subset left the code inconsistent with the
  README that names those agents.

Both files + their tests restored from origin/main. Also re-add the dropped
emit-spawn `opts.ikeyJsonPath` (no-env-override) regression test — the
production-faithful variant guarding the symlinked-dispatcher config path.

shared 162/0, plugin 1153/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
amitjoshi438 added a commit that referenced this pull request Jun 17, 2026
#190)

* feat(telemetry) 1/3: shared library — dispatcher, consent, sync, builders      (#142)

* docs: add design spec for 1DS telemetry infra

Design for a shared telemetry library at shared/telemetry/ consumed
by the power-pages plugin first, with interactive first-run consent,
strict-allowlist payloads, and fail-closed emission via the existing
PreToolUse/PostToolUse:Skill hook surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(telemetry): add shared 1DS telemetry library

Introduces the shared 1DS telemetry infrastructure used by all
adopting plugins. Anonymous, default-on, env-var opt-out only
(POWER_PLATFORM_SKILLS_TELEMETRY=0). No persistent consent file,
no first-run prompt.

Library (shared/telemetry/lib/):
- events.js — type-aware allowlist builders (PowerPagesPluginEvent
  envelope, top-level columns, per-event field set, durationMs as int)
- emit-dispatcher.js — detached child that POSTs to OneCollector
  with the Common Schema 4.0 envelope, exits silently on any failure
- emit-spawn.js — fire-and-forget spawn helper with restricted child env
- emit-from-prompt.js — orchestrator for slash-command skill_started
  events (reads ikey.json, calls pac auth + agent info, builds event)
- pac-auth.js — shells out to `pac auth who` for orgId/tenantId
- agent-info.js — detects host AI agent (Claude Code, Copilot CLI)
  + pac CLI version via env override → auto-detect → empty
- prompt-detector.js — matches /plugin:skill from user prompt text
- local-log.js — placeholder-iKey JSONL fallback at
  ~/.power-platform-skills/events.jsonl
- with-telemetry.js — runInstrumented() wrapper for script-level signals
- correlation.js, session.js, scrubber.js — utilities

Config: shared/telemetry/ikey.json carries the iKey, collector URL,
per-plugin event_stream_name, and a 'disabled' kill switch. The flag
ships true so infrastructure can land before tenant-side annotation
+ Kusto provisioning is complete; a single PR flips it false later.

Sync: shared/telemetry/sync-to-plugin.js copies lib/ + ikey.json
into <plugin>/scripts/lib/telemetry/. Adopting plugins never
hand-edit the synced copy.

Tests: full node:test coverage for every module plus the dispatcher
and spawn integration (109 tests, all hermetic; envelope-shape probe
via POWER_PLATFORM_SKILLS_FAKE_HTTPS).

Docs: rebuild design spec (2026-05-04) + plugin adoption guide
(2026-04-29) under docs/superpowers/specs/. Older draft spec
(2026-04-20) removed.

* docs(telemetry): align README with code — camelCase fields, correct PAC posture

shared/telemetry/README.md drifted from the rebuild work:

- Field names listed in snake_case (plugin_name, session_id, ...) but
  the code uses camelCase (pluginName, sessionId, ...). Names match
  Kusto column names directly.
- "What is NEVER sent" claimed tenant IDs are not sent — but
  pac-auth.js explicitly emits orgId and tenantId GUIDs from the
  active PAC profile. Moved them to "What is sent" with the rest of
  the PAC + agent fields.
- Missing from "What is sent": pluginVersion, osName/osVersion (was
  os_family), pacCliVersion, aiAgentName, aiAgentVersion, eventInfo,
  errorDescription.
- Added a note about errorDescription being a 500-char-truncated
  Error.message — callers must keep PII out of exception messages.
- Documented the ikey.json "disabled" repo-side kill switch under
  Privacy posture.

* refactor(telemetry): address review feedback (PR #142)

Four fixes from PR review:

1. **iKey full form in envelope** — emit-dispatcher.js now sends the
   complete iKey value in the Common Schema `iKey` field
   (`"o:" + IKEY`) instead of truncating to just the first
   hyphen-separated segment. Aligns with the powerplatform-vscode
   pattern. Multi-cloud key restructuring deferred to a follow-up PR.

2. **Drop POWER_PLATFORM_SKILLS_BYPASS_KILL_SWITCH** — production code
   no longer carries a test-only escape hatch. Tests now override the
   ikey.json path via POWER_PLATFORM_SKILLS_IKEY_JSON (parallels the
   existing IKEY/COLLECTOR/CONFIG_DIR/FAKE_HTTPS env-var pattern) and
   supply their own `disabled: false` config when they need to
   exercise emission paths. emit-spawn.js forwards the new var to the
   detached child.

3. **Defense-in-depth FIELD_TYPES enforcement in dispatcher** — events.js
   now exports `pick`. The dispatcher imports both `FIELD_TYPES` and
   `pick`, then re-runs the allowlist filter on `event.data` before
   building the envelope. Any field that bypasses the builders is
   dropped before it reaches the wire. The three reserved meta fields
   (eventName, eventType, severity) survive via an explicit allow list.

4. **errorDescription emits err.code only, never err.message** —
   with-telemetry.js no longer reads `err.message` (which can contain
   file paths, GUIDs, tokens). The field now captures `err.code` —
   short non-PII metadata like `ENOENT` or `ERR_INVALID_ARG_TYPE`.
   Test "errorDescription truncated to 500 chars" replaced with one
   that asserts a PII-bearing message is NOT emitted anywhere when
   `err.code` is set. README updated accordingly.

New dispatcher test: `dispatcher strips unknown fields from event.data`
hand-crafts an event with filePath/stackTrace/rawPrompt/tokenValue
and asserts none survive the FIELD_TYPES filter.

Tests: 110/110.

* refactor(telemetry): rename ikey.json key — ikey → instrumentationKey

Per PR #142 comment 1: the JSON property is now spelled in its full
form (`instrumentationKey`) to match the powerplatform-vscode
constants pattern instead of the abbreviated `ikey`. The envelope
`iKey` Common Schema field stays at the standard tenant-routing
short form (`o:<32-char-tenant>`); the previous attempt to put the
full hyphenated key in the envelope is reverted because the full
form already lives in the `x-apikey` header.

- shared/telemetry/ikey.json: `"ikey"` → `"instrumentationKey"`.
- shared/telemetry/lib/emit-from-prompt.js: read
  `cfg.instrumentationKey` (other JSON keys —
  `collector_url`, `event_stream_name`, `disabled` — unchanged).
- shared/telemetry/lib/emit-dispatcher.js: envelope reverted to
  `iKey: "o:" + IKEY.split("-")[0]`.
- Test fixtures updated across emit-dispatcher / emit-spawn /
  emit-from-prompt tests.

Multi-cloud iKey restructuring (the second half of PR #142
comment 1) remains deferred to a follow-up PR.

Tests: 110/110.

---------

Co-authored-by: Amit Joshi <amitjoshi@microsoft.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(power-pages): adopt 1DS telemetry — synced library, hooks, valid… (#153)

* feat(power-pages): adopt 1DS telemetry — synced library, hooks, validators

Wires the shared 1DS telemetry library into the power-pages plugin.
Synced copy of the library lives under scripts/lib/telemetry/ and runs
at user time. PreToolUse:Skill emits skill_started; PostToolUse:Skill
emits skill_completed wrapped around the existing validator chain.

Plugin-side additions:
- scripts/lib/telemetry/ — synced from shared/telemetry/ via
  sync-to-plugin.js (12 lib modules + ikey.json carrying the
  PowerPagesPluginEvent stream name + disabled:true kill switch).
- scripts/lib/telemetry-runner.js — runInstrumented() shim plus the
  PowerPagesPluginEvent envelope name and tracked-script registry.
- hooks/run-skill-pretool-telemetry.js, hooks/run-skill-posttool-validation.js
  — fire-and-forget telemetry around the Skill tool, preserving the
  existing validator's behavior and exit code.
- hooks/hooks.json — registers the two Skill hooks. (UserPromptSubmit
  is registered separately by the slash-command branch.)
- Validators (validate-activation/seo/site/webroles/audit/auth/datamodel/
  cloudflow/serverlogic/webapi-integration) and four other plugin
  scripts (check-activation-status, clear-site-cache, render-audit-report,
  verify-dataverse-access) wrapped with runInstrumented() so each
  script run produces script_started/script_completed signals.
- scripts/tests/{telemetry-hook-pretool,telemetry-hook-posttool,telemetry-runner}.test.js
  — hermetic node:test coverage for the new wiring.

Repo updates:
- AGENTS.md (plugin) gains the Telemetry section pointing at
  shared/telemetry/ as the canonical source of truth.
- README.md (root) mentions anonymous telemetry + env opt-out.
- docs/superpowers/handoff/PowerPagesPluginEvent-annotation.xml
  is the tenant-team handoff describing the EventStreamingAnnotation
  for the per-plugin Kusto stream.

Privacy posture: default-on, env-var opt-out only
(POWER_PLATFORM_SKILLS_TELEMETRY=0). No first-run prompt, no
consent file. Telemetry stays gated by ikey.json's disabled:true
flag until the tenant-side annotation + Kusto table are provisioned.

* fix(power-pages): telemetry-runner — pass envelopeName, read instrumentationKey

Two regressions caught during E2E verification of the local-log path:

1. **envelopeName was never forwarded** — `runInstrumented()` called
   `withTelemetry()` without an `envelopeName`, so events shipped with
   `name: ""`. In production this means the tenant's
   `EventStreamingAnnotation name="^PowerPagesPluginEvent$"` matcher
   silently drops every event (`acc:1` from the collector but zero rows
   in Kusto). Now reads `event_stream_name` from `ikey.json` and passes
   it through.

2. **iKey read the old JSON property** — `deps.ikeyCfg.ikey` was
   undefined after the `ikey` → `instrumentationKey` rename in the
   foundation library (PR #142). The dispatcher saw an empty
   `POWER_PLATFORM_SKILLS_IKEY` and always took the placeholder /
   local-log path, even with a real iKey provisioned. Now reads
   `deps.ikeyCfg.instrumentationKey`.

Adds an opt-in `_overrides` test seam on `runInstrumented()` so unit
tests can inject a fake `withTelemetry` and assert opts are propagated
correctly. New tests:
- `runInstrumented forwards envelopeName from ikey.json event_stream_name`
- `runInstrumented reads iKey from instrumentationKey property (not legacy 'ikey')`

Verified end-to-end by piping a synthetic event directly to the
dispatcher: envelope `name` is now `"PowerPagesPluginEvent"`,
`scriptName` is correctly set, event lands in the local JSONL.

Tests: 4/4 telemetry-runner, full plugin suite still green.

* telemetry: detect GitHub Copilot CLI as ai agent host

readAiAgent now recognizes COPILOT_CLI=1 and reads COPILOT_CLI_BINARY_VERSION, in addition to Claude Code. Priority remains: explicit AI_AGENT_NAME env > Claude Code > Copilot CLI. Adds test coverage and re-syncs into plugins/power-pages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(telemetry): drop power-pages testing iKey from shared/telemetry/ikey.json

`shared/telemetry/ikey.json` is the dev-time placeholder copied by
`sync-to-plugin.js`. It was committed with power-pages's real testing
iKey and INT collector URL, which meant any future adopter
(canvas-apps, code-apps, model-apps) running the sync would inherit
those values and — if they ever flipped `disabled: false` without
replacing the iKey — silently route their telemetry to power-pages's
Kusto stream.

This commit replaces both with explicit placeholders so every plugin
is forced to provision its own cluster config before going live. The
plugin's own synced copy at `plugins/power-pages/scripts/lib/telemetry/ikey.json`
keeps its real plugin-specific values; only the shared template is sanitized.

- instrumentationKey: <pp's value> → "PLACEHOLDER_REPLACE_BEFORE_SHIPPING"
- collector_url:      <INT URL>    → ""

`event_stream_name` ("PluginEventStreamPlaceholder") and `disabled: true`
were already plugin-agnostic and stay unchanged.

Tests: 110/110 shared (no behavior change — the placeholder iKey
constant matches the dispatcher's existing keyMissing check).

* fix(power-pages): address PR #153 review — await, dedup, hook fast-path

Three review fixes from priyanshu92 on PR #153:

1. **telemetry-runner.js: `return await asyncFn()`** — added the `await`
   on the no-telemetry-deps fallback path so stack traces include the
   `runInstrumented` frame. Behavioural no-op; debug-friendliness only.

2. **validate-auth.js: drop duplicate `runInstrumented` call** — the
   file invoked `runInstrumented('validate-setup-auth', main)` twice:
   once unconditionally at module top-level (line 69-72), and again
   inside the standard `if (require.main === module)` guard (line
   137-142). When executed as a script (the actual PostToolUse-hook
   use case), `main()` ran twice — duplicating validator I/O and
   emitting two `script_started`/`script_completed` pairs per run.
   When required as a module (tests), the top-level call still fired,
   breaking import semantics. Deleted the top-level invocation, kept
   the require.main guard.

3. **Skill hooks: fast-path opt-out / kill switch BEFORE pac shell-outs**
   — `run-skill-pretool-telemetry.js` and `run-skill-posttool-validation.js`
   ran `pac auth who` (3s timeout) and `pac --version` (2s timeout)
   unconditionally on every tracked Skill invocation, even when the
   user had opted out (`POWER_PLATFORM_SKILLS_TELEMETRY=0`) or the
   shipped kill switch was on (`ikey.json` `disabled: true`). Worst
   case ~10s added latency per Skill tool use, currently hit by every
   user since `disabled: true` is the shipped default.

   Both hooks now check, in this order, BEFORE any pac shell-out:
     a) env opt-out (`POWER_PLATFORM_SKILLS_TELEMETRY === "0"`)
     b) repo-side kill switch (`ikey.json` `disabled === true`)
     c) unconfigured (`!ikey.instrumentationKey`)

   Any of those → exit early. The posttool hook still preserves the
   validator's exit code regardless of telemetry state.

   `readIkey()` in the pretool hook now also exposes the `disabled`
   flag so the gate can read it without re-parsing.

Comment #3 (extend agent-info detection to Codex / OpenCode) is
pending reviewer confirmation of those hosts' env-var conventions —
not implemented here. Any host can already self-identify via the
AI_AGENT_NAME / AI_AGENT_VERSION override.

Tests: 10/10 plugin telemetry hooks + runner. Shared suite unchanged.

---------

Co-authored-by: Amit Joshi <amitjoshi@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(power-pages): emit skill_started for slash-command invocations (#168)

Adds telemetry for skills invoked via slash command (e.g.
/power-pages:add-seo) at the user-prompt boundary, which the
existing PreToolUse:Skill hook does not cover (that fires only for
programmatic Skill tool calls).

- hooks/run-user-prompt-telemetry.js: UserPromptSubmit hook that
  matches /<plugin>:<skill> via shared/telemetry/lib/prompt-detector.js
  and calls emitSkillStartedFromPrompt() with PAC auth + agent info
  enrichment. Fire-and-forget; never blocks prompt submission.
- hooks/hooks.json: registers the new UserPromptSubmit hook alongside
  the existing PreToolUse / PostToolUse:Skill hooks.
- scripts/tests/run-user-prompt-telemetry.test.js: hermetic coverage
  for the hook (tracked-skill match, no-match no-op, malformed input,
  env opt-out).
- docs/superpowers/specs/2026-04-23-slash-command-telemetry-design.md:
  design doc covering why this hook exists and how it composes with
  the Skill-tool path.

skill_completed remains the responsibility of the PostToolUse:Skill
hook for skills that flow through the Skill tool. For slash-only
skills, completion is tracked indirectly via script-level
runInstrumented() spans.

Co-authored-by: Amit Joshi <amitjoshi@microsoft.com>

* docs(telemetry): design spec for region-aware telemetry routing

Spec for resolving (iKey, collector URL) per user's tenant geography
in the Power Pages plugin. Mirrors VSCode's pattern (Artemis service
for geo discovery, static geo→region map) without pulling in the 1DS
SDK.

Key design choices:
- One Artemis HTTPS GET (not 7-way fan-out) — we already know the
  sovereign cloud from `pac auth who`'s Cloud: field.
- Disk cache keyed by orgId, 24h TTL — org-switching is naturally
  handled (different key), only true geo-migration causes a one-day
  staleness window.
- Region resolution happens in the detached dispatcher child, NOT in
  the hook. User's prompt latency is unchanged.
- PAC shellouts stay in the hook for now (deferred to future work).
- Every failure path falls back to default_region; nothing throws.

3 new shared modules: artemis-service, region-resolver, region-cache.
ikey.json restructures from flat (iKey, url) into regions map + default.

Saved to docs/superpowers/specs/2026-05-27-region-routing-design.md.

* docs(telemetry): implementation plan for region-aware telemetry routing

14 bite-sized tasks (TDD throughout). Order:
1. Extend pac-auth.js to parse Cloud line
2-4. New shared modules: region-cache, artemis-service, region-resolver
5-7. Extend emit-spawn / emit-dispatcher / with-telemetry to forward cloud
8. Restructure plugin's ikey.json into regions map
9-10. Update hooks + telemetry-runner to read cloud and pass through
11. Sync shared → plugin and run full suites
12. Update shared template ikey.json
13. README refresh
14. Push + manual verification + open PR

Spec: docs/superpowers/specs/2026-05-27-region-routing-design.md

* feat(telemetry): parse Cloud field from pac auth who

* feat(telemetry): add region-cache disk cache (24h TTL, keyed by orgId)

* test(telemetry): add guard-clause coverage for region-cache read/write

* feat(telemetry): add artemis-service for one-call geo discovery

* test(telemetry): add falsy-orgId guard coverage for artemis fetchGeo

* feat(telemetry): add region-resolver (cache → Artemis → regions map)

* feat(telemetry): forward POWER_PLATFORM_SKILLS_CLOUD to dispatcher

* feat(telemetry): dispatcher resolves region from ikey.json + Artemis

* docs(telemetry): restore intent comments in dispatcher

* feat(telemetry): with-telemetry threads cloud through to spawn opts

* feat(power-pages): restructure ikey.json into per-region map (kill switch on)

* feat(power-pages): hooks read regions structure and pass cloud to dispatcher

* feat(power-pages): telemetry-runner passes cloud through withTelemetry

* chore(power-pages): sync region-routing changes into plugin copy

* feat(telemetry): shared ikey.json template uses regions structure

* docs(telemetry): README — describe regions map and Artemis-based resolution

* docs(telemetry): remove implementation specs + handoff XML

The 1DS telemetry foundation has landed and is in active use. The design
specs (slash-command-telemetry-design, telemetry-rebuild-design, plugin
adoption guide) and the PowerPagesPluginEvent annotation XML were
implementation-time artifacts — the live integration is now documented
in shared/telemetry/README.md, which covers what is sent, the privacy
posture, and how to adopt the library in a new plugin.

Updates plugins/power-pages/AGENTS.md to point readers at the README
instead of the deleted spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(telemetry): honor disabled flag before PAC + spawn in user-prompt + script paths

The pretool and posttool hooks already short-circuit on cfg.disabled
before invoking pac auth who (~3s) and pac --version (~2s). The two
remaining entry points — emit-from-prompt (used by the UserPromptSubmit
hook) and telemetry-runner (used by scripts wrapped in runInstrumented)
— were calling PAC and spawning the detached dispatcher regardless;
the dispatcher caught disabled later but the user had already paid the
shellout cost.

Add three fast-path gates at both entry points, in this order:
- POWER_PLATFORM_SKILLS_TELEMETRY=0 (env opt-out)
- cfg.disabled === true (repo-side kill switch)
- empty instrumentationKey (unprovisioned)

Any of these returns immediately and runs the user's original async fn
(telemetry-runner) or a no-op result (emit-from-prompt). Zero PAC, zero
agent-info, zero spawn.

Also fix a pre-existing destructuring bug in
emit-from-prompt.test.js's mkTelemetryDir helper: callers passed
'instrumentationKey' but the helper destructured 'ikey', so every test
ran with an empty iKey field by accident. Fix the helper and add
test coverage for all three new fast paths in both modules.

Synced shared/telemetry/lib/emit-from-prompt.js into the power-pages
plugin copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(telemetry): rewrite README as integration guide for new adopters

Reorganize around the questions a plugin author actually asks:
- What does the library do? (data flow diagram)
- What goes on the wire? (fields + privacy posture)
- Where do the modules live? (lib/ layout)
- How do I adopt this in my plugin? (5 numbered steps)
- How do I update the shared lib without clobbering my ikey.json?
- What test seams exist?

Drop the dangling reference to the deleted design spec. The new
"Adopting in a new plugin" section walks through sync, ikey.json
config, hook registration, optional script wrapping, and local
verification — covering everything a new plugin needs without
requiring the implementation history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(telemetry): make PAC shellouts actually capture data on Windows

Two real bugs causing every emitted event to silently drop orgId,
tenantId, and pacCliVersion:

1) pac-auth.js TIMEOUT_MS was 3000, but `pac auth who` cold-start on
   Windows consistently takes 3.5-4s (.NET runtime + cached-token
   validation). Every call timed out, the catch handler swallowed it,
   readPacAuth returned null, and the hooks emitted events without
   the PAC identity fields. Bumped to 8000 — still well under the
   30s hook budget.

2) agent-info.js readPacCliVersion ran `pac --version`, but PAC 2.x
   treats `--version` as an unknown command: it prints the version
   banner ("Version: 2.7.4+g06bb2eb ...") to stdout but exits with
   status 1. execFileSync throws on non-zero exit, the old catch
   discarded everything, and the version was lost. Now we read
   err.stdout from the thrown error and parse the banner. Tightened
   the regex from a bare semver match to one anchored on the
   "Version:" prefix so we don't accidentally pick up the .NET
   Framework version from the same line. Bumped its timeout to 8s
   too.

Verified on this machine: cold-start total of both reads is ~5.7s,
returning real orgId/tenantId/cloud and pacCliVersion "2.7.4".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(telemetry): stabilize sessionId across hook invocations in a Claude Code session

session.getSessionId() cached a random UUID in module-level state, but
each hook spawns as a fresh Node process — the cache reset every
invocation, so every event in a Claude Code session carried a different
sessionId. That broke session-scoped joins in Kusto.

Claude Code sends session_id in the hook stdin payload. Wire it
through:

- session.js: getSessionId(override?) — if a truthy string is passed,
  prime the cache with it. Subsequent calls (with or without override)
  return the primed value. _resetCache() exported for tests.
- pretool / posttool / user-prompt hooks: extract parsed.session_id
  from the hook payload and pass it to getSessionId / through
  emitSkillStartedFromPrompt opts.
- emit-from-prompt.js: accept opts.sessionId and forward to
  getSessionId.

Synced into the plugin copy. New session tests cover override winning
over a previously-cached UUID, multi-process stability when callers
pass the same primed value, and falsy-override safety.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(telemetry): design spec for skill-only events + session/correlation cleanup

Records the audit findings and approved design for collapsing the
telemetry event vocabulary to skill_started + skill_completed only.
Removes the script-wrapping path (with-telemetry, telemetry-runner,
runInstrumented) and adds a TTL sweep to correlation.js to bound
leaks when PostToolUse never fires. Ratifies the just-shipped
sessionId fix that sources Claude Code's session_id from hook stdin.

Finding E (missing skill_completed in local trace) deferred to a
separate investigation after this cleanup lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(telemetry): make sessionId sourcing host-agnostic (Claude Code + Copilot CLI)

Per review feedback: sessionId should come from whichever host agent
the user is running under. Add extractSessionIdFromPayload helper that
checks both session_id (Claude Code convention) and sessionId
(camelCase fallback for hosts that use that). aiAgentName already
detects both hosts via agent-info.js; this brings sessionId into the
same multi-host pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(telemetry): resolve session_id from COPILOT_SESSION_ID env for Copilot CLI

Copilot CLI exposes its session ID via the env var COPILOT_SESSION_ID,
not through the hook stdin payload. Rename the helper to
resolveHostSessionId and have it consult, in precedence order:
payload.session_id, payload.sessionId, env.COPILOT_SESSION_ID, empty.
Each hook stays a single line; new hosts can be added by extending
the resolver without touching hook code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(telemetry): drop COPILOT_SESSION_ID env path; both hosts use payload

Per user feedback: GitHub Copilot CLI uses the same hook stdin payload
mechanism as Claude Code to surface its session id. Drop the
COPILOT_SESSION_ID env var fallback; resolveHostSessionId now only
inspects payload.session_id and payload.sessionId. Simpler resolver,
one fewer test, no env coupling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(telemetry): implementation plan for skill-only events + session/correlation cleanup

Bite-sized TDD plan covering: resolveHostSessionId helper, correlation
TTL sweep, removal of buildScript builders, deletion of with-telemetry
+ telemetry-runner, unwrap runInstrumented from 10 validators + 4
standalone scripts, switch 3 hooks to resolveHostSessionId, sync,
README update, manual verification, push. 12 tasks total. Spec
reference: docs/superpowers/specs/2026-05-28-session-correlation-cleanup-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(telemetry): add resolveHostSessionId helper for multi-host session sourcing

* feat(telemetry): TTL-sweep stale correlation files in correlation.write

* feat(telemetry): drop buildScript builders + scriptName field

* feat(telemetry): delete with-telemetry script wrapper

* feat(power-pages): delete telemetry-runner plugin wrapper

* feat(power-pages): unwrap runInstrumented from validators

* feat(power-pages): unwrap runInstrumented from standalone scripts

* feat(power-pages): hooks use resolveHostSessionId for multi-host session sourcing

* chore(power-pages): sync session/correlation cleanup into plugin copy

* docs(telemetry): drop script-wrapper section from README; align with skill-only events

* fix(power-pages): hoist input parse so posttool emits skill_completed

The const input was scoped to the first try block while the telemetry
block (second try) also references it for resolveHostSessionId. The
ReferenceError was silently swallowed by the outer telemetry catch,
preventing all skill_completed events from emitting. This is the root
cause of Finding E (9 skill_started, 0 skill_completed). Hoist to outer
async scope so both blocks see the parsed payload.

* fix(telemetry): backfill aiAgentVersion from CLAUDECODE when only AI_AGENT_NAME is set

If the host sets AI_AGENT_NAME (e.g., via .claude/settings.local.json)
but omits AI_AGENT_VERSION, the previous code returned aiAgentVersion=""
and the hook silently dropped the field. Now: when AI_AGENT_NAME is
explicit but the version is empty, fall through to the same built-in
detection used when AI_AGENT_NAME isn't set at all — read the version
from CLAUDE_CODE_EXECPATH/../package.json for Claude Code, or
COPILOT_CLI_BINARY_VERSION for Copilot CLI.

Explicit AI_AGENT_VERSION still wins over the backfill when present.
Extracted readClaudeCodeVersion(env) helper to share the package.json
read path between the explicit-name and built-in detection branches.

Synced into the plugin copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(telemetry): correct aiAgent detection description in README

Reflect actual agent-info logic: Claude Code version is read from the
installed package.json via CLAUDE_CODE_EXECPATH, Copilot CLI version from
COPILOT_CLI_BINARY_VERSION, and an explicit AI_AGENT_NAME backfills its
version from the matching built-in detector when AI_AGENT_VERSION is empty.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(telemetry): remove session-correlation cleanup design + plan

The session/correlation cleanup work has shipped; drop the superpowers
design spec and implementation plan that tracked it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* revert(power-pages): restore main's validators + standalone scripts

These 10 validators and 4 standalone scripts diverged from main only by
the leftover runInstrumented-unwrap shape (async main() wrapper +
require.main guard + module.exports). That structure is a vestige of an
earlier per-script telemetry approach that was dropped in favor of
hook-based skill telemetry, so it's no longer needed. Restore main's
versions to keep the telemetry PR scoped to telemetry-only changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): derive Claude Code version from AI_AGENT on non-npm installs

aiAgentVersion was derived solely from CLAUDE_CODE_EXECPATH -> ../package.json,
which only exists in the npm-global install layout. On native-installer Claude
Code (standalone binary, now the default), that read fails and the version is
dropped while aiAgentName still logs as "Claude Code" -- name present, version
blank.

Add an install-method-independent fallback: when the package.json read yields
nothing, parse the dotted semver out of AI_AGENT (claude-code_<maj>-<min>-<patch>_agent),
which Claude Code sets regardless of install method. The npm path stays primary,
so npm installs are unaffected.

Edited shared/telemetry and re-synced to plugins/power-pages. Adds test coverage
for native-install fallback, EXECPATH-unset fallback, npm-wins precedence, and
malformed AI_AGENT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): address Copilot review on PR #176

- emit-dispatcher: pass url.port to https.request so a non-443 collector
  URL POSTs to the configured port instead of always 443
- events: tighten isPlainStructured() to only accept arrays and plain
  objects (prototype Object.prototype/null), rejecting class instances
  like Error from the dynamic eventInfo field
- emit-from-prompt + posttool hook: add POWER_PLATFORM_SKILLS_IKEY_JSON
  override seam so tests point at a temp ikey.json instead of mutating
  the checked-in config (removes a parallel-test race)
- posttool hook: skip telemetry emission when no tracked skill was
  resolved (malformed stdin / pre-detection error) so no incomplete
  skill_completed event is emitted; exit code unchanged
- run-user-prompt-telemetry test: replace busy-wait loops with a
  non-spinning Atomics.wait sleep + waitForFile helper, and use the
  ikey override instead of mutating the shipped ikey.json

Shared lib edits propagated to the power-pages synced copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): migrate emit-from-prompt to regions structure

The user-prompt skill-detection path still read the legacy flat ikey.json
shape: readIkey() returned only { eventStreamName, disabled }, so the
destructured `ikey` was always undefined and the `if (!ikey)` gate made
emitSkillStartedFromPrompt always return { emitted: false }.

Read default_region + regions[default_region].instrumentation_key instead
and gate on defaultInstrumentationKey, matching the hooks and dispatcher.
Update the test helper to the regions shape and correct one copy-paste
assertion (skillName was asserted null for a detected slash command).

Fixes 9 shared + 1 plugin test. Full suites now green: 184/184 shared,
1040/1040 plugin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(power-pages): provision real iKeys for gov/high/dod/mooncake regions

Replace the four sovereign-cloud PLACEHOLDER_REPLACE_BEFORE_SHIPPING
instrumentation keys with the tenant-team-provisioned values. internal/us/eu
keys and all collector URLs were already correct. Ships disabled:true — a
separate PR flips the kill switch once all six regions are verified end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(power-pages): fire skill_started telemetry hook in Copilot CLI

Copilot CLI's skill-invocation tool is named `skill` (lowercase) while
Claude Code's is `Skill`. Copilot applies the PreToolUse matcher as an
anchored, case-sensitive regex against tool_name, so matcher `Skill` never
matched and the pretool hook (which emits skill_started) never fired.
PostToolUse ignores the matcher, so skill_completed still worked.

Broaden both matchers to `Skill|skill` so the hooks fire on both surfaces,
and add a regression test asserting the matcher matches both tool names.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(telemetry): design for removing skill_completed logging flow

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(telemetry): stop emitting skill_completed from posttool hook

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(telemetry): inline correlationId in pretool hook, drop disk correlation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(telemetry): delete unused correlation.js and its test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(telemetry): remove orphaned synced correlation.js

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(telemetry): document removal of skill_completed flow

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(telemetry): describe only live telemetry; drop skill_completed references

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: remove brainstorm/plan docs for skill_completed removal

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): route GCC (UsGov) to the gov region

normalizeCloud did not recognize "usgov" — the token `pac auth who`
actually emits for GCC (per validation-helpers.js CLOUD_TO_API). GCC
tenants fell through to Public, so both the Artemis geo probe and the
final 1DS collector/iKey were routed to the public US endpoint — a
sovereign-cloud data-residency violation.

Add "usgov" to the Gov branch of normalizeCloud. Exact === matching
means it cannot shadow usgovhigh/usgovdod. Add tests pinning the real
PAC token on both layers (urlFor + mapToRegion); the prior tests only
exercised the synthetic "Gov" string, which is why this slipped through.
Re-synced into the power-pages plugin copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(telemetry): spec — decouple region routing from shared via pluggable resolver

Design for moving artemis/region code out of shared/telemetry into the
power-pages plugin behind a generic resolver contract (resolve/isProvisioned),
discovered by convention next to ikey.json. Shared keeps a static-key fallback;
resolution stays in the background dispatcher. Awaiting review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(telemetry): drop resolver-path env var from spec — convention-only discovery

The POWER_PLATFORM_SKILLS_RESOLVER override was a redundant test seam: tests
already create a temp ikey.json and can drop a stub resolver.js beside it, so
convention discovery covers it. One discovery mechanism, less adopter surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(telemetry): lock in option (i) fast-gate + note dedup follow-up

§6: keep the full pac-shellout fast-gate, made generic via the resolver's
isProvisioned contract (no shared file reads the region shape). Record the
decision in §13 and add an out-of-scope §14 to de-dupe the two emit entry
points in a later cleanup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(telemetry): implementation plan for resolver decoupling

Task-by-task TDD plan: resolver-loader, switchover (move region into plugin +
adapter + generic dispatcher), generic isProvisioned fast-gate, docs/verify.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(telemetry): add generic resolver-loader (convention discovery)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(telemetry): move region routing into power-pages behind resolver contract

Region trio relocates to plugins/power-pages/.../telemetry/region/; power-pages
ships resolver.js adapting it to the shared dispatcher's resolve()/isProvisioned()
contract. Shared dispatcher resolves via convention-discovered resolver.js with a
static-key fallback and no longer knows about regions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(telemetry): generic provisioning fast-gate via resolver.isProvisioned

emit-from-prompt (shared) and the pretool hook no longer read the region shape;
they call the plugin resolver's isProvisioned, defaulting to a static-key check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(telemetry): document the resolver contract + onboarding tiers

Region routing is now a power-pages-owned resolver.js; shared ships the contract
+ a static-key fallback. README, AGENTS updated; memory note refreshed separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): user-prompt path forwards ikeyJsonPath so the dispatcher reads the plugin config

emit-from-prompt now passes ikeyJsonPath into fireAndForget, mirroring the pretool
hook. Without it the detached dispatcher fell back to shared/'s placeholder ikey.json
(disabled) and skipped resolver discovery + emission for the user-prompt skill_started.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(telemetry): set power-pages event_stream_name to PagesAIPluginEvent

Single-line ikey.json change; disabled stays true (hard-off preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(telemetry): fix stale dispatcher comment (region resolution → resolver context)

Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: remove superpowers spec/plan process docs from the branch

These brainstorming/implementation artifacts are dev-time only and should not
ship to main. The product docs (README, AGENTS) are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): restore main's prompt-detector + agent-info dropped by the merge

A bulk `--ours` resolution during the earlier main→branch merge kept the branch's
older (pre-#176-squash) versions of two NON-region files, reverting main's work:

- prompt-detector.js: restore bare-form slash-command detection (`/add-seo` as
  well as `/power-pages:add-seo`). The branch's namespaced-only regex would have
  reverted main's enhancement and silently dropped telemetry for bare commands.
- agent-info.js: restore the full AGENT_DETECTORS framework (Codex/OpenCode/
  Hermes/OpenClaw in addition to Claude Code + Copilot). main is a strict
  superset (it already includes the branch's AI_AGENT-version + backfill fixes);
  keeping the branch's hand-rolled subset left the code inconsistent with the
  README that names those agents.

Both files + their tests restored from origin/main. Also re-add the dropped
emit-spawn `opts.ikeyJsonPath` (no-env-override) regression test — the
production-faithful variant guarding the symlinked-dispatcher config path.

shared 162/0, plugin 1153/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): harden resolver/config handling to fail closed

Address PR #190 review: defensive fixes so a caller bug or a throwing
plugin resolver can never break prompt handling, the pretool hook, or
the dispatcher.

- emit-from-prompt readIkey(): guard a missing/non-string telemetryDir
  before path.join (the throw was outside the try); wrap
  resolver.isProvisioned() so a throw → not provisioned.
- pretool hook: restore the POWER_PLATFORM_SKILLS_IKEY_JSON override seam
  (the only readIkey that had dropped it), forward the resolved ikeyPath
  to the dispatcher, and wrap resolver.isProvisioned() in try/catch.
- dispatcher: wrap await resolver.resolve() so a throw/reject degrades to
  "no resolution" (local mirror already written, POST skipped) instead of
  exiting via the global unhandledRejection seam.
- pretool test: use a temp ikey.json via the override seam instead of
  mutating the checked-in config (race-prone under parallel runs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): honor resolver→static precedence, abs resolver path, restore test budget

Round 2 of PR #190 review:

- emit-dispatcher: the static instrumentationKey/collector_url fallback now
  runs whether or not a resolver is present. A resolver that returns
  null/undefined (or threw) previously skipped the static branch entirely,
  contradicting the documented resolver → static → none precedence and
  silently disabling transmission even when a static key was configured.
  Added a dispatcher test covering resolver-resolves-to-nothing → static POST.
- resolver-loader: require the resolver via path.resolve (absolute) instead of
  path.join, so a relative ikey-dir (e.g. a relative IKEY_JSON override) isn't
  misread by require() as a node_modules module ID.
- run-user-prompt-telemetry test: restore the 30s spawn timeout + comment that
  a merge had reverted to 10s. The enabled path makes two 8s pac shellouts
  (16s worst case); 10s flakes when pac is installed and cold-starting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): restore fail-closed (disabled:true) on unreadable ikey.json

Merge-drop sweep of PR #190 vs main: main's emit-from-prompt readIkey()
returned `disabled: true` when ikey.json was missing/unreadable ("if we
can't read the config we cannot confirm emission is authorized, so
suppress"). The branch had softened both readIkey() catches to
`disabled: false`, relying solely on the downstream provisioned-gate to
suppress. Restore the explicit hard-off so all three layers
(emit-from-prompt, pretool hook, dispatcher's isDisabledByConfig) agree:
unreadable config = disabled. Also flips the round-1 missing-telemetryDir
guard to disabled:true for the same reason.

Behavior is unchanged for readable configs; only makes the
missing/unreadable path explicitly fail-closed rather than incidentally so.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(telemetry): drop dead telemetry.json setup from user-prompt test helper

PR #190 review: mkConfigDir wrote a telemetry.json ({version, enabled,
recorded_at}) and took an `enabled` arg that nothing reads — the per-plugin
opt-out is config.json -> telemetry[plugin] (see user-config.js), and
telemetry.json appears nowhere in the telemetry code. Removed the dead
write so the helper no longer implies telemetry.json gates emission;
matches the sibling telemetry-hook-pretool test's mkConfigDir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): make region-cache writes atomic to survive concurrent dispatchers

PR #190 review (priyanshu92): region-cache write() did a non-atomic
read-modify-writeFileSync of the shared region-cache.json. Each tracked-skill
invocation spawns its own detached dispatcher, so concurrent writers could
produce torn/half-written reads (a cache miss for ALL orgs) and lose entries.

Write to a per-process temp file (pid + seq, so concurrent writers don't
collide) then fs.renameSync over the target — an atomic replace (incl. Windows
via MoveFileEx), so a reader always sees a complete old-or-new file. Best-effort
temp cleanup on failure. The cross-org last-writer-wins residual is left as-is:
rare (two parallel sessions, different orgs, same instant) and self-heals on the
next resolve, so a file lock isn't warranted for a best-effort 24h cache.

Added a test asserting the write leaves no .tmp litter and yields a complete file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): cache org→region only, per-org files — race-free and plugin-safe

PR #190 review (priyanshu92): the region cache shared one machine-global file
keyed by orgId but storing plugin-specific iKey/collector. Two problems beyond
the torn-write already fixed in df0ea69:

- Cross-org lost-update: shared read-modify-write let concurrent detached
  dispatchers clobber each other's entries, evicting orgs before the 24h TTL.
- Cross-plugin misrouting (latent): the next plugin to adopt region routing
  would read power-pages' cached iKey for the same org and POST to the wrong
  plugin's collector.

Root cause: the cache conflated plugin-independent data (org→region) with
plugin-dependent data (region→iKey). Fix:

- region-cache: store ONLY { region, expiresAt }, one file per org
  (region-cache/<orgId>.json), atomic temp+rename per file. Per-org files remove
  the shared RMW entirely (no cross-org lost-update); orgId is GUID-validated
  before use as a filename (path-traversal guard + falsy guard).
- region-resolver: map the cached region → this plugin's iKey via its own
  regionsMap on every hit (closes the misrouting; makes the cache safely
  SHARED across plugins). Add deriveRegion() returning null for unrecognized
  geos so a per-plugin defaultRegion fallback is never cached. mapToRegion kept
  as a thin wrapper.

Old region-cache.json is orphaned (it's a cache; no migration). Design doc:
docs/superpowers/specs/2026-06-16-region-cache-redesign-design.md. 100 telemetry
tests pass (region-cache/resolver tests rewritten for the new layout + shape).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: remove region-cache redesign spec doc

The rationale lives in the commit history (51a213d) and code comments; the
superpowers spec doc isn't needed in the repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Amit Joshi <amitjoshi@microsoft.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants