@guilz-dev/belay 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +60 -10
  2. package/dist/adapters/shared/gate-runtime.d.ts +5 -0
  3. package/dist/adapters/shared/gate-runtime.js +237 -33
  4. package/dist/bundle/claude-runtime.mjs +4082 -1230
  5. package/dist/bundle/codex-runtime.mjs +4057 -1205
  6. package/dist/bundle/cursor-runtime.mjs +4057 -1205
  7. package/dist/commands/classify-for-report.js +5 -0
  8. package/dist/commands/doctor.js +90 -27
  9. package/dist/commands/explain.js +3 -2
  10. package/dist/commands/metrics.js +75 -4
  11. package/dist/commands/quality.js +2 -6
  12. package/dist/commands/recovery-checkpoints.d.ts +8 -1
  13. package/dist/commands/recovery-checkpoints.js +44 -11
  14. package/dist/commands/session.d.ts +5 -0
  15. package/dist/commands/session.js +8 -1
  16. package/dist/commands/status.js +1 -1
  17. package/dist/config-io.js +2 -2
  18. package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
  19. package/dist/conformance/contained-execution-guarantee.js +121 -0
  20. package/dist/conformance/guarantee-table.js +7 -0
  21. package/dist/core/audit-io.js +8 -0
  22. package/dist/core/audit-metrics.d.ts +34 -0
  23. package/dist/core/audit-metrics.js +87 -15
  24. package/dist/core/audit-recovery-metrics.d.ts +33 -0
  25. package/dist/core/audit-recovery-metrics.js +146 -0
  26. package/dist/core/audit-types.d.ts +7 -1
  27. package/dist/core/audit-types.js +1 -1
  28. package/dist/core/bounded-output.d.ts +11 -0
  29. package/dist/core/bounded-output.js +26 -0
  30. package/dist/core/capability/attestation.d.ts +56 -0
  31. package/dist/core/capability/attestation.js +110 -3
  32. package/dist/core/capability/boundary-session.d.ts +5 -0
  33. package/dist/core/capability/boundary-session.js +38 -1
  34. package/dist/core/capability/index.d.ts +1 -1
  35. package/dist/core/capability/index.js +1 -1
  36. package/dist/core/config.d.ts +18 -0
  37. package/dist/core/config.js +89 -10
  38. package/dist/core/contained-execution/docker-policy.d.ts +18 -0
  39. package/dist/core/contained-execution/docker-policy.js +97 -0
  40. package/dist/core/contained-execution/docker.d.ts +223 -0
  41. package/dist/core/contained-execution/docker.js +846 -0
  42. package/dist/core/contained-execution/eligibility.d.ts +9 -0
  43. package/dist/core/contained-execution/eligibility.js +103 -0
  44. package/dist/core/contained-execution/failure.d.ts +18 -0
  45. package/dist/core/contained-execution/failure.js +105 -0
  46. package/dist/core/contained-execution/mirror.d.ts +54 -0
  47. package/dist/core/contained-execution/mirror.js +587 -0
  48. package/dist/core/contained-execution/policy.d.ts +10 -0
  49. package/dist/core/contained-execution/policy.js +20 -0
  50. package/dist/core/effect-ir/index.d.ts +1 -1
  51. package/dist/core/effect-ir/index.js +1 -1
  52. package/dist/core/effect-ir/normalize.d.ts +5 -0
  53. package/dist/core/effect-ir/normalize.js +17 -7
  54. package/dist/core/effect-ir/shell-build.js +2 -14
  55. package/dist/core/effect-ir/shell-lower.js +100 -25
  56. package/dist/core/gate-contract.d.ts +4 -1
  57. package/dist/core/gate-contract.js +2 -0
  58. package/dist/core/process-runner.d.ts +10 -3
  59. package/dist/core/process-runner.js +66 -10
  60. package/dist/core/recovery/checkpoint.d.ts +3 -0
  61. package/dist/core/recovery/checkpoint.js +33 -4
  62. package/dist/core/recovery/types.d.ts +1 -0
  63. package/dist/core/scrub.d.ts +17 -0
  64. package/dist/core/scrub.js +499 -5
  65. package/dist/core/standing-allow.d.ts +0 -20
  66. package/dist/core/standing-allow.js +0 -71
  67. package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
  68. package/dist/core/transactional/apply-observed-changes.js +11 -0
  69. package/dist/core/transactional/backend-selector.d.ts +2 -1
  70. package/dist/core/transactional/backend-selector.js +32 -29
  71. package/dist/core/transactional/backend.d.ts +3 -0
  72. package/dist/core/transactional/file-checkpoint-backend.js +242 -54
  73. package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
  74. package/dist/core/transactional/file-checkpoint-git.js +53 -0
  75. package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
  76. package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
  77. package/dist/core/transactional/git-worktree-backend.js +3 -0
  78. package/dist/core/transactional/git-worktree.d.ts +1 -0
  79. package/dist/core/transactional/git-worktree.js +1 -0
  80. package/dist/core/transactional/runner.js +59 -13
  81. package/dist/core/types.d.ts +13 -0
  82. package/dist/core/verdict/parser.d.ts +10 -0
  83. package/dist/core/verdict/parser.js +271 -40
  84. package/dist/core/verdict/shell-semantics.js +21 -0
  85. package/dist/corpus/adversarial-probe.d.ts +1 -1
  86. package/dist/corpus/adversarial-probe.js +3 -3
  87. package/dist/corpus/benign-probe-cores.d.ts +6 -0
  88. package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
  89. package/dist/corpus/coverage-compare.d.ts +36 -0
  90. package/dist/corpus/coverage-compare.js +197 -0
  91. package/dist/corpus/coverage-contexts.d.ts +17 -0
  92. package/dist/corpus/coverage-contexts.js +88 -0
  93. package/dist/corpus/coverage-matrix.d.ts +37 -0
  94. package/dist/corpus/coverage-matrix.js +159 -0
  95. package/dist/corpus/coverage-probe.d.ts +91 -0
  96. package/dist/corpus/coverage-probe.js +341 -0
  97. package/dist/corpus/evaluate.d.ts +1 -1
  98. package/dist/corpus/evaluate.js +1 -1
  99. package/dist/corpus/structural-fixture-root.d.ts +2 -0
  100. package/dist/corpus/structural-fixture-root.js +5 -0
  101. package/dist/operational-insights.d.ts +3 -0
  102. package/dist/operational-insights.js +7 -3
  103. package/dist/runtime-provenance.d.ts +8 -0
  104. package/dist/runtime-provenance.js +34 -0
  105. package/dist/types.d.ts +3 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/package.json +3 -1
  109. package/skills/belay/SKILL.md +19 -0
  110. package/skills/belay/belay-status.md +5 -0
  111. package/dist/corpus/must-allow-commands.d.ts +0 -6
@@ -3,6 +3,7 @@ import { getAdapter } from '../adapters/registry.js';
3
3
  import { repoShellClassifierOptions } from '../adapters/shared/gate-runtime.js';
4
4
  import { detectAdapterName, loadConfigFile } from '../config-io.js';
5
5
  import { isCapabilityBrokerDemotionActive } from '../core/capability/broker.js';
6
+ import { isContainedUnknownExecutionEligible } from '../core/contained-execution/eligibility.js';
6
7
  import { classifySubagent, classifyToolUse } from '../core/index.js';
7
8
  import { isTransactionalEligible } from '../core/transactional/index.js';
8
9
  import { classifyShell } from '../core/verdict/adapter.js';
@@ -26,6 +27,10 @@ export async function classifyForReport(params) {
26
27
  throw new Error('classify-for-report requires a command for shell classification.');
27
28
  }
28
29
  result = await classifyShell(params.command, cwd, repoRoot, config, classifierOptions);
30
+ result = {
31
+ ...result,
32
+ wouldMediate: isContainedUnknownExecutionEligible(config, { kind: 'shell', repoRoot }, result),
33
+ };
29
34
  input = params.command;
30
35
  }
31
36
  else if (kind === 'subagent') {
@@ -4,42 +4,33 @@ import path from 'node:path';
4
4
  import { getClaudeManagedHookEntries } from '../adapters/claude/hooks.js';
5
5
  import { getCodexManagedHookEntries } from '../adapters/codex/hooks.js';
6
6
  import { getAdapterLayout } from '../adapters/layouts/index.js';
7
+ import { protectedArtifactRoots } from '../adapters/layouts/protected-paths.js';
7
8
  import { resolveScopedPaths } from '../adapters/layouts/scope.js';
8
9
  import { cleanupOrphanApprovalState } from '../cleanup-orphans.js';
9
- import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredConfig, pendingApprovalsPath, repoLocalStateDirFor, } from '../config-io.js';
10
+ import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredConfig, pendingApprovalsPath, repoLocalStateDirFor, writeConfigFile, } from '../config-io.js';
10
11
  import { approvalSigningKeyPath } from '../core/approval-token.js';
11
12
  import { detectFenceDrift, summarizeAuditVisibility } from '../core/audit-summary.js';
12
13
  import { inspectBoundaryAttestationFile } from '../core/capability/boundary-attestation-sign.js';
13
14
  import { boundaryAttestationPath, boundarySessionStatus, } from '../core/capability/boundary-session.js';
14
- import { defaultControlPlaneDir } from '../core/config.js';
15
+ import { configuredControlPlaneDir, defaultControlPlaneDir, hasForbiddenShellOverrideLists, stripForbiddenShellOverrideLists, } from '../core/config.js';
15
16
  import { verifyIntegrityManifest } from '../core/integrity.js';
16
17
  import { diagnoseJudge, stopJudgeSessionBrokers } from '../core/judge-doctor.js';
17
18
  import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
18
19
  import { listRecoveryCheckpoints } from '../core/recovery/checkpoint.js';
19
20
  import { recoveryApprovalSetupNotes, recoveryNotificationConfigured, recoveryNotificationSetupWarning, summarizeRecoveryCheckpointDiagnostics, } from '../core/recovery/operator-guidance.js';
21
+ import { probeFileCheckpointBackend } from '../core/transactional/backend-selector.js';
22
+ import { fileCheckpointIsolationReason } from '../core/transactional/file-checkpoint-isolation.js';
20
23
  import { probeFileCloneStrategy } from '../core/transactional/file-clone.js';
24
+ import { isGitWorktreeAvailable } from '../core/transactional/git-worktree.js';
21
25
  import { getManagedHookEntries } from '../defaults.js';
22
26
  import { resolveNodeBinary } from '../node-resolution.js';
27
+ import { readInstalledRuntimeProvenance } from '../runtime-provenance.js';
23
28
  import { egressStatus } from '../services/egress-service.js';
24
29
  import { sandboxStatus } from '../services/sandbox-service.js';
25
30
  import { PACKAGE_VERSION } from '../version.js';
26
31
  import { loadAuditRecords } from './audit.js';
27
32
  import { collectHealthSnapshot } from './health-snapshot.js';
28
33
  import { metricsProject } from './metrics.js';
29
- async function readRuntimeVersion(corePath) {
30
- try {
31
- const content = await readFile(corePath, 'utf8');
32
- const stampMatch = content.match(/RUNTIME_BUILD_STAMP\s*=\s*"([^"]+)"/);
33
- const versionMatch = content.match(/RUNTIME_PACKAGE_VERSION\s*=\s*"([^"]+)"/);
34
- return {
35
- stamp: stampMatch?.[1],
36
- version: versionMatch?.[1],
37
- };
38
- }
39
- catch {
40
- return {};
41
- }
42
- }
43
34
  function resolveDoctorAdapter(options, configAdapter) {
44
35
  if (options.adapter) {
45
36
  return options.adapter;
@@ -87,7 +78,7 @@ export async function doctorProject(options = {}) {
87
78
  ...(loadedConfig.overrides.external.length > 0 ? ['overrides.external'] : []),
88
79
  ];
89
80
  if (ignoredShellOverrideLists.length > 0) {
90
- warnings.push(`Legacy shell ${ignoredShellOverrideLists.join(' and ')} ${ignoredShellOverrideLists.length === 1 ? 'is' : 'are'} deprecated and ignored. Remove the command list; inspect the EffectPlan and correct effect semantics or resource scope instead.`);
81
+ issues.push(`Legacy shell ${ignoredShellOverrideLists.join(' and ')} ${ignoredShellOverrideLists.length === 1 ? 'is' : 'are'} forbidden (ADR-005). Remove the command list (or run belay doctor --fix); improve EffectPlan semantics, use one-shot approval, or approve an exact resource scope instead.`);
91
82
  }
92
83
  if (loadedConfig.version !== 4) {
93
84
  warnings.push(`Config version is ${loadedConfig.version}; expected 4. Run belay upgrade to migrate.`);
@@ -234,7 +225,7 @@ export async function doctorProject(options = {}) {
234
225
  notes.push(`Node resolved at ${nodeResolution.path}`);
235
226
  }
236
227
  if (existsSync(corePath)) {
237
- const runtimeVersions = await readRuntimeVersion(corePath);
228
+ const runtimeVersions = await readInstalledRuntimeProvenance(corePath);
238
229
  if (runtimeVersions.stamp && !runtimeVersions.stamp.startsWith(`${PACKAGE_VERSION}@`)) {
239
230
  warnings.push(`Installed runtime stamp (${runtimeVersions.stamp}) differs from package (${PACKAGE_VERSION}). Run belay upgrade.`);
240
231
  }
@@ -246,6 +237,23 @@ export async function doctorProject(options = {}) {
246
237
  }
247
238
  }
248
239
  if (options.fix && loadedConfig) {
240
+ if (hasForbiddenShellOverrideLists(loadedConfig)) {
241
+ if (options.dryRun !== true) {
242
+ const stripped = stripForbiddenShellOverrideLists(loadedConfig);
243
+ await writeConfigFile(repoRoot, stripped, adapterName);
244
+ loadedConfig = stripped;
245
+ notes.push('Removed forbidden legacy shell override lists (overrides.allow / overrides.external).');
246
+ for (let index = issues.length - 1; index >= 0; index -= 1) {
247
+ const issue = issues[index] ?? '';
248
+ if (issue.includes('overrides.allow') || issue.includes('overrides.external')) {
249
+ issues.splice(index, 1);
250
+ }
251
+ }
252
+ }
253
+ else {
254
+ notes.push('Dry run: would remove forbidden legacy shell override lists.');
255
+ }
256
+ }
249
257
  const cleanup = await cleanupOrphanApprovalState(repoRoot, loadedConfig, {
250
258
  dryRun: options.dryRun === true,
251
259
  });
@@ -269,25 +277,34 @@ export async function doctorProject(options = {}) {
269
277
  let dogfood = null;
270
278
  if (loadedConfig) {
271
279
  const auditRecords = await loadAuditRecords(repoRoot);
272
- const auditVisibility = summarizeAuditVisibility(auditRecords);
280
+ const metrics = await metricsProject({ targetDir: repoRoot });
281
+ const cohortIdentity = metrics.currentCohort.identity;
282
+ const cohortAuditRecords = cohortIdentity
283
+ ? auditRecords.filter((record) => record.runtimeBuildStamp === cohortIdentity.runtimeBuildStamp &&
284
+ record.configFingerprint === cohortIdentity.configFingerprint)
285
+ : [];
286
+ const auditVisibility = summarizeAuditVisibility(cohortAuditRecords);
273
287
  const drift = detectFenceDrift(auditVisibility, {
274
288
  threshold: loadedConfig.policy.fenceWarnThreshold,
275
289
  });
276
290
  warnings.push(...drift.warnings);
277
291
  notes.push(...drift.notes);
278
- const metrics = await metricsProject({ targetDir: repoRoot });
292
+ const cohort = metrics.currentCohort;
279
293
  dogfood = {
280
294
  active: loadedConfig.mode === 'audit' && loadedConfig.policy.unknownLocalEffect === 'deny',
281
295
  mode: loadedConfig.mode,
282
296
  unknownLocalEffect: loadedConfig.policy.unknownLocalEffect,
283
297
  readyForEnforce: metrics.dogfood.readyForEnforce,
284
- gateEvents: metrics.gateEvents,
285
- wouldBlockCount: metrics.wouldBlockCount,
286
- wouldBlockRate: metrics.wouldBlockRate,
298
+ gateEvents: cohort.gateEvents,
299
+ wouldBlockCount: cohort.wouldBlockCount,
300
+ wouldBlockRate: cohort.wouldBlockRate,
301
+ excludedGateEvents: cohort.excludedGateEvents,
302
+ runtimeBuildStamp: cohort.identity?.runtimeBuildStamp,
303
+ configFingerprint: cohort.identity?.configFingerprint,
287
304
  notes: metrics.dogfood.notes,
288
305
  };
289
306
  if (dogfood.active) {
290
- notes.push(`Dogfood active: ${dogfood.gateEvents} gate events, ${dogfood.wouldBlockCount} would-block (${(dogfood.wouldBlockRate * 100).toFixed(1)}%).`);
307
+ notes.push(`Dogfood active cohort: ${dogfood.gateEvents} gate events, ${dogfood.wouldBlockCount} would-block (${(dogfood.wouldBlockRate * 100).toFixed(1)}%); ${dogfood.excludedGateEvents} historical/mismatched event(s) excluded.`);
291
308
  if (dogfood.readyForEnforce) {
292
309
  notes.push('Dogfood metrics suggest enforce mode is ready (belay dogfood --enforce).');
293
310
  }
@@ -295,10 +312,27 @@ export async function doctorProject(options = {}) {
295
312
  else if (dogfood.unknownLocalEffect === 'deny' && dogfood.mode !== 'audit') {
296
313
  notes.push('Fail-closed policy is enabled in enforce mode.');
297
314
  }
315
+ const recoveryCohort = metrics.currentCohortRecovery;
316
+ const restoreTotal = recoveryCohort.restore.applied +
317
+ recoveryCohort.restore.conflict +
318
+ recoveryCohort.restore.rejected;
319
+ if (recoveryCohort.snapshot.attempts > 0 || restoreTotal > 0) {
320
+ notes.push(`Recovery cohort metrics: ${recoveryCohort.snapshot.attempts} snapshot attempt(s) (${recoveryCohort.snapshot.applied} applied, ${recoveryCohort.snapshot.skipped} skipped); restore outcomes ${recoveryCohort.restore.applied} applied / ${recoveryCohort.restore.conflict} conflict / ${recoveryCohort.restore.rejected} rejected.`);
321
+ if (recoveryCohort.snapshot.prepareSampleCount > 0) {
322
+ notes.push(`Recovery snapshot prepare latency (cohort): p50 ${recoveryCohort.snapshot.prepareMsP50 ?? 0}ms, p95 ${recoveryCohort.snapshot.prepareMsP95 ?? 0}ms (${recoveryCohort.snapshot.prepareSampleCount} samples).`);
323
+ }
324
+ if (Object.keys(recoveryCohort.snapshot.failuresByReason).length > 0) {
325
+ notes.push(`Recovery snapshot failures (cohort): ${Object.entries(recoveryCohort.snapshot.failuresByReason)
326
+ .map(([reason, count]) => `${reason}=${count}`)
327
+ .join(', ')}.`);
328
+ }
329
+ }
298
330
  if (loadedConfig.policy.transactional.enabled) {
299
- notes.push('Transactional execution: enabled — low-confidence shell mutations run in an isolated git worktree; observed-safe effects are applied once and the hook denies re-execution.');
300
- if (!existsSync(path.join(repoRoot, '.git'))) {
301
- warnings.push('Transactional execution is enabled but this directory is not a git repository. Transactional worktrees will be skipped until git is available.');
331
+ notes.push('Transactional execution: enabled — low-confidence shell mutations run in an isolated git worktree or file-checkpoint mirror; observed-safe effects are applied once and the hook denies re-execution.');
332
+ const fileCheckpointEnabled = loadedConfig.policy.transactional.fileCheckpoint.enabled;
333
+ const allowNonGit = loadedConfig.policy.transactional.fileCheckpoint.allowNonGit;
334
+ if (!existsSync(path.join(repoRoot, '.git')) && !(fileCheckpointEnabled && allowNonGit)) {
335
+ warnings.push('Transactional execution is enabled but this directory is not a git repository. Enable file checkpoint with allowNonGit or initialize git before transactional recovery can run.');
302
336
  }
303
337
  }
304
338
  const fileCheckpoint = loadedConfig.policy.transactional.fileCheckpoint;
@@ -311,6 +345,35 @@ export async function doctorProject(options = {}) {
311
345
  if (!loadedConfig.policy.transactional.checkpoint?.enabled) {
312
346
  warnings.push('File checkpoint is enabled but durable Recovery checkpointing is disabled; backend selection will fail closed.');
313
347
  }
348
+ const checkpointConfig = loadedConfig.policy.transactional.checkpoint;
349
+ const configuredBoundary = loadedConfig.capability?.boundaryDriver ??
350
+ (loadedConfig.sandbox.runtime === 'container' ? 'container' : null);
351
+ let attestation = null;
352
+ if (configuredBoundary) {
353
+ try {
354
+ const session = await boundarySessionStatus({ repoRoot, config: loadedConfig });
355
+ attestation = session.attestation;
356
+ }
357
+ catch {
358
+ attestation = null;
359
+ }
360
+ }
361
+ const gitAvailable = await isGitWorktreeAvailable(repoRoot);
362
+ const backendContext = {
363
+ repoRoot,
364
+ stateDir: belayStateDir(loadedConfig, repoLocalDir),
365
+ cwd: repoRoot,
366
+ dirtyIgnoreRoots: protectedArtifactRoots(activeLayout, repoRoot, loadedConfig.controlPlane.enabled ? configuredControlPlaneDir(loadedConfig) : null),
367
+ fileCheckpoint,
368
+ durableCheckpointEnabled: checkpointConfig?.enabled === true,
369
+ boundaryAttestation: attestation,
370
+ boundaryAttestationFresh: false,
371
+ boundaryDriverId: configuredBoundary ?? undefined,
372
+ };
373
+ const isolationAvailable = fileCheckpointIsolationReason(backendContext) === null;
374
+ const fileCheckpointProbe = await probeFileCheckpointBackend(backendContext);
375
+ const fileCheckpointAvailable = loadedConfig.policy.transactional.enabled && fileCheckpointProbe.eligible;
376
+ notes.push(`File checkpoint eligibility: probe=${fileCheckpointAvailable ? 'available' : 'unavailable'}, workspace=${gitAvailable ? 'git' : 'non-git'}, isolation=${isolationAvailable ? (configuredBoundary ?? 'attested') : 'unavailable'}.`);
314
377
  }
315
378
  if (loadedConfig.policy.transactional.checkpoint?.enabled) {
316
379
  notes.push('Recovery checkpoint: enabled — repo-local pre-images are persisted before observed-safe transactional apply.');
@@ -103,11 +103,12 @@ export function formatExplainReport(report) {
103
103
  : 'Egress proxy: not configured',
104
104
  `Sandbox (L1 broker): ${report.sandbox.enabled ? 'enabled' : 'disabled'} (runtime=${report.sandbox.runtime}, fs broker active=${report.sandboxBrokerActive}, L1-full=${report.l1FullActive})`,
105
105
  `Transactional (L2): ${report.policy.transactional.enabled ? 'enabled' : 'disabled'} (eligible for this command=${report.transactionalEligible})`,
106
+ `Contained unknown execution: ${result.wouldMediate ? 'would mediate' : 'not eligible'}`,
106
107
  report.policy.transactional.enabled
107
108
  ? `Transactional band: [${report.policy.transactional.minConfidence}, ${report.policy.transactional.maxConfidence})`
108
109
  : 'Transactional band: not configured',
109
- `Legacy overrides allow (deprecated, ignored): ${report.overrides.allow.join(', ') || '(none)'}`,
110
- `Legacy overrides external (deprecated, ignored): ${report.overrides.external.join(', ') || '(none)'}`,
110
+ `Legacy overrides allow (forbidden, ignored): ${report.overrides.allow.join(', ') || '(none)'}`,
111
+ `Legacy overrides external (forbidden, ignored): ${report.overrides.external.join(', ') || '(none)'}`,
111
112
  '',
112
113
  `Verdict: ${result.verdict}`,
113
114
  `Reason: ${result.reason}`,
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { loadConfigFile } from '../config-io.js';
4
4
  import { computeAuditMetrics, parseAuditNdjson } from '../core/audit-metrics.js';
5
+ import { resolveActiveAuditCohort } from '../runtime-provenance.js';
5
6
  function formatFingerprintPreview(fingerprint) {
6
7
  if (fingerprint.length <= 12) {
7
8
  return fingerprint;
@@ -20,21 +21,87 @@ export async function metricsProject(options = {}) {
20
21
  raw = '';
21
22
  }
22
23
  const records = parseAuditNdjson(raw);
24
+ const activeCohort = await resolveActiveAuditCohort(repoRoot, config);
23
25
  return computeAuditMetrics(records, {
24
26
  auditLogPath: config.audit.logPath,
25
27
  mode: config.mode,
26
28
  unknownLocalEffect: config.policy.unknownLocalEffect,
29
+ activeCohort,
27
30
  });
28
31
  }
32
+ function formatRecoveryMetricsSection(label, metrics, excluded) {
33
+ const lines = [
34
+ '',
35
+ label,
36
+ `Snapshot attempts: ${metrics.snapshot.attempts} (${metrics.snapshot.applied} applied, ${metrics.snapshot.skipped} skipped)`,
37
+ ];
38
+ if (excluded) {
39
+ lines.push(`- excluded historical/mismatched snapshot attempts: ${excluded.snapshotAttempts}`, `- excluded historical/mismatched restore events: ${excluded.restoreEvents}`);
40
+ }
41
+ if (metrics.snapshot.prepareSampleCount > 0) {
42
+ lines.push(`Snapshot prepare latency: p50 ${metrics.snapshot.prepareMsP50 ?? 0}ms, p95 ${metrics.snapshot.prepareMsP95 ?? 0}ms (${metrics.snapshot.prepareSampleCount} samples)`);
43
+ }
44
+ if (Object.keys(metrics.snapshot.byBackend).length > 0) {
45
+ lines.push('Snapshot backends:');
46
+ for (const [backend, count] of Object.entries(metrics.snapshot.byBackend).sort((left, right) => right[1] - left[1])) {
47
+ lines.push(`- ${backend}: ${count}`);
48
+ }
49
+ }
50
+ if (Object.keys(metrics.snapshot.byResourceKind).length > 0) {
51
+ lines.push('Snapshot resource kinds:');
52
+ for (const [resourceKind, count] of Object.entries(metrics.snapshot.byResourceKind).sort((left, right) => right[1] - left[1])) {
53
+ lines.push(`- ${resourceKind}: ${count}`);
54
+ }
55
+ }
56
+ if (Object.keys(metrics.snapshot.failuresByReason).length > 0) {
57
+ lines.push('Snapshot failures by reason:');
58
+ for (const [reason, count] of Object.entries(metrics.snapshot.failuresByReason).sort((left, right) => right[1] - left[1])) {
59
+ lines.push(`- ${reason}: ${count}`);
60
+ }
61
+ }
62
+ const restoreTotal = metrics.restore.applied + metrics.restore.conflict + metrics.restore.rejected;
63
+ if (restoreTotal > 0) {
64
+ lines.push(`Restore outcomes: ${metrics.restore.applied} applied, ${metrics.restore.conflict} conflict, ${metrics.restore.rejected} rejected`);
65
+ }
66
+ return lines;
67
+ }
29
68
  export function formatMetricsReport(report) {
30
69
  const lines = [
31
70
  `belay metrics for ${report.auditLogPath}`,
32
71
  `Schema: v${report.schemaVersion}`,
33
- `Gate events: ${report.gateEvents}`,
34
- `Would-block: ${report.wouldBlockCount} (${(report.wouldBlockRate * 100).toFixed(1)}%)`,
35
- `Classifier-quality would-block: ${report.classifierWouldBlockCount} (${(report.classifierWouldBlockRate * 100).toFixed(1)}%)`,
36
- `Approvals recorded during audit: ${report.approvalRecordedCount}`,
72
+ `All-time gate events: ${report.gateEvents}`,
73
+ `All-time would-block: ${report.wouldBlockCount} (${(report.wouldBlockRate * 100).toFixed(1)}%)`,
74
+ `All-time classifier-quality would-block: ${report.classifierWouldBlockCount} (${(report.classifierWouldBlockRate * 100).toFixed(1)}%)`,
75
+ `All-time approvals recorded during audit: ${report.approvalRecordedCount}`,
76
+ `Contained execution: would mediate ${report.containedExecution.wouldMediate}; complete ${report.containedExecution.complete}; failed ${report.containedExecution.failed}; timed out ${report.containedExecution.timedOut}`,
37
77
  ];
78
+ lines.push('', 'Current readiness cohort:');
79
+ if (report.currentCohort.identity) {
80
+ lines.push(`- runtime build: ${report.currentCohort.identity.runtimeBuildStamp}`);
81
+ lines.push(`- config fingerprint: ${formatFingerprintPreview(report.currentCohort.identity.configFingerprint)}`);
82
+ }
83
+ else {
84
+ lines.push('- identity: unavailable');
85
+ }
86
+ lines.push(`- matching gate events: ${report.currentCohort.gateEvents}`);
87
+ lines.push(`- excluded historical/mismatched gate events: ${report.currentCohort.excludedGateEvents}`);
88
+ lines.push(`- would-block: ${report.currentCohort.wouldBlockCount} (${(report.currentCohort.wouldBlockRate * 100).toFixed(1)}%)`);
89
+ lines.push(`- classifier-quality would-block: ${report.currentCohort.classifierWouldBlockCount} (${(report.currentCohort.classifierWouldBlockRate * 100).toFixed(1)}%)`);
90
+ lines.push(`- approvals recorded during audit: ${report.currentCohort.approvalRecordedCount}`);
91
+ lines.push(`- contained execution: would mediate ${report.currentCohort.containedExecution.wouldMediate}; complete ${report.currentCohort.containedExecution.complete}; failed ${report.currentCohort.containedExecution.failed}; timed out ${report.currentCohort.containedExecution.timedOut}`);
92
+ lines.push(`- availability-caused asks: ${report.currentCohort.availabilityAsks.total}`);
93
+ if (Object.keys(report.currentCohort.wouldBlockByReason).length > 0) {
94
+ lines.push('', 'Current-cohort would-block by reason:');
95
+ for (const [reason, count] of Object.entries(report.currentCohort.wouldBlockByReason).sort((left, right) => right[1] - left[1])) {
96
+ lines.push(`- ${reason}: ${count}`);
97
+ }
98
+ }
99
+ if (report.currentCohort.topWouldBlockSummaries.length > 0) {
100
+ lines.push('', 'Current-cohort top would-block summaries:');
101
+ for (const entry of report.currentCohort.topWouldBlockSummaries) {
102
+ lines.push(`- [${entry.reason}] x${entry.count}: ${entry.summary}`);
103
+ }
104
+ }
38
105
  if (report.approvalLatency.count > 0) {
39
106
  lines.push(`Approval latency: median ${report.approvalLatency.medianMs ?? 0}ms, p95 ${report.approvalLatency.p95Ms ?? 0}ms (${report.approvalLatency.count} samples)`);
40
107
  }
@@ -123,5 +190,9 @@ export function formatMetricsReport(report) {
123
190
  }
124
191
  lines.push('', report.dogfood.readyForEnforce ? 'Ready for enforce: yes' : 'Ready for enforce: not yet');
125
192
  }
193
+ lines.push(...formatRecoveryMetricsSection('All-time recovery metrics:', report.recovery), ...formatRecoveryMetricsSection('Current-cohort recovery metrics:', report.currentCohortRecovery, {
194
+ snapshotAttempts: report.currentCohortRecovery.excludedSnapshotAttempts,
195
+ restoreEvents: report.currentCohortRecovery.excludedRestoreEvents,
196
+ }));
126
197
  return `${lines.join('\n')}\n`;
127
198
  }
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import { loadConfigFile } from '../config-io.js';
3
- import { computeAuditMetrics } from '../core/audit-metrics.js';
4
3
  import { runCorpusEvaluation } from '../corpus/evaluate.js';
5
4
  import { passesHardGates } from '../corpus/gates.js';
6
5
  import { loadAuditRecords } from './audit.js';
7
6
  import { harvestReportFromRecords } from './harvest.js';
7
+ import { metricsProject } from './metrics.js';
8
8
  export const QUALITY_REPORT_SCHEMA_VERSION = 1;
9
9
  export async function qualityCheck(options = {}) {
10
10
  const repoRoot = path.resolve(options.targetDir ?? process.cwd());
@@ -13,11 +13,7 @@ export async function qualityCheck(options = {}) {
13
13
  const corpusMetrics = await runCorpusEvaluation(corpusDir);
14
14
  const hardGatesOk = passesHardGates(corpusMetrics.gates);
15
15
  const auditRecords = await loadAuditRecords(repoRoot);
16
- const metrics = computeAuditMetrics(auditRecords, {
17
- auditLogPath: config.audit.logPath,
18
- mode: config.mode,
19
- unknownLocalEffect: config.policy.unknownLocalEffect,
20
- });
16
+ const metrics = await metricsProject({ targetDir: repoRoot });
21
17
  const harvest = harvestReportFromRecords(auditRecords);
22
18
  const notes = [
23
19
  'Overall ok reflects corpus hard gates only; audit and harvest signals are advisory.',
@@ -12,10 +12,13 @@ export declare function recoveryCheckpointCommand(options: {
12
12
  backendCounts: {
13
13
  [k: string]: number;
14
14
  };
15
+ resourceKindCounts: {
16
+ [k: string]: number;
17
+ };
15
18
  fileCheckpoint: {
16
19
  enabled: boolean;
17
20
  allowNonGit: boolean;
18
- isolation: string | null;
21
+ isolation: import("../core/capability/attestation.js").BoundaryDriverId | null;
19
22
  copyStrategy: import("../core/transactional/file-clone.js").FileCloneStrategy | undefined;
20
23
  probe: string;
21
24
  };
@@ -55,6 +58,7 @@ export declare function recoveryCheckpointCommand(options: {
55
58
  backend?: undefined;
56
59
  availableBackends?: undefined;
57
60
  backendCounts?: undefined;
61
+ resourceKindCounts?: undefined;
58
62
  fileCheckpoint?: undefined;
59
63
  enabled?: undefined;
60
64
  checkpointCount?: undefined;
@@ -86,6 +90,7 @@ export declare function recoveryCheckpointCommand(options: {
86
90
  backend?: undefined;
87
91
  availableBackends?: undefined;
88
92
  backendCounts?: undefined;
93
+ resourceKindCounts?: undefined;
89
94
  fileCheckpoint?: undefined;
90
95
  enabled?: undefined;
91
96
  checkpointCount?: undefined;
@@ -117,6 +122,7 @@ export declare function recoveryCheckpointCommand(options: {
117
122
  backend?: undefined;
118
123
  availableBackends?: undefined;
119
124
  backendCounts?: undefined;
125
+ resourceKindCounts?: undefined;
120
126
  fileCheckpoint?: undefined;
121
127
  enabled?: undefined;
122
128
  checkpointCount?: undefined;
@@ -144,6 +150,7 @@ export declare function recoveryCheckpointCommand(options: {
144
150
  backend?: undefined;
145
151
  availableBackends?: undefined;
146
152
  backendCounts?: undefined;
153
+ resourceKindCounts?: undefined;
147
154
  fileCheckpoint?: undefined;
148
155
  enabled?: undefined;
149
156
  checkpointCount?: undefined;
@@ -1,5 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import path from 'node:path';
3
+ import { getAdapterLayout } from '../adapters/layouts/index.js';
4
+ import { protectedArtifactRoots } from '../adapters/layouts/protected-paths.js';
3
5
  import { ensureBelayStateDir, loadConfigFile } from '../config-io.js';
4
6
  import { compactApprovals, createApprovalRecordWithEnvelope } from '../core/approval.js';
5
7
  import { createGateApprovalStore } from '../core/approval-service.js';
@@ -12,6 +14,8 @@ import { notifyDeny } from '../core/notify.js';
12
14
  import { canonicalPath } from '../core/path-utils.js';
13
15
  import { listRecoveryCheckpoints, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_RESTORE_CONFLICT, RECOVERY_RESTORE_REASON, recoveryCheckpointStorageBytes, recoveryRestoreBinding, restoreRecoveryCheckpoint, showRecoveryCheckpoint, } from '../core/recovery/checkpoint.js';
14
16
  import { formatRecoveryStateDiagnostic, recoveryNotificationConfigured, recoveryNotificationSetupWarning, summarizeRecoveryCheckpointDiagnostics, } from '../core/recovery/operator-guidance.js';
17
+ import { probeFileCheckpointBackend } from '../core/transactional/backend-selector.js';
18
+ import { fileCheckpointIsolationReason } from '../core/transactional/file-checkpoint-isolation.js';
15
19
  import { probeFileCloneStrategy } from '../core/transactional/file-clone.js';
16
20
  async function appendRecoveryAudit(repoRoot, config, event) {
17
21
  try {
@@ -116,28 +120,43 @@ export async function recoveryCheckpointCommand(options) {
116
120
  backend,
117
121
  checkpoints.filter((checkpoint) => checkpoint.backend === backend).length,
118
122
  ]));
123
+ const resourceKindCounts = Object.fromEntries([
124
+ ...new Set(checkpoints.map((checkpoint) => checkpoint.resourceKind ?? 'git_repository')),
125
+ ].map((resourceKind) => [
126
+ resourceKind,
127
+ checkpoints.filter((checkpoint) => (checkpoint.resourceKind ?? 'git_repository') === resourceKind).length,
128
+ ]));
119
129
  const checkpointBackends = Object.keys(backendCounts);
120
130
  const fileCheckpointConfig = config.policy.transactional.fileCheckpoint;
121
131
  const configuredBoundary = config.capability?.boundaryDriver ??
122
132
  (config.sandbox.runtime === 'container' ? 'container' : null);
123
- let isolation = configuredBoundary;
124
- let isolationAvailable = false;
133
+ let attestation = null;
125
134
  if (configuredBoundary) {
126
135
  try {
127
136
  const session = await boundarySessionStatus({ repoRoot, config });
128
- isolationAvailable =
129
- session.fresh === true && session.attestation?.isolatesWorkspaceMounts === true;
130
- isolation = isolationAvailable ? (session.attestation?.driver ?? configuredBoundary) : null;
137
+ attestation = session.attestation;
131
138
  }
132
139
  catch {
133
- isolation = null;
140
+ attestation = null;
134
141
  }
135
142
  }
136
143
  const copyStrategy = fileCheckpointConfig.enabled ? await probeFileCloneStrategy() : null;
137
- const fileCheckpointAvailable = fileCheckpointConfig.enabled &&
138
- config.policy.transactional.enabled &&
139
- checkpointConfig.enabled &&
140
- isolationAvailable;
144
+ const adapterLayout = getAdapterLayout(config.adapter);
145
+ const backendContext = {
146
+ repoRoot,
147
+ stateDir,
148
+ cwd: repoRoot,
149
+ dirtyIgnoreRoots: protectedArtifactRoots(adapterLayout, repoRoot, config.controlPlane.enabled ? configuredControlPlaneDir(config) : null),
150
+ fileCheckpoint: fileCheckpointConfig,
151
+ durableCheckpointEnabled: checkpointConfig.enabled,
152
+ boundaryAttestation: attestation,
153
+ boundaryAttestationFresh: false,
154
+ boundaryDriverId: configuredBoundary ?? undefined,
155
+ };
156
+ const isolationAvailable = fileCheckpointIsolationReason(backendContext) === null;
157
+ const fileCheckpointProbe = await probeFileCheckpointBackend(backendContext);
158
+ const fileCheckpointAvailable = config.policy.transactional.enabled && fileCheckpointProbe.eligible;
159
+ const isolation = isolationAvailable ? (attestation?.driver ?? configuredBoundary) : null;
141
160
  return {
142
161
  ok: true,
143
162
  subcommand: 'status',
@@ -149,6 +168,7 @@ export async function recoveryCheckpointCommand(options) {
149
168
  : 'git_worktree',
150
169
  availableBackends: ['git_worktree', 'file_checkpoint'],
151
170
  backendCounts,
171
+ resourceKindCounts,
152
172
  fileCheckpoint: {
153
173
  enabled: fileCheckpointConfig.enabled,
154
174
  allowNonGit: fileCheckpointConfig.allowNonGit,
@@ -304,9 +324,13 @@ export async function recoveryCheckpointCommand(options) {
304
324
  }
305
325
  export function formatRecoveryCheckpointResult(result) {
306
326
  if (result.subcommand === 'status') {
327
+ const fileCheckpoint = result.fileCheckpoint;
328
+ const backendCounts = result.backendCounts;
329
+ const resourceKindCounts = result.resourceKindCounts;
307
330
  const lines = [
308
331
  `belay recover status for ${result.repoRoot}`,
309
332
  `Backend: ${result.backend}`,
333
+ `Available backends: ${result.availableBackends?.join(', ') ?? 'git_worktree, file_checkpoint'}`,
310
334
  `Checkpointing: ${result.enabled ? 'enabled' : 'disabled'}`,
311
335
  `Checkpoints: ${result.checkpointCount} (${result.recoverableCount} recoverable)`,
312
336
  `Needs manual repair: ${result.needsManualRepairCount ?? 0}`,
@@ -314,6 +338,15 @@ export function formatRecoveryCheckpointResult(result) {
314
338
  `Storage: ${result.storageBytes} bytes`,
315
339
  `States: ${JSON.stringify(result.states)}`,
316
340
  ];
341
+ if (backendCounts && Object.keys(backendCounts).length > 0) {
342
+ lines.push(`Backend counts: ${JSON.stringify(backendCounts)}`);
343
+ }
344
+ if (resourceKindCounts && Object.keys(resourceKindCounts).length > 0) {
345
+ lines.push(`Resource kind counts: ${JSON.stringify(resourceKindCounts)}`);
346
+ }
347
+ if (fileCheckpoint) {
348
+ lines.push(`File checkpoint: enabled=${fileCheckpoint.enabled ? 'yes' : 'no'}, allowNonGit=${fileCheckpoint.allowNonGit ? 'yes' : 'no'}, probe=${fileCheckpoint.probe ?? 'unknown'}, isolation=${fileCheckpoint.isolation ?? 'none'}${fileCheckpoint.copyStrategy ? `, copyStrategy=${fileCheckpoint.copyStrategy}` : ''}`);
349
+ }
317
350
  const advisories = result.advisories;
318
351
  if (advisories && advisories.length > 0) {
319
352
  lines.push('', 'Advisories:');
@@ -330,7 +363,7 @@ export function formatRecoveryCheckpointResult(result) {
330
363
  return 'No recovery checkpoints found.\n';
331
364
  const lines = checkpoints.map((item) => {
332
365
  const diagnostic = formatRecoveryStateDiagnostic(item.state, item.stateDetail);
333
- const base = `${item.checkpointId}\t${item.state}\t${item.changeCount} changes\treceipt:${item.receiptHash?.slice(0, 12) ?? '-'}\t${item.createdAt}`;
366
+ const base = `${item.checkpointId}\t${item.state}\t${item.backend}\t${item.resourceKind}\t${item.changeCount} changes\treceipt:${item.receiptHash?.slice(0, 12) ?? '-'}\t${item.createdAt}`;
334
367
  return diagnostic ? `${base}\n ${diagnostic}` : base;
335
368
  });
336
369
  return `${lines.join('\n')}\n`;
@@ -15,6 +15,11 @@ export declare function sessionStatusProject(params: {
15
15
  ok: boolean;
16
16
  attestationPath: string;
17
17
  fresh: boolean;
18
+ containedExecutionFresh?: boolean;
18
19
  attestation: Awaited<ReturnType<typeof boundarySessionStatus>>['attestation'];
19
20
  }>;
21
+ export declare function sessionStatusOk(status: {
22
+ fresh: boolean;
23
+ containedExecutionFresh?: boolean;
24
+ }): boolean;
20
25
  export declare function formatSessionStatusReport(report: Awaited<ReturnType<typeof sessionStatusProject>>): string;
@@ -31,12 +31,16 @@ export async function sessionStatusProject(params) {
31
31
  const config = await loadConfigFile(repoRoot);
32
32
  const status = await boundarySessionStatus({ repoRoot, config });
33
33
  return {
34
- ok: status.fresh,
34
+ ok: sessionStatusOk(status),
35
35
  attestationPath: status.attestationPath,
36
36
  fresh: status.fresh,
37
+ containedExecutionFresh: status.containedExecutionFresh,
37
38
  attestation: status.attestation,
38
39
  };
39
40
  }
41
+ export function sessionStatusOk(status) {
42
+ return status.fresh || status.containedExecutionFresh === true;
43
+ }
40
44
  export function formatSessionStatusReport(report) {
41
45
  if (!report.attestation) {
42
46
  return `No boundary attestation at ${report.attestationPath}. Run belay session start.`;
@@ -45,6 +49,9 @@ export function formatSessionStatusReport(report) {
45
49
  `Attestation: ${report.attestationPath}`,
46
50
  `Driver: ${report.attestation.driver}`,
47
51
  `Fresh: ${report.fresh ? 'yes' : 'no'}`,
52
+ ...(report.containedExecutionFresh === undefined
53
+ ? []
54
+ : [`Contained execution fresh: ${report.containedExecutionFresh ? 'yes' : 'no'}`]),
48
55
  `Expires: ${report.attestation.expiresAt}`,
49
56
  `Materializes grants: ${report.attestation.materializesGrants ? 'yes' : 'no'}`,
50
57
  ].join('\n');
@@ -54,7 +54,7 @@ export function formatStatusReport(report) {
54
54
  `Approved (awaiting use): ${report.approved.length}`,
55
55
  `Expired pending (not yet compacted): ${report.expiredPendingCount}`,
56
56
  `Dogfood: ${report.dogfood.active ? 'active' : 'inactive'} (mode=${report.dogfood.mode}, unknownLocalEffect=${report.dogfood.unknownLocalEffect})`,
57
- `Metrics: ${report.dogfood.gateEvents} gate events, ${report.dogfood.wouldBlockCount} would-block (${(report.dogfood.wouldBlockRate * 100).toFixed(1)}%)`,
57
+ `Current cohort metrics: ${report.dogfood.gateEvents} gate events, ${report.dogfood.wouldBlockCount} would-block (${(report.dogfood.wouldBlockRate * 100).toFixed(1)}%), ${report.dogfood.excludedGateEvents} historical/mismatched excluded`,
58
58
  `Ready for enforce: ${report.dogfood.readyForEnforce ? 'yes' : 'not yet'}`,
59
59
  `File checkpoint: ${report.fileCheckpoint.enabled ? 'enabled' : 'disabled'} (transactional=${report.fileCheckpoint.transactionalEnabled}, durable=${report.fileCheckpoint.durableCheckpointEnabled}, nonGit=${report.fileCheckpoint.allowNonGit})`,
60
60
  `File checkpoint limits: files=${report.fileCheckpoint.maxFiles}, sourceBytes=${report.fileCheckpoint.maxSourceBytes}, workspaceBytes=${report.fileCheckpoint.maxWorkspaceBytes}, prepareTimeoutMs=${report.fileCheckpoint.prepareTimeoutMs}, copyConcurrency=${report.fileCheckpoint.copyConcurrency}`,
package/dist/config-io.js CHANGED
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
  import { getAdapterLayout } from './adapters/layouts/index.js';
6
6
  import { compactApprovals, isExpired, mergeApprovalStates } from './core/approval.js';
7
7
  import { mutateApprovalStateWithRetry } from './core/capability/approval-state-mutation.js';
8
- import { approvedApprovalsFile, belayStateDir, configuredControlPlaneDir, mergeConfig, pendingApprovalsFile, } from './core/config.js';
8
+ import { approvedApprovalsFile, belayStateDir, configuredControlPlaneDir, mergeConfig, pendingApprovalsFile, stripForbiddenShellOverrideLists, } from './core/config.js';
9
9
  import { resolveLayeredConfig, teamConfigPath, } from './core/config-layers.js';
10
10
  export function resolveAdapterName(config) {
11
11
  if (config.adapter === 'claude') {
@@ -210,7 +210,7 @@ export async function loadConfigFile(repoRoot, adapter = detectAdapterName(repoR
210
210
  export async function writeConfigFile(repoRoot, config, adapter = resolveAdapterName(config)) {
211
211
  const configPath = configPathFor(repoRoot, adapter);
212
212
  await mkdir(path.dirname(configPath), { recursive: true });
213
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
213
+ await writeFile(configPath, `${JSON.stringify(stripForbiddenShellOverrideLists(config), null, 2)}\n`, 'utf8');
214
214
  }
215
215
  export async function mergeAndWriteConfig(repoRoot, adapter = 'cursor') {
216
216
  const layout = getAdapterLayout(adapter);