copilot-tap-extension 2.0.7 → 2.0.8

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
@@ -289,6 +289,8 @@ PLAN.md # ubiquitous language and design decisions
289
289
  | [Use cases and patterns](./docs/use-cases.md) | Recipes for deploy watchers, PR monitors, log tailers, and more |
290
290
  | [Copilot SDK canvas surfaces](./docs/recipes/copilot-sdk-canvas.md) | Local SDK findings for extension-owned canvas UI surfaces |
291
291
  | [Codex Goals lessons for tap-goal](./docs/recipes/codex-goals-for-tap-goal.md) | Goal-loop contract, evidence audit, and autopilot scheduling guidance |
292
+ | [Tap control-plane roadmap](./docs/recipes/tap-control-plane-roadmap.md) | Implemented and deferred control-plane slices from the Codex/Copilot audit |
293
+ | [Provider integration patterns](./docs/recipes/provider-integration-patterns.md) | Jira/GitHub, CI auto-fix, code review, SAST, and browser/Detour provider recipes |
292
294
  | [Evals](./docs/evals.md) | Run or extend the automated test suite |
293
295
  | [Copilot instructions](./src/copilot-instructions.md) | Understand or customize how the agent uses this extension |
294
296
  | [Implementation plan](./PLAN.md) | Ubiquitous language and naming conventions for contributors |
package/bin/install.mjs CHANGED
@@ -268,6 +268,11 @@ function buildAncillaryArtifacts(targetRoot) {
268
268
  dest: path.join(targetRoot, "skills", "tap-goal", "SKILL.md"),
269
269
  label: "skills/tap-goal/SKILL.md"
270
270
  },
271
+ {
272
+ src: path.join(distDir, "skills", "tap-orchestrate", "SKILL.md"),
273
+ dest: path.join(targetRoot, "skills", "tap-orchestrate", "SKILL.md"),
274
+ label: "skills/tap-orchestrate/SKILL.md"
275
+ },
271
276
  {
272
277
  src: path.join(distDir, "copilot-instructions.md"),
273
278
  dest: path.join(targetRoot, "copilot-instructions.md"),
@@ -161,11 +161,21 @@ switching into wrap-up mode when the remaining iteration budget is low, posting
161
161
  structured iteration records to their EventStream with `tap_post`, and stopping
162
162
  themselves when complete or blocked. Completion requires an evidence audit
163
163
  against concrete files, tests, logs, benchmark output, or generated artifacts.
164
+ Use `tap_verify_goal_output` and `tap_audit_claims` when the verification surface
165
+ is a workspace file, stream entry, or already-run command result.
164
166
  If the session may stay continuously busy (for example in autopilot-heavy
165
167
  flows), use a timed PromptEmitter with a backoff schedule such as
166
168
  `everySchedule=["2m","5m","10m"]` instead of relying on idle to trigger the next
167
169
  goal step.
168
170
 
171
+ Use `tap_get_session_state` before mode-sensitive work. It reads the current
172
+ Copilot mode, model/reasoning effort, tasks, schedules, open canvases, and UI
173
+ capabilities without mutating the session.
174
+
175
+ For genuinely multi-role work, prefer `/tap-orchestrate`: create one coordinator
176
+ PromptEmitter that gates role-specific sub-emitters using EventStream handoffs.
177
+ Do not use orchestration for tasks a single `/tap-goal` loop can complete.
178
+
169
179
  ## Borrow from the official SDK examples
170
180
 
171
181
  When working on the extension itself, not just using its emitter tools, prefer these SDK patterns:
@@ -4654,12 +4654,29 @@ function renderCanvasOpenResult(result) {
4654
4654
  result?.availability ? `availability=${result.availability}` : null
4655
4655
  ].filter(Boolean).join("\n");
4656
4656
  }
4657
+ function summarizeRuntimeState(state) {
4658
+ const mode = state?.mode?.ok ? state.mode.value : `unavailable (${state?.mode?.error ?? "unknown"})`;
4659
+ const model = state?.model?.ok ? state.model.value : null;
4660
+ const taskCount = state?.tasks?.ok ? state.tasks.value?.tasks?.length ?? 0 : null;
4661
+ const scheduleCount = state?.schedules?.ok ? state.schedules.value?.entries?.length ?? 0 : null;
4662
+ const canvasCount = state?.openCanvases?.ok ? state.openCanvases.value?.openCanvases?.length ?? 0 : null;
4663
+ return [
4664
+ `sessionId=${state?.sessionId ?? "(none)"}`,
4665
+ `mode=${typeof mode === "string" ? mode : JSON.stringify(mode)}`,
4666
+ model ? `model=${model.modelId ?? "unknown"} reasoning=${model.reasoningEffort ?? "default"} context=${model.contextTier ?? "default"}` : null,
4667
+ taskCount !== null ? `tasks=${taskCount}` : null,
4668
+ scheduleCount !== null ? `schedules=${scheduleCount}` : null,
4669
+ canvasCount !== null ? `openCanvases=${canvasCount}` : null,
4670
+ `elicitation=${state?.capabilities?.ui?.elicitation === true ? "available" : "unavailable"}`,
4671
+ `canvases=${state?.capabilities?.ui?.canvases === true ? "available" : "host-gated"}`
4672
+ ].filter(Boolean).join("\n");
4673
+ }
4657
4674
  function createDiagnosticsTools(deps) {
4658
4675
  const diagnostics2 = deps.diagnostics ?? deps.runtime;
4659
4676
  if (!diagnostics2 || typeof diagnostics2.openCanvas !== "function") {
4660
4677
  return [];
4661
4678
  }
4662
- return [
4679
+ const tools = [
4663
4680
  {
4664
4681
  name: "tap_open_diagnostics_canvas",
4665
4682
  description: "Opens or focuses the Tap diagnostics canvas, a live flight recorder for streams, emitters, providers, logs, injection queues, and session events.",
@@ -4684,6 +4701,105 @@ function createDiagnosticsTools(deps) {
4684
4701
  })
4685
4702
  }
4686
4703
  ];
4704
+ if (typeof diagnostics2.getSessionRuntimeState === "function") {
4705
+ tools.push({
4706
+ name: "tap_get_session_state",
4707
+ description: "Reads current Copilot session runtime state for mode-aware tap workflows: mode, model, tasks, schedules, open canvases, and UI capabilities. This is read-only.",
4708
+ parameters: {
4709
+ type: "object",
4710
+ properties: {}
4711
+ },
4712
+ handler: wrapToolHandler("tap_get_session_state", {}, async () => {
4713
+ const state = await diagnostics2.getSessionRuntimeState();
4714
+ return summarizeRuntimeState(state);
4715
+ })
4716
+ });
4717
+ }
4718
+ return tools;
4719
+ }
4720
+
4721
+ // src/tools/goal-verification.mjs
4722
+ function renderVerification(prefix, result) {
4723
+ const lines = [
4724
+ `${prefix}: ${result.passed ? "passed" : "failed"}`,
4725
+ ...result.results.map((item) => {
4726
+ const label = item.description ?? item.claim ?? item.path ?? item.channel ?? item.label ?? `check ${item.index}`;
4727
+ return `- ${item.passed ? "PASS" : "FAIL"} ${label}${item.error ? ` \u2014 ${item.error}` : ""}`;
4728
+ })
4729
+ ];
4730
+ return lines.join("\n");
4731
+ }
4732
+ var CHECK_SCHEMA = {
4733
+ type: "object",
4734
+ properties: {
4735
+ type: {
4736
+ type: "string",
4737
+ description: "Check type: 'file', 'stream', or 'command_evidence'."
4738
+ },
4739
+ description: { type: "string" },
4740
+ path: { type: "string", description: "Workspace-relative file path for file checks." },
4741
+ nonEmpty: { type: "boolean", description: "For file checks, require a non-empty file." },
4742
+ channel: { type: "string", description: "EventStream name for stream checks." },
4743
+ limit: { type: "integer", description: "Recent stream entries to inspect." },
4744
+ minEntries: { type: "integer", description: "Minimum retained entries required." },
4745
+ contains: { type: "string", description: "Literal text that must be present." },
4746
+ pattern: { type: "string", description: "Regex pattern that must match." },
4747
+ label: { type: "string", description: "Human label for command evidence." },
4748
+ exitCode: { type: "integer", description: "Exit code from an already-run command." },
4749
+ success: { type: "boolean", description: "Whether already-run command evidence succeeded." }
4750
+ }
4751
+ };
4752
+ function createGoalVerificationTools(deps) {
4753
+ const verification = deps.verification ?? deps.runtime;
4754
+ if (!verification || typeof verification.verifyGoalOutput !== "function") {
4755
+ return [];
4756
+ }
4757
+ return [
4758
+ {
4759
+ name: "tap_verify_goal_output",
4760
+ description: "Verifies goal completion evidence without executing commands. Checks workspace files, EventStream history, or caller-supplied command evidence.",
4761
+ parameters: {
4762
+ type: "object",
4763
+ properties: {
4764
+ checks: {
4765
+ type: "array",
4766
+ items: CHECK_SCHEMA,
4767
+ description: "Evidence checks to perform. Command checks must use already-run command evidence; this tool does not execute shell commands."
4768
+ }
4769
+ },
4770
+ required: ["checks"]
4771
+ },
4772
+ handler: wrapToolHandler("tap_verify_goal_output", {}, async (args) => {
4773
+ const result = verification.verifyGoalOutput(args ?? {});
4774
+ return renderVerification("Goal output verification", result);
4775
+ })
4776
+ },
4777
+ {
4778
+ name: "tap_audit_claims",
4779
+ description: "Audits machine-readable goal claims against file, stream, or command-evidence surfaces before marking a goal complete.",
4780
+ parameters: {
4781
+ type: "object",
4782
+ properties: {
4783
+ claims: {
4784
+ type: "array",
4785
+ items: {
4786
+ type: "object",
4787
+ properties: {
4788
+ claim: { type: "string" },
4789
+ evidence: CHECK_SCHEMA
4790
+ },
4791
+ required: ["claim", "evidence"]
4792
+ }
4793
+ }
4794
+ },
4795
+ required: ["claims"]
4796
+ },
4797
+ handler: wrapToolHandler("tap_audit_claims", {}, async (args) => {
4798
+ const result = verification.auditClaims(args ?? {});
4799
+ return renderVerification("Claim audit", result);
4800
+ })
4801
+ }
4802
+ ];
4687
4803
  }
4688
4804
 
4689
4805
  // src/tools/index.mjs
@@ -4692,10 +4808,12 @@ function createTools(deps) {
4692
4808
  const streams = deps?.streams ?? source.streams ?? deps?.runtime;
4693
4809
  const emitters = deps?.emitters ?? source.emitters ?? deps?.runtime;
4694
4810
  const diagnostics2 = deps?.diagnostics ?? source.diagnostics ?? deps?.runtime;
4811
+ const verification = deps?.verification ?? source.verification ?? deps?.runtime;
4695
4812
  return [
4696
4813
  ...createStreamTools({ streams }),
4697
4814
  ...createEmitterTools({ emitters }),
4698
- ...createDiagnosticsTools({ diagnostics: diagnostics2 })
4815
+ ...createDiagnosticsTools({ diagnostics: diagnostics2 }),
4816
+ ...createGoalVerificationTools({ verification })
4699
4817
  ];
4700
4818
  }
4701
4819
 
@@ -8632,7 +8750,7 @@ function shouldPersistLoadedConfig(parsedConfig, normalizedConfig) {
8632
8750
  return !isDeepStrictEqual(parsedConfig, serializeConfigForComparison(normalizedConfig));
8633
8751
  }
8634
8752
  function createConfigStore(options = {}) {
8635
- const fs2 = options.fs ?? { existsSync: existsSync2, readFileSync: readFileSync2, writeFileSync: writeFileSync2 };
8753
+ const fs3 = options.fs ?? { existsSync: existsSync2, readFileSync: readFileSync2, writeFileSync: writeFileSync2 };
8636
8754
  const state = {
8637
8755
  cwd: normalizeBaseCwd(options.cwd),
8638
8756
  filePath: null,
@@ -8660,7 +8778,7 @@ function createConfigStore(options = {}) {
8660
8778
  const exists = withConfigLoadPhase(
8661
8779
  "checking config path",
8662
8780
  "Unable to check whether the tap config file exists.",
8663
- () => fs2.existsSync(filePath),
8781
+ () => fs3.existsSync(filePath),
8664
8782
  { filePath }
8665
8783
  );
8666
8784
  if (!exists) {
@@ -8669,7 +8787,7 @@ function createConfigStore(options = {}) {
8669
8787
  const rawConfig = withConfigLoadPhase(
8670
8788
  "reading config file",
8671
8789
  "Unable to read the tap config file.",
8672
- () => fs2.readFileSync(filePath, "utf8"),
8790
+ () => fs3.readFileSync(filePath, "utf8"),
8673
8791
  { filePath }
8674
8792
  );
8675
8793
  const parsedConfig = withConfigLoadPhase(
@@ -8727,7 +8845,7 @@ function createConfigStore(options = {}) {
8727
8845
  state.filePath = defaultConfigPath(state.cwd);
8728
8846
  }
8729
8847
  const payload = serializeConfig(state.config, LATEST_CONFIG_VERSION);
8730
- fs2.writeFileSync(state.filePath, `${JSON.stringify(payload, null, 2)}
8848
+ fs3.writeFileSync(state.filePath, `${JSON.stringify(payload, null, 2)}
8731
8849
  `, "utf8");
8732
8850
  }
8733
8851
  function findStreamIndex(name) {
@@ -9231,9 +9349,39 @@ function createDefaultProcessAdapter() {
9231
9349
  readLines
9232
9350
  };
9233
9351
  }
9352
+ function recordScheduledTrace(emitter, context, { startedAt, endedAt = nowIso(), runIndex, result = null, error = null, consumedRun = true } = {}) {
9353
+ try {
9354
+ context.diagnostics?.trace?.({
9355
+ traceId: `${stableTraceComponent(emitter.name)}-${Number(runIndex ?? emitter.runCount) || 0}-${Date.parse(startedAt ?? endedAt) || Date.now()}`,
9356
+ emitterId: emitter.name,
9357
+ emitterName: emitter.name,
9358
+ runIndex: Number(runIndex ?? emitter.runCount) || null,
9359
+ emitterType: emitter.emitterType,
9360
+ runSchedule: emitter.runSchedule,
9361
+ startedAt: startedAt ?? endedAt,
9362
+ endedAt,
9363
+ status: result?.deferred ? "deferred" : result?.ok ? "success" : "failure",
9364
+ ok: result?.ok === true,
9365
+ consumedRun,
9366
+ lineCount: emitter.lineCount,
9367
+ droppedLineCount: emitter.droppedLineCount,
9368
+ error: error ?? result?.error ?? null,
9369
+ metadata: {
9370
+ every: emitter.every ?? null,
9371
+ everySchedule: emitter.everySchedule ?? null,
9372
+ maxRuns: emitter.maxRuns ?? null,
9373
+ stopRequested: emitter.stopRequested === true
9374
+ }
9375
+ });
9376
+ } catch {
9377
+ }
9378
+ }
9234
9379
  var SESSION_ATTACH_RETRY_MS = 100;
9235
9380
  var DEFAULT_STOP_WAIT_TIMEOUT_MS = 1e4;
9236
9381
  var STOP_WAIT_POLL_MS = 50;
9382
+ function stableTraceComponent(value) {
9383
+ return String(value ?? "unknown").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
9384
+ }
9237
9385
  function errorMessage(error) {
9238
9386
  return error?.message ?? String(error ?? "unknown error");
9239
9387
  }
@@ -9569,6 +9717,8 @@ async function runScheduledIteration(emitter, context) {
9569
9717
  emitter.status = EMITTER_STATUS.RUNNING;
9570
9718
  emitter.runCount += 1;
9571
9719
  emitter.lastRunAt = nowIso();
9720
+ const traceStartedAt = emitter.lastRunAt;
9721
+ const traceRunIndex = emitter.runCount;
9572
9722
  let result;
9573
9723
  try {
9574
9724
  result = emitter.emitterType === EMITTER_TYPE.PROMPT ? await runPromptIteration(emitter, context) : await runCommandLoopIteration(emitter, context);
@@ -9586,6 +9736,12 @@ async function runScheduledIteration(emitter, context) {
9586
9736
  if (result?.consumeRun === false) {
9587
9737
  emitter.runCount = previousRunCount;
9588
9738
  emitter.lastRunAt = previousLastRunAt;
9739
+ recordScheduledTrace(emitter, context, {
9740
+ startedAt: traceStartedAt,
9741
+ runIndex: traceRunIndex,
9742
+ result,
9743
+ consumedRun: false
9744
+ });
9589
9745
  if (emitter.stopRequested) {
9590
9746
  applyLifecycleTransition(emitter, {
9591
9747
  type: LIFECYCLE_EVENT.ITERATION_RESULT,
@@ -9604,7 +9760,20 @@ async function runScheduledIteration(emitter, context) {
9604
9760
  result,
9605
9761
  timestamp: nowIso()
9606
9762
  }, context);
9763
+ recordScheduledTrace(emitter, context, {
9764
+ startedAt: traceStartedAt,
9765
+ runIndex: traceRunIndex,
9766
+ result,
9767
+ consumedRun: true
9768
+ });
9607
9769
  } catch (error) {
9770
+ recordScheduledTrace(emitter, context, {
9771
+ startedAt: traceStartedAt,
9772
+ runIndex: traceRunIndex,
9773
+ result: { ok: false, error: errorMessage(error) },
9774
+ error: errorMessage(error),
9775
+ consumedRun: true
9776
+ });
9608
9777
  recordScheduledTransitionFailure(emitter, error, context);
9609
9778
  } finally {
9610
9779
  emitter.inFlight = false;
@@ -9618,7 +9787,8 @@ function createLifecycle({
9618
9787
  sessionPort,
9619
9788
  timerAdapter = createDefaultTimerAdapter(),
9620
9789
  processAdapter = createDefaultProcessAdapter(),
9621
- loggerAdapter = createDefaultLoggerAdapter(sessionPort)
9790
+ loggerAdapter = createDefaultLoggerAdapter(sessionPort),
9791
+ diagnostics: diagnostics2 = null
9622
9792
  }) {
9623
9793
  function start(emitter) {
9624
9794
  if (emitter.runSchedule === RUN_SCHEDULE.CONTINUOUS) {
@@ -9628,6 +9798,7 @@ function createLifecycle({
9628
9798
  contextStartScheduled(emitter);
9629
9799
  }
9630
9800
  function contextStartScheduled(emitter) {
9801
+ const context = { lineRouter, timerAdapter, processAdapter, loggerAdapter, sessionPort, diagnostics: diagnostics2 };
9631
9802
  const scheduleLabel2 = 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;
9632
9803
  lineRouter.appendSystemMessage(
9633
9804
  emitter,
@@ -9640,17 +9811,17 @@ function createLifecycle({
9640
9811
  );
9641
9812
  if (emitter.runSchedule === RUN_SCHEDULE.IDLE) {
9642
9813
  if (sessionPort.isIdle()) {
9643
- scheduleIteration(emitter, { lineRouter, timerAdapter, processAdapter, loggerAdapter, sessionPort }, IDLE_PROMPT_DELAY_MS);
9814
+ scheduleIteration(emitter, context, IDLE_PROMPT_DELAY_MS);
9644
9815
  }
9645
9816
  return;
9646
9817
  }
9647
- scheduleIteration(emitter, { lineRouter, timerAdapter, processAdapter, loggerAdapter, sessionPort }, 0);
9818
+ scheduleIteration(emitter, context, 0);
9648
9819
  }
9649
9820
  async function stop(emitter) {
9650
9821
  applyLifecycleTransition(
9651
9822
  emitter,
9652
9823
  { type: LIFECYCLE_EVENT.STOP, timestamp: nowIso() },
9653
- { lineRouter, timerAdapter, processAdapter, loggerAdapter, sessionPort }
9824
+ { lineRouter, timerAdapter, processAdapter, loggerAdapter, sessionPort, diagnostics: diagnostics2 }
9654
9825
  );
9655
9826
  if (isTerminalEmitterStatus(emitter.status)) {
9656
9827
  return;
@@ -9826,14 +9997,14 @@ function restorePersistentStreamConfigBestEffort(configStore, snapshot) {
9826
9997
 
9827
9998
  // src/emitter/supervisor.mjs
9828
9999
  var ROLLBACK_STOP_WAIT_TIMEOUT_MS = 1e4;
9829
- function createEmitterSupervisor({ streams, configStore, notifications, sessionPort, emitterWorkspace, persist, lifecycle: lifecycleOverride }) {
10000
+ function createEmitterSupervisor({ streams, configStore, notifications, sessionPort, emitterWorkspace, persist, lifecycle: lifecycleOverride, diagnostics: diagnostics2 = null }) {
9830
10001
  const emitters = /* @__PURE__ */ new Map();
9831
10002
  const lineRouter = createLineRouter({
9832
10003
  streams,
9833
10004
  notifications,
9834
10005
  surface: (message, options) => sessionPort.log(message, options)
9835
10006
  });
9836
- const lifecycle = lifecycleOverride ?? createLifecycle({ lineRouter, sessionPort });
10007
+ const lifecycle = lifecycleOverride ?? createLifecycle({ lineRouter, sessionPort, diagnostics: diagnostics2 });
9837
10008
  function hasExplicitPolicyValue(value) {
9838
10009
  return value !== void 0 && value !== null;
9839
10010
  }
@@ -10367,6 +10538,29 @@ function createSessionPort(initialSession = null) {
10367
10538
  }
10368
10539
  return canvasApi.open(params);
10369
10540
  }
10541
+ async function getRuntimeState() {
10542
+ if (!session2) {
10543
+ throw new LifecycleError("Session is not attached; cannot inspect runtime state.");
10544
+ }
10545
+ const rpc = session2.rpc ?? {};
10546
+ const read = async (label, fn) => {
10547
+ try {
10548
+ return { ok: true, value: await fn() };
10549
+ } catch (error) {
10550
+ return { ok: false, error: error?.message ?? String(error ?? "unknown error") };
10551
+ }
10552
+ };
10553
+ return {
10554
+ sessionId: session2.sessionId ?? null,
10555
+ capabilities: session2.capabilities ?? null,
10556
+ mode: await read("mode", () => rpc.mode?.get?.()),
10557
+ model: await read("model", () => rpc.model?.getCurrent?.()),
10558
+ tasks: await read("tasks", () => rpc.tasks?.list?.()),
10559
+ schedules: await read("schedules", () => rpc.schedule?.list?.()),
10560
+ skills: await read("skills", () => rpc.skills?.list?.()),
10561
+ openCanvases: await read("openCanvases", () => rpc.canvas?.listOpen?.())
10562
+ };
10563
+ }
10370
10564
  function registerTools(tools) {
10371
10565
  if (!session2) return;
10372
10566
  try {
@@ -10403,6 +10597,7 @@ function createSessionPort(initialSession = null) {
10403
10597
  send,
10404
10598
  sendAndWait,
10405
10599
  openCanvas,
10600
+ getRuntimeState,
10406
10601
  registerTools,
10407
10602
  reloadExtension
10408
10603
  };
@@ -10683,7 +10878,8 @@ function createRuntimeSubsystems(options = {}) {
10683
10878
  notifications,
10684
10879
  sessionPort,
10685
10880
  emitterWorkspace,
10686
- persist
10881
+ persist,
10882
+ diagnostics: options.diagnostics
10687
10883
  });
10688
10884
  return {
10689
10885
  sessionContext,
@@ -10858,10 +11054,170 @@ function createStreamService(deps) {
10858
11054
  };
10859
11055
  }
10860
11056
 
11057
+ // src/services/goal-verification-service.mjs
11058
+ import fs2 from "node:fs";
11059
+ import path7 from "node:path";
11060
+ function normalizeText(value) {
11061
+ return String(value ?? "").trim();
11062
+ }
11063
+ function safeRegex(pattern) {
11064
+ try {
11065
+ return new RegExp(String(pattern));
11066
+ } catch {
11067
+ return null;
11068
+ }
11069
+ }
11070
+ function resolveWithinBase(baseCwd, requestedPath) {
11071
+ const raw = normalizeText(requestedPath);
11072
+ if (!raw) {
11073
+ return { ok: false, error: "path is required" };
11074
+ }
11075
+ const base = path7.resolve(baseCwd || process.cwd());
11076
+ const resolved = path7.isAbsolute(raw) ? path7.resolve(raw) : path7.resolve(base, raw);
11077
+ const relative = path7.relative(base, resolved);
11078
+ if (relative.startsWith("..") || path7.isAbsolute(relative)) {
11079
+ return { ok: false, error: `path '${raw}' is outside the session workspace` };
11080
+ }
11081
+ return { ok: true, path: resolved, displayPath: relative || "." };
11082
+ }
11083
+ function textMatches(text, check = {}) {
11084
+ const contains = normalizeText(check.contains);
11085
+ if (contains && !String(text).includes(contains)) {
11086
+ return { ok: false, error: `expected text to contain '${contains}'` };
11087
+ }
11088
+ const pattern = normalizeText(check.pattern);
11089
+ if (pattern) {
11090
+ const regex = safeRegex(pattern);
11091
+ if (!regex) {
11092
+ return { ok: false, error: `invalid regex '${pattern}'` };
11093
+ }
11094
+ if (!regex.test(String(text))) {
11095
+ return { ok: false, error: `expected text to match /${pattern}/` };
11096
+ }
11097
+ }
11098
+ return { ok: true };
11099
+ }
11100
+ function verifyFile(check, { baseCwd }) {
11101
+ const resolved = resolveWithinBase(baseCwd, check.path);
11102
+ if (!resolved.ok) {
11103
+ return { ...resolved, passed: false };
11104
+ }
11105
+ if (!fs2.existsSync(resolved.path)) {
11106
+ return { passed: false, path: resolved.displayPath, error: "file does not exist" };
11107
+ }
11108
+ const stat = fs2.statSync(resolved.path);
11109
+ if (!stat.isFile()) {
11110
+ return { passed: false, path: resolved.displayPath, error: "path is not a file" };
11111
+ }
11112
+ if (check.nonEmpty === true && stat.size === 0) {
11113
+ return { passed: false, path: resolved.displayPath, error: "file is empty" };
11114
+ }
11115
+ if (check.contains || check.pattern) {
11116
+ const content = fs2.readFileSync(resolved.path, "utf8");
11117
+ const match = textMatches(content, check);
11118
+ if (!match.ok) {
11119
+ return { passed: false, path: resolved.displayPath, error: match.error };
11120
+ }
11121
+ }
11122
+ return { passed: true, path: resolved.displayPath, size: stat.size };
11123
+ }
11124
+ function verifyStream(check, { getStreamHistory }) {
11125
+ const channel = normalizeText(check.channel);
11126
+ if (!channel) {
11127
+ return { passed: false, error: "channel is required" };
11128
+ }
11129
+ let stream;
11130
+ try {
11131
+ stream = getStreamHistory(channel, check.limit)?.stream;
11132
+ } catch (error) {
11133
+ return { passed: false, channel, error: error?.message ?? String(error) };
11134
+ }
11135
+ const entries = Array.isArray(stream?.entries) ? stream.entries : [];
11136
+ if (check.minEntries !== void 0 && entries.length < Number(check.minEntries)) {
11137
+ return { passed: false, channel, entries: entries.length, error: `expected at least ${check.minEntries} entries` };
11138
+ }
11139
+ const text = entries.map((entry) => entry.text ?? "").join("\n");
11140
+ const match = textMatches(text, check);
11141
+ if (!match.ok) {
11142
+ return { passed: false, channel, entries: entries.length, error: match.error };
11143
+ }
11144
+ return { passed: true, channel, entries: entries.length };
11145
+ }
11146
+ function verifyCommandEvidence(check) {
11147
+ const label = normalizeText(check.label ?? check.command ?? "command evidence");
11148
+ if (check.exitCode === void 0 && check.success !== true) {
11149
+ return { passed: false, label, error: "provide exitCode or success=true from an already-run command" };
11150
+ }
11151
+ const passed = check.success === true || Number(check.exitCode) === 0;
11152
+ return {
11153
+ passed,
11154
+ label,
11155
+ exitCode: check.exitCode ?? null,
11156
+ error: passed ? null : `command evidence did not indicate success`
11157
+ };
11158
+ }
11159
+ function verifyCheck(check = {}, context) {
11160
+ const type = normalizeText(check.type || check.kind);
11161
+ if (type === "file" || type === "file_exists") {
11162
+ return verifyFile(check, context);
11163
+ }
11164
+ if (type === "stream" || type === "stream_contains" || type === "channel") {
11165
+ return verifyStream(check, context);
11166
+ }
11167
+ if (type === "command" || type === "command_evidence") {
11168
+ return verifyCommandEvidence(check);
11169
+ }
11170
+ return { passed: false, error: `unsupported check type '${type || "<missing>"}'` };
11171
+ }
11172
+ function createGoalVerificationService({ getBaseCwd, getStreamHistory } = {}) {
11173
+ function context() {
11174
+ return {
11175
+ baseCwd: typeof getBaseCwd === "function" ? getBaseCwd() : process.cwd(),
11176
+ getStreamHistory
11177
+ };
11178
+ }
11179
+ function verifyGoalOutput(input = {}) {
11180
+ const checks = Array.isArray(input.checks) ? input.checks : [];
11181
+ const results = checks.map((check, index) => ({
11182
+ index,
11183
+ description: check.description ?? check.claim ?? null,
11184
+ type: check.type ?? check.kind ?? null,
11185
+ ...verifyCheck(check, context())
11186
+ }));
11187
+ return {
11188
+ passed: results.length > 0 && results.every((result) => result.passed === true),
11189
+ results
11190
+ };
11191
+ }
11192
+ function auditClaims(input = {}) {
11193
+ const claims = Array.isArray(input.claims) ? input.claims : [];
11194
+ const results = claims.map((claim, index) => {
11195
+ const evidence = claim.evidence && typeof claim.evidence === "object" ? claim.evidence : {};
11196
+ const verification = verifyCheck({
11197
+ ...evidence,
11198
+ description: evidence.description ?? claim.claim,
11199
+ claim: claim.claim
11200
+ }, context());
11201
+ return {
11202
+ index,
11203
+ claim: claim.claim ?? null,
11204
+ status: verification.passed ? "confirmed" : "blocked",
11205
+ ...verification
11206
+ };
11207
+ });
11208
+ return {
11209
+ passed: results.length > 0 && results.every((result) => result.passed === true),
11210
+ results
11211
+ };
11212
+ }
11213
+ return { verifyGoalOutput, auditClaims };
11214
+ }
11215
+
10861
11216
  // src/diagnostics/store.mjs
10862
11217
  var DEFAULT_MAX_LOGS = 300;
10863
11218
  var DEFAULT_MAX_EVENTS = 300;
10864
11219
  var DEFAULT_MAX_RUNTIME_EVENTS = 300;
11220
+ var DEFAULT_MAX_TRACES = 300;
10865
11221
  var MAX_STRING_LENGTH = 1200;
10866
11222
  var MAX_COLLECTION_ITEMS = 40;
10867
11223
  var MAX_DEPTH = 4;
@@ -10997,10 +11353,12 @@ function createDiagnosticsStore(options = {}) {
10997
11353
  const logs = createRingBuffer(options.maxLogs ?? DEFAULT_MAX_LOGS);
10998
11354
  const sessionEvents = createRingBuffer(options.maxSessionEvents ?? DEFAULT_MAX_EVENTS);
10999
11355
  const runtimeEvents = createRingBuffer(options.maxRuntimeEvents ?? DEFAULT_MAX_RUNTIME_EVENTS);
11356
+ const traces = createRingBuffer(options.maxTraces ?? DEFAULT_MAX_TRACES);
11000
11357
  const sessionEventCounts = /* @__PURE__ */ new Map();
11001
11358
  let logCount = 0;
11002
11359
  let runtimeEventCount = 0;
11003
11360
  let sessionEventCount = 0;
11361
+ let traceCount = 0;
11004
11362
  let cleanupSessionListener = () => {
11005
11363
  };
11006
11364
  function recordLog(source, message, options2 = {}) {
@@ -11032,6 +11390,38 @@ function createDiagnosticsStore(options = {}) {
11032
11390
  })
11033
11391
  });
11034
11392
  }
11393
+ function recordTrace(trace = {}) {
11394
+ traceCount += 1;
11395
+ const startedAt = trace.startedAt ?? trace.timestamp ?? nowIso();
11396
+ const endedAt = trace.endedAt ?? nowIso();
11397
+ const startMs = Date.parse(startedAt);
11398
+ const endMs = Date.parse(endedAt);
11399
+ const durationMs = Number.isFinite(startMs) && Number.isFinite(endMs) ? Math.max(0, endMs - startMs) : null;
11400
+ return traces.append({
11401
+ id: createId("trace", traceCount),
11402
+ traceId: String(trace.traceId ?? `trace-${traceCount.toString(36)}`),
11403
+ timestamp: endedAt,
11404
+ startedAt,
11405
+ endedAt,
11406
+ durationMs,
11407
+ emitterId: trace.emitterId ?? trace.emitterName ?? null,
11408
+ emitterName: trace.emitterName ?? trace.emitterId ?? null,
11409
+ runIndex: Number.isFinite(Number(trace.runIndex)) ? Number(trace.runIndex) : null,
11410
+ emitterType: trace.emitterType ?? null,
11411
+ runSchedule: trace.runSchedule ?? null,
11412
+ status: trace.status ?? "unknown",
11413
+ ok: trace.ok === true,
11414
+ consumedRun: trace.consumedRun !== false,
11415
+ lineCount: Number.isFinite(Number(trace.lineCount)) ? Number(trace.lineCount) : null,
11416
+ droppedLineCount: Number.isFinite(Number(trace.droppedLineCount)) ? Number(trace.droppedLineCount) : null,
11417
+ error: trace.error ? truncateString(trace.error, MAX_STRING_LENGTH) : null,
11418
+ metadata: safeClone(trace.metadata ?? null, {
11419
+ maxDepth: 3,
11420
+ maxStringLength: 700,
11421
+ maxCollectionItems: 20
11422
+ })
11423
+ });
11424
+ }
11035
11425
  function recordSessionEvent(event) {
11036
11426
  sessionEventCount += 1;
11037
11427
  const type = String(event?.type ?? "unknown");
@@ -11074,11 +11464,13 @@ function createDiagnosticsStore(options = {}) {
11074
11464
  generatedAt: nowIso(),
11075
11465
  logs: logs.snapshot(options2.logLimit ?? 140),
11076
11466
  runtimeEvents: runtimeEvents.snapshot(options2.runtimeEventLimit ?? 140),
11467
+ traces: traces.snapshot(options2.traceLimit ?? 140),
11077
11468
  sessionEvents: sessionEvents.snapshot(options2.sessionEventLimit ?? 140),
11078
11469
  sessionEventCounts: Object.fromEntries([...sessionEventCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
11079
11470
  stats: {
11080
11471
  logs: logs.stats(),
11081
11472
  runtimeEvents: runtimeEvents.stats(),
11473
+ traces: traces.stats(),
11082
11474
  sessionEvents: sessionEvents.stats()
11083
11475
  }
11084
11476
  };
@@ -11086,6 +11478,7 @@ function createDiagnosticsStore(options = {}) {
11086
11478
  return {
11087
11479
  log: recordLog,
11088
11480
  event: recordRuntimeEvent,
11481
+ trace: recordTrace,
11089
11482
  attachSession,
11090
11483
  detachSession,
11091
11484
  snapshot
@@ -11125,7 +11518,10 @@ function createTapRuntimeService(options = {}) {
11125
11518
  configWorkspace,
11126
11519
  emitterWorkspace,
11127
11520
  persist
11128
- } = createRuntimeSubsystems(options);
11521
+ } = createRuntimeSubsystems({
11522
+ ...options,
11523
+ diagnostics: diagnosticsStore
11524
+ });
11129
11525
  const streamService = createStreamService({
11130
11526
  streams,
11131
11527
  configStore,
@@ -11138,6 +11534,10 @@ function createTapRuntimeService(options = {}) {
11138
11534
  supervisor,
11139
11535
  emitterWorkspace
11140
11536
  });
11537
+ const goalVerificationService = createGoalVerificationService({
11538
+ getBaseCwd: sessionContext.getBaseCwd,
11539
+ getStreamHistory: (channel, limit) => streamService.getStreamHistory(channel, limit)
11540
+ });
11141
11541
  const configBootstrapService = createConfigBootstrapService({
11142
11542
  streams,
11143
11543
  configStore,
@@ -11145,7 +11545,8 @@ function createTapRuntimeService(options = {}) {
11145
11545
  sessionPort,
11146
11546
  configWorkspace
11147
11547
  });
11148
- const { loadPersistentConfig } = configBootstrapService;
11548
+ const { loadPersistentConfig: loadPersistentConfigRaw } = configBootstrapService;
11549
+ let persistentConfigLoadPromise = null;
11149
11550
  const sessionActivityBridge = createSessionActivityBridge({ sessionPort, supervisor });
11150
11551
  const providerPushService = createProviderPushService({
11151
11552
  streams,
@@ -11157,11 +11558,27 @@ function createTapRuntimeService(options = {}) {
11157
11558
  const session2 = sessionPort.current();
11158
11559
  return sessionContext.getSessionInfo(session2);
11159
11560
  }
11561
+ function loadPersistentConfigOnce(inputCwd) {
11562
+ if (persistentConfigLoadPromise) {
11563
+ return persistentConfigLoadPromise;
11564
+ }
11565
+ persistentConfigLoadPromise = Promise.resolve().then(() => loadPersistentConfigRaw(inputCwd)).catch((error) => {
11566
+ persistentConfigLoadPromise = null;
11567
+ throw error;
11568
+ });
11569
+ return persistentConfigLoadPromise;
11570
+ }
11160
11571
  function attachSession(session2) {
11161
11572
  sessionContext.attachSession(session2);
11162
11573
  sessionPort.attach(session2);
11163
11574
  sessionActivityBridge.attach(session2);
11164
11575
  diagnosticsStore.attachSession(session2);
11576
+ void loadPersistentConfigOnce(sessionContext.getBaseCwd()).then((summary) => {
11577
+ void sessionPort.log(summary);
11578
+ }).catch((error) => {
11579
+ const message = error?.message ?? String(error ?? "unknown error");
11580
+ void sessionPort.log(`Config load failed during extension attach: ${message}`, { level: "warning" });
11581
+ });
11165
11582
  }
11166
11583
  function clearNotificationsForLifecycle(options2 = {}) {
11167
11584
  if (options2.clearNotifications === true && typeof notifications.clear === "function") {
@@ -11216,7 +11633,7 @@ function createTapRuntimeService(options = {}) {
11216
11633
  } = createRuntimeHooks({
11217
11634
  streams,
11218
11635
  sessionPort,
11219
- loadPersistentConfig,
11636
+ loadPersistentConfig: loadPersistentConfigOnce,
11220
11637
  stopAllEmitters,
11221
11638
  stopAllEmittersAndWait,
11222
11639
  shutdownSession,
@@ -11279,6 +11696,7 @@ function createTapRuntimeService(options = {}) {
11279
11696
  input: canvasInput
11280
11697
  });
11281
11698
  },
11699
+ getSessionRuntimeState: () => sessionPort.getRuntimeState(),
11282
11700
  attachSession: diagnosticsStore.attachSession,
11283
11701
  detachSession: diagnosticsStore.detachSession
11284
11702
  };
@@ -11286,12 +11704,14 @@ function createTapRuntimeService(options = {}) {
11286
11704
  tools: {
11287
11705
  streams: streamCapabilities,
11288
11706
  emitters: emitterCapabilities,
11289
- diagnostics: diagnosticsCapabilities
11707
+ diagnostics: diagnosticsCapabilities,
11708
+ verification: goalVerificationService
11290
11709
  },
11291
11710
  hooks: hookCapabilities,
11292
11711
  session: sessionCapabilities,
11293
11712
  provider: providerCapabilities,
11294
11713
  diagnostics: diagnosticsCapabilities,
11714
+ verification: goalVerificationService,
11295
11715
  getBaseCwd: sessionContext.getBaseCwd,
11296
11716
  getSessionInfo,
11297
11717
  attachSession,
@@ -11300,7 +11720,7 @@ function createTapRuntimeService(options = {}) {
11300
11720
  appendStreamMessage,
11301
11721
  getSessionStartContext,
11302
11722
  getPromptContext,
11303
- loadPersistentConfig,
11723
+ loadPersistentConfig: loadPersistentConfigOnce,
11304
11724
  listEmitters: emitterCapabilities.listEmitters,
11305
11725
  listStreams: streamCapabilities.listStreams,
11306
11726
  postToStream: streamCapabilities.postToStream,
@@ -11341,6 +11761,7 @@ function sanitizeSnapshotOptions(input = {}) {
11341
11761
  return {
11342
11762
  streamEntryLimit: Number.isFinite(limit) ? Math.max(10, Math.min(200, Math.floor(limit))) : 80,
11343
11763
  logLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160,
11764
+ traceLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160,
11344
11765
  sessionEventLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160,
11345
11766
  runtimeEventLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160
11346
11767
  };
@@ -11351,8 +11772,9 @@ function summarizeSnapshot(snapshot) {
11351
11772
  const streams = Array.isArray(snapshot?.streams) ? snapshot.streams.length : 0;
11352
11773
  const providers = Array.isArray(snapshot?.gateway?.providers) ? snapshot.gateway.providers.length : 0;
11353
11774
  const logs = snapshot?.diagnostics?.stats?.logs?.retained ?? 0;
11775
+ const traces = snapshot?.diagnostics?.stats?.traces?.retained ?? 0;
11354
11776
  const sessionEvents = snapshot?.diagnostics?.stats?.sessionEvents?.retained ?? 0;
11355
- return { streams, runningEmitters, configuredEmitters, providers, logs, sessionEvents };
11777
+ return { streams, runningEmitters, configuredEmitters, providers, logs, traces, sessionEvents };
11356
11778
  }
11357
11779
  function createHtml() {
11358
11780
  return `<!doctype html>
@@ -11582,7 +12004,7 @@ function createHtml() {
11582
12004
  overflow: auto;
11583
12005
  }
11584
12006
 
11585
- .stream-grid, .emitter-grid, .provider-grid {
12007
+ .stream-grid, .emitter-grid, .provider-grid, .trace-grid {
11586
12008
  display: grid;
11587
12009
  gap: 12px;
11588
12010
  }
@@ -11754,6 +12176,16 @@ function createHtml() {
11754
12176
  </div>
11755
12177
  </section>
11756
12178
 
12179
+ <section class="panel">
12180
+ <div class="panel-head">
12181
+ <h2>Goals and traces</h2>
12182
+ <small id="trace-count">0 traces</small>
12183
+ </div>
12184
+ <div class="panel-body">
12185
+ <div class="trace-grid" id="traces"></div>
12186
+ </div>
12187
+ </section>
12188
+
11757
12189
  <section class="panel timeline-panel">
11758
12190
  <div class="panel-head">
11759
12191
  <h2>Evidence timeline</h2>
@@ -11790,12 +12222,14 @@ function createHtml() {
11790
12222
  const streams = snapshot.streams?.length ?? 0;
11791
12223
  const providers = snapshot.gateway?.providers?.length ?? 0;
11792
12224
  const queue = snapshot.notifications?.queueSize ?? 0;
12225
+ const traces = snapshot.diagnostics?.stats?.traces?.retained ?? 0;
11793
12226
  const sessionEvents = snapshot.diagnostics?.stats?.sessionEvents?.retained ?? 0;
11794
12227
  el("metrics").innerHTML = [
11795
12228
  metric("streams", streams),
11796
12229
  metric("running emitters", running),
11797
12230
  metric("providers", providers),
11798
12231
  metric("queued injections", queue),
12232
+ metric("traces", traces),
11799
12233
  metric("configured emitters", configured),
11800
12234
  metric("session events", sessionEvents)
11801
12235
  ].join("");
@@ -11835,6 +12269,21 @@ function createHtml() {
11835
12269
  el("providers").innerHTML = gatewayRecord + rows;
11836
12270
  }
11837
12271
 
12272
+ function renderTraces(snapshot) {
12273
+ const traces = snapshot.diagnostics?.traces ?? [];
12274
+ const goalStreams = (snapshot.streams ?? []).filter((stream) => String(stream.name ?? "").startsWith("goal-"));
12275
+ el("trace-count").textContent = traces.length + " traces";
12276
+ const traceRows = traces.slice(-12).reverse().map((trace) => {
12277
+ const tone = trace.ok ? "ready" : trace.status === "deferred" ? "warning" : "error";
12278
+ return '<div class="record"><strong>' + escapeHtml(trace.emitterName ?? trace.emitterId ?? "unknown") + ' <span class="badge ' + tone + '">' + escapeHtml(trace.status ?? "unknown") + '</span></strong><div class="meta">run=' + escapeHtml(trace.runIndex ?? "?") + ' duration=' + escapeHtml(trace.durationMs ?? "?") + 'ms | trace=' + escapeHtml(trace.traceId ?? "?") + '</div>' + (trace.error ? '<div class="details">' + escapeHtml(trace.error) + '</div>' : '') + '</div>';
12279
+ }).join("");
12280
+ const goalRows = goalStreams.map((stream) => {
12281
+ 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
+ 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
+ }).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 : "");
12285
+ }
12286
+
11838
12287
  function collectTimeline(snapshot) {
11839
12288
  const items = [];
11840
12289
  for (const stream of snapshot.streams ?? []) {
@@ -11851,6 +12300,9 @@ function createHtml() {
11851
12300
  for (const event of snapshot.diagnostics?.runtimeEvents ?? []) {
11852
12301
  items.push({ group: "runtime", timestamp: event.timestamp, source: event.type, level: "info", message: event.message, detail: compactJson(event.metadata) });
11853
12302
  }
12303
+ for (const trace of snapshot.diagnostics?.traces ?? []) {
12304
+ items.push({ group: "runtime", timestamp: trace.endedAt ?? trace.timestamp, source: "trace/" + (trace.emitterName ?? trace.emitterId ?? "unknown"), level: trace.ok ? "info" : "warning", message: "emitter run " + (trace.status ?? "unknown"), detail: compactJson({ traceId: trace.traceId, runIndex: trace.runIndex, durationMs: trace.durationMs, error: trace.error }) });
12305
+ }
11854
12306
  return items.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
11855
12307
  }
11856
12308
 
@@ -11874,6 +12326,7 @@ function createHtml() {
11874
12326
  renderMetrics(snapshot);
11875
12327
  renderStreams(snapshot);
11876
12328
  renderProviders(snapshot);
12329
+ renderTraces(snapshot);
11877
12330
  renderTimeline(snapshot);
11878
12331
  }
11879
12332
 
@@ -133,13 +133,19 @@ Continuation rules:
133
133
  - If only 1 iteration remains and the goal is not complete, do not start broad new work. Produce a budget-limited handoff instead.
134
134
  - Do not treat budget exhaustion or a lifecycle "reached run budget" message as success.
135
135
  - If this iteration makes no progress-producing tool calls beyond required status/ledger bookkeeping and does not change evidence, call `tap_post` with `channel='<goal-emitter-name>'` and a no-action handoff, then stop the emitter rather than spinning.
136
+ - If the remaining delta is unchanged from the previous ITERATION RECORD, post a STALLED LOOP record and stop rather than spending the rest of the budget.
136
137
 
137
138
  Evidence-audit rules:
138
139
  - Before marking complete, identify the verification surface from the goal contract.
139
140
  - Check the evidence directly: test output, benchmark result, file content, diff, generated artifact, source material, or other concrete proof.
141
+ - When the evidence is a workspace file, EventStream entry, or already-run command result, call `tap_verify_goal_output` or `tap_audit_claims` before GOAL COMPLETE.
140
142
  - Check listed constraints for regressions.
141
143
  - If the verification surface cannot be checked, treat the goal as blocked, not complete.
142
144
  - Completion requires an explicit evidence audit in the final response and in the EventStream.
145
+ - Wrap machine-readable EventStream records with explicit markers:
146
+ `=== BEGIN_ITERATION_RECORD ===` / `=== END_ITERATION_RECORD ===`,
147
+ `=== BEGIN_GOAL_COMPLETE ===` / `=== END_GOAL_COMPLETE ===`,
148
+ `=== BEGIN_GOAL_BLOCKED ===` / `=== END_GOAL_BLOCKED ===`.
143
149
 
144
150
  Research/audit goal rules:
145
151
  - For research, reproduction, audit, or investigation goals, maintain a claim ledger.
@@ -148,21 +154,26 @@ Research/audit goal rules:
148
154
 
149
155
  On this iteration:
150
156
  1. Briefly assess current progress toward the goal and the remaining iteration budget.
151
- 2. If the goal is already achieved, first call `tap_post` with `channel='<goal-emitter-name>'` and a GOAL COMPLETE evidence audit in `message`, then call tap_stop_emitter for '<goal-emitter-name>' with scope='temporary', report that the goal is complete, and stop.
157
+ 2. If the goal is already achieved, first call `tap_verify_goal_output` or `tap_audit_claims` against the verification surface. If verification passes, call `tap_post` with `channel='<goal-emitter-name>'` and a marked GOAL COMPLETE evidence audit in `message`, then call tap_stop_emitter for '<goal-emitter-name>' with scope='temporary', report that the goal is complete, and stop.
152
158
  3. If the goal is blocked by missing information, permissions, failing external systems, or an unsafe action, first call `tap_post` with `channel='<goal-emitter-name>'` and a GOAL BLOCKED report in `message`, then call tap_stop_emitter for '<goal-emitter-name>' with scope='temporary', report the blocker, and stop.
153
- 4. If this is the final iteration and the goal is not complete, do not start substantive new work. Call `tap_post` with `channel='<goal-emitter-name>'` and a BUDGET LIMITED summary in `message`: progress, evidence gathered, remaining work, and recommended next goal or budget. Then leave a concise handoff.
159
+ 4. If this is the final iteration and the goal is not complete, do not start substantive new work. Call `tap_post` with `channel='<goal-emitter-name>'` and a BUDGET LIMITED summary in `message`: progress, evidence gathered, remaining work, recommended next `/tap-goal ...` invocation, and suggested fresh budget. Then leave a concise handoff.
154
160
  5. Otherwise, choose the next smallest useful action toward the goal that fits the remaining budget and perform it.
155
161
  6. Validate the action using the repository's existing checks when relevant.
156
162
  7. End by calling `tap_post` with `channel='<goal-emitter-name>'` and an ITERATION RECORD in `message` containing:
157
163
  - iteration and budget used
158
164
  - action taken
159
165
  - evidence checked and result
166
+ - claim ledger entries when this is a research/audit goal
167
+ - remaining_delta or unchanged_delta status
160
168
  - current status: progressing, complete, blocked, or budget-limited
161
169
  - next best action
170
+ - branch, commit SHA, PR URL, run URL, or issue key when relevant
162
171
  8. End the user-visible response with the same concise progress update, what remains, and the next best step if the loop stops before completion.
163
172
 
164
173
  Safety rules:
165
174
  - Do not make unrelated changes.
175
+ - Do not modify this goal emitter's own `every`, `everySchedule`, `maxRuns`, event filter, or goal contract while it is running unless the user explicitly asks.
176
+ - Do not spawn additional emitters from this goal unless orchestration is explicitly part of the goal contract.
166
177
  - Do not mark the goal complete unless the objective is actually achieved and no required work remains.
167
178
  - Do not treat reaching the iteration budget as success.
168
179
  - Do not continue if the next step requires explicit user approval.
@@ -7,6 +7,12 @@ user-invocable: true
7
7
 
8
8
  Create a timed or idle PromptEmitter with `tap_start_emitter`.
9
9
 
10
+ If the request includes a completion condition such as "until", "keep going
11
+ until", "stop when", "work until done", or "iterate until complete", do not
12
+ create a plain loop. Redirect to `/tap-goal` semantics instead, because the
13
+ user is asking for a completion contract with evidence, budget, and stop
14
+ conditions rather than a recurring prompt.
15
+
10
16
  ## Expected input
11
17
 
12
18
  Interpret the invocation as:
@@ -63,9 +63,25 @@ Steps:
63
63
  - Lines that indicate important events (errors, warnings, state changes) → candidates for `{ "match": "...", "outcome": "inject" }`.
64
64
  - Lines that are never relevant at all → candidates for tighter keep/drop rules.
65
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.
66
+ 5. Use this shared contract when judging the stream:
67
+ - stream_purpose: <why the user wanted this monitor>
68
+ - signal_vocabulary: errors, warnings, failures, state changes, explicit success/failure markers
69
+ - noise_vocabulary: timestamps-only, heartbeat-only, repeated unchanged status, empty pings
70
+ 6. Only update if the evidence clearly justifies a change (signal-to-noise is poor or a pattern is clearly wrong).
71
+ 7. If an update is needed, call tap_set_event_filter with the revised patterns for emitter '<command-emitter-name>'.
72
+ 8. Always call tap_post with channel '<stream-name>' and a REVIEW RECORD wrapped in markers:
73
+ === BEGIN_REVIEW_RECORD ===
74
+ {
75
+ "reviewed_at": "<ISO timestamp>",
76
+ "entries_examined": <number>,
77
+ "issue_type": "noise_pattern|missed_signal|over_filtering|duplicate_inject|no_change",
78
+ "patterns_changed": ["short label for each change"],
79
+ "remaining_noise_delta": ["what still looks noisy or uncertain"],
80
+ "signal_vocabulary": ["terms treated as signal"],
81
+ "noise_vocabulary": ["terms treated as noise"]
82
+ }
83
+ === END_REVIEW_RECORD ===
84
+ 9. 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
85
  ```
70
86
 
71
87
  Substitute the real emitter name and stream name into the prompt before passing it to `tap_start_emitter`.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: tap-orchestrate
3
+ description: "Create a coordinator PromptEmitter for multi-agent tap workflows with role-specific sub-emitters, gated handoffs, and evidence records. Use when the user asks to orchestrate multiple agents, roles, workstreams, or parallel implementation/review/test phases."
4
+ argument-hint: "<objective and roles>"
5
+ user-invocable: true
6
+ ---
7
+
8
+ Create a coordinator PromptEmitter that manages a multi-agent workflow using tap
9
+ emitters and EventStreams.
10
+
11
+ Use this for work that naturally decomposes into roles such as planner,
12
+ implementer, reviewer, tester, documenter, provider-builder, or release
13
+ coordinator. Do not use it for a single straightforward task.
14
+
15
+ ## What to create
16
+
17
+ Use `tap_start_emitter` to create a **coordinator PromptEmitter**:
18
+
19
+ - Name: `orchestrate-<objective-slug>`.
20
+ - Prompt: a self-contained orchestration contract.
21
+ - Schedule: `everySchedule = ["2m", "5m", "10m"]`.
22
+ - `lifespan = "temporary"` unless the user explicitly asks for persistence.
23
+ - `ownership = "modelOwned"`.
24
+ - `subscribe = false`.
25
+ - `maxRuns = 50` unless the user gives a budget.
26
+
27
+ The coordinator may create role-specific PromptEmitters only when the role has a
28
+ clear deliverable and verification surface. Each role emitter should write its
29
+ handoff to an EventStream with a stable name:
30
+
31
+ ```text
32
+ orchestrate-<objective>-<role>
33
+ ```
34
+
35
+ ## Coordinator prompt contract
36
+
37
+ The coordinator prompt must include:
38
+
39
+ ```text
40
+ Objective: <user objective>
41
+ Roles: <role list, deliverables, and verification surface>
42
+ Gate policy:
43
+ - Do not hand off to the next role until required artifacts or EventStream notes exist.
44
+ - Read role EventStreams with tap_stream_history before deciding a gate is satisfied.
45
+ - If parallel work is safe, create independent role emitters in the same iteration.
46
+ - If a role blocks, post ORCHESTRATION BLOCKED and stop the coordinator.
47
+ Audit trail:
48
+ - After every decision, call tap_post to the coordinator stream with ORCHESTRATION RECORD:
49
+ role, gate, evidence checked, decision, next handoff.
50
+ Safety:
51
+ - Do not spawn duplicate role emitters.
52
+ - Do not mutate another role's scope unless the coordinator evidence supports it.
53
+ - Stop all role emitters when the orchestration completes or blocks.
54
+ ```
55
+
56
+ ## Required behavior
57
+
58
+ 1. Parse the objective and any requested roles.
59
+ 2. If roles are missing, infer a minimal role set from the objective:
60
+ planner, implementer, reviewer, validator.
61
+ 3. Create the coordinator PromptEmitter only; do not immediately create role
62
+ emitters in the setup turn. The coordinator will create them when it runs.
63
+ 4. Confirm:
64
+ - coordinator emitter name and stream
65
+ - roles
66
+ - gate policy
67
+ - max iteration budget
68
+
69
+ ## Good role patterns
70
+
71
+ - **planner**: produce plan and boundaries; verification is a plan note.
72
+ - **implementer**: make code/doc changes; verification is diff + focused checks.
73
+ - **reviewer**: inspect changes; verification is review note with findings.
74
+ - **validator**: run tests/build/evals; verification is command evidence.
75
+ - **release**: bump/push/publish only after validator passes.
76
+
77
+ ## When not to use
78
+
79
+ Do not create orchestration for a normal `/tap-goal` objective that one agent can
80
+ complete directly. Orchestration adds coordination cost and should only be used
81
+ when parallel roles or gated handoffs are genuinely useful.
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "2.0.7"
2
+ "version": "2.0.8"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-tap-extension",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Copilot CLI extension for background event emitters, event streams, and session injection.",
5
5
  "type": "module",
6
6
  "license": "MIT",