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
@@ -4659,6 +4659,7 @@ function summarizeRuntimeState(state) {
4659
4659
  const model = state?.model?.ok ? state.model.value : null;
4660
4660
  const taskCount = state?.tasks?.ok ? state.tasks.value?.tasks?.length ?? 0 : null;
4661
4661
  const scheduleCount = state?.schedules?.ok ? state.schedules.value?.entries?.length ?? 0 : null;
4662
+ const permissionCount = state?.permissions?.ok ? state.permissions.value?.items?.length ?? 0 : null;
4662
4663
  const canvasCount = state?.openCanvases?.ok ? state.openCanvases.value?.openCanvases?.length ?? 0 : null;
4663
4664
  return [
4664
4665
  `sessionId=${state?.sessionId ?? "(none)"}`,
@@ -4666,6 +4667,7 @@ function summarizeRuntimeState(state) {
4666
4667
  model ? `model=${model.modelId ?? "unknown"} reasoning=${model.reasoningEffort ?? "default"} context=${model.contextTier ?? "default"}` : null,
4667
4668
  taskCount !== null ? `tasks=${taskCount}` : null,
4668
4669
  scheduleCount !== null ? `schedules=${scheduleCount}` : null,
4670
+ permissionCount !== null ? `pendingPermissions=${permissionCount}` : null,
4669
4671
  canvasCount !== null ? `openCanvases=${canvasCount}` : null,
4670
4672
  `elicitation=${state?.capabilities?.ui?.elicitation === true ? "available" : "unavailable"}`,
4671
4673
  `canvases=${state?.capabilities?.ui?.canvases === true ? "available" : "host-gated"}`
@@ -4714,6 +4716,64 @@ function createDiagnosticsTools(deps) {
4714
4716
  return summarizeRuntimeState(state);
4715
4717
  })
4716
4718
  });
4719
+ if (typeof diagnostics2.setSessionMode === "function") {
4720
+ tools.push({
4721
+ name: "tap_set_session_mode",
4722
+ description: "Guarded Copilot mode switch for tap workflows. Requires explicit confirmation text and is intended for interactive/plan/autopilot transitions only.",
4723
+ parameters: {
4724
+ type: "object",
4725
+ properties: {
4726
+ mode: {
4727
+ type: "string",
4728
+ enum: ["interactive", "plan", "autopilot"],
4729
+ description: "Target Copilot session mode."
4730
+ },
4731
+ reason: {
4732
+ type: "string",
4733
+ description: "Why this mode switch is needed."
4734
+ },
4735
+ confirm: {
4736
+ type: "string",
4737
+ description: "Must be exactly 'set-session-mode' to confirm this user-visible mode change."
4738
+ }
4739
+ },
4740
+ required: ["mode", "reason", "confirm"]
4741
+ },
4742
+ handler: wrapToolHandler("tap_set_session_mode", (args) => ({ mode: args.mode }), async (args) => {
4743
+ if (args.confirm !== "set-session-mode") {
4744
+ throw new Error("Refusing to change session mode without confirm='set-session-mode'.");
4745
+ }
4746
+ const nextMode = await diagnostics2.setSessionMode(args.mode);
4747
+ return `Session mode set to ${nextMode}. reason=${args.reason}`;
4748
+ })
4749
+ });
4750
+ }
4751
+ }
4752
+ if (typeof diagnostics2.queryRecords === "function") {
4753
+ tools.push({
4754
+ name: "tap_query_records",
4755
+ description: "Reads structured tap records persisted in the session workspace, such as traces and stream-posts.",
4756
+ parameters: {
4757
+ type: "object",
4758
+ properties: {
4759
+ collection: {
4760
+ type: "string",
4761
+ description: "Record collection name, for example 'traces' or 'stream-posts'."
4762
+ },
4763
+ limit: {
4764
+ type: "integer",
4765
+ minimum: 1,
4766
+ maximum: 500,
4767
+ description: "Maximum recent records to return."
4768
+ }
4769
+ },
4770
+ required: ["collection"]
4771
+ },
4772
+ handler: wrapToolHandler("tap_query_records", (args) => ({ collection: args.collection }), async (args) => {
4773
+ const result = diagnostics2.queryRecords(args.collection, { limit: args.limit });
4774
+ return JSON.stringify(result, null, 2);
4775
+ })
4776
+ });
4717
4777
  }
4718
4778
  return tools;
4719
4779
  }
@@ -6919,6 +6979,10 @@ function createProviderGateway(options = {}, adapters = {}) {
6919
6979
  };
6920
6980
  }
6921
6981
 
6982
+ // src/services/tap-runtime-service.mjs
6983
+ import fs4 from "node:fs";
6984
+ import path9 from "node:path";
6985
+
6922
6986
  // src/session/listeners.mjs
6923
6987
  var SESSION_ACTIVITY_EVENTS = [
6924
6988
  "session.start",
@@ -8750,7 +8814,7 @@ function shouldPersistLoadedConfig(parsedConfig, normalizedConfig) {
8750
8814
  return !isDeepStrictEqual(parsedConfig, serializeConfigForComparison(normalizedConfig));
8751
8815
  }
8752
8816
  function createConfigStore(options = {}) {
8753
- const fs3 = options.fs ?? { existsSync: existsSync2, readFileSync: readFileSync2, writeFileSync: writeFileSync2 };
8817
+ const fs5 = options.fs ?? { existsSync: existsSync2, readFileSync: readFileSync2, writeFileSync: writeFileSync2 };
8754
8818
  const state = {
8755
8819
  cwd: normalizeBaseCwd(options.cwd),
8756
8820
  filePath: null,
@@ -8778,7 +8842,7 @@ function createConfigStore(options = {}) {
8778
8842
  const exists = withConfigLoadPhase(
8779
8843
  "checking config path",
8780
8844
  "Unable to check whether the tap config file exists.",
8781
- () => fs3.existsSync(filePath),
8845
+ () => fs5.existsSync(filePath),
8782
8846
  { filePath }
8783
8847
  );
8784
8848
  if (!exists) {
@@ -8787,7 +8851,7 @@ function createConfigStore(options = {}) {
8787
8851
  const rawConfig = withConfigLoadPhase(
8788
8852
  "reading config file",
8789
8853
  "Unable to read the tap config file.",
8790
- () => fs3.readFileSync(filePath, "utf8"),
8854
+ () => fs5.readFileSync(filePath, "utf8"),
8791
8855
  { filePath }
8792
8856
  );
8793
8857
  const parsedConfig = withConfigLoadPhase(
@@ -8845,7 +8909,7 @@ function createConfigStore(options = {}) {
8845
8909
  state.filePath = defaultConfigPath(state.cwd);
8846
8910
  }
8847
8911
  const payload = serializeConfig(state.config, LATEST_CONFIG_VERSION);
8848
- fs3.writeFileSync(state.filePath, `${JSON.stringify(payload, null, 2)}
8912
+ fs5.writeFileSync(state.filePath, `${JSON.stringify(payload, null, 2)}
8849
8913
  `, "utf8");
8850
8914
  }
8851
8915
  function findStreamIndex(name) {
@@ -9351,21 +9415,46 @@ function createDefaultProcessAdapter() {
9351
9415
  }
9352
9416
  function recordScheduledTrace(emitter, context, { startedAt, endedAt = nowIso(), runIndex, result = null, error = null, consumedRun = true } = {}) {
9353
9417
  try {
9418
+ const start = startedAt ?? endedAt;
9419
+ const status = result?.deferred ? "deferred" : result?.ok ? "success" : "failure";
9354
9420
  context.diagnostics?.trace?.({
9355
- traceId: `${stableTraceComponent(emitter.name)}-${Number(runIndex ?? emitter.runCount) || 0}-${Date.parse(startedAt ?? endedAt) || Date.now()}`,
9421
+ traceId: `${stableTraceComponent(emitter.name)}-${Number(runIndex ?? emitter.runCount) || 0}-${Date.parse(start) || Date.now()}`,
9356
9422
  emitterId: emitter.name,
9357
9423
  emitterName: emitter.name,
9358
9424
  runIndex: Number(runIndex ?? emitter.runCount) || null,
9359
9425
  emitterType: emitter.emitterType,
9360
9426
  runSchedule: emitter.runSchedule,
9361
- startedAt: startedAt ?? endedAt,
9427
+ startedAt: start,
9362
9428
  endedAt,
9363
- status: result?.deferred ? "deferred" : result?.ok ? "success" : "failure",
9429
+ status,
9364
9430
  ok: result?.ok === true,
9365
9431
  consumedRun,
9366
9432
  lineCount: emitter.lineCount,
9367
9433
  droppedLineCount: emitter.droppedLineCount,
9368
9434
  error: error ?? result?.error ?? null,
9435
+ spans: [
9436
+ {
9437
+ spanId: "emitter-run",
9438
+ kind: "emitter.run",
9439
+ name: emitter.name,
9440
+ startedAt: start,
9441
+ endedAt,
9442
+ status
9443
+ },
9444
+ {
9445
+ spanId: emitter.emitterType === EMITTER_TYPE.PROMPT ? "prompt-dispatch" : "command-process",
9446
+ parentSpanId: "emitter-run",
9447
+ kind: emitter.emitterType === EMITTER_TYPE.PROMPT ? "prompt.dispatch" : "command.process",
9448
+ name: emitter.emitterType,
9449
+ startedAt: start,
9450
+ endedAt,
9451
+ status,
9452
+ metadata: {
9453
+ stream: emitter.stream,
9454
+ cwd: emitter.cwd ?? null
9455
+ }
9456
+ }
9457
+ ],
9369
9458
  metadata: {
9370
9459
  every: emitter.every ?? null,
9371
9460
  everySchedule: emitter.everySchedule ?? null,
@@ -10558,9 +10647,21 @@ function createSessionPort(initialSession = null) {
10558
10647
  tasks: await read("tasks", () => rpc.tasks?.list?.()),
10559
10648
  schedules: await read("schedules", () => rpc.schedule?.list?.()),
10560
10649
  skills: await read("skills", () => rpc.skills?.list?.()),
10650
+ permissions: await read("permissions", () => rpc.permissions?.pendingRequests?.()),
10561
10651
  openCanvases: await read("openCanvases", () => rpc.canvas?.listOpen?.())
10562
10652
  };
10563
10653
  }
10654
+ async function setMode(mode) {
10655
+ if (!session2) {
10656
+ throw new LifecycleError("Session is not attached; cannot set mode.");
10657
+ }
10658
+ const modeApi = session2.rpc?.mode;
10659
+ if (!modeApi || typeof modeApi.set !== "function") {
10660
+ throw new LifecycleError("Session mode API is not available in this Copilot session.");
10661
+ }
10662
+ await modeApi.set({ mode });
10663
+ return modeApi.get();
10664
+ }
10564
10665
  function registerTools(tools) {
10565
10666
  if (!session2) return;
10566
10667
  try {
@@ -10598,6 +10699,7 @@ function createSessionPort(initialSession = null) {
10598
10699
  sendAndWait,
10599
10700
  openCanvas,
10600
10701
  getRuntimeState,
10702
+ setMode,
10601
10703
  registerTools,
10602
10704
  reloadExtension
10603
10705
  };
@@ -10968,7 +11070,7 @@ function projectStream(stream) {
10968
11070
 
10969
11071
  // src/services/stream-service.mjs
10970
11072
  function createStreamService(deps) {
10971
- const { streams, configStore, sessionPort, persist } = deps;
11073
+ const { streams, configStore, sessionPort, persist, recordStore = null } = deps;
10972
11074
  function requireStreamChannel(channel, operation) {
10973
11075
  return requireNormalizedName(channel, {
10974
11076
  label: "Stream channel",
@@ -11012,6 +11114,17 @@ function createStreamService(deps) {
11012
11114
  context: { channel: stream.name }
11013
11115
  });
11014
11116
  }
11117
+ try {
11118
+ recordStore?.appendRecord?.("stream-posts", {
11119
+ channel: stream.name,
11120
+ source,
11121
+ text,
11122
+ timestamp: appended.timestamp,
11123
+ monitorName: appended.monitorName ?? null,
11124
+ metadata: appended.metadata ?? null
11125
+ });
11126
+ } catch {
11127
+ }
11015
11128
  void sessionPort.log(`Posted message to stream '${stream.name}'.`);
11016
11129
  return { stream: projectStream(stream) };
11017
11130
  }
@@ -11213,6 +11326,92 @@ function createGoalVerificationService({ getBaseCwd, getStreamHistory } = {}) {
11213
11326
  return { verifyGoalOutput, auditClaims };
11214
11327
  }
11215
11328
 
11329
+ // src/services/structured-record-store.mjs
11330
+ import fs3 from "node:fs";
11331
+ import path8 from "node:path";
11332
+ var DEFAULT_COLLECTION_LIMIT = 500;
11333
+ var COLLECTION_NAME_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
11334
+ function safeCollectionName(value) {
11335
+ const name = String(value ?? "").trim().toLowerCase();
11336
+ return COLLECTION_NAME_PATTERN.test(name) ? name : null;
11337
+ }
11338
+ function sessionWorkspacePath(sessionPort) {
11339
+ const session2 = typeof sessionPort?.current === "function" ? sessionPort.current() : null;
11340
+ return session2?.workspacePath ?? null;
11341
+ }
11342
+ function recordRoot(sessionPort) {
11343
+ const workspace = sessionWorkspacePath(sessionPort);
11344
+ if (!workspace) {
11345
+ return null;
11346
+ }
11347
+ return path8.join(workspace, "files", "tap-records");
11348
+ }
11349
+ function collectionPath(root, collection) {
11350
+ return path8.join(root, `${collection}.jsonl`);
11351
+ }
11352
+ function trimJsonlFile(filePath, maxRecords = DEFAULT_COLLECTION_LIMIT) {
11353
+ try {
11354
+ const lines = fs3.readFileSync(filePath, "utf8").split(/\r?\n/).filter(Boolean);
11355
+ if (lines.length <= maxRecords) {
11356
+ return;
11357
+ }
11358
+ fs3.writeFileSync(filePath, `${lines.slice(-maxRecords).join("\n")}
11359
+ `, "utf8");
11360
+ } catch {
11361
+ }
11362
+ }
11363
+ function createStructuredRecordStore({ sessionPort, maxRecords = DEFAULT_COLLECTION_LIMIT } = {}) {
11364
+ function appendRecord(collectionInput, record) {
11365
+ const collection = safeCollectionName(collectionInput);
11366
+ const root = recordRoot(sessionPort);
11367
+ if (!collection || !root) {
11368
+ return { stored: false, reason: !collection ? "invalid-collection" : "no-session-workspace" };
11369
+ }
11370
+ try {
11371
+ fs3.mkdirSync(root, { recursive: true });
11372
+ const filePath = collectionPath(root, collection);
11373
+ fs3.appendFileSync(filePath, `${JSON.stringify({ ...record, storedAt: (/* @__PURE__ */ new Date()).toISOString() })}
11374
+ `, "utf8");
11375
+ trimJsonlFile(filePath, maxRecords);
11376
+ return { stored: true, collection, path: filePath };
11377
+ } catch (error) {
11378
+ return { stored: false, collection, reason: error?.message ?? String(error ?? "unknown error") };
11379
+ }
11380
+ }
11381
+ function listRecords(collectionInput, options = {}) {
11382
+ const collection = safeCollectionName(collectionInput);
11383
+ const root = recordRoot(sessionPort);
11384
+ if (!collection || !root) {
11385
+ return { collection: collectionInput, records: [], available: false };
11386
+ }
11387
+ const limit = Math.max(1, Math.min(500, Math.floor(Number(options.limit ?? 50) || 50)));
11388
+ const filePath = collectionPath(root, collection);
11389
+ try {
11390
+ const records = fs3.readFileSync(filePath, "utf8").split(/\r?\n/).filter(Boolean).slice(-limit).map((line) => {
11391
+ try {
11392
+ return JSON.parse(line);
11393
+ } catch {
11394
+ return { parseError: true, raw: line };
11395
+ }
11396
+ });
11397
+ return { collection, path: filePath, available: true, records };
11398
+ } catch (error) {
11399
+ if (error?.code === "ENOENT") {
11400
+ return { collection, path: filePath, available: true, records: [] };
11401
+ }
11402
+ return { collection, path: filePath, available: false, records: [], error: error?.message ?? String(error) };
11403
+ }
11404
+ }
11405
+ function getRoot() {
11406
+ return recordRoot(sessionPort);
11407
+ }
11408
+ return {
11409
+ appendRecord,
11410
+ listRecords,
11411
+ getRoot
11412
+ };
11413
+ }
11414
+
11216
11415
  // src/diagnostics/store.mjs
11217
11416
  var DEFAULT_MAX_LOGS = 300;
11218
11417
  var DEFAULT_MAX_EVENTS = 300;
@@ -11361,6 +11560,7 @@ function createDiagnosticsStore(options = {}) {
11361
11560
  let traceCount = 0;
11362
11561
  let cleanupSessionListener = () => {
11363
11562
  };
11563
+ let recordSink = typeof options.recordSink === "function" ? options.recordSink : null;
11364
11564
  function recordLog(source, message, options2 = {}) {
11365
11565
  logCount += 1;
11366
11566
  return logs.append({
@@ -11397,7 +11597,7 @@ function createDiagnosticsStore(options = {}) {
11397
11597
  const startMs = Date.parse(startedAt);
11398
11598
  const endMs = Date.parse(endedAt);
11399
11599
  const durationMs = Number.isFinite(startMs) && Number.isFinite(endMs) ? Math.max(0, endMs - startMs) : null;
11400
- return traces.append({
11600
+ const entry = {
11401
11601
  id: createId("trace", traceCount),
11402
11602
  traceId: String(trace.traceId ?? `trace-${traceCount.toString(36)}`),
11403
11603
  timestamp: endedAt,
@@ -11419,8 +11619,19 @@ function createDiagnosticsStore(options = {}) {
11419
11619
  maxDepth: 3,
11420
11620
  maxStringLength: 700,
11421
11621
  maxCollectionItems: 20
11622
+ }),
11623
+ spans: safeClone(Array.isArray(trace.spans) ? trace.spans : [], {
11624
+ maxDepth: 4,
11625
+ maxStringLength: 700,
11626
+ maxCollectionItems: 30
11422
11627
  })
11423
- });
11628
+ };
11629
+ const appended = traces.append(entry);
11630
+ try {
11631
+ recordSink?.("traces", entry);
11632
+ } catch {
11633
+ }
11634
+ return appended;
11424
11635
  }
11425
11636
  function recordSessionEvent(event) {
11426
11637
  sessionEventCount += 1;
@@ -11479,6 +11690,9 @@ function createDiagnosticsStore(options = {}) {
11479
11690
  log: recordLog,
11480
11691
  event: recordRuntimeEvent,
11481
11692
  trace: recordTrace,
11693
+ setRecordSink: (nextSink) => {
11694
+ recordSink = typeof nextSink === "function" ? nextSink : null;
11695
+ },
11482
11696
  attachSession,
11483
11697
  detachSession,
11484
11698
  snapshot
@@ -11506,6 +11720,33 @@ function projectToolForDiagnostics(tool) {
11506
11720
  overridesBuiltInTool: tool.overridesBuiltInTool === true
11507
11721
  };
11508
11722
  }
11723
+ function collectEvalSummaries(baseCwd, limit = 20) {
11724
+ const root = path9.join(baseCwd, "evals", "results");
11725
+ const summaries = [];
11726
+ function walk(dir) {
11727
+ let entries = [];
11728
+ try {
11729
+ entries = fs4.readdirSync(dir, { withFileTypes: true });
11730
+ } catch {
11731
+ return;
11732
+ }
11733
+ for (const entry of entries) {
11734
+ const fullPath = path9.join(dir, entry.name);
11735
+ if (entry.isDirectory()) {
11736
+ walk(fullPath);
11737
+ } else if (entry.name === "summary.json") {
11738
+ try {
11739
+ const stat = fs4.statSync(fullPath);
11740
+ const parsed = JSON.parse(fs4.readFileSync(fullPath, "utf8"));
11741
+ summaries.push({ path: path9.relative(baseCwd, fullPath), modifiedAt: stat.mtime.toISOString(), summary: parsed });
11742
+ } catch {
11743
+ }
11744
+ }
11745
+ }
11746
+ }
11747
+ walk(root);
11748
+ return summaries.sort((left, right) => new Date(right.modifiedAt) - new Date(left.modifiedAt)).slice(0, limit);
11749
+ }
11509
11750
  function createTapRuntimeService(options = {}) {
11510
11751
  const diagnosticsStore = options.diagnostics ?? createDiagnosticsStore();
11511
11752
  const {
@@ -11522,11 +11763,14 @@ function createTapRuntimeService(options = {}) {
11522
11763
  ...options,
11523
11764
  diagnostics: diagnosticsStore
11524
11765
  });
11766
+ const recordStore = createStructuredRecordStore({ sessionPort });
11767
+ diagnosticsStore.setRecordSink?.((collection, record) => recordStore.appendRecord(collection, record));
11525
11768
  const streamService = createStreamService({
11526
11769
  streams,
11527
11770
  configStore,
11528
11771
  sessionPort,
11529
- persist
11772
+ persist,
11773
+ recordStore
11530
11774
  });
11531
11775
  const emitterService = createEmitterService({
11532
11776
  streams,
@@ -11662,8 +11906,16 @@ function createTapRuntimeService(options = {}) {
11662
11906
  void sessionPort.reloadExtension();
11663
11907
  }
11664
11908
  };
11665
- function getDiagnosticsSnapshot(options2 = {}, extra = {}) {
11909
+ async function getDiagnosticsSnapshot(options2 = {}, extra = {}) {
11666
11910
  const allTools = Array.isArray(extra.tools) ? extra.tools.map(projectToolForDiagnostics) : [];
11911
+ let sessionRuntime = null;
11912
+ try {
11913
+ sessionRuntime = await sessionPort.getRuntimeState();
11914
+ } catch (error) {
11915
+ sessionRuntime = {
11916
+ error: error?.message ?? String(error ?? "unknown error")
11917
+ };
11918
+ }
11667
11919
  return {
11668
11920
  generatedAt: nowIso(),
11669
11921
  session: getSessionInfo(),
@@ -11677,7 +11929,9 @@ function createTapRuntimeService(options = {}) {
11677
11929
  emitters: emitterCapabilities.listEmitters(),
11678
11930
  gateway: extra.gateway ?? null,
11679
11931
  tools: allTools,
11932
+ evals: collectEvalSummaries(sessionContext.getBaseCwd(), options2.evalLimit ?? 20),
11680
11933
  notifications: typeof notifications.snapshot === "function" ? notifications.snapshot({ limit: options2.notificationLimit ?? 20 }) : null,
11934
+ sessionRuntime,
11681
11935
  diagnostics: diagnosticsStore.snapshot(options2)
11682
11936
  };
11683
11937
  }
@@ -11697,6 +11951,9 @@ function createTapRuntimeService(options = {}) {
11697
11951
  });
11698
11952
  },
11699
11953
  getSessionRuntimeState: () => sessionPort.getRuntimeState(),
11954
+ setSessionMode: (mode) => sessionPort.setMode(mode),
11955
+ queryRecords: (collection, options2 = {}) => recordStore.listRecords(collection, options2),
11956
+ getRecordRoot: () => recordStore.getRoot(),
11700
11957
  attachSession: diagnosticsStore.attachSession,
11701
11958
  detachSession: diagnosticsStore.detachSession
11702
11959
  };
@@ -12004,7 +12261,7 @@ function createHtml() {
12004
12261
  overflow: auto;
12005
12262
  }
12006
12263
 
12007
- .stream-grid, .emitter-grid, .provider-grid, .trace-grid {
12264
+ .stream-grid, .emitter-grid, .provider-grid, .trace-grid, .session-grid {
12008
12265
  display: grid;
12009
12266
  gap: 12px;
12010
12267
  }
@@ -12186,6 +12443,26 @@ function createHtml() {
12186
12443
  </div>
12187
12444
  </section>
12188
12445
 
12446
+ <section class="panel">
12447
+ <div class="panel-head">
12448
+ <h2>Session control</h2>
12449
+ <small id="session-state">unknown</small>
12450
+ </div>
12451
+ <div class="panel-body">
12452
+ <div class="session-grid" id="session-control"></div>
12453
+ </div>
12454
+ </section>
12455
+
12456
+ <section class="panel">
12457
+ <div class="panel-head">
12458
+ <h2>Eval gate</h2>
12459
+ <small id="eval-count">0 summaries</small>
12460
+ </div>
12461
+ <div class="panel-body">
12462
+ <div class="session-grid" id="evals"></div>
12463
+ </div>
12464
+ </section>
12465
+
12189
12466
  <section class="panel timeline-panel">
12190
12467
  <div class="panel-head">
12191
12468
  <h2>Evidence timeline</h2>
@@ -12223,6 +12500,8 @@ function createHtml() {
12223
12500
  const providers = snapshot.gateway?.providers?.length ?? 0;
12224
12501
  const queue = snapshot.notifications?.queueSize ?? 0;
12225
12502
  const traces = snapshot.diagnostics?.stats?.traces?.retained ?? 0;
12503
+ const tasks = snapshot.sessionRuntime?.tasks?.ok ? (snapshot.sessionRuntime.tasks.value?.tasks?.length ?? 0) : 0;
12504
+ const evals = snapshot.evals?.length ?? 0;
12226
12505
  const sessionEvents = snapshot.diagnostics?.stats?.sessionEvents?.retained ?? 0;
12227
12506
  el("metrics").innerHTML = [
12228
12507
  metric("streams", streams),
@@ -12230,6 +12509,8 @@ function createHtml() {
12230
12509
  metric("providers", providers),
12231
12510
  metric("queued injections", queue),
12232
12511
  metric("traces", traces),
12512
+ metric("tasks", tasks),
12513
+ metric("eval summaries", evals),
12233
12514
  metric("configured emitters", configured),
12234
12515
  metric("session events", sessionEvents)
12235
12516
  ].join("");
@@ -12269,6 +12550,28 @@ function createHtml() {
12269
12550
  el("providers").innerHTML = gatewayRecord + rows;
12270
12551
  }
12271
12552
 
12553
+ function extractMarkedJson(text, marker) {
12554
+ const raw = String(text ?? "");
12555
+ const start = "=== BEGIN_" + marker + " ===";
12556
+ const end = "=== END_" + marker + " ===";
12557
+ const startIndex = raw.indexOf(start);
12558
+ const endIndex = raw.indexOf(end);
12559
+ if (startIndex < 0 || endIndex < 0 || endIndex <= startIndex) return null;
12560
+ const body = raw.slice(startIndex + start.length, endIndex).trim();
12561
+ try { return JSON.parse(body); } catch { return null; }
12562
+ }
12563
+
12564
+ function collectReviewRecords(snapshot) {
12565
+ const records = [];
12566
+ for (const stream of snapshot.streams ?? []) {
12567
+ for (const entry of stream.entries ?? []) {
12568
+ const parsed = extractMarkedJson(entry.text, "REVIEW_RECORD");
12569
+ if (parsed) records.push({ stream: stream.name, timestamp: entry.timestamp, ...parsed });
12570
+ }
12571
+ }
12572
+ return records.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
12573
+ }
12574
+
12272
12575
  function renderTraces(snapshot) {
12273
12576
  const traces = snapshot.diagnostics?.traces ?? [];
12274
12577
  const goalStreams = (snapshot.streams ?? []).filter((stream) => String(stream.name ?? "").startsWith("goal-"));
@@ -12281,7 +12584,36 @@ function createHtml() {
12281
12584
  const latest = (stream.entries ?? []).slice(-2).reverse().map((entry) => '<div class="entry"><span class="meta">' + escapeHtml(timeOnly(entry.timestamp)) + '</span>\\n' + escapeHtml(entry.text) + '</div>').join("");
12282
12585
  return '<div class="record"><strong>' + escapeHtml(stream.name) + ' <span class="badge info">goal stream</span></strong><div class="meta">' + escapeHtml(stream.entries?.length ?? 0) + ' retained entries</div>' + (latest || '<div class="entry">No goal ledger entries retained.</div>') + '</div>';
12283
12586
  }).join("");
12284
- el("traces").innerHTML = (traceRows || '<div class="empty">No emitter-run traces retained yet.</div>') + (goalRows ? '<div class="panel-head" style="margin:14px -14px 12px"><h2>Goal ledgers</h2><small>' + goalStreams.length + ' streams</small></div>' + goalRows : "");
12587
+ const reviewRecords = collectReviewRecords(snapshot).slice(0, 8).map((record) => '<div class="record"><strong>' + escapeHtml(record.stream) + ' <span class="badge info">' + escapeHtml(record.issue_type ?? "review") + '</span></strong><div class="meta">entries=' + escapeHtml(record.entries_examined ?? "?") + ' | reviewed=' + escapeHtml(timeOnly(record.reviewed_at ?? record.timestamp)) + '</div><div class="details">' + escapeHtml(compactJson({ patterns_changed: record.patterns_changed ?? [], remaining_noise_delta: record.remaining_noise_delta ?? [] })) + '</div></div>').join("");
12588
+ el("traces").innerHTML = (traceRows || '<div class="empty">No emitter-run traces retained yet.</div>') + (goalRows ? '<div class="panel-head" style="margin:14px -14px 12px"><h2>Goal ledgers</h2><small>' + goalStreams.length + ' streams</small></div>' + goalRows : "") + (reviewRecords ? '<div class="panel-head" style="margin:14px -14px 12px"><h2>Monitor reviews</h2><small>' + collectReviewRecords(snapshot).length + ' records</small></div>' + reviewRecords : "");
12589
+ }
12590
+
12591
+ function renderSessionControl(snapshot) {
12592
+ const runtime = snapshot.sessionRuntime ?? {};
12593
+ const mode = runtime.mode?.ok ? runtime.mode.value : "unknown";
12594
+ const model = runtime.model?.ok ? runtime.model.value : null;
12595
+ const tasks = runtime.tasks?.ok ? (runtime.tasks.value?.tasks ?? []) : [];
12596
+ const schedules = runtime.schedules?.ok ? (runtime.schedules.value?.entries ?? []) : [];
12597
+ el("session-state").textContent = String(mode);
12598
+ const stateRecord = '<div class="record"><strong>session <span class="badge info">' + escapeHtml(mode) + '</span></strong><div class="meta">model=' + escapeHtml(model?.modelId ?? "unknown") + ' | reasoning=' + escapeHtml(model?.reasoningEffort ?? "default") + ' | schedules=' + escapeHtml(schedules.length) + '</div></div>';
12599
+ const taskRows = tasks.slice(0, 10).map((task) => '<div class="record"><strong>' + escapeHtml(task.description ?? task.id ?? "task") + ' <span class="badge ' + escapeHtml(task.status ?? "info") + '">' + escapeHtml(task.status ?? "?") + '</span></strong><div class="meta">' + escapeHtml(task.type ?? "task") + ' | id=' + escapeHtml(task.id ?? "?") + ' | mode=' + escapeHtml(task.executionMode ?? "?") + '</div></div>').join("");
12600
+ el("session-control").innerHTML = stateRecord + (taskRows || '<div class="empty">No native Copilot tasks are currently tracked.</div>');
12601
+ }
12602
+
12603
+ function renderEvals(snapshot) {
12604
+ const evals = snapshot.evals ?? [];
12605
+ el("eval-count").textContent = evals.length + " summaries";
12606
+ if (evals.length === 0) {
12607
+ el("evals").innerHTML = '<div class="empty">No eval summaries found under evals/results.</div>';
12608
+ return;
12609
+ }
12610
+ el("evals").innerHTML = evals.slice(0, 12).map((item) => {
12611
+ const summary = item.summary ?? {};
12612
+ const caseId = summary.caseId ?? summary.mode ?? "run";
12613
+ const verdict = summary.verdict?.verdict ?? (Array.isArray(summary) ? "batch" : "unknown");
12614
+ const tone = verdict === "pass" || summary.extensionToolAvailable === true ? "ready" : verdict === "fail" ? "error" : "info";
12615
+ return '<div class="record"><strong>' + escapeHtml(caseId) + ' <span class="badge ' + tone + '">' + escapeHtml(verdict) + '</span></strong><div class="meta">' + escapeHtml(item.path) + ' | ' + escapeHtml(timeOnly(item.modifiedAt)) + '</div><div class="details">' + escapeHtml(summary.verdict?.summary ?? summary.detail ?? "") + '</div></div>';
12616
+ }).join("");
12285
12617
  }
12286
12618
 
12287
12619
  function collectTimeline(snapshot) {
@@ -12327,6 +12659,8 @@ function createHtml() {
12327
12659
  renderStreams(snapshot);
12328
12660
  renderProviders(snapshot);
12329
12661
  renderTraces(snapshot);
12662
+ renderSessionControl(snapshot);
12663
+ renderEvals(snapshot);
12330
12664
  renderTimeline(snapshot);
12331
12665
  }
12332
12666
 
@@ -12375,7 +12709,7 @@ function createHtml() {
12375
12709
  }
12376
12710
  function createTapDiagnosticsCanvas({ getSnapshot, diagnostics: diagnostics2 } = {}) {
12377
12711
  const instances = /* @__PURE__ */ new Map();
12378
- function snapshot(options = {}) {
12712
+ async function snapshot(options = {}) {
12379
12713
  return typeof getSnapshot === "function" ? getSnapshot(sanitizeSnapshotOptions(options)) : { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), error: "No diagnostics snapshot provider configured." };
12380
12714
  }
12381
12715
  function log(message, level = "info", metadata = {}) {
@@ -12389,7 +12723,7 @@ function createTapDiagnosticsCanvas({ getSnapshot, diagnostics: diagnostics2 } =
12389
12723
  return;
12390
12724
  }
12391
12725
  if (url.pathname === "/api/snapshot") {
12392
- jsonResponse(res, 200, snapshot({ limit: url.searchParams.get("limit") }));
12726
+ void snapshot({ limit: url.searchParams.get("limit") }).then((data) => jsonResponse(res, 200, data));
12393
12727
  return;
12394
12728
  }
12395
12729
  if (url.pathname === "/events") {
@@ -12400,10 +12734,17 @@ function createTapDiagnosticsCanvas({ getSnapshot, diagnostics: diagnostics2 } =
12400
12734
  "X-Accel-Buffering": "no"
12401
12735
  });
12402
12736
  const send = () => {
12403
- res.write(`event: snapshot
12404
- data: ${JSON.stringify(snapshot())}
12737
+ void snapshot().then((data) => {
12738
+ res.write(`event: snapshot
12739
+ data: ${JSON.stringify(data)}
12740
+
12741
+ `);
12742
+ }).catch((error) => {
12743
+ res.write(`event: snapshot
12744
+ data: ${JSON.stringify({ error: error?.message ?? String(error) })}
12405
12745
 
12406
12746
  `);
12747
+ });
12407
12748
  };
12408
12749
  send();
12409
12750
  const interval = setInterval(send, DEFAULT_REFRESH_MS);
@@ -12454,7 +12795,7 @@ data: ${JSON.stringify(snapshot())}
12454
12795
  },
12455
12796
  handler: async ({ input }) => ({
12456
12797
  ok: true,
12457
- summary: summarizeSnapshot(snapshot(input))
12798
+ summary: summarizeSnapshot(await snapshot(input))
12458
12799
  })
12459
12800
  },
12460
12801
  {
@@ -12530,7 +12871,7 @@ function createCopilotChannelsRuntime(options = {}) {
12530
12871
  log: runtimeService.provider.log
12531
12872
  });
12532
12873
  logRuntime("gateway created");
12533
- function getDiagnosticSnapshot(options2 = {}) {
12874
+ async function getDiagnosticSnapshot(options2 = {}) {
12534
12875
  return runtimeService.diagnostics.snapshot(options2, {
12535
12876
  gateway: typeof gateway.getDiagnosticState === "function" ? gateway.getDiagnosticState() : null,
12536
12877
  tools: gateway.isRunning() ? gateway.getAllTools(tools) : tools
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "2.0.8"
2
+ "version": "2.0.9"
3
3
  }
@@ -0,0 +1,33 @@
1
+ # ADR 0001: Persistent config emitters default to user ownership
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ No `docs/adr/0000-template.md` existed when this ADR was written, so this file uses the standard ADR sections.
10
+
11
+ The project documentation treats on-disk emitter definitions as persistent workflows and recommends `userOwned` ownership for persistent, recurring, or policy-bearing emitters. Existing normalization code defaulted missing persisted emitter ownership and lifespan to `modelOwned` and `temporary`, even though configured emitters are restored from disk and listed as persistent definitions.
12
+
13
+ That mismatch can weaken the protection model for manually edited config files: an emitter with no explicit ownership may be treated as model-owned during normalization even though it came from persistent user config.
14
+
15
+ ## Decision
16
+
17
+ Persisted/on-disk emitter definitions default to:
18
+
19
+ - `ownership: "userOwned"`
20
+ - `lifespan: "persistent"`
21
+
22
+ Normalization still honors explicit compatibility fields:
23
+
24
+ - `ownership` or legacy `managedBy`, including explicit `modelOwned`
25
+ - `lifespan` or legacy `scope`, including explicit `temporary`
26
+
27
+ Runtime-created temporary/model-owned emitters keep their explicit normalized fields when serialized, so this default only applies when persisted config omits ownership/lifespan signals.
28
+
29
+ ## Consequences
30
+
31
+ - Manually authored config aligns with the documented safety default for persistent workflows.
32
+ - Legacy config with explicit `modelOwned` or `temporary` remains compatible.
33
+ - Future changes that alter config ownership/lifespan defaults should update or supersede this ADR.