copilot-tap-extension 1.1.4 → 2.0.1
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/README.md +36 -12
- package/bin/install.mjs +76 -25
- package/dist/copilot-instructions.md +18 -6
- package/dist/extension.mjs +112 -2
- package/dist/skills/{create-provider → tap-create-provider}/SKILL.md +1 -1
- package/dist/skills/tap-goal/SKILL.md +136 -0
- package/dist/skills/{loop → tap-loop}/SKILL.md +4 -4
- package/dist/skills/{monitor → tap-monitor}/SKILL.md +2 -2
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ npx copilot-tap-extension
|
|
|
62
62
|
npx copilot-tap-extension --local
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
This installs the bundled extension, the `/loop` skill, the `/monitor` skill, and the agent instructions to the appropriate Copilot directory. Run `npx copilot-tap-extension --help` for all options.
|
|
65
|
+
This installs the bundled extension, the `/tap-loop` skill, the `/tap-monitor` skill, the `/tap-goal` skill, and the agent instructions to the appropriate Copilot directory. Run `npx copilot-tap-extension --help` for all options.
|
|
66
66
|
|
|
67
67
|
To update to the latest version, re-run the same command with `--force`:
|
|
68
68
|
|
|
@@ -103,13 +103,15 @@ The config file tells the extension which emitters to auto-start. The example de
|
|
|
103
103
|
|
|
104
104
|
This runs the heartbeat script on session start, drops boot messages, injects warnings and errors, and keeps everything else in the stream.
|
|
105
105
|
|
|
106
|
-
Once inside the session, describe what you want in natural language. You can also use `/loop` to set up scheduled prompts directly:
|
|
106
|
+
Once inside the session, describe what you want in natural language. You can also use `/tap-loop` to set up scheduled prompts directly:
|
|
107
107
|
|
|
108
108
|
> _"Watch my build logs and tell me if anything fails"_
|
|
109
109
|
|
|
110
|
-
> _"/loop 5m check for new PR review comments"_
|
|
110
|
+
> _"/tap-loop 5m check for new PR review comments"_
|
|
111
111
|
|
|
112
|
-
> _"/monitor tail -f /var/log/app.log"_
|
|
112
|
+
> _"/tap-monitor tail -f /var/log/app.log"_
|
|
113
|
+
|
|
114
|
+
> _"/tap-goal migrate the repo to the new API and keep going until tests pass"_
|
|
113
115
|
|
|
114
116
|
> _"Tail the API logs, inject errors, drop health checks"_
|
|
115
117
|
|
|
@@ -175,11 +177,11 @@ You keep coding. Twenty minutes later, Copilot interrupts: "Run 48291: deploymen
|
|
|
175
177
|
|
|
176
178
|
**Monitor a command with self-tuning filters**
|
|
177
179
|
|
|
178
|
-
Use `/monitor` to run a shell command continuously while a companion agent periodically reads the output and updates the filter expressions to separate noise from signal automatically.
|
|
180
|
+
Use `/tap-monitor` to run a shell command continuously while a companion agent periodically reads the output and updates the filter expressions to separate noise from signal automatically.
|
|
179
181
|
|
|
180
182
|
```
|
|
181
|
-
/monitor tail -f /var/log/app.log
|
|
182
|
-
/monitor 10m docker logs -f mycontainer
|
|
183
|
+
/tap-monitor tail -f /var/log/app.log
|
|
184
|
+
/tap-monitor 10m docker logs -f mycontainer
|
|
183
185
|
```
|
|
184
186
|
|
|
185
187
|
The command stream starts with a sensible initial `notifyPattern`. Every few minutes (configurable) the companion reviews recent log lines and calls `tap_set_event_filter` if the patterns need adjustment. The filter tightens itself based on real output — no manual tuning required.
|
|
@@ -189,7 +191,7 @@ The command stream starts with a sensible initial `notifyPattern`. Every few min
|
|
|
189
191
|
A PromptEmitter re-runs an agent prompt at a fixed interval. Useful for PR comments, CI status, or ticket queues.
|
|
190
192
|
|
|
191
193
|
```
|
|
192
|
-
/loop 15m Check for new failing CI runs or PR review comments.
|
|
194
|
+
/tap-loop 15m Check for new failing CI runs or PR review comments.
|
|
193
195
|
Summarize only actionable items.
|
|
194
196
|
```
|
|
195
197
|
|
|
@@ -197,14 +199,34 @@ Every 15 minutes the agent scans and reports back. No news means no interruption
|
|
|
197
199
|
|
|
198
200
|
**Run a prompt when idle**
|
|
199
201
|
|
|
200
|
-
Use `/loop idle` to re-run a prompt whenever the session has nothing else to do. Set `maxRuns` to cap iterations.
|
|
202
|
+
Use `/tap-loop idle` to re-run a prompt whenever the session has nothing else to do. Set `maxRuns` to cap iterations.
|
|
201
203
|
|
|
202
204
|
```
|
|
203
|
-
/loop idle Scan for new issues labeled urgent. Summarize what changed.
|
|
205
|
+
/tap-loop idle Scan for new issues labeled urgent. Summarize what changed.
|
|
204
206
|
```
|
|
205
207
|
|
|
206
208
|
The prompt fires immediately, then re-fires after each idle period. It stops after reaching the iteration limit.
|
|
207
209
|
|
|
210
|
+
**Work toward a goal autonomously**
|
|
211
|
+
|
|
212
|
+
Use `/tap-goal` to create an idle goal loop that keeps advancing a concrete objective until it finishes, hits a blocker, or reaches its iteration budget. Goals are explicit, control commands are user-owned, and the loop should stop itself only when the objective is actually complete or blocked.
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
/tap-goal migrate the repo to the new API and keep going until tests pass
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The skill creates a temporary idle PromptEmitter with a self-contained goal prompt. Each iteration inspects its own emitter state, assesses progress, takes the next small action, validates when relevant, and stops the emitter when the goal is complete or blocked. As the remaining iteration budget gets low, the prompt shifts into wrap-up mode so it leaves a useful handoff instead of starting broad new work.
|
|
219
|
+
|
|
220
|
+
Goal loops default to 50 iterations unless you specify another budget.
|
|
221
|
+
|
|
222
|
+
Use `/tap-goal status` to list current goal emitters.
|
|
223
|
+
|
|
224
|
+
Use `/tap-goal stop <name>` or `/tap-goal clear <name>` to stop a specific goal emitter. If there is exactly one active `goal-*` emitter, the skill can stop it without a name; otherwise run `/tap-goal status` first and then stop the goal by name.
|
|
225
|
+
|
|
226
|
+
Use `/tap-goal resume <objective>` to start a new loop from an objective. Stopped goal loops do not preserve resumable internal state; resuming creates a new emitter from the supplied objective.
|
|
227
|
+
|
|
228
|
+
Because `/tap-goal` uses an idle PromptEmitter, it is best when the session has natural idle gaps. For always-busy autopilot-style flows, prefer a timed prompt loop or hook/session-injector based delivery so follow-up context can still reach the session.
|
|
229
|
+
|
|
208
230
|
**Tune the filter live**
|
|
209
231
|
|
|
210
232
|
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:
|
|
@@ -222,8 +244,10 @@ Rules can be added or changed while the emitter is running. You never need to re
|
|
|
222
244
|
```text
|
|
223
245
|
.github/
|
|
224
246
|
extensions/tap/extension.mjs # extension entry point (loads the runtime)
|
|
225
|
-
skills/loop/ # /loop skill for scheduled and idle prompts
|
|
226
|
-
skills/monitor/ # /monitor skill for self-tuning command monitors
|
|
247
|
+
skills/tap-loop/ # /tap-loop skill for scheduled and idle prompts
|
|
248
|
+
skills/tap-monitor/ # /tap-monitor skill for self-tuning command monitors
|
|
249
|
+
skills/tap-goal/ # /tap-goal skill for autonomous goal loops
|
|
250
|
+
skills/tap-create-provider/ # /tap-create-provider skill for scaffolding external tool providers
|
|
227
251
|
copilot-instructions.md # agent guidance for using this extension
|
|
228
252
|
src/
|
|
229
253
|
emitter/ # supervisor, lifecycle, spawn, line router
|
package/bin/install.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, mkdirSync, copyFileSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import path from "node:path";
|
|
@@ -33,22 +33,23 @@ and preserves customizable artifacts. If fresh, does a full install.
|
|
|
33
33
|
Options:
|
|
34
34
|
--global, -g Install to ~/.copilot/ (default)
|
|
35
35
|
--local, -l Install to .github/ (project-scoped)
|
|
36
|
-
--
|
|
36
|
+
--force, -f Force a full reinstall even if already installed
|
|
37
37
|
--help, -h Show this help message
|
|
38
38
|
|
|
39
39
|
Installs:
|
|
40
40
|
extensions/tap/extension.mjs The bundled ※ tap extension
|
|
41
41
|
extensions/tap/version.json Installed version metadata
|
|
42
|
-
skills/loop/SKILL.md The /loop skill for prompt-based loops
|
|
43
|
-
skills/create-provider/SKILL.md The /create-provider skill for scaffolding providers
|
|
44
|
-
skills/monitor/SKILL.md The /monitor skill for self-tuning command monitors
|
|
42
|
+
skills/tap-loop/SKILL.md The /tap-loop skill for prompt-based loops
|
|
43
|
+
skills/tap-create-provider/SKILL.md The /tap-create-provider skill for scaffolding providers
|
|
44
|
+
skills/tap-monitor/SKILL.md The /tap-monitor skill for self-tuning command monitors
|
|
45
|
+
skills/tap-goal/SKILL.md The /tap-goal skill for autonomous goal loops
|
|
45
46
|
copilot-instructions.md Agent instructions for using ※ tap
|
|
46
47
|
`);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function parseArgs(argv) {
|
|
50
51
|
const args = argv.slice(2);
|
|
51
|
-
const flags = { scope: "global",
|
|
52
|
+
const flags = { scope: "global", force: false, help: false };
|
|
52
53
|
for (const arg of args) {
|
|
53
54
|
switch (arg) {
|
|
54
55
|
case "--global":
|
|
@@ -59,12 +60,12 @@ function parseArgs(argv) {
|
|
|
59
60
|
case "-l":
|
|
60
61
|
flags.scope = "local";
|
|
61
62
|
break;
|
|
63
|
+
case "--force":
|
|
64
|
+
case "-f":
|
|
62
65
|
case "--full":
|
|
63
|
-
flags.
|
|
66
|
+
flags.force = true;
|
|
64
67
|
break;
|
|
65
68
|
// Keep legacy flags working
|
|
66
|
-
case "--force":
|
|
67
|
-
case "-f":
|
|
68
69
|
case "--update":
|
|
69
70
|
case "-u":
|
|
70
71
|
break;
|
|
@@ -128,6 +129,36 @@ function isCopilotCliInstalled() {
|
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
function removeDeprecatedSkills(targetRoot) {
|
|
133
|
+
const deprecated = ["loop", "monitor", "create-provider"];
|
|
134
|
+
let allOk = true;
|
|
135
|
+
let removedAny = false;
|
|
136
|
+
|
|
137
|
+
for (const name of deprecated) {
|
|
138
|
+
const oldPath = path.join(targetRoot, "skills", name, "SKILL.md");
|
|
139
|
+
if (!existsSync(oldPath)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
unlinkSync(oldPath);
|
|
144
|
+
if (!removedAny) {
|
|
145
|
+
console.log();
|
|
146
|
+
removedAny = true;
|
|
147
|
+
}
|
|
148
|
+
console.log(` ✓ Removed deprecated skill: skills/${name}/SKILL.md`);
|
|
149
|
+
} catch {
|
|
150
|
+
allOk = false;
|
|
151
|
+
console.warn(` ⚠ Could not remove deprecated skill at ${oldPath} — remove it manually`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (removedAny) {
|
|
156
|
+
console.log(`\n Use the new namespaced commands: /tap-loop /tap-monitor /tap-create-provider`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return allOk;
|
|
160
|
+
}
|
|
161
|
+
|
|
131
162
|
function install(flags) {
|
|
132
163
|
const targetRoot = getTargetRoot(flags.scope);
|
|
133
164
|
const scopeLabel = flags.scope === "global" ? "global (~/.copilot)" : "local (.github)";
|
|
@@ -141,16 +172,20 @@ function install(flags) {
|
|
|
141
172
|
}
|
|
142
173
|
|
|
143
174
|
const installed = isAlreadyInstalled(targetRoot);
|
|
144
|
-
const isUpdate = installed && !flags.
|
|
175
|
+
const isUpdate = installed && !flags.force;
|
|
176
|
+
const isReinstall = installed && flags.force;
|
|
177
|
+
const installedVersion = installed ? getInstalledVersion(targetRoot) : null;
|
|
145
178
|
|
|
146
179
|
if (isUpdate) {
|
|
147
|
-
const installedVersion = getInstalledVersion(targetRoot);
|
|
148
180
|
if (installedVersion && installedVersion === packageVersion) {
|
|
149
181
|
console.log(`\n${BRAND} — already up to date (v${installedVersion})\n`);
|
|
150
182
|
process.exit(0);
|
|
151
183
|
}
|
|
152
184
|
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
153
185
|
console.log(`\n${BRAND} — updating ${fromLabel} → v${packageVersion} (${scopeLabel})\n`);
|
|
186
|
+
} else if (isReinstall) {
|
|
187
|
+
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
188
|
+
console.log(`\n${BRAND} — reinstalling ${fromLabel} → v${packageVersion} (${scopeLabel})\n`);
|
|
154
189
|
} else {
|
|
155
190
|
console.log(`\n${BRAND} — installing v${packageVersion} (${scopeLabel})\n`);
|
|
156
191
|
}
|
|
@@ -170,19 +205,24 @@ function install(flags) {
|
|
|
170
205
|
|
|
171
206
|
const ancillaryArtifacts = [
|
|
172
207
|
{
|
|
173
|
-
src: path.join(distDir, "skills", "loop", "SKILL.md"),
|
|
174
|
-
dest: path.join(targetRoot, "skills", "loop", "SKILL.md"),
|
|
175
|
-
label: "skills/loop/SKILL.md"
|
|
208
|
+
src: path.join(distDir, "skills", "tap-loop", "SKILL.md"),
|
|
209
|
+
dest: path.join(targetRoot, "skills", "tap-loop", "SKILL.md"),
|
|
210
|
+
label: "skills/tap-loop/SKILL.md"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
src: path.join(distDir, "skills", "tap-create-provider", "SKILL.md"),
|
|
214
|
+
dest: path.join(targetRoot, "skills", "tap-create-provider", "SKILL.md"),
|
|
215
|
+
label: "skills/tap-create-provider/SKILL.md"
|
|
176
216
|
},
|
|
177
217
|
{
|
|
178
|
-
src: path.join(distDir, "skills", "
|
|
179
|
-
dest: path.join(targetRoot, "skills", "
|
|
180
|
-
label: "skills/
|
|
218
|
+
src: path.join(distDir, "skills", "tap-monitor", "SKILL.md"),
|
|
219
|
+
dest: path.join(targetRoot, "skills", "tap-monitor", "SKILL.md"),
|
|
220
|
+
label: "skills/tap-monitor/SKILL.md"
|
|
181
221
|
},
|
|
182
222
|
{
|
|
183
|
-
src: path.join(distDir, "skills", "
|
|
184
|
-
dest: path.join(targetRoot, "skills", "
|
|
185
|
-
label: "skills/
|
|
223
|
+
src: path.join(distDir, "skills", "tap-goal", "SKILL.md"),
|
|
224
|
+
dest: path.join(targetRoot, "skills", "tap-goal", "SKILL.md"),
|
|
225
|
+
label: "skills/tap-goal/SKILL.md"
|
|
186
226
|
},
|
|
187
227
|
{
|
|
188
228
|
src: path.join(distDir, "copilot-instructions.md"),
|
|
@@ -191,7 +231,12 @@ function install(flags) {
|
|
|
191
231
|
}
|
|
192
232
|
];
|
|
193
233
|
|
|
194
|
-
|
|
234
|
+
// During updates, also install ancillary artifacts that don't yet exist at the destination
|
|
235
|
+
// (e.g. new skills added in a newer version). Existing ones are preserved to keep user customizations.
|
|
236
|
+
const newAncillaryArtifacts = isUpdate
|
|
237
|
+
? ancillaryArtifacts.filter(({ dest }) => !existsSync(dest))
|
|
238
|
+
: ancillaryArtifacts;
|
|
239
|
+
const artifacts = [...coreArtifacts, ...newAncillaryArtifacts];
|
|
195
240
|
|
|
196
241
|
let allOk = true;
|
|
197
242
|
for (const { src, dest, label } of artifacts) {
|
|
@@ -200,14 +245,20 @@ function install(flags) {
|
|
|
200
245
|
}
|
|
201
246
|
}
|
|
202
247
|
|
|
248
|
+
if (installed && !removeDeprecatedSkills(targetRoot)) {
|
|
249
|
+
allOk = false;
|
|
250
|
+
}
|
|
251
|
+
|
|
203
252
|
console.log();
|
|
204
253
|
if (allOk) {
|
|
205
|
-
const verb = isUpdate ? "updated" : "installed";
|
|
254
|
+
const verb = isUpdate ? "updated" : isReinstall ? "reinstalled" : "installed";
|
|
206
255
|
console.log(`✓ ${BRAND} ${verb} to ${targetRoot}`);
|
|
207
|
-
|
|
208
|
-
console.error(`⚠ Some artifacts could not be ${isUpdate ? "updated" : "installed"}.`);
|
|
209
|
-
process.exit(1);
|
|
256
|
+
return;
|
|
210
257
|
}
|
|
258
|
+
|
|
259
|
+
const verb = isUpdate ? "updated" : isReinstall ? "reinstalled" : "installed";
|
|
260
|
+
console.error(`⚠ Some artifacts could not be ${verb}.`);
|
|
261
|
+
process.exit(1);
|
|
211
262
|
}
|
|
212
263
|
|
|
213
264
|
const flags = parseArgs(process.argv);
|
|
@@ -38,7 +38,7 @@ Reach for **PromptEmitters** when the user wants the agent itself to periodicall
|
|
|
38
38
|
- add `{ "match": "<noise>", "outcome": "drop" }` rules first
|
|
39
39
|
- add `{ "match": "<signal>", "outcome": "inject" }` rules for important events
|
|
40
40
|
- use `{ "match": ".*", "outcome": "keep" }` as a catch-all to store everything else
|
|
41
|
-
8. If the work should repeat inside the session, add `
|
|
41
|
+
8. If the work should repeat inside the session, add `every="<interval>"` or `everySchedule=[...]`.
|
|
42
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
43
|
|
|
44
44
|
## Recommended tool sequence
|
|
@@ -65,7 +65,7 @@ Use these tools in roughly this order:
|
|
|
65
65
|
### For prompt-driven maintenance
|
|
66
66
|
|
|
67
67
|
- use `prompt` instead of `command` (creates a PromptEmitter)
|
|
68
|
-
- add `
|
|
68
|
+
- add `every="<interval>"` for a fixed session-scoped timed schedule
|
|
69
69
|
- use oneTime PromptEmitter when the user wants a background check only once
|
|
70
70
|
- keep the first prompt concise and action-oriented
|
|
71
71
|
|
|
@@ -144,9 +144,21 @@ Prefer normalized output over raw dumps. EventFilters work much better when each
|
|
|
144
144
|
If the work is mostly reasoning rather than data collection, prefer a PromptEmitter:
|
|
145
145
|
|
|
146
146
|
- prompt once for a background check (oneTime)
|
|
147
|
-
- prompt + `
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
- prompt + `every="<interval>"` for a fixed maintenance loop (timed)
|
|
148
|
+
- prompt + `every="idle"` + `maxRuns` for autonomous goal loops with explicit iteration budgets (`/tap-goal`)
|
|
149
|
+
|
|
150
|
+
This is the closest analogue to Claude's session-scoped `/tap-loop` behavior in this extension.
|
|
151
|
+
|
|
152
|
+
For "keep working until done" requests, prefer `/tap-goal`: create an
|
|
153
|
+
idle PromptEmitter with a self-contained goal prompt, an explicit `maxRuns`
|
|
154
|
+
budget, and instructions to stop itself when complete or blocked. Goals must be
|
|
155
|
+
explicit user requests; do not infer them from ordinary one-shot tasks, and do
|
|
156
|
+
not treat budget exhaustion as successful completion. Goal prompts should
|
|
157
|
+
self-steer by reading their own emitter state with `tap_list_emitters` and
|
|
158
|
+
switching into wrap-up mode when the remaining iteration budget is low.
|
|
159
|
+
If the session may stay continuously busy (for example in autopilot-heavy
|
|
160
|
+
flows), prefer a timed PromptEmitter or hook-driven/session-injector delivery
|
|
161
|
+
instead of relying on idle to trigger the next goal step.
|
|
150
162
|
|
|
151
163
|
## Borrow from the official SDK examples
|
|
152
164
|
|
|
@@ -154,7 +166,7 @@ When working on the extension itself, not just using its emitter tools, prefer t
|
|
|
154
166
|
|
|
155
167
|
- use `session.log()` for user-visible diagnostics; never rely on `console.log()`
|
|
156
168
|
- use hooks such as `onUserPromptSubmitted`, `onPreToolUse`, `onPostToolUse`, and `onErrorOccurred` to shape behavior
|
|
157
|
-
- use `session.on(...)` listeners for
|
|
169
|
+
- use `session.on(...)` listeners for event-driven behavior such as `session.idle`, `assistant.message`, `tool.execution_start`, `tool.execution_complete`, `session.error`, and `session.start`/`session.resume` when resuming persistent goal state
|
|
158
170
|
- use `session.send()` for asynchronous follow-up prompts and `session.sendAndWait()` only when the extension must wait for an answer
|
|
159
171
|
- use `onPermissionRequest` and `onUserInputRequest` for guarded flows instead of custom ad hoc prompting
|
|
160
172
|
- use `fs.watch` or `watchFile` when the extension should react to manual file edits or workspace artifacts such as `plan.md`
|
package/dist/extension.mjs
CHANGED
|
@@ -3736,13 +3736,21 @@ var SOURCE = Object.freeze({
|
|
|
3736
3736
|
// src/session/port.mjs
|
|
3737
3737
|
function createSessionPort(initialSession = null) {
|
|
3738
3738
|
let session2 = initialSession;
|
|
3739
|
+
let idle = false;
|
|
3739
3740
|
function attach(nextSession) {
|
|
3740
3741
|
session2 = nextSession ?? null;
|
|
3742
|
+
idle = false;
|
|
3741
3743
|
return session2;
|
|
3742
3744
|
}
|
|
3743
3745
|
function current() {
|
|
3744
3746
|
return session2;
|
|
3745
3747
|
}
|
|
3748
|
+
function setIdle(nextIdle) {
|
|
3749
|
+
idle = nextIdle === true;
|
|
3750
|
+
}
|
|
3751
|
+
function isIdle() {
|
|
3752
|
+
return Boolean(session2) && idle === true;
|
|
3753
|
+
}
|
|
3746
3754
|
async function safeLog(message, options) {
|
|
3747
3755
|
if (!session2) {
|
|
3748
3756
|
return;
|
|
@@ -3787,6 +3795,8 @@ function createSessionPort(initialSession = null) {
|
|
|
3787
3795
|
return {
|
|
3788
3796
|
attach,
|
|
3789
3797
|
current,
|
|
3798
|
+
setIdle,
|
|
3799
|
+
isIdle,
|
|
3790
3800
|
log,
|
|
3791
3801
|
send,
|
|
3792
3802
|
sendAndWait,
|
|
@@ -4543,6 +4553,24 @@ function readLines(input, onLine) {
|
|
|
4543
4553
|
|
|
4544
4554
|
// src/emitter/lifecycle.mjs
|
|
4545
4555
|
function createLifecycle({ lineRouter, sessionPort }) {
|
|
4556
|
+
function isIdleEmitter(emitter) {
|
|
4557
|
+
return emitter?.runSchedule === RUN_SCHEDULE.IDLE;
|
|
4558
|
+
}
|
|
4559
|
+
function shouldSkipIdleScheduling(emitter) {
|
|
4560
|
+
return emitter.stopRequested || emitter.inFlight || !isIdleEmitter(emitter) || isTerminalEmitterStatus(emitter.status);
|
|
4561
|
+
}
|
|
4562
|
+
function shouldSkipActivityCancellation(emitter) {
|
|
4563
|
+
return !isIdleEmitter(emitter) || isTerminalEmitterStatus(emitter.status);
|
|
4564
|
+
}
|
|
4565
|
+
function waitForNextIdle(emitter) {
|
|
4566
|
+
emitter.status = EMITTER_STATUS.WAITING;
|
|
4567
|
+
}
|
|
4568
|
+
function prepareIdleEmitter(emitter) {
|
|
4569
|
+
waitForNextIdle(emitter);
|
|
4570
|
+
if (sessionPort.isIdle()) {
|
|
4571
|
+
scheduleIteration(emitter, IDLE_PROMPT_DELAY_MS);
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4546
4574
|
function wireStreams(emitter) {
|
|
4547
4575
|
const child = emitter.process;
|
|
4548
4576
|
emitter.stdoutReader = readLines(child.stdout, (line) => {
|
|
@@ -4671,6 +4699,10 @@ function createLifecycle({ lineRouter, sessionPort }) {
|
|
|
4671
4699
|
if (emitter.stopRequested || emitter.inFlight) {
|
|
4672
4700
|
return;
|
|
4673
4701
|
}
|
|
4702
|
+
if (isIdleEmitter(emitter) && !sessionPort.isIdle()) {
|
|
4703
|
+
emitter.status = EMITTER_STATUS.WAITING;
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4674
4706
|
emitter.inFlight = true;
|
|
4675
4707
|
emitter.status = EMITTER_STATUS.RUNNING;
|
|
4676
4708
|
emitter.runCount += 1;
|
|
@@ -4703,6 +4735,10 @@ function createLifecycle({ lineRouter, sessionPort }) {
|
|
|
4703
4735
|
);
|
|
4704
4736
|
return;
|
|
4705
4737
|
}
|
|
4738
|
+
if (isIdleEmitter(emitter)) {
|
|
4739
|
+
waitForNextIdle(emitter);
|
|
4740
|
+
return;
|
|
4741
|
+
}
|
|
4706
4742
|
emitter.status = EMITTER_STATUS.WAITING;
|
|
4707
4743
|
scheduleIteration(emitter, nextDelay(emitter));
|
|
4708
4744
|
return;
|
|
@@ -4746,6 +4782,10 @@ function createLifecycle({ lineRouter, sessionPort }) {
|
|
|
4746
4782
|
emitter,
|
|
4747
4783
|
`Emitter '${emitter.name}' queued ${emitter.emitterType} work (${scheduleLabel}) with ${describeEmitterWork(emitter)}.${firstRunLabel}`
|
|
4748
4784
|
);
|
|
4785
|
+
if (isIdleEmitter(emitter)) {
|
|
4786
|
+
prepareIdleEmitter(emitter);
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4749
4789
|
scheduleIteration(emitter, initialDelayMs);
|
|
4750
4790
|
}
|
|
4751
4791
|
function start(emitter) {
|
|
@@ -4778,7 +4818,25 @@ function createLifecycle({ lineRouter, sessionPort }) {
|
|
|
4778
4818
|
emitter.process.kill();
|
|
4779
4819
|
}
|
|
4780
4820
|
}
|
|
4781
|
-
|
|
4821
|
+
function onSessionIdle(emitter) {
|
|
4822
|
+
if (shouldSkipIdleScheduling(emitter)) {
|
|
4823
|
+
return;
|
|
4824
|
+
}
|
|
4825
|
+
scheduleIteration(emitter, IDLE_PROMPT_DELAY_MS);
|
|
4826
|
+
}
|
|
4827
|
+
function onSessionActivity(emitter) {
|
|
4828
|
+
if (shouldSkipActivityCancellation(emitter)) {
|
|
4829
|
+
return;
|
|
4830
|
+
}
|
|
4831
|
+
if (emitter.timer) {
|
|
4832
|
+
clearTimeout(emitter.timer);
|
|
4833
|
+
emitter.timer = null;
|
|
4834
|
+
}
|
|
4835
|
+
if (!emitter.inFlight) {
|
|
4836
|
+
emitter.status = EMITTER_STATUS.WAITING;
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
return { start, stop, onSessionIdle, onSessionActivity };
|
|
4782
4840
|
}
|
|
4783
4841
|
|
|
4784
4842
|
// src/emitter/supervisor.mjs
|
|
@@ -4914,6 +4972,16 @@ function createEmitterSupervisor({ streams, configStore, notifications, sessionP
|
|
|
4914
4972
|
function list() {
|
|
4915
4973
|
return [...emitters.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
4916
4974
|
}
|
|
4975
|
+
function onSessionIdle() {
|
|
4976
|
+
for (const emitter of emitters.values()) {
|
|
4977
|
+
lifecycle.onSessionIdle(emitter);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
function onSessionActivity() {
|
|
4981
|
+
for (const emitter of emitters.values()) {
|
|
4982
|
+
lifecycle.onSessionActivity(emitter);
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4917
4985
|
function has(name) {
|
|
4918
4986
|
return emitters.has(normalizeName(name));
|
|
4919
4987
|
}
|
|
@@ -4927,7 +4995,9 @@ function createEmitterSupervisor({ streams, configStore, notifications, sessionP
|
|
|
4927
4995
|
updateEventFilter,
|
|
4928
4996
|
list,
|
|
4929
4997
|
has,
|
|
4930
|
-
get
|
|
4998
|
+
get,
|
|
4999
|
+
onSessionIdle,
|
|
5000
|
+
onSessionActivity
|
|
4931
5001
|
};
|
|
4932
5002
|
}
|
|
4933
5003
|
|
|
@@ -6352,6 +6422,13 @@ function createProviderGateway(options = {}) {
|
|
|
6352
6422
|
// src/tap-runtime.mjs
|
|
6353
6423
|
function createCopilotChannelsRuntime(options = {}) {
|
|
6354
6424
|
let baseCwd = options.cwd ?? process.cwd();
|
|
6425
|
+
let cleanupSessionListeners = () => {
|
|
6426
|
+
};
|
|
6427
|
+
const resetSessionListeners = () => {
|
|
6428
|
+
cleanupSessionListeners();
|
|
6429
|
+
cleanupSessionListeners = () => {
|
|
6430
|
+
};
|
|
6431
|
+
};
|
|
6355
6432
|
const getBaseCwd = () => baseCwd;
|
|
6356
6433
|
const setBaseCwd = (next) => {
|
|
6357
6434
|
baseCwd = next;
|
|
@@ -6386,9 +6463,41 @@ function createCopilotChannelsRuntime(options = {}) {
|
|
|
6386
6463
|
sessionPort.registerTools(mergedTools);
|
|
6387
6464
|
void sessionPort.reloadExtension();
|
|
6388
6465
|
});
|
|
6466
|
+
const wireSessionListeners = (session2) => {
|
|
6467
|
+
resetSessionListeners();
|
|
6468
|
+
const unsubscribers = [
|
|
6469
|
+
session2.on("session.idle", () => {
|
|
6470
|
+
sessionPort.setIdle(true);
|
|
6471
|
+
supervisor.onSessionIdle();
|
|
6472
|
+
})
|
|
6473
|
+
];
|
|
6474
|
+
for (const eventType of [
|
|
6475
|
+
"session.start",
|
|
6476
|
+
"session.resume",
|
|
6477
|
+
"user.message",
|
|
6478
|
+
"assistant.message",
|
|
6479
|
+
"tool.execution_start",
|
|
6480
|
+
"tool.execution_complete",
|
|
6481
|
+
"session.error"
|
|
6482
|
+
]) {
|
|
6483
|
+
unsubscribers.push(session2.on(eventType, () => {
|
|
6484
|
+
sessionPort.setIdle(false);
|
|
6485
|
+
supervisor.onSessionActivity();
|
|
6486
|
+
}));
|
|
6487
|
+
}
|
|
6488
|
+
cleanupSessionListeners = () => {
|
|
6489
|
+
for (const unsubscribe of unsubscribers) {
|
|
6490
|
+
try {
|
|
6491
|
+
unsubscribe?.();
|
|
6492
|
+
} catch {
|
|
6493
|
+
}
|
|
6494
|
+
}
|
|
6495
|
+
};
|
|
6496
|
+
};
|
|
6389
6497
|
return {
|
|
6390
6498
|
attachSession: (nextSession) => {
|
|
6391
6499
|
sessionPort.attach(nextSession);
|
|
6500
|
+
wireSessionListeners(nextSession);
|
|
6392
6501
|
if (!gateway.isRunning()) {
|
|
6393
6502
|
try {
|
|
6394
6503
|
gateway.start();
|
|
@@ -6399,6 +6508,7 @@ function createCopilotChannelsRuntime(options = {}) {
|
|
|
6399
6508
|
tools,
|
|
6400
6509
|
hooks,
|
|
6401
6510
|
stopAllEmitters: async () => {
|
|
6511
|
+
resetSessionListeners();
|
|
6402
6512
|
gateway.stop();
|
|
6403
6513
|
await supervisor.stopAll();
|
|
6404
6514
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: create-provider
|
|
2
|
+
name: tap-create-provider
|
|
3
3
|
description: "Create an external tool provider that connects to ※ tap. Use when the user says 'create a provider', 'build a provider', 'scaffold a provider', 'add an external tool', 'connect a service to tap', or wants to extend Copilot with tools written in any language."
|
|
4
4
|
argument-hint: "<what the provider should do>"
|
|
5
5
|
user-invocable: true
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tap-goal
|
|
3
|
+
description: "Run an autonomous goal loop. Use when the user says 'goal', 'keep working until done', 'work autonomously', 'iterate until complete', or wants long-horizon progress toward an objective."
|
|
4
|
+
argument-hint: "<objective>"
|
|
5
|
+
user-invocable: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Create an idle PromptEmitter with `tap_start_emitter` that keeps advancing one explicit objective until the goal is achieved, blocked, stopped, or the iteration limit is reached.
|
|
9
|
+
|
|
10
|
+
Use these goal-loop rules:
|
|
11
|
+
|
|
12
|
+
- Goals are explicit; do not infer one from ordinary user tasks.
|
|
13
|
+
- A bare goal command reports the current goal state.
|
|
14
|
+
- Control commands are user-owned (`status`, `stop`, `resume`, `clear`, `replace`).
|
|
15
|
+
- The model can complete a goal only when the objective is actually achieved.
|
|
16
|
+
- Runtime budget exhaustion is not proof of completion; only achieving the objective marks completion.
|
|
17
|
+
|
|
18
|
+
## Expected input
|
|
19
|
+
|
|
20
|
+
Interpret the invocation as one of:
|
|
21
|
+
|
|
22
|
+
1. No arguments — show current `goal-*` emitters with `tap_list_emitters`.
|
|
23
|
+
2. A control command — `status`, `stop`, `resume`, `clear`, or `replace`.
|
|
24
|
+
3. Otherwise, the full invocation is the goal objective.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
/tap-goal migrate the repo to the new API and keep going until tests pass
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
means:
|
|
33
|
+
|
|
34
|
+
- `objective = "migrate the repo to the new API and keep going until tests pass"`
|
|
35
|
+
|
|
36
|
+
If the objective is missing or too vague, ask the user for a concrete objective instead of guessing.
|
|
37
|
+
|
|
38
|
+
If another `goal-*` emitter already exists, ask before replacing it unless the user explicitly said `replace`.
|
|
39
|
+
|
|
40
|
+
## What to create
|
|
41
|
+
|
|
42
|
+
Use `tap_start_emitter` to create a **PromptEmitter**:
|
|
43
|
+
|
|
44
|
+
- `prompt` — a fully self-contained goal-loop prompt using the template below.
|
|
45
|
+
- `every = "idle"` — the loop advances only when the session is idle.
|
|
46
|
+
- `scope = "temporary"`, `managedBy = "modelOwned"`.
|
|
47
|
+
- `subscribe = false` — PromptEmitter output already reaches the session through `session.send()`.
|
|
48
|
+
- `maxRuns` — use the user's requested budget if provided; otherwise default to `50`.
|
|
49
|
+
- Name the emitter after the objective, prefixed with `goal-` (for example `goal-api-migration`).
|
|
50
|
+
- The EventStream is created automatically with the same name.
|
|
51
|
+
|
|
52
|
+
Do not set EventFilter rules. PromptEmitters dispatch their prompts fire-and-forget through `session.send()`, so their output bypasses line filtering. EventFilter rules would not affect goal-loop output.
|
|
53
|
+
|
|
54
|
+
## Goal-loop prompt template
|
|
55
|
+
|
|
56
|
+
Write the prompt so it stands alone because it will run later without the original chat context:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
You are running a tap-goal autonomous goal loop.
|
|
60
|
+
|
|
61
|
+
Goal:
|
|
62
|
+
<untrusted_objective>
|
|
63
|
+
<objective>
|
|
64
|
+
</untrusted_objective>
|
|
65
|
+
|
|
66
|
+
Emitter name: <goal-emitter-name>
|
|
67
|
+
Iteration budget: <max-runs>
|
|
68
|
+
|
|
69
|
+
At the start of each iteration:
|
|
70
|
+
1. Call tap_list_emitters and locate the emitter entry in the returned list whose name is exactly '<goal-emitter-name>'.
|
|
71
|
+
2. Read its current runs and maxRuns values.
|
|
72
|
+
3. If the emitter is missing, report that the goal loop is no longer running and stop.
|
|
73
|
+
4. Estimate remaining iterations.
|
|
74
|
+
|
|
75
|
+
Auto-steering rules:
|
|
76
|
+
- If remaining iterations are low (3 or fewer), switch into wrap-up mode.
|
|
77
|
+
- In wrap-up mode, prefer finishing the smallest high-value task, validating what changed, and leaving a precise handoff.
|
|
78
|
+
- If only 1 iteration remains and the goal is not complete, do not start broad new work. Leave the best concise handoff you can.
|
|
79
|
+
- Do not treat budget exhaustion as success.
|
|
80
|
+
|
|
81
|
+
On this iteration:
|
|
82
|
+
1. Briefly assess current progress toward the goal and the remaining iteration budget.
|
|
83
|
+
2. If the goal is already achieved, call tap_stop_emitter for '<goal-emitter-name>' with scope='temporary', report that the goal is complete, and stop.
|
|
84
|
+
3. If the goal is blocked by missing information, permissions, failing external systems, or an unsafe action, report the blocker, call tap_stop_emitter for '<goal-emitter-name>' with scope='temporary', and stop.
|
|
85
|
+
4. Otherwise, choose the next smallest useful action toward the goal that fits the remaining budget and perform it.
|
|
86
|
+
5. Validate the action using the repository's existing checks when relevant.
|
|
87
|
+
6. End with a concise progress update, what remains, and the best next step if the loop stops before completion.
|
|
88
|
+
|
|
89
|
+
Safety rules:
|
|
90
|
+
- Do not make unrelated changes.
|
|
91
|
+
- Do not mark the goal complete unless the objective is actually achieved and no required work remains.
|
|
92
|
+
- Do not treat reaching the iteration budget as success.
|
|
93
|
+
- Do not continue if the next step requires explicit user approval.
|
|
94
|
+
- Prefer small reversible steps.
|
|
95
|
+
- Stop yourself when done or blocked; do not rely on the user to notice.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Substitute the real objective, emitter name, and max iteration count before passing the prompt to `tap_start_emitter`.
|
|
99
|
+
|
|
100
|
+
## Required behavior
|
|
101
|
+
|
|
102
|
+
When this skill is invoked:
|
|
103
|
+
|
|
104
|
+
1. Parse the goal objective and any explicit iteration budget.
|
|
105
|
+
2. For a bare `/tap-goal` or `/tap-goal status`, call `tap_list_emitters`, summarize any `goal-*` emitters, and stop.
|
|
106
|
+
3. If the user is asking to stop, cancel, or clear an existing goal:
|
|
107
|
+
- call `tap_list_emitters` and look for `goal-*` emitters
|
|
108
|
+
- if the user named a specific goal emitter, stop that one
|
|
109
|
+
- otherwise, if exactly one `goal-*` emitter exists, stop it
|
|
110
|
+
- if none exist, report that no goal loop is running
|
|
111
|
+
- if multiple exist and the user did not name one, ask them to choose one after showing `/tap-goal status`
|
|
112
|
+
- when you do stop one, call `tap_stop_emitter` with its exact name and confirm that it will not fire again
|
|
113
|
+
4. If the user is asking to pause an existing goal, explain that pausing is not supported for goal loops because idle PromptEmitters do not preserve resumable internal state. Offer to stop the loop instead. Only call `tap_stop_emitter` if they confirm; otherwise take no action and leave the goal loop running.
|
|
114
|
+
5. If the user is asking to resume a goal, create a new `/tap-goal` loop with the resumed objective; ask for the objective if it is not clear.
|
|
115
|
+
6. Before creating a new goal, check for existing `goal-*` emitters. If one exists and the user did not explicitly ask to replace it, ask for confirmation before starting another goal loop.
|
|
116
|
+
7. If the user wants the loop to keep nudging the session even while Copilot stays busy in autopilot-style work, explain that idle goal loops may not fire until the session becomes idle. Suggest a timed PromptEmitter or hook/session-injector based delivery instead.
|
|
117
|
+
8. Otherwise, create the idle PromptEmitter using the template above.
|
|
118
|
+
9. Confirm to the user:
|
|
119
|
+
- Goal emitter name
|
|
120
|
+
- EventStream name
|
|
121
|
+
- Objective
|
|
122
|
+
- Max iteration count
|
|
123
|
+
- That it will advance when the session is idle and stop itself when complete or blocked
|
|
124
|
+
10. Stop there. Do not immediately perform the first goal iteration unless the user explicitly asks you to start working now.
|
|
125
|
+
|
|
126
|
+
## Iteration budget
|
|
127
|
+
|
|
128
|
+
Idle goal loops must always have `maxRuns`.
|
|
129
|
+
|
|
130
|
+
- If the user gives a budget, use it.
|
|
131
|
+
- Otherwise, default to `50`.
|
|
132
|
+
- If the objective is large, tell the user they can invoke `/tap-goal` again with a higher budget.
|
|
133
|
+
|
|
134
|
+
## Persistence
|
|
135
|
+
|
|
136
|
+
Default goal loops are temporary. If the user explicitly asks for a goal to survive future sessions, set `scope = "persistent"` and `autoStart = true`, but warn that long-running persistent goals should be used carefully because they will resume automatically on the next session start.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: loop
|
|
2
|
+
name: tap-loop
|
|
3
3
|
description: "Run a prompt on a recurring schedule or when idle. Use when the user says 'loop', 'every 5 minutes', 'check periodically', 'keep watching', 'repeat this', 'run when idle', or wants any prompt to re-run automatically."
|
|
4
4
|
argument-hint: "<interval|idle> <prompt>"
|
|
5
5
|
user-invocable: true
|
|
@@ -17,7 +17,7 @@ Interpret the invocation as:
|
|
|
17
17
|
Example (timed):
|
|
18
18
|
|
|
19
19
|
```text
|
|
20
|
-
/loop 5m check the deploy
|
|
20
|
+
/tap-loop 5m check the deploy
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
means:
|
|
@@ -28,7 +28,7 @@ means:
|
|
|
28
28
|
Example (idle):
|
|
29
29
|
|
|
30
30
|
```text
|
|
31
|
-
/loop idle check the deploy
|
|
31
|
+
/tap-loop idle check the deploy
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
means:
|
|
@@ -36,7 +36,7 @@ means:
|
|
|
36
36
|
- `every = "idle"` (re-runs whenever the session is idle)
|
|
37
37
|
- `prompt = "check the deploy"`
|
|
38
38
|
|
|
39
|
-
Timed emitters fire immediately, then repeat on the interval. Idle emitters
|
|
39
|
+
Timed emitters fire immediately, then repeat on the interval. Idle emitters wait for the session to become idle, then re-fire on later `session.idle` transitions (with a short delay between runs to avoid monopolizing the session).
|
|
40
40
|
|
|
41
41
|
## Max iterations
|
|
42
42
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: monitor
|
|
2
|
+
name: tap-monitor
|
|
3
3
|
description: "Start a self-tuning command monitor. Use when the user says 'monitor', 'watch', 'tail', 'track', 'keep an eye on', or wants a shell command to run continuously while Copilot automatically reviews and tunes the output filters over time."
|
|
4
4
|
argument-hint: "<shell-command>"
|
|
5
5
|
user-invocable: true
|
|
@@ -14,7 +14,7 @@ The entire invocation is the shell command to run continuously.
|
|
|
14
14
|
Example:
|
|
15
15
|
|
|
16
16
|
```text
|
|
17
|
-
/monitor tail -f /var/log/app.log
|
|
17
|
+
/tap-monitor tail -f /var/log/app.log
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
means:
|
package/dist/version.json
CHANGED