copilot-tap-extension 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ <p align="center">
2
+ <img src="./tap.svg" width="80" height="80" alt="※ tap">
3
+ </p>
4
+
5
+ <h1 align="center">※ tap</h1>
6
+
7
+ <p align="center">
8
+ <em>Background event filtering and injection for Copilot CLI.</em><br>
9
+ <sub>Look here, this matters.</sub>
10
+ </p>
11
+
12
+ ---
13
+
14
+ Copilot CLI already runs background tasks, but their output sits idle until you check it. This extension adds **filtering and auto-injection** on top of that capability.
15
+
16
+ Background commands and agent prompts produce output line by line. An EventFilter decides what to drop, what to store, and what to push into your session. Important events arrive without you asking.
17
+
18
+ | Without this extension | With it |
19
+ | --- | --- |
20
+ | You check background output manually | Important lines are pushed into your conversation |
21
+ | No way to filter noisy output | Rules drop noise, keep context, inject signal |
22
+ | No scheduled prompt re-runs | Prompts repeat on a timer or fire when idle |
23
+ | Output stays in the background task | Matched events arrive in your session as they happen |
24
+
25
+ ## Who is this for?
26
+
27
+ - You tail logs and want failures injected into your session while you keep coding.
28
+ - You maintain a repo and want PR reviews, CI failures, or new issues surfaced automatically.
29
+ - You run long builds and want to know when they finish or break -- without watching.
30
+ - You poll an API or dashboard and want the agent to react when something changes.
31
+ - You re-ask the same prompt periodically and want it on a timer or running whenever idle.
32
+
33
+ ## Get started
34
+
35
+ Prerequisites: [Node.js](https://nodejs.org/) and [Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli).
36
+
37
+ ```bash
38
+ git clone https://github.com/amitse/copilot-tap-extension
39
+ cd copilot-tap-extension
40
+ npm install
41
+ cp tap.config.example.json tap.config.json
42
+ copilot
43
+ ```
44
+
45
+ On Windows, replace `cp` with `copy`.
46
+
47
+ The config file tells the extension which emitters to auto-start. The example defines a heartbeat emitter:
48
+
49
+ ```json
50
+ {
51
+ "emitters": [
52
+ {
53
+ "name": "heartbeat",
54
+ "command": "node ./examples/heartbeat.mjs",
55
+ "autoStart": true,
56
+ "eventFilter": [
57
+ { "match": "booting", "outcome": "drop" },
58
+ { "match": "warning|error", "outcome": "inject" },
59
+ { "match": ".*", "outcome": "keep" }
60
+ ]
61
+ }
62
+ ]
63
+ }
64
+ ```
65
+
66
+ This runs the heartbeat script on session start, drops boot messages, injects warnings and errors, and keeps everything else in the stream.
67
+
68
+ Once inside the session, describe what you want in natural language. You can also use `/loop` to set up scheduled prompts directly:
69
+
70
+ > _"Watch my build logs and tell me if anything fails"_
71
+
72
+ > _"/loop 5m check for new PR review comments"_
73
+
74
+ > _"Tail the API logs, inject errors, drop health checks"_
75
+
76
+ The agent translates these into emitter and filter configurations behind the scenes.
77
+
78
+ ## How it works
79
+
80
+ An **EventEmitter** is a background worker attached to your session. There are two kinds:
81
+
82
+ - A **CommandEmitter** runs a shell command and captures stdout line by line.
83
+ - A **PromptEmitter** runs an agent prompt -- once, on a recurring interval, or whenever the session is idle.
84
+
85
+ Each emitter writes to an **EventStream**, an in-memory log of accepted output. The stream is created automatically and shares the emitter's name.
86
+
87
+ For CommandEmitters, an **EventFilter** decides what happens to each line. It is an ordered list of regex rules -- first match wins:
88
+
89
+ | Outcome | What happens |
90
+ | --- | --- |
91
+ | **drop** | Discarded. Never enters the stream. |
92
+ | **keep** | Stored in the EventStream for later review. |
93
+ | **surface** | Stored and shown in the session timeline. |
94
+ | **inject** | Stored, shown, and pushed into your conversation. |
95
+
96
+ Outcomes are inclusive: **inject** implies **surface**, and **surface** implies **keep**. Only **drop** is outside this chain.
97
+
98
+ PromptEmitter output bypasses the filter and always injects.
99
+
100
+ A **SessionInjector** controls whether stream updates are pushed into your session proactively. Enable it when you want important events to arrive as they happen.
101
+
102
+ Filters are hot-swappable while the emitter runs. `ownership="modelOwned"` lets the agent tune rules; `ownership="userOwned"` locks them to your specification.
103
+
104
+ Emitters are **temporary** by default and last only for the current session. Set `lifespan="persistent"` to save an emitter to config and restore it next session.
105
+
106
+ Run schedules control timing: **continuous** (command runs until stopped), **timed** (repeats on an interval), **oneTime** (runs once), or **idle** (prompt re-runs when the session has nothing else to do).
107
+
108
+ ## What you can do
109
+
110
+ **Watch something in the background**
111
+
112
+ Tell Copilot to watch a log, build, or command. It creates a CommandEmitter, filters the output, and only interrupts you when something needs attention.
113
+
114
+ ```
115
+ "Start a deploy watcher that tails our CI logs.
116
+ Drop health checks, inject any failures or rollbacks."
117
+ ```
118
+
119
+ You keep coding. Twenty minutes later, Copilot interrupts: "Run 48291: deployment rollback triggered on prod."
120
+
121
+ **Loop a prompt on a schedule**
122
+
123
+ A PromptEmitter re-runs an agent prompt at a fixed interval. Useful for PR comments, CI status, or ticket queues.
124
+
125
+ ```
126
+ /loop 15m Check for new failing CI runs or PR review comments.
127
+ Summarize only actionable items.
128
+ ```
129
+
130
+ Every 15 minutes the agent scans and reports back. No news means no interruption.
131
+
132
+ **Run a prompt when idle**
133
+
134
+ Use `/loop idle` to re-run a prompt whenever the session has nothing else to do. Set `maxRuns` to cap iterations.
135
+
136
+ ```
137
+ /loop idle Scan for new issues labeled urgent. Summarize what changed.
138
+ ```
139
+
140
+ The prompt fires immediately, then re-fires after each idle period. It stops after reaching the iteration limit.
141
+
142
+ **Tune the filter live**
143
+
144
+ The recommended approach is a **keep-all bootstrap**: start with no EventFilter rules so all output flows into the stream. Read the stream history to learn what the output looks like, then add rules progressively:
145
+
146
+ ```
147
+ 1. Drop the noise: { "match": "health_check|heartbeat", "outcome": "drop" }
148
+ 2. Inject the signal: { "match": "error|failure|rollback", "outcome": "inject" }
149
+ 3. Keep the rest: { "match": ".*", "outcome": "keep" }
150
+ ```
151
+
152
+ Rules can be added or changed while the emitter is running. You never need to restart it to adjust filtering.
153
+
154
+ ## Repo layout
155
+
156
+ ```text
157
+ .github/
158
+ extensions/tap/extension.mjs # extension entry point (loads the runtime)
159
+ skills/loop/ # /loop skill for scheduled and idle prompts
160
+ copilot-instructions.md # agent guidance for using this extension
161
+ src/
162
+ emitter/ # supervisor, lifecycle, spawn, line router
163
+ streams/ # EventStream store and notification dispatcher
164
+ tools/ # tool definitions (emitters, streams, filters)
165
+ config/ # persistent config store (tap.config.json)
166
+ format/ # display formatters for emitters and streams
167
+ session/ # session port abstraction
168
+ util/ # normalization, text, time, path helpers
169
+ hooks.mjs # session lifecycle hooks
170
+ tap-runtime.mjs # runtime factory (wires everything together)
171
+ tap.svg # ※ mark — the tap icon
172
+ docs/
173
+ evolution-of-tap-icon.html # design evolution: 20 agents, 20 metaphors, one mark
174
+ examples/heartbeat.mjs # demo CommandEmitter
175
+ evals/ # eval harness and test cases
176
+ tap.config.example.json # starter config (copy to tap.config.json)
177
+ PLAN.md # ubiquitous language and design decisions
178
+ ```
179
+
180
+ ## Further reading
181
+
182
+ | Document | When to read it |
183
+ | --- | --- |
184
+ | [Reference](./docs/reference.md) | Look up tool parameters, config fields, or the event pipeline |
185
+ | [Use cases and patterns](./docs/use-cases.md) | Recipes for deploy watchers, PR monitors, log tailers, and more |
186
+ | [Evals](./docs/evals.md) | Run or extend the automated test suite |
187
+ | [Copilot instructions](./.github/copilot-instructions.md) | Understand or customize how the agent uses this extension |
188
+ | [Implementation plan](./PLAN.md) | Ubiquitous language and naming conventions for contributors |
189
+ | [Evolution of the ※ icon](./docs/evolution-of-tap-icon.html) | 20 metaphors, 10 variants, one mark — the design story behind ※ tap |
190
+
191
+ ## Contributing
192
+
193
+ Before opening a PR, run the local checks:
194
+
195
+ ```bash
196
+ npm run check # syntax check
197
+ npm run evals:smoke # smoke test
198
+ npm run evals:validate-modes # interactive vs prompt-mode gap
199
+ ```
200
+
201
+ The runtime has no production dependencies. Dev dependencies (`@github/copilot-sdk`, `yaml`) are used for the eval harness and extension loading.
202
+
203
+ If you add a new tool or change the event pipeline, update the [reference](./docs/reference.md). If you add a new workflow pattern, add it to [use cases](./docs/use-cases.md).
204
+
205
+ ## License
206
+
207
+ [MIT](./LICENSE)
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, copyFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const pkgRoot = path.resolve(__dirname, "..");
9
+ const distDir = path.join(pkgRoot, "dist");
10
+
11
+ const BRAND = "※ tap";
12
+ const EXT_DIR_NAME = "tap";
13
+
14
+ function usage() {
15
+ console.log(`
16
+ ${BRAND} — Copilot CLI extension installer
17
+
18
+ Usage:
19
+ npx copilot-tap-extension [options]
20
+
21
+ Options:
22
+ --global, -g Install to ~/.copilot/ (default)
23
+ --local, -l Install to .github/ (project-scoped)
24
+ --force, -f Overwrite existing files without prompting
25
+ --help, -h Show this help message
26
+
27
+ Installs:
28
+ extensions/tap/extension.mjs The bundled ※ tap extension
29
+ skills/loop/SKILL.md The /loop skill for prompt-based loops
30
+ copilot-instructions.md Agent instructions for using ※ tap
31
+ `);
32
+ }
33
+
34
+ function parseArgs(argv) {
35
+ const args = argv.slice(2);
36
+ const flags = { scope: "global", force: false, help: false };
37
+ for (const arg of args) {
38
+ switch (arg) {
39
+ case "--global":
40
+ case "-g":
41
+ flags.scope = "global";
42
+ break;
43
+ case "--local":
44
+ case "-l":
45
+ flags.scope = "local";
46
+ break;
47
+ case "--force":
48
+ case "-f":
49
+ flags.force = true;
50
+ break;
51
+ case "--help":
52
+ case "-h":
53
+ flags.help = true;
54
+ break;
55
+ default:
56
+ console.error(`Unknown option: ${arg}`);
57
+ usage();
58
+ process.exit(1);
59
+ }
60
+ }
61
+ return flags;
62
+ }
63
+
64
+ function getTargetRoot(scope) {
65
+ if (scope === "global") {
66
+ return path.join(os.homedir(), ".copilot");
67
+ }
68
+ return path.join(process.cwd(), ".github");
69
+ }
70
+
71
+ function copyArtifact(src, dest, label, flags) {
72
+ if (!existsSync(src)) {
73
+ console.error(` ✗ ${label}: source not found (${src})`);
74
+ return false;
75
+ }
76
+ if (existsSync(dest) && !flags.force) {
77
+ console.log(` ⊘ ${label}: already exists, skipping (use --force to overwrite)`);
78
+ return true;
79
+ }
80
+ mkdirSync(path.dirname(dest), { recursive: true });
81
+ copyFileSync(src, dest);
82
+ console.log(` ✓ ${label}`);
83
+ return true;
84
+ }
85
+
86
+ function install(flags) {
87
+ const targetRoot = getTargetRoot(flags.scope);
88
+ const scopeLabel = flags.scope === "global" ? "global (~/.copilot)" : "local (.github)";
89
+
90
+ console.log(`\n${BRAND} — installing (${scopeLabel})\n`);
91
+
92
+ const artifacts = [
93
+ {
94
+ src: path.join(distDir, "extension.mjs"),
95
+ dest: path.join(targetRoot, "extensions", EXT_DIR_NAME, "extension.mjs"),
96
+ label: "extensions/tap/extension.mjs"
97
+ },
98
+ {
99
+ src: path.join(distDir, "skills", "loop", "SKILL.md"),
100
+ dest: path.join(targetRoot, "skills", "loop", "SKILL.md"),
101
+ label: "skills/loop/SKILL.md"
102
+ },
103
+ {
104
+ src: path.join(distDir, "copilot-instructions.md"),
105
+ dest: path.join(targetRoot, "copilot-instructions.md"),
106
+ label: "copilot-instructions.md"
107
+ }
108
+ ];
109
+
110
+ let allOk = true;
111
+ for (const { src, dest, label } of artifacts) {
112
+ if (!copyArtifact(src, dest, label, flags)) {
113
+ allOk = false;
114
+ }
115
+ }
116
+
117
+ console.log();
118
+ if (allOk) {
119
+ console.log(`✓ ${BRAND} installed to ${targetRoot}`);
120
+ } else {
121
+ console.error(`⚠ Some artifacts could not be installed.`);
122
+ process.exit(1);
123
+ }
124
+ }
125
+
126
+ const flags = parseArgs(process.argv);
127
+
128
+ if (flags.help) {
129
+ usage();
130
+ process.exit(0);
131
+ }
132
+
133
+ install(flags);
@@ -0,0 +1,187 @@
1
+ # Copilot instructions for ※ tap
2
+
3
+ Use this extension as a background-awareness layer for long-running or polled signals.
4
+
5
+ ## Mental model
6
+
7
+ - An **EventEmitter** is the only primary resource users define — a background shell command (CommandEmitter) or agent prompt (PromptEmitter).
8
+ - An **EventStream** is automatically created for each emitter (same name) and stores accepted output.
9
+ - An **EventFilter** is an ordered rule list owned by the emitter: `[{ match, outcome }]` — first match wins.
10
+ - Outcomes: `drop` (discard), `keep` (store in EventStream), `surface` (keep + show in timeline), `inject` (keep + surface + inject into Copilot)
11
+ - A **SessionInjector** is derived automatically per EventStream and controls whether updates are proactively injected into the session.
12
+
13
+ PromptEmitter events always inject (no filter applied). CommandEmitter events go through the EventFilter. The EventFilter is hot-swappable while the emitter runs.
14
+
15
+ The extension injects EventStream updates directly from emitter output with `session.send()`. It does not depend on transcript events like `user.message` or `assistant.message`.
16
+
17
+ ## When to use it
18
+
19
+ Reach for EventEmitters when the user wants to:
20
+
21
+ - watch something over time
22
+ - babysit a PR, build, issue queue, deploy, or inbox
23
+ - keep working while a background process or poller runs
24
+ - get interrupted only for important changes
25
+ - store short rolling history for a live stream
26
+
27
+ Reach for **PromptEmitters** when the user wants the agent itself to periodically re-check something, summarize changes, or perform maintenance without waiting for another manual prompt.
28
+
29
+ ## Default operating pattern
30
+
31
+ 1. Start with a **temporary** emitter (`lifespan="temporary"`) unless the workflow is obviously recurring across multiple agent sessions.
32
+ 2. The EventStream is created automatically with the same name as the emitter.
33
+ 3. Enable the SessionInjector if the emitter should proactively surface updates.
34
+ 4. Start with a keep-all bootstrap policy (no EventFilter rules) to learn the stream shape.
35
+ 5. Let a few events arrive.
36
+ 6. Inspect EventStream history.
37
+ 7. Tighten the EventFilter:
38
+ - add `{ "match": "<noise>", "outcome": "drop" }` rules first
39
+ - add `{ "match": "<signal>", "outcome": "inject" }` rules for important events
40
+ - use `{ "match": ".*", "outcome": "keep" }` as a catch-all to store everything else
41
+ 8. If the work should repeat inside the session, add `runInterval`.
42
+ 9. If the emitter proves useful across sessions, persist it and switch ownership to `ownership="userOwned"` unless the user explicitly wants ongoing model control.
43
+
44
+ ## Recommended tool sequence
45
+
46
+ Use these tools in roughly this order:
47
+
48
+ 1. `tap_start_emitter` — create the EventEmitter
49
+ 2. `tap_enable_injector` — enable the SessionInjector if the emitter should proactively surface updates
50
+ 3. `tap_stream_history` — read EventStream history after a few events
51
+ 4. `tap_set_event_filter` — update the EventFilter rules
52
+ 5. `tap_post` — leave structured notes or summaries in the EventStream
53
+ 6. `tap_stop_emitter` — stop the emitter when the task ends or if the stream is no longer useful
54
+
55
+ ## Good defaults
56
+
57
+ ### For unknown or noisy streams
58
+
59
+ - `lifespan="temporary"`
60
+ - `ownership="modelOwned"`
61
+ - `subscribe=true`
62
+ - No EventFilter rules initially (keep-all bootstrap policy)
63
+ - Let events accumulate, then add rules progressively
64
+
65
+ ### For prompt-driven maintenance
66
+
67
+ - use `prompt` instead of `command` (creates a PromptEmitter)
68
+ - add `runInterval` for a fixed session-scoped timed schedule
69
+ - use oneTime PromptEmitter when the user wants a background check only once
70
+ - keep the first prompt concise and action-oriented
71
+
72
+ ### For recurring team workflows
73
+
74
+ - `lifespan="persistent"`
75
+ - `ownership="userOwned"`
76
+ - `autoStart=true` only if the user wants it every session
77
+ - stable EventStream naming
78
+ - user-approved EventFilter rules
79
+
80
+ ## Ownership rules
81
+
82
+ Ownership lives on the EventEmitter only. EventStream and SessionInjector are derived.
83
+
84
+ Treat these as **userOwned** by default:
85
+
86
+ - persistent emitters
87
+ - security, compliance, finance, or release-gating workflows
88
+ - email or external notification rules
89
+ - org-specific routing rules or thresholds
90
+
91
+ Treat these as safe for **modelOwned**:
92
+
93
+ - temporary emitters created for one task
94
+ - temporary SessionInjectors
95
+ - live EventFilter tuning to reduce noise
96
+ - exploratory emitters where the stream shape is not yet known
97
+
98
+ Never override a userOwned persistent emitter or its EventFilter unless the user explicitly asks. If the extension requires `transferOwnership=true`, use it only for an explicit user request.
99
+
100
+ ## How to tighten EventFilters
101
+
102
+ The EventFilter is an ordered rule list — first match wins. Prefer this progression:
103
+
104
+ 1. **Observe first.** Let the raw stream teach you the vocabulary (keep-all bootstrap).
105
+ 2. **Drop obvious noise.** Add `{ "match": "<noise>", "outcome": "drop" }` rules for polling chatter, heartbeats, bot messages, deprecations, duplicate summaries.
106
+ 3. **Inject important signals.** Add `{ "match": "<signal>", "outcome": "inject" }` for events that should interrupt the session.
107
+ 4. **Surface useful context.** Add `{ "match": "<context>", "outcome": "surface" }` for events worth showing in the timeline.
108
+ 5. **Catch-all.** End with `{ "match": ".*", "outcome": "keep" }` to store everything else in the EventStream.
109
+
110
+ Good examples:
111
+
112
+ - log tail: drop timestamps, retries, and health-check chatter; inject `error|fatal|panic`
113
+ - PR watcher: drop bot comments and repeated status updates; inject `changes requested|failed|approved`
114
+ - ticket queue: inject `sla-breach|high-priority|escalated`; keep everything else
115
+
116
+ ## Temporary vs persistent
117
+
118
+ Use **temporary** (`lifespan="temporary"`) when:
119
+
120
+ - the user is debugging, triaging, or investigating
121
+ - the correct EventFilter is not obvious yet
122
+ - the stream exists only for one task, incident, PR, or release window
123
+ - the timed schedule should end with the session
124
+
125
+ Use **persistent** (`lifespan="persistent"`) when:
126
+
127
+ - the same workflow should come back next session
128
+ - the command and thresholds are stable
129
+ - the user wants a reusable operating pattern
130
+
131
+ ## Everything is code
132
+
133
+ If no ready-made CLI exists, create or use a small script that prints one meaningful line per event. Good CommandEmitters are often:
134
+
135
+ - API pollers
136
+ - webhook log tails
137
+ - release-note fetchers
138
+ - GitHub CLI pollers
139
+ - local watch scripts
140
+ - validation scripts for builds, tests, deploys, ETL, or compliance
141
+
142
+ Prefer normalized output over raw dumps. EventFilters work much better when each line already carries a stable tag or status word.
143
+
144
+ If the work is mostly reasoning rather than data collection, prefer a PromptEmitter:
145
+
146
+ - prompt once for a background check (oneTime)
147
+ - prompt + `runInterval` for a fixed maintenance loop (timed)
148
+
149
+ This is the closest analogue to Claude's session-scoped `/loop` behavior in this extension.
150
+
151
+ ## Borrow from the official SDK examples
152
+
153
+ When working on the extension itself, not just using its emitter tools, prefer these SDK patterns:
154
+
155
+ - use `session.log()` for user-visible diagnostics; never rely on `console.log()`
156
+ - use hooks such as `onUserPromptSubmitted`, `onPreToolUse`, `onPostToolUse`, and `onErrorOccurred` to shape behavior
157
+ - use `session.on(...)` listeners for tool lifecycle, assistant messages, session idle, and errors when you need event-driven behavior
158
+ - use `session.send()` for asynchronous follow-up prompts and `session.sendAndWait()` only when the extension must wait for an answer
159
+ - use `onPermissionRequest` and `onUserInputRequest` for guarded flows instead of custom ad hoc prompting
160
+ - use `fs.watch` or `watchFile` when the extension should react to manual file edits or workspace artifacts such as `plan.md`
161
+
162
+ Good non-emitter examples to adapt into this repo:
163
+
164
+ - after an edit tool runs, trigger a lint or test emitter automatically
165
+ - watch a config file and refresh the corresponding emitter when the user edits it
166
+ - add a helper tool that fetches one-shot data from an API while emitters continue to watch background streams
167
+ - log EventFilter updates and emitter lifecycle events to the timeline for observability
168
+
169
+ ## What not to do
170
+
171
+ - Do not create one giant mixed EventStream for unrelated workflows.
172
+ - Do not make a noisy stream persistent before you understand it.
173
+ - Do not skip the keep-all bootstrap policy for chatty sources — observe first, then add rules.
174
+ - Do not mutate userOwned persistent emitters or their EventFilter without explicit permission.
175
+ - Do not use EventStreams as a transcript mirror; use them for emitter-driven context.
176
+
177
+ ## A strong operating recipe
178
+
179
+ When the user says "watch this" and the stream shape is unclear:
180
+
181
+ 1. Create a temporary EventEmitter (CommandEmitter or PromptEmitter).
182
+ 2. Enable the SessionInjector.
183
+ 3. Start with keep-all bootstrap (no EventFilter rules).
184
+ 4. Wait for a few real events.
185
+ 5. Read EventStream history.
186
+ 6. Add EventFilter rules progressively (drop noise → inject signal → keep the rest).
187
+ 7. If the workflow proves valuable, ask or decide to create the persistent, userOwned version.