@replayci/replay 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3213,6 +3213,34 @@ function redactCapture(input) {
3213
3213
  pattern_fingerprint: PATTERN_FINGERPRINT
3214
3214
  };
3215
3215
  }
3216
+ function redactTrace(trace, captureLevel) {
3217
+ if (captureLevel === "full") return trace;
3218
+ return {
3219
+ ...trace,
3220
+ entries: trace.entries.map((entry) => redactTraceEntry(entry, captureLevel))
3221
+ };
3222
+ }
3223
+ function redactTraceEntry(entry, captureLevel) {
3224
+ if (captureLevel === "metadata") {
3225
+ return {
3226
+ ...entry,
3227
+ checked: redactRecord(entry.checked),
3228
+ found: redactRecord(entry.found)
3229
+ };
3230
+ }
3231
+ return entry;
3232
+ }
3233
+ function redactRecord(record) {
3234
+ const result = {};
3235
+ for (const [key, value] of Object.entries(record)) {
3236
+ if (typeof value === "string") {
3237
+ result[key] = redactString(value);
3238
+ } else {
3239
+ result[key] = value;
3240
+ }
3241
+ }
3242
+ return result;
3243
+ }
3216
3244
 
3217
3245
  // src/errors/replay.ts
3218
3246
  var ReplayContractError = class extends Error {
@@ -3509,8 +3537,9 @@ function toRecord8(value) {
3509
3537
  import crypto2 from "crypto";
3510
3538
 
3511
3539
  // src/phases.ts
3512
- function validatePhaseTransition(toolCalls, sessionState, compiledSession) {
3540
+ function validatePhaseTransition(toolCalls, sessionState, compiledSession, ctx) {
3513
3541
  if (!compiledSession.phases) {
3542
+ ctx?.trace.push({ stage: "phase", tool: null, verdict: "skip", reason: "no_phases_configured", checked: {}, found: {} });
3514
3543
  return { legal: true, newPhase: sessionState.currentPhase };
3515
3544
  }
3516
3545
  const attemptedTransitions = [];
@@ -3521,6 +3550,14 @@ function validatePhaseTransition(toolCalls, sessionState, compiledSession) {
3521
3550
  sessionState.currentPhase ?? ""
3522
3551
  );
3523
3552
  if (!allowedTransitions?.includes(contract.transitions.advances_to)) {
3553
+ ctx?.trace.push({
3554
+ stage: "phase",
3555
+ tool: toolCall.name,
3556
+ verdict: "block",
3557
+ reason: "illegal_phase_transition",
3558
+ checked: { advances_to: contract.transitions.advances_to, from: sessionState.currentPhase },
3559
+ found: { allowed_transitions: allowedTransitions ?? [] }
3560
+ });
3524
3561
  return {
3525
3562
  legal: false,
3526
3563
  newPhase: sessionState.currentPhase,
@@ -3537,6 +3574,14 @@ function validatePhaseTransition(toolCalls, sessionState, compiledSession) {
3537
3574
  if (attemptedTransitions.length > 1) {
3538
3575
  const distinctTargets = new Set(attemptedTransitions.map((t) => t.target));
3539
3576
  if (distinctTargets.size > 1) {
3577
+ ctx?.trace.push({
3578
+ stage: "phase",
3579
+ tool: attemptedTransitions.map((t) => t.tool).join(", "),
3580
+ verdict: "block",
3581
+ reason: "ambiguous_phase_transition",
3582
+ checked: { targets: Array.from(distinctTargets) },
3583
+ found: { from: sessionState.currentPhase }
3584
+ });
3540
3585
  return {
3541
3586
  legal: false,
3542
3587
  newPhase: sessionState.currentPhase,
@@ -3547,7 +3592,17 @@ function validatePhaseTransition(toolCalls, sessionState, compiledSession) {
3547
3592
  }
3548
3593
  }
3549
3594
  if (attemptedTransitions.length > 0) {
3550
- return { legal: true, newPhase: attemptedTransitions[0].target };
3595
+ const target = attemptedTransitions[0].target;
3596
+ const allowedTransitions = compiledSession.transitions.get(sessionState.currentPhase ?? "") ?? [];
3597
+ ctx?.trace.push({
3598
+ stage: "phase",
3599
+ tool: attemptedTransitions[0].tool,
3600
+ verdict: "allow",
3601
+ reason: "phase_advanced",
3602
+ checked: { advances_to: target, from: sessionState.currentPhase },
3603
+ found: { allowed_transitions: allowedTransitions }
3604
+ });
3605
+ return { legal: true, newPhase: target };
3551
3606
  }
3552
3607
  return { legal: true, newPhase: sessionState.currentPhase };
3553
3608
  }
@@ -3797,7 +3852,7 @@ function checkCircuitBreaker(state, config) {
3797
3852
  }
3798
3853
 
3799
3854
  // src/crossStep.ts
3800
- function validateCrossStep(toolCalls, sessionState, contracts) {
3855
+ function validateCrossStep(toolCalls, sessionState, contracts, ctx) {
3801
3856
  const failures = [];
3802
3857
  const contractByTool = new Map(contracts.map((c) => [c.tool, c]));
3803
3858
  const workingForbidden = new Set(sessionState.forbiddenTools);
@@ -3825,8 +3880,17 @@ function validateCrossStep(toolCalls, sessionState, contracts) {
3825
3880
  reason: "forbidden_tool",
3826
3881
  detail: resourceValue !== void 0 ? `Tool "${tc.name}" is forbidden in this session for resource ${JSON.stringify(resourceValue)}` : `Tool "${tc.name}" is forbidden in this session`
3827
3882
  });
3883
+ ctx?.trace.push({
3884
+ stage: "cross_step",
3885
+ tool: tc.name,
3886
+ verdict: "block",
3887
+ reason: "forbidden_tool",
3888
+ checked: { tool: tc.name },
3889
+ found: { is_resource_scoped: resourceValue !== void 0, resource_value: resourceValue ?? null }
3890
+ });
3828
3891
  continue;
3829
3892
  }
3893
+ let crossStepPassed = true;
3830
3894
  if (contract?.preconditions && contract.preconditions.length > 0) {
3831
3895
  const results = evaluatePreconditions(
3832
3896
  contract.preconditions,
@@ -3835,6 +3899,7 @@ function validateCrossStep(toolCalls, sessionState, contracts) {
3835
3899
  );
3836
3900
  for (const result of results) {
3837
3901
  if (!result.satisfied) {
3902
+ crossStepPassed = false;
3838
3903
  failures.push({
3839
3904
  toolName: tc.name,
3840
3905
  reason: "precondition_not_met",
@@ -3843,6 +3908,25 @@ function validateCrossStep(toolCalls, sessionState, contracts) {
3843
3908
  }
3844
3909
  }
3845
3910
  }
3911
+ if (crossStepPassed) {
3912
+ ctx?.trace.push({
3913
+ stage: "cross_step",
3914
+ tool: tc.name,
3915
+ verdict: "allow",
3916
+ reason: "preconditions_satisfied",
3917
+ checked: { requires_prior_tool: contract?.preconditions?.[0]?.requires_prior_tool ?? null },
3918
+ found: { resource_value: resourceValue ?? null }
3919
+ });
3920
+ } else {
3921
+ ctx?.trace.push({
3922
+ stage: "cross_step",
3923
+ tool: tc.name,
3924
+ verdict: "block",
3925
+ reason: "precondition_not_met",
3926
+ checked: { requires_prior_tool: contract?.preconditions?.[0]?.requires_prior_tool ?? null },
3927
+ found: { resource_value: resourceValue ?? null, matching_step_index: null }
3928
+ });
3929
+ }
3846
3930
  if (contract?.forbids_after) {
3847
3931
  for (const entry of contract.forbids_after) {
3848
3932
  if (typeof entry === "string") {
@@ -4066,20 +4150,23 @@ function extractPath2(obj, path) {
4066
4150
  }
4067
4151
 
4068
4152
  // src/narrow.ts
4069
- function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPolicy, manualFilter) {
4153
+ function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPolicy, manualFilter, ctx) {
4070
4154
  const allowed = [];
4071
4155
  const removed = [];
4072
4156
  for (const tool of requestedTools) {
4073
4157
  if (manualFilter && !manualFilter.includes(tool.name)) {
4074
4158
  removed.push({ tool: tool.name, reason: "manual_filter" });
4159
+ ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "remove", reason: "manual_filter", checked: { filter: manualFilter }, found: {} });
4075
4160
  continue;
4076
4161
  }
4077
4162
  const contract = compiledSession.perToolContracts.get(tool.name);
4078
4163
  if (!contract) {
4079
4164
  if (unmatchedPolicy === "allow") {
4080
4165
  allowed.push(tool);
4166
+ ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "allow", reason: "no_contract_passthrough", checked: { unmatched_policy: "allow" }, found: {} });
4081
4167
  } else {
4082
4168
  removed.push({ tool: tool.name, reason: "no_contract" });
4169
+ ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "remove", reason: "no_contract", checked: { unmatched_policy: "block" }, found: {} });
4083
4170
  }
4084
4171
  continue;
4085
4172
  }
@@ -4092,6 +4179,7 @@ function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPol
4092
4179
  reason: "wrong_phase",
4093
4180
  detail: `Tool valid in [${contract.transitions.valid_in_phases.join(", ")}], current phase: ${sessionState.currentPhase}`
4094
4181
  });
4182
+ ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "remove", reason: "wrong_phase", checked: { valid_in_phases: contract.transitions.valid_in_phases }, found: { current_phase: sessionState.currentPhase } });
4095
4183
  continue;
4096
4184
  }
4097
4185
  }
@@ -4102,6 +4190,18 @@ function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPol
4102
4190
  );
4103
4191
  const unsatisfied = results.find((r) => !r.satisfied);
4104
4192
  if (unsatisfied) {
4193
+ const firstPre = contract.preconditions[0];
4194
+ ctx?.trace.push({
4195
+ stage: "narrow",
4196
+ tool: tool.name,
4197
+ verdict: "remove",
4198
+ reason: "precondition_not_met",
4199
+ checked: {
4200
+ requires_prior_tool: firstPre.requires_prior_tool ?? null,
4201
+ with_output: firstPre.with_output ?? []
4202
+ },
4203
+ found: { satisfied_precondition_cache_hit: false }
4204
+ });
4105
4205
  removed.push({
4106
4206
  tool: tool.name,
4107
4207
  reason: "precondition_not_met",
@@ -4115,6 +4215,7 @@ function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPol
4115
4215
  tool: tool.name,
4116
4216
  reason: "forbidden_in_state"
4117
4217
  });
4218
+ ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "remove", reason: "forbidden_in_state", checked: { tool: tool.name }, found: { is_resource_scoped: false } });
4118
4219
  continue;
4119
4220
  }
4120
4221
  if (compiledSession.policyProgram && compiledSession.principal !== null && compiledSession.principal !== void 0) {
@@ -4131,9 +4232,24 @@ function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPol
4131
4232
  reason: "policy_denied",
4132
4233
  detail: verdict.reason ?? "Policy deny rule matched"
4133
4234
  });
4235
+ ctx?.trace.push({ stage: "narrow", tool: tool.name, verdict: "remove", reason: "policy_denied", checked: { rule_type: "session_deny" }, found: { matched: true } });
4134
4236
  continue;
4135
4237
  }
4136
4238
  }
4239
+ ctx?.trace.push({
4240
+ stage: "narrow",
4241
+ tool: tool.name,
4242
+ verdict: "allow",
4243
+ reason: "all_checks_passed",
4244
+ checked: {
4245
+ has_contract: true,
4246
+ phase_ok: true,
4247
+ preconditions_ok: true,
4248
+ not_forbidden: true,
4249
+ policy_ok: true
4250
+ },
4251
+ found: {}
4252
+ });
4137
4253
  allowed.push(tool);
4138
4254
  }
4139
4255
  return { allowed, removed };
@@ -4627,7 +4743,7 @@ function replay(client, opts = {}) {
4627
4743
  const unmatchedPolicy = opts.unmatchedPolicy ?? "block";
4628
4744
  const maxRetries = Math.min(Math.max(0, opts.maxRetries ?? 0), MAX_RETRIES);
4629
4745
  const compatEnforcement = opts.compatEnforcement ?? "protective";
4630
- const diagnostics = opts.diagnostics;
4746
+ const diagnostics = opts.diagnostics ?? defaultReplayDiagnosticsHandler;
4631
4747
  let provider;
4632
4748
  try {
4633
4749
  provider = detectProvider(client);
@@ -4660,6 +4776,12 @@ function replay(client, opts = {}) {
4660
4776
  emitDiagnostic2(diagnostics, { type: "replay_compile_error", details: detail });
4661
4777
  return createBlockingInactiveSession(client, sessionId, detail);
4662
4778
  }
4779
+ if (opts.contractsDir && !discoveredSessionYaml && !opts.sessionYamlPath) {
4780
+ emitDiagnostic2(diagnostics, {
4781
+ type: "replay_compile_warning",
4782
+ details: "No session.yaml found in contractsDir \u2014 session-level features (phases, policy, session_limits) are inactive. Per-tool contracts still apply."
4783
+ });
4784
+ }
4663
4785
  let sessionYaml = discoveredSessionYaml;
4664
4786
  if (!sessionYaml && opts.providerConstraints) {
4665
4787
  sessionYaml = { schema_version: "1.0", agent, provider_constraints: opts.providerConstraints };
@@ -4819,6 +4941,7 @@ function replay(client, opts = {}) {
4819
4941
  let bypassDetected = false;
4820
4942
  let lastShadowDeltaValue = null;
4821
4943
  let lastNarrowResult = null;
4944
+ let lastTrace = null;
4822
4945
  let shadowEvaluationCount = 0;
4823
4946
  let manualFilter = null;
4824
4947
  const deferredReceipts = /* @__PURE__ */ new Map();
@@ -4826,6 +4949,17 @@ function replay(client, opts = {}) {
4826
4949
  const compiledLimits = compiledSession?.sessionLimits;
4827
4950
  const mergedLimits = { ...contractLimits ?? {}, ...compiledLimits ?? {} };
4828
4951
  const resolvedSessionLimits = Object.keys(mergedLimits).length > 0 ? mergedLimits : null;
4952
+ if (resolvedSessionLimits?.max_tool_calls_mode === "narrow" && resolvedSessionLimits.max_calls_per_tool) {
4953
+ const budgetedTools = new Set(Object.keys(resolvedSessionLimits.max_calls_per_tool));
4954
+ const unbudgeted = contracts.map((c) => c.tool).filter((t) => !budgetedTools.has(t));
4955
+ if (unbudgeted.length > 0) {
4956
+ emitDiagnostic2(diagnostics, {
4957
+ type: "replay_narrow_unbudgeted_tools",
4958
+ session_id: sessionId,
4959
+ tools: unbudgeted
4960
+ });
4961
+ }
4962
+ }
4829
4963
  const store = opts.store ?? null;
4830
4964
  let storeLoadPromise = null;
4831
4965
  let storeLoadDone = false;
@@ -4893,6 +5027,18 @@ function replay(client, opts = {}) {
4893
5027
  } catch {
4894
5028
  }
4895
5029
  }
5030
+ function createTrace(stepIndex) {
5031
+ const entries = [];
5032
+ return {
5033
+ sessionId,
5034
+ stepIndex,
5035
+ complete: false,
5036
+ entries,
5037
+ push(entry) {
5038
+ entries.push(entry);
5039
+ }
5040
+ };
5041
+ }
4896
5042
  const enforcementCreate = async function replayEnforcementCreate(...args) {
4897
5043
  if (killed) {
4898
5044
  throw new ReplayKillError(sessionId, killedAt);
@@ -4935,8 +5081,19 @@ function replay(client, opts = {}) {
4935
5081
  total_ms: 0,
4936
5082
  enforcement_ms: 0
4937
5083
  };
5084
+ const trace = createTrace(sessionState.totalStepCount);
5085
+ const traceCtx = { trace };
5086
+ let currentTraceStage = "narrow";
4938
5087
  const request = toRecord10(args[0]);
4939
5088
  const requestToolNames = extractRequestToolNames(request);
5089
+ const messages = Array.isArray(request.messages) ? request.messages : [];
5090
+ if (messages.length > 0) {
5091
+ const toolResults = extractToolResults(messages, provider);
5092
+ if (toolResults.length > 0) {
5093
+ const outputUpdates = extractOutputFromToolResults(toolResults, sessionState, contracts);
5094
+ sessionState = applyOutputExtracts(sessionState, outputUpdates);
5095
+ }
5096
+ }
4940
5097
  let narrowResult = null;
4941
5098
  let activeArgs = args;
4942
5099
  if (compiledSession && Array.isArray(request.tools) && request.tools.length > 0) {
@@ -4947,7 +5104,8 @@ function replay(client, opts = {}) {
4947
5104
  sessionState,
4948
5105
  compiledSession,
4949
5106
  unmatchedPolicy,
4950
- manualFilter
5107
+ manualFilter,
5108
+ traceCtx
4951
5109
  );
4952
5110
  lastNarrowResult = narrowResult;
4953
5111
  if (narrowResult.removed.length > 0) {
@@ -4985,55 +5143,96 @@ function replay(client, opts = {}) {
4985
5143
  timing.narrow_ms = Date.now() - guardStart;
4986
5144
  const preCheckStart = Date.now();
4987
5145
  try {
5146
+ currentTraceStage = "pre_check";
4988
5147
  if (mode === "enforce" && resolvedSessionLimits) {
4989
5148
  const limitResult = checkSessionLimits(sessionState, resolvedSessionLimits);
4990
5149
  if (limitResult.exceeded) {
4991
- const decision = {
4992
- action: "block",
4993
- tool_calls: [],
4994
- blocked: [{
4995
- tool_name: "_session",
4996
- arguments: "",
4997
- reason: "session_limit_exceeded",
4998
- contract_file: "",
4999
- failures: [{ path: "$", operator: "session_limit", expected: "", found: "", message: limitResult.reason ?? "session limit exceeded" }]
5000
- }],
5001
- response_modification: gateMode
5002
- };
5003
- sessionState = recordDecisionOutcome(sessionState, "blocked");
5004
- if (resolvedSessionLimits.circuit_breaker) {
5005
- const cbResult = checkCircuitBreaker(sessionState, resolvedSessionLimits.circuit_breaker);
5006
- if (cbResult.triggered) {
5007
- killed = true;
5008
- killedAt = (/* @__PURE__ */ new Date()).toISOString();
5009
- sessionState = killSession(sessionState);
5010
- emitDiagnostic2(diagnostics, { type: "replay_kill", session_id: sessionId });
5150
+ let narrowedPastLimit = false;
5151
+ if (limitResult.reason?.startsWith("max_tool_calls") && resolvedSessionLimits.max_tool_calls_mode === "narrow" && resolvedSessionLimits.max_calls_per_tool) {
5152
+ const costOk = !(typeof resolvedSessionLimits.max_cost_per_session === "number" && sessionState.actualCost >= resolvedSessionLimits.max_cost_per_session);
5153
+ if (costOk) {
5154
+ const currentRequest = toRecord10(activeArgs[0]);
5155
+ const currentTools = Array.isArray(currentRequest.tools) ? extractToolDefinitions(currentRequest.tools) : [];
5156
+ const budgetedTools = currentTools.filter((tool) => {
5157
+ const max = resolvedSessionLimits.max_calls_per_tool[tool.name];
5158
+ if (typeof max !== "number") return false;
5159
+ return (sessionState.toolCallCounts.get(tool.name) ?? 0) < max;
5160
+ });
5161
+ if (budgetedTools.length > 0) {
5162
+ const modifiedRequest = { ...currentRequest, tools: budgetedTools };
5163
+ activeArgs = [modifiedRequest, ...Array.prototype.slice.call(activeArgs, 1)];
5164
+ narrowedPastLimit = true;
5165
+ trace.push({
5166
+ stage: "pre_check",
5167
+ tool: null,
5168
+ verdict: "narrow",
5169
+ reason: "max_tool_calls_narrow_mode",
5170
+ checked: { max_tool_calls: resolvedSessionLimits.max_tool_calls ?? null, budgeted_tools: budgetedTools.map((t) => t.name) },
5171
+ found: { total_tool_calls: sessionState.totalToolCalls }
5172
+ });
5173
+ }
5011
5174
  }
5012
5175
  }
5013
- timing.pre_check_ms = Date.now() - preCheckStart;
5014
- captureDecision(
5015
- decision,
5016
- null,
5017
- request,
5018
- guardStart,
5019
- requestToolNames,
5020
- null,
5021
- narrowResult,
5022
- null,
5023
- null,
5024
- null,
5025
- void 0,
5026
- timing
5027
- );
5028
- if (isCompatAdvisory) {
5029
- emitDiagnostic2(diagnostics, {
5030
- type: "replay_compat_advisory",
5031
- session_id: sessionId,
5032
- would_block: decision.blocked,
5033
- details: limitResult.reason ?? "session limit exceeded"
5176
+ if (!narrowedPastLimit) {
5177
+ trace.push({
5178
+ stage: "pre_check",
5179
+ tool: null,
5180
+ verdict: "block",
5181
+ reason: "session_limit_exceeded",
5182
+ checked: {
5183
+ max_steps: resolvedSessionLimits.max_steps ?? null,
5184
+ max_tool_calls: resolvedSessionLimits.max_tool_calls ?? null,
5185
+ max_cost: resolvedSessionLimits.max_cost_per_session ?? null
5186
+ },
5187
+ found: { total_steps: sessionState.totalStepCount, total_tool_calls: sessionState.totalToolCalls, actual_cost: sessionState.actualCost }
5034
5188
  });
5035
- } else {
5036
- throw buildContractError2(decision);
5189
+ const decision = {
5190
+ action: "block",
5191
+ tool_calls: [],
5192
+ blocked: [{
5193
+ tool_name: "_session",
5194
+ arguments: "",
5195
+ reason: "session_limit_exceeded",
5196
+ contract_file: "",
5197
+ failures: [{ path: "$", operator: "session_limit", expected: "", found: "", message: limitResult.reason ?? "session limit exceeded" }]
5198
+ }],
5199
+ response_modification: gateMode
5200
+ };
5201
+ sessionState = recordDecisionOutcome(sessionState, "blocked");
5202
+ if (resolvedSessionLimits.circuit_breaker) {
5203
+ const cbResult = checkCircuitBreaker(sessionState, resolvedSessionLimits.circuit_breaker);
5204
+ if (cbResult.triggered) {
5205
+ killed = true;
5206
+ killedAt = (/* @__PURE__ */ new Date()).toISOString();
5207
+ sessionState = killSession(sessionState);
5208
+ emitDiagnostic2(diagnostics, { type: "replay_kill", session_id: sessionId });
5209
+ }
5210
+ }
5211
+ timing.pre_check_ms = Date.now() - preCheckStart;
5212
+ captureDecision(
5213
+ decision,
5214
+ null,
5215
+ request,
5216
+ guardStart,
5217
+ requestToolNames,
5218
+ null,
5219
+ narrowResult,
5220
+ null,
5221
+ null,
5222
+ null,
5223
+ void 0,
5224
+ timing
5225
+ );
5226
+ if (isCompatAdvisory) {
5227
+ emitDiagnostic2(diagnostics, {
5228
+ type: "replay_compat_advisory",
5229
+ session_id: sessionId,
5230
+ would_block: decision.blocked,
5231
+ details: limitResult.reason ?? "session limit exceeded"
5232
+ });
5233
+ } else {
5234
+ throw buildContractError2(decision);
5235
+ }
5037
5236
  }
5038
5237
  }
5039
5238
  if (isAtHardStepCap(sessionState)) {
@@ -5066,8 +5265,23 @@ function replay(client, opts = {}) {
5066
5265
  );
5067
5266
  throw buildContractError2(decision);
5068
5267
  }
5268
+ if (!checkSessionLimits(sessionState, resolvedSessionLimits).exceeded) {
5269
+ trace.push({
5270
+ stage: "pre_check",
5271
+ tool: null,
5272
+ verdict: "allow",
5273
+ reason: "session_limits_ok",
5274
+ checked: {
5275
+ max_steps: resolvedSessionLimits.max_steps ?? null,
5276
+ max_tool_calls: resolvedSessionLimits.max_tool_calls ?? null,
5277
+ max_cost: resolvedSessionLimits.max_cost_per_session ?? null
5278
+ },
5279
+ found: { total_steps: sessionState.totalStepCount, total_tool_calls: sessionState.totalToolCalls, actual_cost: sessionState.actualCost }
5280
+ });
5281
+ }
5282
+ } else if (mode === "enforce") {
5283
+ trace.push({ stage: "pre_check", tool: null, verdict: "skip", reason: "no_session_limits", checked: {}, found: {} });
5069
5284
  }
5070
- const messages = Array.isArray(request.messages) ? request.messages : [];
5071
5285
  if (messages.length > 0) {
5072
5286
  const msgResult = validateToolResultMessages(messages, contracts, provider);
5073
5287
  if (!msgResult.passed) {
@@ -5077,13 +5291,6 @@ function replay(client, opts = {}) {
5077
5291
  });
5078
5292
  }
5079
5293
  }
5080
- if (messages.length > 0) {
5081
- const toolResults = extractToolResults(messages, provider);
5082
- if (toolResults.length > 0) {
5083
- const outputUpdates = extractOutputFromToolResults(toolResults, sessionState, contracts);
5084
- sessionState = applyOutputExtracts(sessionState, outputUpdates);
5085
- }
5086
- }
5087
5294
  const inputFailures = evaluateInputInvariants(request, contracts);
5088
5295
  if (mode === "enforce" && inputFailures.length > 0) {
5089
5296
  if (onError === "block") {
@@ -5158,6 +5365,10 @@ function replay(client, opts = {}) {
5158
5365
  sessionState = updateActualCost(sessionState, costDelta);
5159
5366
  }
5160
5367
  if (mode === "log-only") {
5368
+ trace.push({ stage: "gate", tool: null, verdict: "allow", reason: "log_only_mode", checked: {}, found: {} });
5369
+ trace.complete = true;
5370
+ lastTrace = trace;
5371
+ emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
5161
5372
  captureDecision(
5162
5373
  { action: "allow", tool_calls: extractToolCalls(response, provider) },
5163
5374
  response,
@@ -5170,13 +5381,26 @@ function replay(client, opts = {}) {
5170
5381
  null,
5171
5382
  null,
5172
5383
  void 0,
5173
- timing
5384
+ timing,
5385
+ trace
5174
5386
  );
5175
5387
  return response;
5176
5388
  }
5389
+ currentTraceStage = "validate";
5177
5390
  const toolCalls = extractToolCalls(response, provider);
5178
5391
  const validateStart = Date.now();
5179
5392
  const validation = validateResponse2(response, toolCalls, contracts, requestToolNames, unmatchedPolicy, provider);
5393
+ for (const f of validation.failures) {
5394
+ const toolName = extractToolNameFromFailure(f, toolCalls);
5395
+ trace.push({
5396
+ stage: "validate",
5397
+ tool: toolName === "_response" ? null : toolName,
5398
+ verdict: "block",
5399
+ reason: f.operator === "response_format" ? "response_format_failed" : "output_invariant_failed",
5400
+ checked: { path: f.path, operator: f.operator, invariant_type: f.operator === "response_format" ? "response_format" : "output" },
5401
+ found: { value: f.found }
5402
+ });
5403
+ }
5180
5404
  timing.validate_ms += Date.now() - validateStart;
5181
5405
  if (isActiveGovern && !attemptDegraded && attemptPreparedRequestId) {
5182
5406
  const rtProposalStart = Date.now();
@@ -5211,9 +5435,10 @@ function replay(client, opts = {}) {
5211
5435
  }
5212
5436
  timing.runtime_ms += Date.now() - rtProposalStart;
5213
5437
  }
5438
+ currentTraceStage = "cross_step";
5214
5439
  const crossStepStart = Date.now();
5215
5440
  const crossStepContracts = compiledSession ? Array.from(compiledSession.perToolContracts.values()) : contracts;
5216
- const crossStepResult = validateCrossStep(toolCalls, sessionState, crossStepContracts);
5441
+ const crossStepResult = validateCrossStep(toolCalls, sessionState, crossStepContracts, traceCtx);
5217
5442
  if (!crossStepResult.passed) {
5218
5443
  for (const f of crossStepResult.failures) {
5219
5444
  validation.failures.push({
@@ -5227,10 +5452,11 @@ function replay(client, opts = {}) {
5227
5452
  }
5228
5453
  }
5229
5454
  timing.cross_step_ms += Date.now() - crossStepStart;
5455
+ currentTraceStage = "phase";
5230
5456
  let phaseResult = null;
5231
5457
  const phaseStart = Date.now();
5232
5458
  if (compiledSession) {
5233
- phaseResult = validatePhaseTransition(toolCalls, sessionState, compiledSession);
5459
+ phaseResult = validatePhaseTransition(toolCalls, sessionState, compiledSession, traceCtx);
5234
5460
  if (!phaseResult.legal) {
5235
5461
  validation.failures.push({
5236
5462
  path: `$.tool_calls.${phaseResult.blockedTool}`,
@@ -5260,7 +5486,7 @@ function replay(client, opts = {}) {
5260
5486
  for (const f of avResult.failures) {
5261
5487
  validation.failures.push({
5262
5488
  path: f.path,
5263
- operator: f.operator,
5489
+ operator: "argument_value_mismatch",
5264
5490
  expected: String(f.expected),
5265
5491
  found: String(f.actual),
5266
5492
  message: f.detail,
@@ -5271,10 +5497,12 @@ function replay(client, opts = {}) {
5271
5497
  }
5272
5498
  }
5273
5499
  }
5500
+ currentTraceStage = "limit";
5274
5501
  if (resolvedSessionLimits) {
5275
5502
  const workingState = { ...sessionState, toolCallCounts: workingToolCallCounts };
5276
5503
  const perToolResult = checkPerToolLimits(workingState, tc.name, resolvedSessionLimits);
5277
5504
  if (perToolResult.exceeded) {
5505
+ trace.push({ stage: "limit", tool: tc.name, verdict: "block", reason: "per_tool_limit_exceeded", checked: { max_calls: resolvedSessionLimits.max_calls_per_tool?.[tc.name] ?? null }, found: { current_calls: workingToolCallCounts.get(tc.name) ?? 0 } });
5278
5506
  validation.failures.push({
5279
5507
  path: `$.tool_calls.${tc.name}`,
5280
5508
  operator: "session_limit",
@@ -5283,6 +5511,8 @@ function replay(client, opts = {}) {
5283
5511
  message: perToolResult.reason ?? "per-tool limit exceeded",
5284
5512
  contract_file: ""
5285
5513
  });
5514
+ } else {
5515
+ trace.push({ stage: "limit", tool: tc.name, verdict: "allow", reason: "per_tool_limit_ok", checked: { max_calls: resolvedSessionLimits.max_calls_per_tool?.[tc.name] ?? null }, found: { current_calls: workingToolCallCounts.get(tc.name) ?? 0 } });
5286
5516
  }
5287
5517
  }
5288
5518
  workingToolCallCounts.set(tc.name, (workingToolCallCounts.get(tc.name) ?? 0) + 1);
@@ -5299,6 +5529,7 @@ function replay(client, opts = {}) {
5299
5529
  ).length;
5300
5530
  const totalMatches = loopResult.matchCount + intraMatches;
5301
5531
  if (totalMatches >= resolvedSessionLimits.loop_detection.threshold) {
5532
+ trace.push({ stage: "limit", tool: tc.name, verdict: "block", reason: "loop_detected", checked: { window: resolvedSessionLimits.loop_detection.window, threshold: resolvedSessionLimits.loop_detection.threshold }, found: { match_count: totalMatches, arguments_hash: argsHash } });
5302
5533
  validation.failures.push({
5303
5534
  path: `$.tool_calls.${tc.name}`,
5304
5535
  operator: "loop_detected",
@@ -5312,6 +5543,7 @@ function replay(client, opts = {}) {
5312
5543
  }
5313
5544
  }
5314
5545
  timing.argument_values_ms += Date.now() - argValuesStart;
5546
+ currentTraceStage = "policy";
5315
5547
  let policyVerdicts = null;
5316
5548
  const policyStart = Date.now();
5317
5549
  if (compiledSession?.policyProgram && compiledSession.principal !== null && compiledSession.principal !== void 0) {
@@ -5332,6 +5564,14 @@ function replay(client, opts = {}) {
5332
5564
  );
5333
5565
  policyVerdicts.set(tc.name, verdict);
5334
5566
  if (!verdict.allowed) {
5567
+ trace.push({
5568
+ stage: "policy",
5569
+ tool: tc.name,
5570
+ verdict: "block",
5571
+ reason: verdict.reason?.startsWith("Session deny") ? "session_deny_matched" : verdict.reason?.startsWith("default_deny") ? "default_deny_no_allow" : "policy_denied",
5572
+ checked: { has_policy: true, default_deny: compiledSession.policyProgram.defaultDeny },
5573
+ found: { matched: true }
5574
+ });
5335
5575
  validation.failures.push({
5336
5576
  path: `$.tool_calls.${tc.name}`,
5337
5577
  operator: "policy_denied",
@@ -5340,10 +5580,22 @@ function replay(client, opts = {}) {
5340
5580
  message: `Policy denied: ${tc.name} \u2014 ${verdict.reason}`,
5341
5581
  contract_file: ""
5342
5582
  });
5583
+ } else {
5584
+ trace.push({
5585
+ stage: "policy",
5586
+ tool: tc.name,
5587
+ verdict: "allow",
5588
+ reason: "policy_allowed",
5589
+ checked: { has_policy: true, default_deny: compiledSession.policyProgram.defaultDeny },
5590
+ found: { session_deny_matched: false, tool_deny_matched: false }
5591
+ });
5343
5592
  }
5344
5593
  }
5594
+ } else {
5595
+ trace.push({ stage: "policy", tool: null, verdict: "skip", reason: "no_policy_configured", checked: {}, found: {} });
5345
5596
  }
5346
5597
  timing.policy_ms += Date.now() - policyStart;
5598
+ currentTraceStage = "gate";
5347
5599
  if (mode === "shadow") {
5348
5600
  const shadowGateStart = Date.now();
5349
5601
  const shadowDecision = validation.failures.length > 0 ? {
@@ -5352,6 +5604,15 @@ function replay(client, opts = {}) {
5352
5604
  blocked: buildBlockedCalls(toolCalls, validation.failures, validation.unmatchedBlocked),
5353
5605
  response_modification: gateMode
5354
5606
  } : { action: "allow", tool_calls: toolCalls };
5607
+ const blockedTools = shadowDecision.action === "block" ? shadowDecision.blocked.map((b) => b.tool_name) : [];
5608
+ trace.push({
5609
+ stage: "gate",
5610
+ tool: null,
5611
+ verdict: blockedTools.length > 0 ? "info" : "allow",
5612
+ reason: blockedTools.length > 0 ? "violations_found" : "no_violations",
5613
+ checked: { gate_mode: gateMode },
5614
+ found: { blocked_count: blockedTools.length, action: shadowDecision.action, ...blockedTools.length > 0 ? { blocked_tools: blockedTools } : {} }
5615
+ });
5355
5616
  const shadowDelta = {
5356
5617
  would_have_blocked: shadowDecision.action === "block" ? shadowDecision.blocked : [],
5357
5618
  would_have_narrowed: narrowResult?.removed ?? [],
@@ -5361,7 +5622,11 @@ function replay(client, opts = {}) {
5361
5622
  lastShadowDeltaValue = shadowDelta;
5362
5623
  shadowEvaluationCount++;
5363
5624
  timing.gate_ms += Date.now() - shadowGateStart;
5364
- captureDecision(shadowDecision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, shadowDelta, timing);
5625
+ trace.push({ stage: "finalize", tool: null, verdict: "info", reason: "cycle_complete", checked: {}, found: { state_version: sessionState.stateVersion, phase_before: sessionState.currentPhase, phase_after: sessionState.currentPhase, tools_committed: [], tools_blocked: blockedTools, killed: false, step_index: sessionState.totalStepCount } });
5626
+ trace.complete = true;
5627
+ lastTrace = trace;
5628
+ emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
5629
+ captureDecision(shadowDecision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, shadowDelta, timing, trace);
5365
5630
  return response;
5366
5631
  }
5367
5632
  if (isCompatAdvisory) {
@@ -5402,7 +5667,21 @@ function replay(client, opts = {}) {
5402
5667
  sessionState = recordDecisionOutcome(sessionState, "allowed");
5403
5668
  }
5404
5669
  timing.finalize_ms += Date.now() - advisoryFinalizeStart;
5405
- captureDecision(advisoryDecision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing);
5670
+ const advisoryBlockedTools = advisoryDecision.action === "block" ? advisoryDecision.blocked.map((b) => b.tool_name) : [];
5671
+ trace.push({
5672
+ stage: "gate",
5673
+ tool: null,
5674
+ verdict: advisoryBlockedTools.length > 0 ? "info" : "allow",
5675
+ reason: advisoryBlockedTools.length > 0 ? "violations_found" : "no_violations",
5676
+ checked: { gate_mode: gateMode },
5677
+ found: { blocked_count: advisoryBlockedTools.length, action: advisoryDecision.action, ...advisoryBlockedTools.length > 0 ? { blocked_tools: advisoryBlockedTools } : {} }
5678
+ });
5679
+ const advisoryNewPhase = phaseResult && phaseResult.legal && phaseResult.newPhase !== sessionState.currentPhase ? phaseResult.newPhase : sessionState.currentPhase;
5680
+ trace.push({ stage: "finalize", tool: null, verdict: "info", reason: "cycle_complete", checked: {}, found: { state_version: sessionState.stateVersion, phase_before: sessionState.currentPhase, phase_after: advisoryNewPhase, tools_committed: toolCalls.map((tc) => tc.name), tools_blocked: advisoryBlockedTools, killed: false, step_index: sessionState.totalStepCount } });
5681
+ trace.complete = true;
5682
+ lastTrace = trace;
5683
+ emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
5684
+ captureDecision(advisoryDecision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing, trace);
5406
5685
  return response;
5407
5686
  }
5408
5687
  const enforceGateStart = Date.now();
@@ -5440,7 +5719,20 @@ function replay(client, opts = {}) {
5440
5719
  });
5441
5720
  }
5442
5721
  }
5443
- captureDecision(decision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing);
5722
+ trace.push({
5723
+ stage: "gate",
5724
+ tool: null,
5725
+ verdict: "allow",
5726
+ reason: "no_violations",
5727
+ checked: { gate_mode: gateMode },
5728
+ found: { blocked_count: 0, action: "allow" }
5729
+ });
5730
+ const allowNewPhase = phaseResult && phaseResult.legal && phaseResult.newPhase !== sessionState.currentPhase ? phaseResult.newPhase : sessionState.currentPhase;
5731
+ trace.push({ stage: "finalize", tool: null, verdict: "info", reason: "cycle_complete", checked: {}, found: { state_version: sessionState.stateVersion, phase_before: completedStep.phase, phase_after: allowNewPhase, tools_committed: toolCalls.map((tc) => tc.name), tools_blocked: [], killed: false, step_index: sessionState.totalStepCount } });
5732
+ trace.complete = true;
5733
+ lastTrace = trace;
5734
+ emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
5735
+ captureDecision(decision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing, trace);
5444
5736
  return response;
5445
5737
  }
5446
5738
  sessionState = recordDecisionOutcome(sessionState, "blocked");
@@ -5505,15 +5797,42 @@ function replay(client, opts = {}) {
5505
5797
  );
5506
5798
  continue;
5507
5799
  }
5508
- captureDecision(decision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing);
5800
+ const blockBlockedTools = decision.action === "block" ? decision.blocked.map((b) => b.tool_name) : [];
5801
+ trace.push({
5802
+ stage: "gate",
5803
+ tool: null,
5804
+ verdict: "block",
5805
+ reason: "violations_found",
5806
+ checked: { gate_mode: gateMode },
5807
+ found: { blocked_count: blockBlockedTools.length, action: "block", blocked_tools: blockBlockedTools }
5808
+ });
5809
+ trace.push({ stage: "finalize", tool: null, verdict: "info", reason: "cycle_complete", checked: {}, found: { state_version: sessionState.stateVersion, phase_before: sessionState.currentPhase, phase_after: sessionState.currentPhase, tools_committed: [], tools_blocked: blockBlockedTools, killed, step_index: sessionState.totalStepCount } });
5810
+ trace.complete = true;
5811
+ lastTrace = trace;
5812
+ emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
5813
+ captureDecision(decision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing, trace);
5509
5814
  return applyGateDecision(decision, response, provider, gateMode, opts.onBlock);
5510
5815
  }
5511
5816
  if (lastError) throw lastError;
5512
5817
  throw new ReplayInternalError("Retry loop exhausted without result", { sessionId });
5513
5818
  } catch (err) {
5514
5819
  if (err instanceof ReplayContractError || err instanceof ReplayKillError) {
5820
+ if (!trace.complete) {
5821
+ lastTrace = trace;
5822
+ emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
5823
+ }
5515
5824
  throw err;
5516
5825
  }
5826
+ trace.push({
5827
+ stage: currentTraceStage,
5828
+ tool: null,
5829
+ verdict: "error",
5830
+ reason: "stage_threw",
5831
+ checked: {},
5832
+ found: { error: err instanceof Error ? err.message : String(err) }
5833
+ });
5834
+ lastTrace = trace;
5835
+ emitDiagnostic2(diagnostics, { type: "replay_trace", session_id: sessionId, trace });
5517
5836
  sessionState = recordDecisionOutcome(sessionState, "error");
5518
5837
  if (resolvedSessionLimits?.circuit_breaker) {
5519
5838
  const cbResult = checkCircuitBreaker(sessionState, resolvedSessionLimits.circuit_breaker);
@@ -5639,6 +5958,9 @@ function replay(client, opts = {}) {
5639
5958
  getLastShadowDelta() {
5640
5959
  return lastShadowDeltaValue;
5641
5960
  },
5961
+ getLastTrace() {
5962
+ return lastTrace;
5963
+ },
5642
5964
  /**
5643
5965
  * v3: Manually restrict available tools within compiled legal space.
5644
5966
  * @see specs/replay-v3.md § narrow() / widen()
@@ -5774,7 +6096,7 @@ function replay(client, opts = {}) {
5774
6096
  }
5775
6097
  return wrapped;
5776
6098
  }
5777
- function captureDecision(decision, response, request, guardStart, requestToolNames, crossStep, narrowing = null, phaseResult = null, policyVerdictMap = null, constraintVerdictVal = null, shadowDelta = void 0, timingParam) {
6099
+ function captureDecision(decision, response, request, guardStart, requestToolNames, crossStep, narrowing = null, phaseResult = null, policyVerdictMap = null, constraintVerdictVal = null, shadowDelta = void 0, timingParam, traceParam) {
5778
6100
  if (!buffer && !store) return;
5779
6101
  if (timingParam) {
5780
6102
  timingParam.total_ms = Date.now() - guardStart;
@@ -5809,6 +6131,7 @@ function replay(client, opts = {}) {
5809
6131
  phase: sessionState.currentPhase,
5810
6132
  phase_transition: phaseTransitionStr,
5811
6133
  shadow_delta: shadowDelta,
6134
+ trace: traceParam ? redactTrace(traceParam, opts.captureLevel ?? "full") : void 0,
5812
6135
  receipt: null
5813
6136
  };
5814
6137
  const capturedCall = {
@@ -6366,6 +6689,7 @@ function resolveSessionLimits(contracts) {
6366
6689
  const sl = c.session_limits;
6367
6690
  if (sl.max_steps !== void 0 && merged.max_steps === void 0) merged.max_steps = sl.max_steps;
6368
6691
  if (sl.max_tool_calls !== void 0 && merged.max_tool_calls === void 0) merged.max_tool_calls = sl.max_tool_calls;
6692
+ if (sl.max_tool_calls_mode !== void 0 && merged.max_tool_calls_mode === void 0) merged.max_tool_calls_mode = sl.max_tool_calls_mode;
6369
6693
  if (sl.max_cost_per_session !== void 0 && merged.max_cost_per_session === void 0) merged.max_cost_per_session = sl.max_cost_per_session;
6370
6694
  if (sl.loop_detection && !merged.loop_detection) merged.loop_detection = sl.loop_detection;
6371
6695
  if (sl.circuit_breaker && !merged.circuit_breaker) merged.circuit_breaker = sl.circuit_breaker;
@@ -6473,6 +6797,7 @@ function createInactiveSession(client, sessionId, reason) {
6473
6797
  getState: () => EMPTY_STATE_SNAPSHOT,
6474
6798
  getLastNarrowing: () => null,
6475
6799
  getLastShadowDelta: () => null,
6800
+ getLastTrace: () => null,
6476
6801
  narrow() {
6477
6802
  },
6478
6803
  widen() {
@@ -6514,6 +6839,7 @@ function createBlockingInactiveSession(client, sessionId, detail, configError) {
6514
6839
  getState: () => EMPTY_STATE_SNAPSHOT,
6515
6840
  getLastNarrowing: () => null,
6516
6841
  getLastShadowDelta: () => null,
6842
+ getLastTrace: () => null,
6517
6843
  narrow() {
6518
6844
  },
6519
6845
  widen() {
@@ -6598,6 +6924,83 @@ function generateSessionId2() {
6598
6924
  function stripHashPrefix(hash) {
6599
6925
  return hash.startsWith("sha256:") ? hash.slice(7) : hash;
6600
6926
  }
6927
+ function resolveLogLevel() {
6928
+ const raw = typeof process !== "undefined" ? process.env.REPLAYCI_LOG : void 0;
6929
+ if (!raw) return "warn";
6930
+ const lower = raw.toLowerCase();
6931
+ if (lower === "trace" || lower === "debug") return "trace";
6932
+ if (lower === "silent" || lower === "off" || lower === "none") return "silent";
6933
+ return "warn";
6934
+ }
6935
+ function defaultReplayDiagnosticsHandler(event) {
6936
+ const level = resolveLogLevel();
6937
+ if (level === "silent") return;
6938
+ switch (event.type) {
6939
+ case "replay_inactive":
6940
+ console.warn(`[replayci] replay() inactive: ${event.reason}${event.error_message ? ` \u2014 ${event.error_message}` : ""}`);
6941
+ break;
6942
+ case "replay_compile_error":
6943
+ console.warn(`[replayci] compile error: ${event.details}`);
6944
+ break;
6945
+ case "replay_compile_warning":
6946
+ console.warn(`[replayci] compile warning: ${event.details}`);
6947
+ break;
6948
+ case "replay_bypass_detected":
6949
+ console.warn(`[replayci] bypass detected on session ${event.session_id}`);
6950
+ break;
6951
+ case "replay_kill":
6952
+ console.warn(`[replayci] session ${event.session_id} killed`);
6953
+ break;
6954
+ case "replay_block":
6955
+ console.warn(`[replayci] blocked ${event.tool_name}: ${event.reason}`);
6956
+ break;
6957
+ case "replay_narrow": {
6958
+ for (const r of event.removed) {
6959
+ console.warn(`[replayci] removed ${r.tool} \u2192 ${r.reason}${r.detail ? ` (${r.detail})` : ""}`);
6960
+ }
6961
+ break;
6962
+ }
6963
+ case "replay_trace": {
6964
+ const t = event.trace;
6965
+ if (level === "trace") {
6966
+ for (const entry of t.entries) {
6967
+ const toolStr = entry.tool ? ` ${entry.tool}` : "";
6968
+ const detail = entry.reason !== entry.verdict ? ` \u2014 ${entry.reason}` : "";
6969
+ const checkedStr = Object.keys(entry.checked).length > 0 ? ` checked=${JSON.stringify(entry.checked)}` : "";
6970
+ const foundStr = Object.keys(entry.found).length > 0 ? ` found=${JSON.stringify(entry.found)}` : "";
6971
+ console.warn(`[replayci] ${entry.stage}${toolStr}: ${entry.verdict}${detail}${checkedStr}${foundStr}`);
6972
+ }
6973
+ if (!t.complete) {
6974
+ console.warn(`[replayci] trace INCOMPLETE (fault in pipeline)`);
6975
+ }
6976
+ } else {
6977
+ const blocks = t.entries.filter((e) => e.verdict === "block");
6978
+ for (const b of blocks) {
6979
+ const toolStr = b.tool ?? "session";
6980
+ console.warn(`[replayci] blocked ${toolStr} at ${b.stage} \u2192 ${b.reason}`);
6981
+ }
6982
+ if (!t.complete) {
6983
+ console.warn(`[replayci] enforcement cycle incomplete (fault) \u2014 session.getLastTrace() for partial trace`);
6984
+ }
6985
+ }
6986
+ break;
6987
+ }
6988
+ case "replay_workflow_error":
6989
+ console.warn(`[replayci] workflow error: ${event.details}`);
6990
+ break;
6991
+ case "replay_state_sync_error":
6992
+ console.warn(`[replayci] state sync error: ${event.details}`);
6993
+ break;
6994
+ case "replay_receipt_error":
6995
+ console.warn(`[replayci] receipt error (${event.tool_name}): ${event.details}`);
6996
+ break;
6997
+ case "replay_capture_error":
6998
+ console.warn(`[replayci] capture error: ${event.details}`);
6999
+ break;
7000
+ default:
7001
+ break;
7002
+ }
7003
+ }
6601
7004
  function emitDiagnostic2(diagnostics, event) {
6602
7005
  try {
6603
7006
  diagnostics?.(event);