copilot-tap-extension 2.0.0 → 2.0.2
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 +24 -1
- package/bin/install.mjs +6 -0
- package/dist/copilot-instructions.md +16 -4
- package/dist/extension.mjs +113 -3
- package/dist/skills/tap-goal/SKILL.md +136 -0
- package/dist/skills/tap-loop/SKILL.md +1 -1
- 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 `/tap-loop` skill, the `/tap-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
|
|
|
@@ -111,6 +111,8 @@ Once inside the session, describe what you want in natural language. You can als
|
|
|
111
111
|
|
|
112
112
|
> _"/tap-monitor tail -f /var/log/app.log"_
|
|
113
113
|
|
|
114
|
+
> _"/tap-goal migrate the repo to the new API and keep going until tests pass"_
|
|
115
|
+
|
|
114
116
|
> _"Tail the API logs, inject errors, drop health checks"_
|
|
115
117
|
|
|
116
118
|
The agent translates these into emitter and filter configurations behind the scenes.
|
|
@@ -205,6 +207,26 @@ Use `/tap-loop idle` to re-run a prompt whenever the session has nothing else to
|
|
|
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:
|
|
@@ -224,6 +246,7 @@ Rules can be added or changed while the emitter is running. You never need to re
|
|
|
224
246
|
extensions/tap/extension.mjs # extension entry point (loads the runtime)
|
|
225
247
|
skills/tap-loop/ # /tap-loop skill for scheduled and idle prompts
|
|
226
248
|
skills/tap-monitor/ # /tap-monitor skill for self-tuning command monitors
|
|
249
|
+
skills/tap-goal/ # /tap-goal skill for autonomous goal loops
|
|
227
250
|
skills/tap-create-provider/ # /tap-create-provider skill for scaffolding external tool providers
|
|
228
251
|
copilot-instructions.md # agent guidance for using this extension
|
|
229
252
|
src/
|
package/bin/install.mjs
CHANGED
|
@@ -42,6 +42,7 @@ Installs:
|
|
|
42
42
|
skills/tap-loop/SKILL.md The /tap-loop skill for prompt-based loops
|
|
43
43
|
skills/tap-create-provider/SKILL.md The /tap-create-provider skill for scaffolding providers
|
|
44
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
|
}
|
|
@@ -218,6 +219,11 @@ function install(flags) {
|
|
|
218
219
|
dest: path.join(targetRoot, "skills", "tap-monitor", "SKILL.md"),
|
|
219
220
|
label: "skills/tap-monitor/SKILL.md"
|
|
220
221
|
},
|
|
222
|
+
{
|
|
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"
|
|
226
|
+
},
|
|
221
227
|
{
|
|
222
228
|
src: path.join(distDir, "copilot-instructions.md"),
|
|
223
229
|
dest: path.join(targetRoot, "copilot-instructions.md"),
|
|
@@ -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,17 +144,29 @@ 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 + `
|
|
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`)
|
|
148
149
|
|
|
149
150
|
This is the closest analogue to Claude's session-scoped `/tap-loop` behavior in this extension.
|
|
150
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.
|
|
162
|
+
|
|
151
163
|
## Borrow from the official SDK examples
|
|
152
164
|
|
|
153
165
|
When working on the extension itself, not just using its emitter tools, prefer these SDK patterns:
|
|
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,
|
|
@@ -3797,7 +3807,7 @@ function createSessionPort(initialSession = null) {
|
|
|
3797
3807
|
|
|
3798
3808
|
// src/util/normalize.mjs
|
|
3799
3809
|
function normalizeName(value, fallback = "") {
|
|
3800
|
-
const normalized = String(value ?? "").trim().toLowerCase().replace(/[
|
|
3810
|
+
const normalized = String(value ?? "").normalize("NFKC").trim().toLowerCase().replace(/[^\p{L}\p{N}\p{M}._-]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
3801
3811
|
return normalized || fallback;
|
|
3802
3812
|
}
|
|
3803
3813
|
function normalizeLifespan(value, fallback = LIFESPAN.TEMPORARY) {
|
|
@@ -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
|
},
|
|
@@ -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.
|
|
@@ -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
|
|
package/dist/version.json
CHANGED