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 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 `runInterval`.
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 `runInterval` for a fixed session-scoped timed schedule
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 + `runInterval` for a fixed maintenance loop (timed)
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 tool lifecycle, assistant messages, session idle, and errors when you need event-driven behavior
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`
@@ -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(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
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
- return { start, stop };
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 fire immediately, then re-fire whenever the session becomes idle again (with a short delay between runs to avoid monopolizing the session).
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
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "2.0.0"
2
+ "version": "2.0.2"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-tap-extension",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Copilot CLI extension for background event emitters, event streams, and session injection.",
5
5
  "type": "module",
6
6
  "license": "MIT",