copilot-tap-extension 1.1.2 → 1.1.4

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 `/loop` 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 `/loop` skill, the `/monitor` 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
 
@@ -109,6 +109,8 @@ Once inside the session, describe what you want in natural language. You can als
109
109
 
110
110
  > _"/loop 5m check for new PR review comments"_
111
111
 
112
+ > _"/monitor tail -f /var/log/app.log"_
113
+
112
114
  > _"Tail the API logs, inject errors, drop health checks"_
113
115
 
114
116
  The agent translates these into emitter and filter configurations behind the scenes.
@@ -171,6 +173,17 @@ Tell Copilot to watch a log, build, or command. It creates a CommandEmitter, fil
171
173
 
172
174
  You keep coding. Twenty minutes later, Copilot interrupts: "Run 48291: deployment rollback triggered on prod."
173
175
 
176
+ **Monitor a command with self-tuning filters**
177
+
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.
179
+
180
+ ```
181
+ /monitor tail -f /var/log/app.log
182
+ /monitor 10m docker logs -f mycontainer
183
+ ```
184
+
185
+ 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.
186
+
174
187
  **Loop a prompt on a schedule**
175
188
 
176
189
  A PromptEmitter re-runs an agent prompt at a fixed interval. Useful for PR comments, CI status, or ticket queues.
@@ -210,6 +223,7 @@ Rules can be added or changed while the emitter is running. You never need to re
210
223
  .github/
211
224
  extensions/tap/extension.mjs # extension entry point (loads the runtime)
212
225
  skills/loop/ # /loop skill for scheduled and idle prompts
226
+ skills/monitor/ # /monitor skill for self-tuning command monitors
213
227
  copilot-instructions.md # agent guidance for using this extension
214
228
  src/
215
229
  emitter/ # supervisor, lifecycle, spawn, line router
package/bin/install.mjs CHANGED
@@ -41,6 +41,7 @@ Installs:
41
41
  extensions/tap/version.json Installed version metadata
42
42
  skills/loop/SKILL.md The /loop skill for prompt-based loops
43
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
44
45
  copilot-instructions.md Agent instructions for using ※ tap
45
46
  `);
46
47
  }
@@ -178,6 +179,11 @@ function install(flags) {
178
179
  dest: path.join(targetRoot, "skills", "create-provider", "SKILL.md"),
179
180
  label: "skills/create-provider/SKILL.md"
180
181
  },
182
+ {
183
+ src: path.join(distDir, "skills", "monitor", "SKILL.md"),
184
+ dest: path.join(targetRoot, "skills", "monitor", "SKILL.md"),
185
+ label: "skills/monitor/SKILL.md"
186
+ },
181
187
  {
182
188
  src: path.join(distDir, "copilot-instructions.md"),
183
189
  dest: path.join(targetRoot, "copilot-instructions.md"),
@@ -3851,6 +3851,20 @@ function clampLimit(value, fallback = 20) {
3851
3851
  function nowIso() {
3852
3852
  return (/* @__PURE__ */ new Date()).toISOString();
3853
3853
  }
3854
+ function parseIntervalSchedule(value) {
3855
+ if (!Array.isArray(value) || value.length === 0) {
3856
+ return null;
3857
+ }
3858
+ return value.map((item, index) => {
3859
+ const parsed = parseLoopInterval(item);
3860
+ if (parsed === null || parsed.idle === true || !Number.isFinite(parsed.ms) || parsed.ms <= 0) {
3861
+ throw new Error(
3862
+ `Invalid interval schedule entry at index ${index}: '${item}'. Schedule entries must be non-blank intervals greater than 0 and cannot be 'idle'.`
3863
+ );
3864
+ }
3865
+ return parsed;
3866
+ });
3867
+ }
3854
3868
  function parseLoopInterval(value) {
3855
3869
  if (value === void 0 || value === null || String(value).trim() === "") {
3856
3870
  return null;
@@ -4306,7 +4320,11 @@ function buildEmitterState(spec, baseCwd, defaults = {}) {
4306
4320
  if (command && prompt) {
4307
4321
  throw new Error(`Emitter '${name}' cannot define both command and prompt. Choose one emitter type.`);
4308
4322
  }
4309
- const interval = parseLoopInterval(spec.every);
4323
+ const schedule = parseIntervalSchedule(spec.everySchedule);
4324
+ if (schedule && spec.every != null && String(spec.every).trim() !== "") {
4325
+ throw new Error(`Emitter '${name}': 'every' and 'everySchedule' are mutually exclusive. Use one or the other.`);
4326
+ }
4327
+ const interval = schedule ? null : parseLoopInterval(spec.every);
4310
4328
  const lifespan = normalizeLifespan(spec.scope, defaults.scope ?? LIFESPAN.TEMPORARY);
4311
4329
  const ownership = normalizeOwnership(spec.managedBy, defaults.managedBy ?? OWNERSHIP.MODEL_OWNED);
4312
4330
  const eventFilter = createEventFilter(
@@ -4321,7 +4339,7 @@ function buildEmitterState(spec, baseCwd, defaults = {}) {
4321
4339
  throw new Error(`Emitter '${name}': every='idle' is only valid for prompt emitters, not command emitters.`);
4322
4340
  }
4323
4341
  runSchedule = RUN_SCHEDULE.IDLE;
4324
- } else if (interval) {
4342
+ } else if (interval || schedule) {
4325
4343
  runSchedule = RUN_SCHEDULE.TIMED;
4326
4344
  } else if (prompt) {
4327
4345
  runSchedule = RUN_SCHEDULE.ONE_TIME;
@@ -4336,8 +4354,10 @@ function buildEmitterState(spec, baseCwd, defaults = {}) {
4336
4354
  prompt: prompt || null,
4337
4355
  emitterType,
4338
4356
  runSchedule,
4339
- every: interval?.text ?? null,
4340
- everyMs: interval?.ms ?? null,
4357
+ every: interval?.text ?? (schedule ? schedule[0].text : null),
4358
+ everyMs: interval?.ms ?? (schedule ? schedule[0].ms : null),
4359
+ everySchedule: schedule ? schedule.map((s) => s.text) : null,
4360
+ everyScheduleMs: schedule ? schedule.map((s) => s.ms) : null,
4341
4361
  requestedCwd: spec.cwd ?? null,
4342
4362
  cwd: resolveRequestedCwd(baseCwd, spec.cwd),
4343
4363
  stream: normalizeName(spec.channel, name),
@@ -4446,15 +4466,16 @@ function formatRunningEmitter(emitter, stream) {
4446
4466
  return [
4447
4467
  `- ${emitter.name}:`,
4448
4468
  ` status=${emitter.status}`,
4449
- ` scope=${emitter.scope}`,
4450
- ` managedBy=${emitter.managedBy}`,
4469
+ ` scope=${emitter.lifespan}`,
4470
+ ` managedBy=${emitter.ownership}`,
4451
4471
  ` emitterType=${emitter.emitterType}`,
4452
4472
  ` runSchedule=${emitter.runSchedule}`,
4453
- ` stream=${emitter.channel}`,
4473
+ ` stream=${emitter.stream}`,
4454
4474
  ` sessionInjector=${stream?.sessionInjector?.enabled ? "on" : "off"}`,
4455
4475
  ` cwd=${emitter.cwd}`,
4456
4476
  ` ${describeEmitterWork(emitter)}`,
4457
- emitter.every ? ` every=${emitter.every}` : null,
4477
+ emitter.everySchedule ? ` everySchedule=[${emitter.everySchedule.join(", ")}]` : null,
4478
+ emitter.every && !emitter.everySchedule ? ` every=${emitter.every}` : null,
4458
4479
  emitter.maxRuns ? ` maxRuns=${emitter.maxRuns}` : null,
4459
4480
  ` autoStart=${emitter.autoStart}`,
4460
4481
  ` includeStderr=${emitter.includeStderr}`,
@@ -4636,6 +4657,16 @@ function createLifecycle({ lineRouter, sessionPort }) {
4636
4657
  void runScheduledIteration(emitter);
4637
4658
  }, delayMs);
4638
4659
  }
4660
+ function nextDelay(emitter) {
4661
+ if (emitter.runSchedule === RUN_SCHEDULE.IDLE) {
4662
+ return IDLE_PROMPT_DELAY_MS;
4663
+ }
4664
+ if (emitter.everyScheduleMs) {
4665
+ const idx = Math.min(Math.max(0, emitter.runCount - 1), emitter.everyScheduleMs.length - 1);
4666
+ return emitter.everyScheduleMs[idx];
4667
+ }
4668
+ return emitter.everyMs;
4669
+ }
4639
4670
  async function runScheduledIteration(emitter) {
4640
4671
  if (emitter.stopRequested || emitter.inFlight) {
4641
4672
  return;
@@ -4673,13 +4704,12 @@ function createLifecycle({ lineRouter, sessionPort }) {
4673
4704
  return;
4674
4705
  }
4675
4706
  emitter.status = EMITTER_STATUS.WAITING;
4676
- const delay = emitter.runSchedule === RUN_SCHEDULE.IDLE ? IDLE_PROMPT_DELAY_MS : emitter.everyMs;
4677
- scheduleIteration(emitter, delay);
4707
+ scheduleIteration(emitter, nextDelay(emitter));
4678
4708
  return;
4679
4709
  }
4680
4710
  if (result.deferred) {
4681
4711
  emitter.status = EMITTER_STATUS.WAITING;
4682
- const retryDelay = emitter.runSchedule === RUN_SCHEDULE.IDLE ? IDLE_PROMPT_BACKOFF_MS : emitter.everyMs;
4712
+ const retryDelay = emitter.runSchedule === RUN_SCHEDULE.IDLE ? IDLE_PROMPT_BACKOFF_MS : nextDelay(emitter);
4683
4713
  if (emitter.runSchedule !== RUN_SCHEDULE.IDLE) {
4684
4714
  lineRouter.appendSystemMessage(
4685
4715
  emitter,
@@ -4705,11 +4735,11 @@ function createLifecycle({ lineRouter, sessionPort }) {
4705
4735
  return;
4706
4736
  }
4707
4737
  emitter.status = EMITTER_STATUS.WAITING;
4708
- const failRetryDelay = emitter.runSchedule === RUN_SCHEDULE.IDLE ? IDLE_PROMPT_BACKOFF_MS : emitter.everyMs;
4738
+ const failRetryDelay = emitter.runSchedule === RUN_SCHEDULE.IDLE ? IDLE_PROMPT_BACKOFF_MS : nextDelay(emitter);
4709
4739
  scheduleIteration(emitter, failRetryDelay);
4710
4740
  }
4711
4741
  function startScheduled(emitter) {
4712
- const scheduleLabel = emitter.runSchedule === RUN_SCHEDULE.TIMED ? `every ${emitter.every}` : emitter.runSchedule === RUN_SCHEDULE.IDLE ? "when idle" : RUN_SCHEDULE.ONE_TIME;
4742
+ const scheduleLabel = emitter.runSchedule === RUN_SCHEDULE.TIMED ? emitter.everySchedule ? `backoff [${emitter.everySchedule.join(", ")}]` : `every ${emitter.every}` : emitter.runSchedule === RUN_SCHEDULE.IDLE ? "when idle" : RUN_SCHEDULE.ONE_TIME;
4713
4743
  const initialDelayMs = 0;
4714
4744
  const firstRunLabel = "";
4715
4745
  lineRouter.appendSystemMessage(
@@ -5088,6 +5118,7 @@ function createEmitterTools({ streams, configStore, supervisor, getBaseCwd }) {
5088
5118
  channel: { type: "string", description: "EventStream to receive accepted events." },
5089
5119
  cwd: { type: "string", description: "Optional working directory relative to the session cwd." },
5090
5120
  every: { type: "string", description: "Optional repeat interval like 30s, 5m, 2h, or 1d. Use 'idle' for prompts that re-run whenever the session is idle. When omitted, commands run continuously and prompts run once." },
5121
+ everySchedule: { type: "array", minItems: 1, items: { type: "string" }, description: "Optional backoff schedule \u2014 an ordered non-empty list of interval strings (e.g. ['10s','20s','30s','1m','2m','5m','10m']). The emitter uses each interval in sequence, then repeats the last one forever. Overrides 'every' when provided. Cannot be 'idle' entries." },
5091
5122
  scope: { type: "string", description: "Use 'temporary' for session-only or 'persistent' to write config." },
5092
5123
  managedBy: { type: "string", description: "Ownership label: 'userOwned' or 'modelOwned'." },
5093
5124
  autoStart: { type: "boolean", description: "When persistent, whether the emitter should auto-start next session." },
@@ -5122,7 +5153,8 @@ function createEmitterTools({ streams, configStore, supervisor, getBaseCwd }) {
5122
5153
  `ownership=${emitter.ownership}`,
5123
5154
  `emitterType=${emitter.emitterType}`,
5124
5155
  `runSchedule=${emitter.runSchedule}`,
5125
- emitter.every ? `every=${emitter.every}` : null,
5156
+ emitter.everySchedule ? `everySchedule=[${emitter.everySchedule.join(", ")}]` : null,
5157
+ emitter.every && !emitter.everySchedule ? `every=${emitter.every}` : null,
5126
5158
  emitter.maxRuns ? `maxRuns=${emitter.maxRuns}` : null,
5127
5159
  `stream=${emitter.stream}`,
5128
5160
  `sessionInjector=${streams.ensure(emitter.stream).sessionInjector.enabled ? "on" : "off"}`,
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: monitor
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
+ argument-hint: "<shell-command>"
5
+ user-invocable: true
6
+ ---
7
+
8
+ Start a CommandEmitter for the given shell command paired with a companion PromptEmitter that periodically reads the stream and dynamically updates the filter expressions with `tap_set_event_filter`.
9
+
10
+ ## Expected input
11
+
12
+ The entire invocation is the shell command to run continuously.
13
+
14
+ Example:
15
+
16
+ ```text
17
+ /monitor tail -f /var/log/app.log
18
+ ```
19
+
20
+ means:
21
+
22
+ - `command = "tail -f /var/log/app.log"`
23
+
24
+ If the command is missing, ask the user for it instead of guessing.
25
+
26
+ ## What to create
27
+
28
+ ### 1. CommandEmitter — the live command stream
29
+
30
+ Use `tap_start_emitter` to start the CommandEmitter:
31
+
32
+ - `command` — the user's shell command.
33
+ - No `every` field — commands default to continuous (always running).
34
+ - Initial `notifyPattern` — derive a sensible starting pattern from the command context (e.g. `error|warn|fail|exception` for log tailing, or omit entirely and let the companion tune it on first review).
35
+ - `subscribe = true`, `delivery = "important"` — injected lines reach the session.
36
+ - `scope = "temporary"`, `managedBy = "modelOwned"` (unless the user asked for persistence).
37
+ - Name the emitter concisely after the command (e.g. `app-logs`, `docker-mycontainer`).
38
+ - The EventStream is created automatically with the same name.
39
+
40
+ ### 2. Companion PromptEmitter — the periodic filter reviewer
41
+
42
+ Use `tap_start_emitter` to start a second emitter immediately after the command emitter:
43
+
44
+ - `prompt` — a **fully self-contained** instruction (see template below).
45
+ - `everySchedule: ["10s", "20s", "30s", "1m", "2m", "5m", "10m"]` — backoff schedule: reviews start very frequent to validate the monitor quickly, then space out as it stabilises.
46
+ - `scope = "temporary"`, `managedBy = "modelOwned"`.
47
+ - Name it `<command-emitter-name>-review`.
48
+ - `subscribe = false` — review is internal housekeeping, not user-facing.
49
+ - `maxRuns` is optional. Only set it if the user explicitly requests a limit.
50
+
51
+ ### Companion prompt template
52
+
53
+ Write the companion prompt so it stands alone — it must reference the command emitter and stream by their exact names, because the companion runs independently with no surrounding context. Use this structure:
54
+
55
+ ```
56
+ Review the event stream for the '<command-emitter-name>' monitor and update its filters if needed.
57
+
58
+ Steps:
59
+ 1. Call tap_stream_history for stream '<stream-name>' (limit 50).
60
+ 2. If there are fewer than 5 entries, stop — not enough data to judge patterns yet.
61
+ 3. Scan the recent lines for recurring patterns:
62
+ - Lines that are always noise (timestamps-only, heartbeats, blank pings) → candidates for excludePattern.
63
+ - Lines that indicate important events (errors, warnings, state changes) → candidates for notifyPattern.
64
+ - Lines that are never relevant at all → candidates for tighter includePattern.
65
+ 4. Compare what you see against the current filter patterns for emitter '<command-emitter-name>'.
66
+ 5. Only update if the evidence clearly justifies a change (signal-to-noise is poor or a pattern is clearly wrong).
67
+ 6. If an update is needed, call tap_set_event_filter with the revised patterns for emitter '<command-emitter-name>'.
68
+ 7. Do not report your findings to the user unless you made a change. If you made a change, send one short message describing what you updated and why.
69
+ ```
70
+
71
+ Substitute the real emitter name and stream name into the prompt before passing it to `tap_start_emitter`.
72
+
73
+ ## Required behavior
74
+
75
+ When this skill is invoked:
76
+
77
+ 1. Parse the command from the invocation.
78
+ 2. Start the CommandEmitter.
79
+ 3. Start the companion PromptEmitter using the self-contained prompt template and the hardcoded backoff schedule.
80
+ 4. Confirm to the user:
81
+ - Command emitter name and stream.
82
+ - Initial filter patterns (or "none set — companion will tune on first review").
83
+ - Companion reviewer name and its review schedule (first check in 10s, backing off to 10m).
84
+ 5. Stop there — do not immediately inspect stream history or simulate a review.
85
+
86
+ ## Stopping the monitor
87
+
88
+ To stop monitoring, both emitters must be stopped:
89
+
90
+ ```
91
+ tap_stop_emitter '<command-emitter-name>'
92
+ tap_stop_emitter '<command-emitter-name>-review'
93
+ ```
94
+
95
+ If the user asks to stop monitoring, stop both in a single response.
96
+
97
+ ## Persistence
98
+
99
+ If the user explicitly asks to keep the monitor across sessions, set `scope = "persistent"` on **both** emitters. Say that both will be restored from config on the next session start.
100
+
101
+ ## Conservative filter updates
102
+
103
+ Remind the companion (via the prompt) to be conservative:
104
+
105
+ - Update only when there is clear evidence from at least 5 recent entries (the same minimum checked inside the companion prompt).
106
+ - Prefer broadening `notifyPattern` over narrowing it — missing a real event is worse than an extra notification.
107
+ - Never remove `notifyPattern` entirely unless the stream is provably silent.
108
+ - Do not change `includePattern` or `excludePattern` on every review cycle — only when a pattern is clearly wrong.
109
+
110
+ ## If the input is incomplete
111
+
112
+ If the invocation contains no recognisable shell command, ask the user for it.
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.1.2"
2
+ "version": "1.1.4"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-tap-extension",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Copilot CLI extension for background event emitters, event streams, and session injection.",
5
5
  "type": "module",
6
6
  "license": "MIT",