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 +2 -0
- package/bin/install.mjs +5 -0
- package/dist/copilot-instructions.md +10 -0
- package/dist/extension.mjs +473 -20
- package/dist/skills/tap-goal/SKILL.md +13 -2
- package/dist/skills/tap-loop/SKILL.md +6 -0
- package/dist/skills/tap-monitor/SKILL.md +19 -3
- package/dist/skills/tap-orchestrate/SKILL.md +81 -0
- package/dist/version.json +1 -1
- package/package.json +1 -1
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:
|
package/dist/extension.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
|
|
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,
|
|
9814
|
+
scheduleIteration(emitter, context, IDLE_PROMPT_DELAY_MS);
|
|
9644
9815
|
}
|
|
9645
9816
|
return;
|
|
9646
9817
|
}
|
|
9647
|
-
scheduleIteration(emitter,
|
|
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(
|
|
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,
|
|
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.
|
|
67
|
-
|
|
68
|
-
|
|
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