This extension is most useful anywhere a user would otherwise say:
- "Keep an eye on this."
- "Tell me when this changes."
- "Only interrupt me for the important parts."
- "Watch it for now, and if it proves useful, keep it around."
The key idea is simple:
- EventEmitter = the ONLY primary resource users define — a background command (CommandEmitter) or prompt (PromptEmitter)
- EventStream = auto-created named stream of accepted output (same name as the emitter)
- EventFilter = ordered rule list:
[{ match, outcome }]— first match wins - SessionInjector = derived automatically; controls whether EventStream updates are proactively injected
- Lifespan =
temporaryfor this session,persistentfor future sessions - Ownership =
userOwnedfor protected emitters,modelOwnedfor live tuning
| Outcome | Behavior |
|---|---|
drop |
Discard — does not enter the EventStream |
keep |
Store in the EventStream |
surface |
Keep + show in Copilot session timeline via session.log() |
inject |
Keep + surface + inject into Copilot via session.send() |
PromptEmitter events always inject (no filter applied). CommandEmitter events go through the EventFilter.
| Shape | Config | When to use it |
|---|---|---|
| Continuous CommandEmitter | command |
Tail a log, run a watch task, or consume a streaming source |
| Timed CommandEmitter | command + runInterval |
Poll an API, re-run validation, or check a recurring state |
| OneTime PromptEmitter | prompt |
Ask the agent to perform one background inspection or maintenance pass |
| Timed PromptEmitter | prompt + runInterval |
Re-run a prompt in a session-scoped /tap-loop style workflow |
- Start with a temporary EventEmitter (
lifespan="temporary"). - Enable the SessionInjector unless the stream is naturally sparse.
- Let the emitter produce a few real events (keep-all bootstrap — no EventFilter rules yet).
- Read EventStream history.
- Add EventFilter rules progressively:
- add
{ "match": "<noise>", "outcome": "drop" }first to remove obvious noise - add
{ "match": "<signal>", "outcome": "inject" }for important events - end with
{ "match": ".*", "outcome": "keep" }as a catch-all
- add
- If the workflow is recurring, add
runIntervaland make it timed. - If the workflow is recurring across sessions, promote it to persistent and make it userOwned.
The EventFilter is hot-swappable while the emitter runs. Start broad, observe, then tighten.
Use a CommandEmitter when the signal already exists outside the agent:
- CI logs
- GitHub CLI queries
- ticket APIs
- release feeds
- file tails
Use a PromptEmitter when the work is mainly reasoning, summarization, or maintenance:
- "check whether there are new review comments and summarize only actionable changes"
- "re-check the deploy and tell me whether it is safe to continue"
- "look for new urgent issues or failing runs and summarize what changed"
- "run a maintenance pass on the current branch"
Use runInterval when either of those should repeat on a session-scoped interval. Timed PromptEmitters fire immediately on creation, then repeat on the interval.
| Scenario | Emitter | EventStream | Good defaults |
|---|---|---|---|
| Hot PR babysitter | Poll gh pr view <n> --json reviews,comments,statusCheckRollup |
pr-activity |
Start temporary; userOwned emitter; modelOwned EventFilter; inject on changes requested, failed, review submitted |
| Reviewer response lag | Poll requested reviewers and timestamps | pr-reviewers |
Persistent for team workflow; userOwned; inject when a PR stays unreviewed too long |
| Merge conflict detector | Compare PR branch with base branch on an interval | pr-conflicts |
Temporary; inject important signals; drop non-critical file paths after first run |
| Label and approval gate | Poll labels, approval count, blocking checks | pr-gate |
Persistent; userOwned thresholds; inject on approved, blocked, missing-review |
| Auto-rerun watcher | Poll reruns or status changes in CI for a PR | pr-ci |
Temporary; keep-all at first; drop bot chatter and duplicate check states |
| Scenario | Emitter | EventStream | Good defaults |
|---|---|---|---|
| Failing test stream | Watch npm test -- --watch, pytest -f, or similar |
test-results |
Temporary; inject FAIL, ERROR, TIMEOUT |
| Typecheck watcher | Run tsc --watch or equivalent |
types |
Temporary or persistent; drop dependency noise; inject compiler errors only |
| Coverage regression tracker | Poll coverage output or parse report files | coverage |
Persistent for mature repos; userOwned thresholds; inject on drops below target |
| Build artifact size drift | Run bundle analyzer or publish-size script | build-artifacts |
Persistent; inject on threshold breaches, not on every successful build |
| Flaky test quarantine | Poll repeated test runs and state changes | flaky-tests |
Persistent; history matters more than injection; inject only on new or worsening flakes |
| Scenario | Emitter | EventStream | Good defaults |
|---|---|---|---|
| Critical bug queue | Poll gh issue list for severity labels |
critical-bugs |
Persistent; userOwned emitter; inject on high-severity new issues |
| Untriaged issue queue | Poll issues with no assignee or no triage label | triage-queue |
Persistent; keep-all if low volume, add EventFilter if noisy |
| Stale backlog debt | Poll old issues or items untouched for 30+ days | backlog-debt |
Persistent; inject only when stale items cross a threshold |
| Release blocker tracker | Poll blockers and post-mortem issues | release-status |
Temporary per release, then archive; inject on open/closed transitions |
| Regression issue detector | Combine failing CI signals with issue creation | regressions |
Temporary during active fire-fighting; model tunes the EventFilter aggressively |
| Scenario | Emitter | EventStream | Good defaults |
|---|---|---|---|
| Executive or escalation inbox | Poll IMAP or an email API through a script | urgent-emails |
Persistent; userOwned; inject on senders, subjects, or mailbox labels that matter |
| Personal inbox triage | Poll unread messages and normalize to one line per email | inbox-digest |
Temporary first; keep-all, then drop newsletters and auto-replies |
| On-call alert bridge | Poll PagerDuty, Opsgenie, or similar | oncall-alerts |
Persistent; inject on severity transitions; drop maintenance-window noise |
| Mention aggregator | Poll Slack, Teams, GitHub, and email mentions into one stream | mentions |
Temporary during focused work; inject only on direct, actionable mentions |
| Suspicious mail or phishing queue | Poll a mail security feed | suspicious-mail |
Persistent and userOwned; inject only on high-confidence signals |
| Scenario | Emitter | EventStream | Good defaults |
|---|---|---|---|
| Kubernetes pod health | Run kubectl get pods -w or a poller |
k8s-health |
Persistent; inject on readiness failures and crash loops |
| Deployment rollout watcher | Monitor deploy script output or pipeline states | deploy-ci |
Temporary during rollout; drop info chatter quickly |
| Error-log tail | tail -f an error log or app log pipeline |
app-errors |
Temporary during incidents; start with keep-all and tighten after first burst |
| DB lag or replica health | Poll replication lag or replica status | db-replication |
Persistent; sparse stream; keep-all with inject on threshold breaches |
| Canary or rollback gate | Poll health endpoints or smoke checks | health-gate |
Temporary; userOwned success criteria; inject on repeated failures only |
| Scenario | Emitter | EventStream | Good defaults |
|---|---|---|---|
| Local test watch | jest --watch, vitest --watch, etc. |
test-output |
Temporary; modelOwned EventFilter okay; drop timing and framework noise |
| Lint watch | eslint --watch, ruff check --watch, etc. |
lint |
Temporary; inject on errors; keep warnings in history if useful |
| Build watch | npm run build -- --watch, cargo watch, etc. |
build |
Temporary; drop routine rebuild lines after first run |
| Integration harness | Run verbose integration suite or local environment harness | integration |
Temporary; inject on failures and timeouts only |
| Multi-stream coding loop | Run tests, types, and lint in separate emitters | types, lint, test-output |
Temporary; enable SessionInjector only for the most blocking stream |
| Scenario | Emitter | EventStream | Good defaults |
|---|---|---|---|
| Dependency vulnerability watch | Run npm audit, pip-audit, cargo audit, etc. |
deps-security |
Persistent; userOwned baseline; inject on high/critical or new CVEs |
| Secret scanning | Run detect-secrets, trufflehog, or a custom regex scanner |
secrets-scan |
Persistent; drop known templates; inject on high-confidence leaks |
| License compliance drift | Run a license scanner | license-compliance |
Persistent and userOwned; inject only on banned or unknown licenses |
| Supply-chain verification | Poll signature/checksum verification output | supply-chain-verify |
Persistent; inject on unsigned or mismatched artifacts |
| Policy audit stream | Run checkov, tfsec, kube-bench, SAST/DAST tools |
compliance-audit |
Persistent; keep full history; inject on critical failures only |
| Scenario | Emitter | EventStream | Good defaults |
|---|---|---|---|
| Support backlog watcher | Poll a ticket API for open/SLA-breach tickets | support-backlog |
Persistent; inject on SLA breaches and escalations only |
| Community signal emitter | Poll Discord, Slack, forums, or Reddit for keywords | community-signals |
Start temporary; drop jokes, bot chatter, and duplicate reposts |
| Feature request stream | Poll Discussions, forms, or webhook logs | feature-requests |
Persistent; keep-all, then drop duplicates once the themes are known |
| Moderation queue | Poll flagged posts or moderation APIs | moderation-queue |
Persistent and userOwned; inject on severe content only |
| Incident communication queue | Poll support or community channels for outage chatter | incident-comms |
Temporary during incidents; model tightens the EventFilter fast |
| Scenario | Emitter | EventStream | Good defaults |
|---|---|---|---|
| Paper feed watcher | Poll arXiv or a research API | research-feeds |
Persistent; keep-all, inject on topics or authors that matter |
| Release-note tracker | Poll GitHub releases or changelog feeds | releases |
Persistent; inject on breaking changes, deprecations, and security notes |
| Competitor news emitter | Poll blogs, RSS, or product feeds | competitive-intel |
Persistent; drop rumor/analysis posts after first week |
| Docs staleness detector | Scan docs by age or Git history | doc-staleness |
Temporary during doc audits; inject on core docs only |
| Deadline and event calendar | Poll calendars or JSON feeds | event-deadlines |
Persistent; inject only when deadlines are approaching |
| Scenario | Emitter | EventStream | Good defaults |
|---|---|---|---|
| Package publish watcher | Monitor npm publish, release scripts, or publishing logs |
publish-log |
Temporary on release day, then persist if recurring |
| Scheduled job health | Poll cron, DAG, or batch-job status | jobs-status |
Persistent; inject on state transitions, not polling chatter |
| Data pipeline validation | Monitor ETL validator output | data-pipeline |
Temporary for new pipelines, persistent for production checks |
| Artifact registry emitter | Poll for RCs or package versions in a registry | release-artifacts |
Temporary during releases; inject on exact version matches |
| Reconciliation and finance checks | Poll reconciliation scripts or audit output | reconciliation |
Persistent and userOwned; inject on material mismatches only |
Choose temporary (lifespan="temporary") when:
- this is tied to one incident, one PR, or one debugging session
- you do not yet know the right EventFilter rules
- the stream shape is unknown and likely noisy
- the model should be free to tune things live
- the timed schedule should stop when the current session ends
Choose persistent (lifespan="persistent") when:
- the same emitter is useful across sessions
- the command and thresholds are stable
- the rules encode team policy or operational practice
- the user wants the workflow to come back automatically
Keep it userOwned when:
- the emitter touches security, compliance, email, finance, or release gates
- the command embeds important org-specific assumptions
- the EventStream is now part of team workflow
- a mistaken change would create real risk
Let it be modelOwned when:
- this is a temporary investigative emitter
- the main problem is noise reduction, not policy
- the user wants the agent to learn what matters from the live stream
- EventFilter tuning is expected to change several times during one task
- Prefer one concern per EventStream (one emitter per concern).
- Normalize your emitter output so each line is meaningful.
- Use the EventFilter outcome hierarchy: drop noise → inject signal → keep the rest.
- Drop noise before narrowing what gets injected.
- If you are polling something repeatedly, prefer
runIntervalover re-running it manually. - If you create the same emitter more than a few times, promote it to persistent config.
- If the user cares about ownership, switch the persistent version to
ownership="userOwned"after the workflow stabilizes.
The official @github/copilot-sdk examples are useful even when a pattern is not specifically about EventStreams or EventEmitters. These are good extension ideas to combine with this repo's emitter model.
Use session.log() instead of console.log() to explain what the extension is doing:
- emitter started or stopped
- EventFilter updated
- config loaded
- retries or recoverable failures
Use ephemeral: true for noisy operational messages that should not stick around forever.
The SDK examples show several high-value hook patterns:
onUserPromptSubmittedto add hidden context or trigger follow-up behavioronPreToolUseto deny risky commands or rewrite argumentsonPostToolUseto add context after a tool finishesonErrorOccurredto retry, skip, or abort cleanly
For this repo, the natural extension is to combine hooks with EventStreams:
- if a risky shell command appears, log it and post a note into an ops or audit EventStream
- after a code-edit tool runs, trigger a temporary build or lint emitter
- when a recurring failure happens, inject a short background follow-up with
session.send()
The official examples include simple tools that:
- run a shell command
- fetch data from an API
- copy text to the clipboard
That maps well to this repo. Good companion tools would be:
fetch_release_notespoll_ticket_queue_oncesummarize_streamsnapshot_emitter_state
Use emitters for ongoing signals and helper tools for one-shot actions.
The SDK examples show how to listen to session events such as:
tool.execution_starttool.execution_completeassistant.messagesession.idlesession.error
That is useful here even though emitters already push updates directly. Examples:
- start a temporary validation emitter after a build tool starts
- clear a transient EventFilter once the session goes idle
- attach extra context when a tool fails repeatedly
- mirror important lifecycle events into an EventStream for auditability
The examples show fs.watch and watchFile patterns for:
plan.md- repo files edited manually by the user
That pairs well with this repo when a workflow mixes code changes and emitters:
- watch
plan.mdand post "plan changed" into a planning EventStream - watch files under
logs/and create an emitter automatically - detect user edits to a config file and refresh the corresponding emitter
Use:
session.send()for fire-and-forget background nudgessession.sendAndWait()only when the extension genuinely needs the agent's answer before continuing
For EventStream injection, session.send() is usually the right fit. For a helper flow like "fetch data, then ask the agent to summarize it before updating config", sendAndWait() can make sense.
The SDK examples also show:
- custom permission logic via
onPermissionRequest - user questions via
onUserInputRequest
These are powerful in this repo for guarded workflows:
- ask before persisting a new emitter
- deny destructive shell commands from helper tools
- request confirmation before overriding a userOwned EventFilter
- collect thresholds or keywords interactively instead of hardcoding them
The local Copilot CLI SDK exposes experimental canvas support through createCanvas and joinSession({ canvases: [...] }). A canvas is an extension-owned UI surface that the agent or host can open, focus, close, and invoke actions against.
This is useful when text-only EventStreams are not enough:
- stream dashboards that show emitter health and recent events
- dependency graphs or PR-review boards
- browser-debug panels backed by a local loopback renderer
- incident timelines with action buttons for refresh, filter, or acknowledge
- tap's built-in diagnostics canvas, opened through
tap_open_diagnostics_canvas, which combines streams, emitters, providers, logs, queues, and session events
Important constraints for this repo:
- canvas actions are declared with JSON Schema and invoked through
invoke_canvas_action open()returns host chrome metadata such astitle,status, and a rendererurl- per-instance resources should be keyed by
instanceIdand cleaned up inonClose - external tap providers cannot declare Copilot SDK canvases over the current WebSocket protocol; canvas work belongs in the extension layer unless the provider protocol is explicitly extended
- diagnostics canvases should use bounded/redacted snapshots rather than unbounded raw transcript or token payloads
See Copilot SDK canvas surfaces for the detailed local SDK findings and a working skeleton.
The examples call out Windows-specific concerns:
- detect Windows with
process.platform === "win32" - prefer the right shell and stderr redirection syntax
- use Windows-safe process launching
That is especially relevant here because emitters are shell-driven and this repo is intended to be copied into real projects on different operating systems.