copilot-tap-extension 2.0.8 → 2.0.9

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.
Files changed (54) hide show
  1. package/README.md +2 -1
  2. package/SOUL.md +51 -0
  3. package/bin/install.mjs +2 -1
  4. package/dist/copilot-instructions.md +5 -0
  5. package/dist/extension.mjs +361 -20
  6. package/dist/version.json +1 -1
  7. package/docs/adr/0001-persistent-config-default-ownership.md +33 -0
  8. package/docs/adr/0002-local-provider-gateway-runtime-security.md +36 -0
  9. package/docs/adr/0003-emitter-delivery-lifecycle.md +68 -0
  10. package/docs/adr/0004-persistent-config-canonical-streams.md +86 -0
  11. package/docs/adr/0005-provider-sdk-push-and-dynamic-tools.md +48 -0
  12. package/docs/adr/0006-command-emitter-cwd-workspace-boundary.md +46 -0
  13. package/docs/adr/0007-runtime-session-workspace-context.md +62 -0
  14. package/docs/evals.md +41 -0
  15. package/docs/evolution-of-tap-icon.html +989 -0
  16. package/docs/providers.md +242 -0
  17. package/docs/recipes/adaptive-agent.md +303 -0
  18. package/docs/recipes/agent-brainstorm/100-extension-ideas.md +288 -0
  19. package/docs/recipes/agent-brainstorm/deep-ideas.md +216 -0
  20. package/docs/recipes/ambient-guardian.md +314 -0
  21. package/docs/recipes/browser-bridge.md +162 -0
  22. package/docs/recipes/codex-goals-for-tap-goal.md +136 -0
  23. package/docs/recipes/copilot-sdk-canvas.md +147 -0
  24. package/docs/recipes/deferred-cognition.md +310 -0
  25. package/docs/recipes/provider-integration-patterns.md +93 -0
  26. package/docs/recipes/provider-interface-advanced.md +1364 -0
  27. package/docs/recipes/provider-interface-core-profile.md +568 -0
  28. package/docs/recipes/tap-control-plane-roadmap.md +60 -0
  29. package/docs/recipes/universal-tool-gateway.md +202 -0
  30. package/docs/reference.md +229 -0
  31. package/docs/use-cases.md +348 -0
  32. package/package.json +4 -1
  33. package/providers/detour/README.md +84 -0
  34. package/providers/detour/bridge.js +219 -0
  35. package/providers/detour/index.mjs +322 -0
  36. package/providers/detour/package-lock.json +577 -0
  37. package/providers/detour/package.json +19 -0
  38. package/providers/detour/scripts/build.mjs +31 -0
  39. package/providers/detour/src/bridge.js +256 -0
  40. package/providers/detour/src/contracts.js +40 -0
  41. package/providers/detour/src/inspector.js +260 -0
  42. package/providers/detour/src/inspector.test.mjs +53 -0
  43. package/providers/detour/src/panel.js +465 -0
  44. package/providers/detour/src/provider-core.js +233 -0
  45. package/providers/detour/src/provider-core.test.mjs +185 -0
  46. package/providers/detour/src/react-context-core.js +143 -0
  47. package/providers/detour/src/react-context.js +44 -0
  48. package/providers/detour/src/react-context.test.mjs +41 -0
  49. package/providers/templates/README.md +23 -0
  50. package/providers/templates/ci-review-provider.mjs +46 -0
  51. package/providers/templates/detour-workflow-provider.mjs +41 -0
  52. package/providers/templates/jira-github-provider.mjs +42 -0
  53. package/providers/templates/provider-utils.mjs +45 -0
  54. package/providers/templates/sast-triage-provider.mjs +51 -0
@@ -0,0 +1,568 @@
1
+ # Provider Interface — Core Profile
2
+
3
+ The Core Profile is the minimal subset of the [Provider Interface v2](./provider-interface-v2.md).
4
+ It covers token-authenticated project providers that expose tools to a single Copilot session.
5
+ Read **only this document** to build a working provider.
6
+
7
+ ---
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ ┌─────────────────┐ WebSocket (JSON) ┌─────────────────┐
13
+ │ Gateway │◄── ws://localhost:9400 ──► │ Provider │
14
+ │ │ │ (your process) │
15
+ │ Owns Copilot SDK │ ── sessions ──────────► │ Knows nothing │
16
+ │ Runs WS server │ ◄── auth ───────────── │ about Copilot │
17
+ │ Registers tools │ ── hello.ack ─────────► │ Declares tools │
18
+ │ Dispatches calls │ ◄── hello ──────────── │ Handles calls │
19
+ │ │ ── tool.call ─────────► │ │
20
+ │ │ ◄── tool.result ────── │ │
21
+ │ │ ◄── push ───────────── │ Pushes events │
22
+ │ │ ◄── tools.update ───── │ Updates tools │
23
+ └─────────────────┘ └─────────────────┘
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Connection state machine
29
+
30
+ ```
31
+ AwaitAuth ──auth──► AwaitHello ──hello──► Bound ──goodbye/disconnect──► Disconnected
32
+ │ │ │
33
+ └── error ◄──────────┴── error ◄────────┘
34
+ ```
35
+
36
+ | State | Provider may send | Gateway sends |
37
+ |---|---|---|
38
+ | **AwaitAuth** | `auth` | `sessions` or `error` |
39
+ | **AwaitHello** | `hello` | `hello.ack` or `error` |
40
+ | **Bound** | `tool.result`, `push`, `tools.update`, `goodbye` | `tool.call`, `tool.cancel`, `session.lifecycle`, `error` |
41
+
42
+ ---
43
+
44
+ ## Authentication
45
+
46
+ The gateway generates a unique token for the running gateway instance. Providers can discover it from:
47
+
48
+ - `TAP_PROVIDER_TOKEN` when launched with the Copilot environment.
49
+ - `<COPILOT_HOME or ~/.copilot>/extensions/tap/.provider-token` when launched from a sibling terminal or by SDK auto-discovery.
50
+
51
+ The gateway writes the token file with restrictive permissions and removes it when the gateway stops. Provider sends the token as the first message:
52
+
53
+ ```json
54
+ { "type": "auth", "token": "ptk-a8f3..." }
55
+ ```
56
+
57
+ **Success →** gateway sends `sessions`. **Failure →** `error { code: "AUTH_FAILED" }`, connection closed.
58
+
59
+ ---
60
+
61
+ ## Messages (12 types)
62
+
63
+ ### `auth` — Provider → Gateway
64
+
65
+ First message. Required fields: `type`, `token`.
66
+
67
+ ### `sessions` — Gateway → Provider
68
+
69
+ Sent after successful auth. Provider picks one `id` for `hello`.
70
+
71
+ ```json
72
+ { "type": "sessions", "active": [{ "id": "abc123", "label": "PR #42", "cwd": "/code/foo" }] }
73
+ ```
74
+
75
+ ### `hello` — Provider → Gateway
76
+
77
+ Register tools, bind to a session.
78
+
79
+ ```json
80
+ {
81
+ "type": "hello", "name": "my-provider", "protocolVersion": 2,
82
+ "session": "abc123",
83
+ "tools": [{
84
+ "name": "greet", "description": "Say hello",
85
+ "parameters": { "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] }
86
+ }]
87
+ }
88
+ ```
89
+
90
+ | Field | Required | Description |
91
+ |---|---|---|
92
+ | `name` | yes | Stable provider identity |
93
+ | `protocolVersion` | yes | Must be `2` |
94
+ | `session` | yes | Session `id` from `sessions.active` |
95
+ | `tools` | no | Array of tool defs. Each: `name`, `description`, `parameters` (JSON Schema). Optional `timeout` (ms). |
96
+
97
+ **Success →** `hello.ack`. **Failure →** `error` (`INVALID_SESSION`, `UNSUPPORTED_VERSION`, `TOOL_CONFLICT`).
98
+
99
+ ### `hello.ack` — Gateway → Provider
100
+
101
+ Provider is now **Bound**.
102
+
103
+ ```json
104
+ { "type": "hello.ack", "protocolVersion": 2, "providerId": "p-8f3a", "sessionId": "abc123" }
105
+ ```
106
+
107
+ `providerId` is a stable debug ID included in subsequent `error` messages. `sessionId` echoes the session selected by `hello`.
108
+
109
+ ### `tool.call` — Gateway → Provider
110
+
111
+ ```json
112
+ { "type": "tool.call", "id": "call-123", "sessionId": "abc123", "tool": "greet", "args": { "name": "Alice" } }
113
+ ```
114
+
115
+ Provider **must** respond with exactly one `tool.result` for this `id`.
116
+
117
+ ### `tool.result` — Provider → Gateway
118
+
119
+ ```json
120
+ { "type": "tool.result", "id": "call-123", "data": "Hello, Alice!" }
121
+ ```
122
+
123
+ On failure, use `error` + `errorCode` instead of `data`:
124
+
125
+ ```json
126
+ { "type": "tool.result", "id": "call-123", "error": "Not found", "errorCode": "NOT_FOUND" }
127
+ ```
128
+
129
+ Error codes: `NOT_FOUND`, `TIMEOUT`, `CANCELLED`, `INTERNAL`. Exactly one of `data` or `error` should be present.
130
+
131
+ ### `push` — Provider → Gateway
132
+
133
+ Bound providers can push events to their selected session:
134
+
135
+ ```json
136
+ { "type": "push", "level": "inject", "event": "Page asks for help", "stream": "detour" }
137
+ ```
138
+
139
+ | Field | Required | Description |
140
+ |---|---|---|
141
+ | `level` | yes | `keep`, `surface`, or `inject` |
142
+ | `event` | yes | Non-empty event text |
143
+ | `stream` | no | EventStream name. Defaults to the provider name in the SDK. |
144
+ | `sessionId` | no | Must match the session selected in `hello` when supplied. |
145
+ | `metadata` | no | JSON object stored with the stream entry. |
146
+
147
+ Delivery:
148
+
149
+ - `keep` stores the event in the stream only.
150
+ - `surface` stores and logs the event to the Copilot timeline.
151
+ - `inject` stores, logs, and sends the event into the session.
152
+
153
+ ### `tools.update` — Provider → Gateway
154
+
155
+ Replace this provider's complete tool list while Bound:
156
+
157
+ ```json
158
+ {
159
+ "type": "tools.update",
160
+ "tools": [{
161
+ "name": "greet", "description": "Say hello",
162
+ "parameters": { "type": "object", "properties": { "name": { "type": "string" } } }
163
+ }]
164
+ }
165
+ ```
166
+
167
+ The `tools` field uses the same validation rules as `hello.tools` and remains capped at 100 tools. If `sessionId` is supplied it must match the session selected in `hello`.
168
+
169
+ Success is silent and triggers the same debounced `registerTools()` + extension reload path used for provider connect/disconnect. On failure, the gateway sends `error` and keeps the previous tool list active. In-flight calls to tools removed by a successful update are not cancelled; they continue to result, timeout, cancellation, or disconnect.
170
+
171
+ ### `tool.cancel` — Gateway → Provider
172
+
173
+ ```json
174
+ { "type": "tool.cancel", "id": "call-123", "sessionId": "abc123", "reason": "timeout" }
175
+ ```
176
+
177
+ Provider **must** respond: `{ "type": "tool.result", "id": "call-123", "error": "Cancelled", "errorCode": "CANCELLED" }`.
178
+
179
+ ### `session.lifecycle` — Gateway → Provider
180
+
181
+ Always sent, no opt-in. Three states:
182
+
183
+ ```json
184
+ { "type": "session.lifecycle", "sessionId": "abc123", "state": "started" }
185
+ { "type": "session.lifecycle", "sessionId": "abc123", "state": "idle" }
186
+ { "type": "session.lifecycle", "sessionId": "abc123", "state": "shutdown.pending", "deadline": 10000 }
187
+ ```
188
+
189
+ On `shutdown.pending`: clean up within `deadline` ms, then send `goodbye`. Gateway tears down after the deadline regardless.
190
+
191
+ ### `error` — Gateway → Provider
192
+
193
+ ```json
194
+ { "type": "error", "code": "INVALID_SESSION", "message": "Session def456 does not exist",
195
+ "replyTo": "hello", "providerId": "p-8f3a" }
196
+ ```
197
+
198
+ Fields: `code` (required), `message` (required), `replyTo` (optional), `providerId` (optional), `sessionId` (optional).
199
+
200
+ ### `goodbye` — Provider → Gateway
201
+
202
+ ```json
203
+ { "type": "goodbye", "reason": "shutting down" }
204
+ ```
205
+
206
+ Send before closing the WebSocket. `reason` is optional.
207
+
208
+ ---
209
+
210
+ ## Protocol rules
211
+
212
+ ### Forward compatibility
213
+
214
+ - **Ignore unknown fields** in any message.
215
+ - **Ignore unknown gateway→provider message types** (log and discard, do not disconnect).
216
+ - Gateway **rejects** unknown provider→gateway types with `error { code: "UNKNOWN_TYPE" }`.
217
+
218
+ ### Version negotiation
219
+
220
+ Provider sends `protocolVersion: 2` in `hello`. Gateway echoes its version in `hello.ack`. Mismatch → `error { code: "UNSUPPORTED_VERSION" }`, connection closed.
221
+
222
+ ### Ordering
223
+
224
+ Per-connection FIFO. Messages on a single WebSocket arrive in send order.
225
+
226
+ ### Tool call terminal semantics
227
+
228
+ Each `tool.call` has **one** terminal outcome. First terminal message wins:
229
+
230
+ 1. `tool.result` arrives → complete. Later duplicates silently ignored.
231
+ 2. `tool.cancel` sent → provider **must** reply `tool.result { errorCode: "CANCELLED" }`. Non-cancelled results arriving after cancel are ignored.
232
+ 3. Provider disconnects with in-flight calls → gateway returns `errorCode: "DISCONNECTED"` to Copilot. **Not** replayed on reconnect.
233
+ 4. Malformed JSON, oversized messages, or invalid `tool.result` messages that
234
+ cannot be correlated while calls are in flight fail fast. With one pending
235
+ call, the gateway rejects that call with the protocol/validation error. With
236
+ multiple pending calls, the gateway disconnects the provider and fails all
237
+ pending calls with `DISCONNECTED`.
238
+
239
+ ### Disconnect cleanup
240
+
241
+ On disconnect (clean or crash): all tools removed, all in-flight calls failed with `DISCONNECTED`. Reconnect = fresh start (re-auth, new `hello`).
242
+
243
+ ### Gateway crash recovery
244
+
245
+ Gateway is a detached background process. Crash = **all state lost**. Providers detect via `onclose`, reconnect fresh when a new gateway spawns.
246
+
247
+ ### Payload limits
248
+
249
+ | Limit | Value |
250
+ |---|---|
251
+ | `tool.result` max size | 5 MB |
252
+ | All other messages | 2 MB |
253
+ | Max tools per provider | 100 |
254
+
255
+ Exceeding → `error { code: "PAYLOAD_TOO_LARGE" }`.
256
+
257
+ If an oversized or malformed bound-provider message arrives while ambiguous
258
+ tool calls are pending, the terminal semantics above also apply so in-flight
259
+ calls cannot remain pending forever.
260
+
261
+ ---
262
+
263
+ ## Error codes
264
+
265
+ | Code | Fatal? | Recovery |
266
+ |---|---|---|
267
+ | `AUTH_FAILED` | **Yes** | Connection closes. Check `TAP_PROVIDER_TOKEN`. |
268
+ | `UNSUPPORTED_VERSION` | **Yes** | Connection closes. Update `protocolVersion`. |
269
+ | `INVALID_SESSION` | No | Pick a different session from the last `sessions` message. |
270
+ | `TOOL_CONFLICT` | No | Rename the tool, re-send `hello`. |
271
+ | `PAYLOAD_TOO_LARGE` | No | Reduce payload, retry. |
272
+ | `RATE_LIMITED` | No | Back off 1s, retry. |
273
+ | `INVALID_JSON` | No | Fix message, retry. |
274
+ | `UNKNOWN_TYPE` | No | Check `protocolVersion`. |
275
+
276
+ ---
277
+
278
+ ## Complete example — Node.js
279
+
280
+ ```js
281
+ import WebSocket from "ws";
282
+
283
+ const TOKEN = process.env.TAP_PROVIDER_TOKEN;
284
+
285
+ function connect() {
286
+ const ws = new WebSocket("ws://localhost:9400");
287
+
288
+ ws.on("open", () => {
289
+ ws.send(JSON.stringify({ type: "auth", token: TOKEN }));
290
+ });
291
+
292
+ ws.on("message", (raw) => {
293
+ const msg = JSON.parse(raw);
294
+
295
+ switch (msg.type) {
296
+ case "sessions":
297
+ if (!msg.active.length) { ws.close(); return; }
298
+ ws.send(JSON.stringify({
299
+ type: "hello", name: "example-provider", protocolVersion: 2,
300
+ session: msg.active[0].id,
301
+ tools: [{
302
+ name: "greet", description: "Greet someone by name",
303
+ parameters: {
304
+ type: "object",
305
+ properties: { name: { type: "string" } },
306
+ required: ["name"]
307
+ }
308
+ }]
309
+ }));
310
+ break;
311
+
312
+ case "hello.ack":
313
+ console.log(`Registered as ${msg.providerId}`);
314
+ break;
315
+
316
+ case "tool.call":
317
+ ws.send(JSON.stringify({
318
+ type: "tool.result", id: msg.id,
319
+ data: `Hello, ${msg.args.name}!`
320
+ }));
321
+ break;
322
+
323
+ case "tool.cancel":
324
+ ws.send(JSON.stringify({
325
+ type: "tool.result", id: msg.id,
326
+ error: "Cancelled", errorCode: "CANCELLED"
327
+ }));
328
+ break;
329
+
330
+ case "session.lifecycle":
331
+ if (msg.state === "shutdown.pending") {
332
+ ws.send(JSON.stringify({ type: "goodbye", reason: "session ending" }));
333
+ ws.close();
334
+ }
335
+ break;
336
+
337
+ case "error":
338
+ console.error(`Error [${msg.code}]: ${msg.message}`);
339
+ if (msg.code === "AUTH_FAILED" || msg.code === "UNSUPPORTED_VERSION") ws.close();
340
+ break;
341
+
342
+ default: break; // forward compat: ignore unknown types
343
+ }
344
+ });
345
+
346
+ ws.on("close", () => setTimeout(connect, 5000));
347
+ }
348
+
349
+ connect();
350
+ ```
351
+
352
+ ## Complete example — Python
353
+
354
+ ```python
355
+ import asyncio, json, os, websockets
356
+
357
+ TOKEN = os.environ["TAP_PROVIDER_TOKEN"]
358
+
359
+ async def connect():
360
+ while True:
361
+ try:
362
+ async with websockets.connect("ws://localhost:9400") as ws:
363
+ await ws.send(json.dumps({"type": "auth", "token": TOKEN}))
364
+
365
+ async for raw in ws:
366
+ msg = json.loads(raw)
367
+
368
+ if msg["type"] == "sessions":
369
+ if not msg["active"]:
370
+ return
371
+ await ws.send(json.dumps({
372
+ "type": "hello", "name": "example-provider",
373
+ "protocolVersion": 2, "session": msg["active"][0]["id"],
374
+ "tools": [{
375
+ "name": "greet", "description": "Greet someone by name",
376
+ "parameters": {
377
+ "type": "object",
378
+ "properties": {"name": {"type": "string"}},
379
+ "required": ["name"],
380
+ },
381
+ }],
382
+ }))
383
+
384
+ elif msg["type"] == "hello.ack":
385
+ print(f"Registered as {msg['providerId']}")
386
+
387
+ elif msg["type"] == "tool.call":
388
+ await ws.send(json.dumps({
389
+ "type": "tool.result", "id": msg["id"],
390
+ "data": f"Hello, {msg['args']['name']}!",
391
+ }))
392
+
393
+ elif msg["type"] == "tool.cancel":
394
+ await ws.send(json.dumps({
395
+ "type": "tool.result", "id": msg["id"],
396
+ "error": "Cancelled", "errorCode": "CANCELLED",
397
+ }))
398
+
399
+ elif msg["type"] == "session.lifecycle":
400
+ if msg["state"] == "shutdown.pending":
401
+ await ws.send(json.dumps({"type": "goodbye", "reason": "session ending"}))
402
+ return
403
+
404
+ elif msg["type"] == "error":
405
+ print(f"Error [{msg['code']}]: {msg['message']}")
406
+ if msg["code"] in ("AUTH_FAILED", "UNSUPPORTED_VERSION"):
407
+ return
408
+ # else: ignore unknown types (forward compat)
409
+
410
+ except (ConnectionError, websockets.ConnectionClosed):
411
+ print("Disconnected. Reconnecting in 5s...")
412
+ await asyncio.sleep(5)
413
+
414
+ asyncio.run(connect())
415
+ ```
416
+
417
+ ---
418
+
419
+ ## Message summary
420
+
421
+ | Direction | Type | When | Response expected? |
422
+ |---|---|---|---|
423
+ | Provider → Gateway | `auth` | First message | `sessions` or `error` |
424
+ | Gateway → Provider | `sessions` | After auth | Provider sends `hello` |
425
+ | Provider → Gateway | `hello` | After `sessions` | `hello.ack` or `error` |
426
+ | Gateway → Provider | `hello.ack` | After `hello` | — |
427
+ | Gateway → Provider | `tool.call` | Copilot invokes tool | `tool.result` (**required**) |
428
+ | Provider → Gateway | `tool.result` | After `tool.call`/`tool.cancel` | — |
429
+ | Gateway → Provider | `tool.cancel` | Timeout/interrupt | `tool.result` with `CANCELLED` (**required**) |
430
+ | Provider → Gateway | `push` | Store/surface/inject provider event | — |
431
+ | Provider → Gateway | `tools.update` | Replace provider tool list | — |
432
+ | Gateway → Provider | `session.lifecycle` | State change | `goodbye` on shutdown (recommended) |
433
+ | Gateway → Provider | `error` | Invalid message | — |
434
+ | Provider → Gateway | `goodbye` | Before disconnect | — |
435
+
436
+ ---
437
+
438
+ ## Dynamic tool registration
439
+
440
+ The Copilot SDK does **not** expose a `tools.add` or `tools.update` RPC. Tools are declared once at session creation/resume time and sent to the CLI in that initial handshake. To add or remove provider tools mid-session the Gateway uses two mechanisms together:
441
+
442
+ ### 1. Local handler map — `session.registerTools()`
443
+
444
+ `CopilotSession.registerTools(tools)` replaces the in-memory handler map (a `Map<name, handler>`). This controls which tool calls the SDK can dispatch locally. The Gateway calls this whenever the combined set of tap + provider tools changes:
445
+
446
+ ```js
447
+ // Provider connects — merge its tools into the existing set
448
+ session.registerTools([...tapTools, ...providerTools]);
449
+
450
+ // Provider disconnects — remove its tools
451
+ session.registerTools([...tapTools]);
452
+ ```
453
+
454
+ This alone is **not sufficient** — the CLI still has the old tool list from the original session handshake.
455
+
456
+ ### 2. Extension reload — `session.rpc.extensions.reload()`
457
+
458
+ The SDK exposes an experimental RPC:
459
+
460
+ ```js
461
+ await session.rpc.extensions.reload();
462
+ // Tells the CLI to reload this extension, which re-runs joinSession()
463
+ // and picks up the updated tool list
464
+ ```
465
+
466
+ This triggers a full re-join: the CLI tears down the current extension session and calls `joinSession()` again, which sends the new `tools` array via the `session.resume` RPC.
467
+
468
+ ### Gateway lifecycle
469
+
470
+ ```
471
+ Provider connects
472
+ → Gateway validates auth, receives hello with tool defs
473
+ → Gateway merges provider tools into the session tool set
474
+ → Gateway calls session.registerTools([...tapTools, ...providerTools])
475
+ → Gateway calls session.rpc.extensions.reload()
476
+ → CLI re-joins, sees updated tools, provider tools become available
477
+
478
+ Provider sends tools.update
479
+ → Gateway validates replacement tool defs
480
+ → Gateway replaces that provider's tools in the merged session tool set
481
+ → Gateway calls session.registerTools([...tapTools, ...providerTools])
482
+ → Gateway calls session.rpc.extensions.reload()
483
+ → CLI re-joins, sees updated tools
484
+
485
+ Provider disconnects
486
+ → Gateway removes provider tools from the session tool set
487
+ → Gateway calls session.registerTools([...tapTools])
488
+ → Gateway calls session.rpc.extensions.reload()
489
+ → CLI re-joins, provider tools disappear cleanly
490
+ ```
491
+
492
+ ### Surviving reloads — `globalThis` singleton
493
+
494
+ `session.rpc.extensions.reload()` re-runs the extension entry point (`extension.mjs`) from scratch. A naïve implementation would lose all runtime state (running emitters, stream history, config). The solution is to cache the runtime on `globalThis` so it persists across reloads:
495
+
496
+ ```js
497
+ // extension.mjs — reload-safe pattern
498
+ import { joinSession } from "@github/copilot-sdk/extension";
499
+ import { createCopilotChannelsRuntime } from "./tap-runtime.mjs";
500
+
501
+ // First run: creates runtime and caches it.
502
+ // Reload: reuses the existing runtime — emitters, streams, config all intact.
503
+ const runtime = globalThis.__tapRuntime ??= createCopilotChannelsRuntime({
504
+ cwd: process.cwd()
505
+ });
506
+
507
+ const session = await joinSession({
508
+ tools: runtime.tools, // includes provider tools if any are connected
509
+ hooks: runtime.hooks
510
+ });
511
+
512
+ runtime.attachSession(session); // re-wires session port to the new session handle
513
+
514
+ session.on("session.shutdown", () => {
515
+ void runtime.stopAllEmitters();
516
+ });
517
+ ```
518
+
519
+ The existing `sessionPort` abstraction already supports session swapping via `attachSession()`, so the new session handle is wired in cleanly without disrupting running emitters or streams.
520
+
521
+ ### Reload frequency — one per provider, not per tool
522
+
523
+ A provider sends **one `hello` message** containing **all** its tools:
524
+
525
+ ```json
526
+ {
527
+ "type": "hello",
528
+ "tools": [tool1, tool2, ..., tool10]
529
+ }
530
+ ```
531
+
532
+ **1 provider connection = 1 `hello` = 1 reload**, regardless of how many tools it declares.
533
+
534
+ For multiple providers connecting around the same time, the Gateway should **debounce** the reload call to batch them into a single reload:
535
+
536
+ ```js
537
+ let reloadTimer = null;
538
+ function scheduleReload() {
539
+ clearTimeout(reloadTimer);
540
+ reloadTimer = setTimeout(() => {
541
+ session.registerTools([...tapTools, ...allProviderTools]);
542
+ session.rpc.extensions.reload();
543
+ }, 200); // batch connections within a 200ms window
544
+ }
545
+ ```
546
+
547
+ This ensures that even 5 providers connecting simultaneously result in a single reload.
548
+
549
+ ### Caveats
550
+
551
+ - `session.rpc.extensions.reload()` is marked **`@experimental`** in the SDK (`dist/generated/rpc.js`). Its behavior or availability may change.
552
+ - There is no incremental tool update — each reload sends the full tool list. This is fine for the expected scale (≤100 tools per provider, per spec).
553
+ - `tools.update` replaces the provider's full tool list. The minimal core profile does not implement request acknowledgments, revision numbers, or per-tool deltas.
554
+
555
+ ---
556
+
557
+ ## What's not in Core
558
+
559
+ These are in the [full spec](./provider-interface-v2.md), not here:
560
+
561
+ - External/browser providers and pairing auth
562
+ - `"all"` session binding, `session.ready`
563
+ - Hooks, gates, transforms
564
+ - Stream filters and `stream.query`
565
+ - Advanced dynamic updates (`hooks.update`, `context.update`, `filter.set`) and update revisions/acks
566
+ - Multi-instance providers, reconnect tokens
567
+ - Concurrency limits, tool progress (`tool.progress`)
568
+ - Session events (`session.event`, `subscribe`)
@@ -0,0 +1,60 @@
1
+ # Tap control-plane roadmap
2
+
3
+ This document records the implementation slices that connect the Codex/Copilot
4
+ extensibility audit to concrete tap capabilities.
5
+
6
+ ## Implemented in this release slice
7
+
8
+ - **Emitter-run traces**: diagnostics now retain structured trace records for
9
+ scheduled emitter runs, including trace id, emitter id, run index, duration,
10
+ status, and error.
11
+ - **Diagnostics control tower foundation**: the diagnostics canvas now surfaces
12
+ trace counts, recent emitter-run traces, and goal EventStream ledgers.
13
+ - **Goal evidence tools**: `tap_verify_goal_output` and `tap_audit_claims`
14
+ verify workspace files, EventStream contents, and caller-supplied command
15
+ evidence without secretly executing commands.
16
+ - **Mode-aware session state**: `tap_get_session_state` reads current mode,
17
+ model, tasks, schedules, canvases, and UI capabilities without mutating the
18
+ session.
19
+ - **Guarded mode switching**: `tap_set_session_mode` can change
20
+ interactive/plan/autopilot mode only with explicit confirmation.
21
+ - **Structured session records**: traces and stream posts are mirrored into
22
+ session-workspace JSONL collections and can be inspected with
23
+ `tap_query_records`.
24
+ - **Eval metadata hooks**: eval cases can now declare required observations,
25
+ prohibited claims, rubrics, deterministic assertions, and pass/fail examples;
26
+ the judge prompt consumes those fields when present.
27
+ - **Monitor audit trail**: `/tap-monitor` companion prompts now write structured
28
+ REVIEW RECORD entries.
29
+ - **Orchestration foundation**: `/tap-orchestrate` defines the coordinator and
30
+ role-emitter pattern for gated multi-agent workflows.
31
+ - **Provider recipe set**: reusable patterns now cover Jira/GitHub, CI auto-fix,
32
+ code review, SAST triage, and browser/Detour workflows.
33
+
34
+ ## Deferred deeper work
35
+
36
+ - **Full trace span hierarchy**: current traces are run-level records. A future
37
+ slice should add child spans for line routing, provider calls, tool calls, and
38
+ prompt dispatch using W3C `traceparent` where available.
39
+ - **SQLite ledgers**: current records are session-workspace JSONL artifacts. A
40
+ future slice can move those collections into the SDK Session FS SQLite
41
+ provider for richer querying.
42
+ - **HALO-style optimizer**: eval metadata is now accepted, but the ranked
43
+ recommendation handoff still needs an `evals/optimize` command.
44
+ - **Elicitation-backed mode policy**: mode switching is guarded by explicit tool
45
+ confirmation. A future slice can add host elicitation prompts and ownership
46
+ policy before permitting mode mutation.
47
+ - **Goal timeline graph**: the canvas has trace and goal panels, but not a
48
+ timeline graph or per-span drilldown yet.
49
+ - **Production provider examples**: recipes are documented; full provider
50
+ implementations for Jira/GitHub and CI review remain future work.
51
+
52
+ ## Validation expectation
53
+
54
+ Every control-plane slice should prove:
55
+
56
+ 1. Runtime behavior has focused tests.
57
+ 2. Tool surfaces have schema/handler tests.
58
+ 3. Docs name the evidence surface and safety constraints.
59
+ 4. Diagnostics expose the new state.
60
+ 5. Smoke evals still show tap tools available.