copilot-tap-extension 0.2.0 → 0.2.5
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 +21 -21
- package/README.md +227 -207
- package/bin/install.mjs +133 -133
- package/dist/copilot-instructions.md +187 -187
- package/dist/skills/loop/SKILL.md +89 -89
- package/package.json +48 -44
package/bin/install.mjs
CHANGED
|
@@ -1,133 +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);
|
|
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);
|
|
@@ -1,187 +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.
|
|
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.
|