@guilz-dev/belay 0.6.0 → 0.8.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 (183) hide show
  1. package/README.md +39 -20
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/shared/gate-runtime.d.ts +7 -0
  4. package/dist/adapters/shared/gate-runtime.js +443 -225
  5. package/dist/bundle/claude-runtime.mjs +13027 -7271
  6. package/dist/bundle/codex-runtime.mjs +12700 -6977
  7. package/dist/bundle/cursor-runtime.mjs +12695 -6977
  8. package/dist/cli.js +37 -1
  9. package/dist/commands/approve.js +34 -10
  10. package/dist/commands/doctor.js +58 -0
  11. package/dist/commands/dogfood.js +1 -1
  12. package/dist/commands/explain.js +2 -2
  13. package/dist/commands/harvest.d.ts +0 -1
  14. package/dist/commands/harvest.js +1 -4
  15. package/dist/commands/metrics.js +6 -0
  16. package/dist/commands/quality.js +1 -3
  17. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  18. package/dist/commands/recovery-checkpoints.js +347 -0
  19. package/dist/commands/revoke.js +19 -7
  20. package/dist/commands/status.js +7 -0
  21. package/dist/config-io.js +49 -18
  22. package/dist/conformance/guarantee-table.js +137 -9
  23. package/dist/conformance/types.d.ts +2 -0
  24. package/dist/core/approval-replay.d.ts +1 -1
  25. package/dist/core/approval-replay.js +4 -1
  26. package/dist/core/approval-service.d.ts +6 -1
  27. package/dist/core/approval-service.js +86 -27
  28. package/dist/core/approval.d.ts +2 -0
  29. package/dist/core/approval.js +6 -3
  30. package/dist/core/audit-metrics.d.ts +1 -0
  31. package/dist/core/audit-metrics.js +12 -4
  32. package/dist/core/audit-types.d.ts +16 -0
  33. package/dist/core/capability/approval-state-mutation.d.ts +19 -0
  34. package/dist/core/capability/approval-state-mutation.js +76 -3
  35. package/dist/core/capability/approval-v3.d.ts +18 -1
  36. package/dist/core/capability/approval-v3.js +63 -23
  37. package/dist/core/capability/attestation.d.ts +3 -0
  38. package/dist/core/capability/attestation.js +7 -0
  39. package/dist/core/capability/boundary-driver-container.d.ts +12 -0
  40. package/dist/core/capability/boundary-driver-container.js +101 -34
  41. package/dist/core/capability/boundary-driver.js +1 -0
  42. package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
  43. package/dist/core/capability/boundary-grant-materialize.js +15 -0
  44. package/dist/core/capability/boundary-run.d.ts +25 -0
  45. package/dist/core/capability/boundary-run.js +37 -9
  46. package/dist/core/capability/boundary-session.d.ts +4 -0
  47. package/dist/core/capability/boundary-session.js +9 -0
  48. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  49. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  50. package/dist/core/capability/gate-policy-shadow.js +23 -1
  51. package/dist/core/capability/grant-consumption.d.ts +4 -0
  52. package/dist/core/capability/grant-consumption.js +11 -0
  53. package/dist/core/capability/grant-lease.d.ts +36 -1
  54. package/dist/core/capability/grant-lease.js +213 -16
  55. package/dist/core/capability/grant-match.js +3 -0
  56. package/dist/core/capability/index.d.ts +2 -2
  57. package/dist/core/capability/index.js +1 -1
  58. package/dist/core/capability/policy-engine.d.ts +38 -1
  59. package/dist/core/capability/policy-engine.js +388 -15
  60. package/dist/core/capability/request.d.ts +12 -0
  61. package/dist/core/capability/resolver.d.ts +0 -1
  62. package/dist/core/capability/resolver.js +0 -1
  63. package/dist/core/config.d.ts +20 -0
  64. package/dist/core/config.js +62 -2
  65. package/dist/core/effect-ir/audit.d.ts +9 -0
  66. package/dist/core/effect-ir/audit.js +87 -0
  67. package/dist/core/effect-ir/build.d.ts +31 -0
  68. package/dist/core/effect-ir/build.js +281 -0
  69. package/dist/core/effect-ir/index.d.ts +13 -0
  70. package/dist/core/effect-ir/index.js +9 -0
  71. package/dist/core/effect-ir/normalize.d.ts +4 -0
  72. package/dist/core/effect-ir/normalize.js +110 -0
  73. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  74. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  75. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  76. package/dist/core/effect-ir/package-exec.js +292 -0
  77. package/dist/core/effect-ir/policy.d.ts +14 -0
  78. package/dist/core/effect-ir/policy.js +164 -0
  79. package/dist/core/effect-ir/shell-build.d.ts +43 -0
  80. package/dist/core/effect-ir/shell-build.js +77 -0
  81. package/dist/core/effect-ir/shell-lower.d.ts +14 -0
  82. package/dist/core/effect-ir/shell-lower.js +1509 -0
  83. package/dist/core/effect-ir/types.d.ts +62 -0
  84. package/dist/core/effect-ir/types.js +1 -0
  85. package/dist/core/egress/allowlist.d.ts +1 -0
  86. package/dist/core/egress/allowlist.js +8 -0
  87. package/dist/core/egress-approval.js +45 -51
  88. package/dist/core/gate-contract.d.ts +2 -0
  89. package/dist/core/gate-contract.js +2 -0
  90. package/dist/core/gate-engine.js +82 -25
  91. package/dist/core/git-resource-identity.d.ts +26 -0
  92. package/dist/core/git-resource-identity.js +284 -0
  93. package/dist/core/harvest.d.ts +3 -7
  94. package/dist/core/harvest.js +4 -27
  95. package/dist/core/index.d.ts +5 -1
  96. package/dist/core/index.js +3 -1
  97. package/dist/core/judge-cloud-consent.js +29 -15
  98. package/dist/core/network-endpoint.d.ts +10 -0
  99. package/dist/core/network-endpoint.js +51 -0
  100. package/dist/core/path-utils.js +42 -23
  101. package/dist/core/process-runner.d.ts +10 -0
  102. package/dist/core/process-runner.js +124 -0
  103. package/dist/core/recover-advice.js +2 -1
  104. package/dist/core/recovery/artifact-store.d.ts +29 -0
  105. package/dist/core/recovery/artifact-store.js +336 -0
  106. package/dist/core/recovery/checkpoint.d.ts +25 -0
  107. package/dist/core/recovery/checkpoint.js +260 -0
  108. package/dist/core/recovery/index.d.ts +3 -1
  109. package/dist/core/recovery/index.js +2 -0
  110. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  111. package/dist/core/recovery/operator-guidance.js +39 -0
  112. package/dist/core/recovery/reconcile.d.ts +3 -0
  113. package/dist/core/recovery/reconcile.js +48 -0
  114. package/dist/core/recovery/resource-identity.d.ts +4 -0
  115. package/dist/core/recovery/resource-identity.js +34 -0
  116. package/dist/core/recovery/restore.d.ts +13 -0
  117. package/dist/core/recovery/restore.js +114 -0
  118. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  119. package/dist/core/recovery/snapshot-node.js +261 -0
  120. package/dist/core/recovery/types.d.ts +85 -1
  121. package/dist/core/shell-substitution.d.ts +1 -0
  122. package/dist/core/shell-substitution.js +61 -0
  123. package/dist/core/standing-allow.d.ts +1 -2
  124. package/dist/core/standing-allow.js +3 -20
  125. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  126. package/dist/core/transactional/apply-observed-changes.js +275 -0
  127. package/dist/core/transactional/backend-selector.d.ts +8 -0
  128. package/dist/core/transactional/backend-selector.js +118 -0
  129. package/dist/core/transactional/backend.d.ts +55 -0
  130. package/dist/core/transactional/backend.js +1 -0
  131. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  132. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  133. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  134. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  135. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  136. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  137. package/dist/core/transactional/file-clone.d.ts +15 -0
  138. package/dist/core/transactional/file-clone.js +139 -0
  139. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  140. package/dist/core/transactional/file-tree-path.js +50 -0
  141. package/dist/core/transactional/file-tree.d.ts +41 -0
  142. package/dist/core/transactional/file-tree.js +186 -0
  143. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  144. package/dist/core/transactional/git-worktree-backend.js +50 -0
  145. package/dist/core/transactional/git-worktree.d.ts +9 -11
  146. package/dist/core/transactional/git-worktree.js +24 -119
  147. package/dist/core/transactional/index.d.ts +11 -0
  148. package/dist/core/transactional/index.js +10 -0
  149. package/dist/core/transactional/reasons.js +1 -0
  150. package/dist/core/transactional/runner.js +157 -25
  151. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  152. package/dist/core/transactional/snapshot-node.js +87 -0
  153. package/dist/core/transactional/types.d.ts +17 -1
  154. package/dist/core/types.d.ts +13 -1
  155. package/dist/core/verdict/adapter.js +23 -15
  156. package/dist/core/verdict/containment.js +4 -0
  157. package/dist/core/verdict/egress-classify.d.ts +12 -0
  158. package/dist/core/verdict/egress-classify.js +746 -0
  159. package/dist/core/verdict/git-classifier.d.ts +12 -0
  160. package/dist/core/verdict/git-classifier.js +320 -0
  161. package/dist/core/verdict/launcher-resolve.js +79 -37
  162. package/dist/core/verdict/parser.d.ts +2 -0
  163. package/dist/core/verdict/parser.js +95 -1
  164. package/dist/core/verdict/types.d.ts +6 -2
  165. package/dist/core/verdict/verdict.js +103 -935
  166. package/dist/corpus/must-allow-commands.d.ts +2 -1
  167. package/dist/corpus/must-allow-commands.js +2 -1
  168. package/dist/corpus/runtime-match.d.ts +2 -1
  169. package/dist/corpus/runtime-match.js +2 -1
  170. package/dist/corpus/types.d.ts +2 -2
  171. package/dist/templates.js +7 -2
  172. package/dist/types.d.ts +11 -0
  173. package/dist/version.d.ts +1 -1
  174. package/dist/version.js +1 -1
  175. package/package.json +2 -2
  176. package/skills/belay/SKILL.md +8 -3
  177. package/skills/belay/belay-approve.md +10 -7
  178. package/dist/core/verdict/overrides.d.ts +0 -7
  179. package/dist/core/verdict/overrides.js +0 -37
  180. package/dist/core/verdict/shell-policy.d.ts +0 -25
  181. package/dist/core/verdict/shell-policy.js +0 -40
  182. package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
  183. package/dist/corpus/standing-allow-catalog.generated.js +0 -99
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { formatHarvestReport, harvestApplyProject, harvestListProject } from './
10
10
  import { formatMetricsReport, metricsProject } from './commands/metrics.js';
11
11
  import { formatQualityReport, qualityCheck } from './commands/quality.js';
12
12
  import { formatRecoverReport, recoverProject } from './commands/recover.js';
13
+ import { formatRecoveryCheckpointResult, recoveryCheckpointCommand, } from './commands/recovery-checkpoints.js';
13
14
  import { formatReport, reportProject } from './commands/report.js';
14
15
  import { revokeApproval } from './commands/revoke.js';
15
16
  import { formatSessionStatusReport, sessionStartProject, sessionStatusProject, } from './commands/session.js';
@@ -433,6 +434,23 @@ function parseArgs(argv) {
433
434
  }
434
435
  throw new Error('harvest requires subcommand: list or apply');
435
436
  }
437
+ if (command === 'recover' && !options.recoverSubcommand) {
438
+ if (token === 'advice' ||
439
+ token === 'status' ||
440
+ token === 'list' ||
441
+ token === 'show' ||
442
+ token === 'apply') {
443
+ options.recoverSubcommand = token;
444
+ continue;
445
+ }
446
+ throw new Error('recover requires subcommand: advice, status, list, show, or apply');
447
+ }
448
+ if (command === 'recover' &&
449
+ (options.recoverSubcommand === 'show' || options.recoverSubcommand === 'apply') &&
450
+ !options.recoverCheckpointId) {
451
+ options.recoverCheckpointId = token;
452
+ continue;
453
+ }
436
454
  if (command === 'judge' && !options.judgeSubcommand) {
437
455
  if (token === 'status' ||
438
456
  token === 'list' ||
@@ -527,8 +545,10 @@ Usage:
527
545
  ${c} metrics [--target <dir>] [--json]
528
546
  ${c} quality [--target <dir>] [--corpus <path>] [--json]
529
547
  ${c} report [--target <dir>] [--since <iso>] [--until <iso>] [--limit <n>] [--json]
530
- ${c} recover [--target <dir>] [--since <iso>] [--fingerprint <fp>] [--command "<text>"] [--limit <n>] [--json]
548
+ ${c} recover [advice] [--target <dir>] [--since <iso>] [--fingerprint <fp>] [--command "<text>"] [--limit <n>] [--json]
531
549
  (--limit picks the Nth recover candidate after priority ranking: local_mutation first, then recency; 1 = highest priority, default 1)
550
+ ${c} recover <status|list> [--target <dir>] [--json]
551
+ ${c} recover <show|apply> <checkpoint-id> [--target <dir>] [--json]
532
552
  ${c} audit <query|summarize|replay> [--target <dir>] [--json] [--since <iso>] [--until <iso>] [--verdict <v>] [--reason <r>] [--kind <k>] [--fingerprint <fp>] [--event <e>] [--location <v>] [--opacity <v>] [--effect <v>] [--confidence <v>] [--limit <n>] [--config <path>]
533
553
  ${c} simulate --config <path> [--target <dir>] [--json]
534
554
  ${c} status [--target <dir>] [--json]
@@ -825,6 +845,22 @@ async function main() {
825
845
  throw new Error('harvest requires subcommand: list or apply');
826
846
  }
827
847
  if (command === 'recover') {
848
+ if (options.recoverSubcommand && options.recoverSubcommand !== 'advice') {
849
+ const result = await recoveryCheckpointCommand({
850
+ targetDir: options.targetDir,
851
+ subcommand: options.recoverSubcommand,
852
+ checkpointId: options.recoverCheckpointId,
853
+ });
854
+ if (options.json) {
855
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
856
+ }
857
+ else {
858
+ process.stdout.write(formatRecoveryCheckpointResult(result));
859
+ }
860
+ if (!result.ok)
861
+ process.exitCode = 2;
862
+ return;
863
+ }
828
864
  const report = await recoverProject({
829
865
  targetDir: options.targetDir,
830
866
  since: options.since,
@@ -1,10 +1,12 @@
1
1
  import path from 'node:path';
2
2
  import { loadApprovalState, loadConfigFile } from '../config-io.js';
3
- import { canAutoReplay, getExecutionLeaseMs, replayShellCommand } from '../core/approval-replay.js';
4
- import { consumeApprovedAfterCliReplay, createGateApprovalStore, recordApproval, } from '../core/approval-service.js';
3
+ import { canAutoReplay, getExecutionLeaseMs } from '../core/approval-replay.js';
4
+ import { claimApprovedForReplay, createGateApprovalStore, recordApproval, } from '../core/approval-service.js';
5
+ import { runBoundaryAgentCommand } from '../core/capability/boundary-session.js';
5
6
  import { JUDGE_CLOUD_CONSENT_REASON } from '../core/capability/reasons.js';
6
7
  import { recordCapabilityApproval } from '../core/capability-approval.js';
7
8
  import { recordEgressApproval } from '../core/egress-approval.js';
9
+ import { RECOVERY_RESTORE_REASON } from '../core/recovery/checkpoint.js';
8
10
  import { createEgressApprovalStore } from '../services/egress-service.js';
9
11
  import { createCapabilityApprovalStore } from '../services/sandbox-service.js';
10
12
  export async function approvePending(options) {
@@ -12,6 +14,19 @@ export async function approvePending(options) {
12
14
  const config = await loadConfigFile(repoRoot);
13
15
  const pending = await loadApprovalState(repoRoot, 'pending-approvals.json', config);
14
16
  const match = pending.approvals.find((approval) => approval.approvalId === options.approvalId);
17
+ if (match?.reason === RECOVERY_RESTORE_REASON) {
18
+ if (options.scope && options.scope !== 'once') {
19
+ return { ok: false, message: 'Recovery restore approvals support one-shot scope only.' };
20
+ }
21
+ const result = await recordApproval({
22
+ approvalId: options.approvalId,
23
+ config,
24
+ token: options.token,
25
+ requireSignedToken: true,
26
+ store: createGateApprovalStore(repoRoot, config),
27
+ });
28
+ return { ok: result.ok, message: result.message };
29
+ }
15
30
  if (match?.reason === JUDGE_CLOUD_CONSENT_REASON) {
16
31
  const result = await recordCapabilityApproval({
17
32
  approvalId: options.approvalId,
@@ -73,22 +88,31 @@ export async function approvePending(options) {
73
88
  message: 'CLI replay is only supported for shell approvals. Retry the original tool or subagent action manually.',
74
89
  };
75
90
  }
76
- const replayResult = await replayShellCommand(approval.input, approval.cwd ?? repoRoot, getExecutionLeaseMs(config));
77
- const output = [replayResult.stdout, replayResult.stderr].filter(Boolean).join('\n').trim();
91
+ const claimed = await claimApprovedForReplay({
92
+ approvalId: options.approvalId,
93
+ store: approvalStore,
94
+ });
95
+ if (!claimed) {
96
+ return { ok: false, message: 'Approval could not be claimed for replay.' };
97
+ }
98
+ const replayResult = await runBoundaryAgentCommand({
99
+ repoRoot,
100
+ config,
101
+ command: claimed.input ?? approval.input,
102
+ cwd: claimed.cwd ?? repoRoot,
103
+ timeoutMs: getExecutionLeaseMs(config),
104
+ runOptions: { mountReadOnly: false },
105
+ });
78
106
  if (replayResult.exitCode === 0) {
79
- await consumeApprovedAfterCliReplay({
80
- approvalId: options.approvalId,
81
- store: approvalStore,
82
- });
83
107
  return {
84
108
  ok: true,
85
- message: `Belay replay succeeded for ${options.approvalId}. Do not retry via hooks; the one-shot grant was consumed.${output ? `\n${output}` : ''}`,
109
+ message: `Belay replay succeeded for ${options.approvalId}. Do not retry via hooks; the one-shot grant was consumed.`,
86
110
  };
87
111
  }
88
112
  const timeoutNote = replayResult.timedOut ? ' Replay timed out.' : '';
89
113
  return {
90
114
  ok: false,
91
- message: `Belay replay failed for ${options.approvalId} (exit ${replayResult.exitCode}).${timeoutNote} Approval remains active for one hook retry.${output ? `\n${output}` : ''}`,
115
+ message: `Belay replay failed for ${options.approvalId} (exit ${replayResult.exitCode}).${timeoutNote} The one-shot approval was consumed before replay and was not re-armed.`,
92
116
  };
93
117
  }
94
118
  return { ok: result.ok, message: result.message };
@@ -7,6 +7,7 @@ import { getAdapterLayout } from '../adapters/layouts/index.js';
7
7
  import { resolveScopedPaths } from '../adapters/layouts/scope.js';
8
8
  import { cleanupOrphanApprovalState } from '../cleanup-orphans.js';
9
9
  import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredConfig, pendingApprovalsPath, repoLocalStateDirFor, } from '../config-io.js';
10
+ import { approvalSigningKeyPath } from '../core/approval-token.js';
10
11
  import { detectFenceDrift, summarizeAuditVisibility } from '../core/audit-summary.js';
11
12
  import { inspectBoundaryAttestationFile } from '../core/capability/boundary-attestation-sign.js';
12
13
  import { boundaryAttestationPath, boundarySessionStatus, } from '../core/capability/boundary-session.js';
@@ -14,6 +15,9 @@ import { defaultControlPlaneDir } from '../core/config.js';
14
15
  import { verifyIntegrityManifest } from '../core/integrity.js';
15
16
  import { diagnoseJudge, stopJudgeSessionBrokers } from '../core/judge-doctor.js';
16
17
  import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
18
+ import { listRecoveryCheckpoints } from '../core/recovery/checkpoint.js';
19
+ import { recoveryApprovalSetupNotes, recoveryNotificationConfigured, recoveryNotificationSetupWarning, summarizeRecoveryCheckpointDiagnostics, } from '../core/recovery/operator-guidance.js';
20
+ import { probeFileCloneStrategy } from '../core/transactional/file-clone.js';
17
21
  import { getManagedHookEntries } from '../defaults.js';
18
22
  import { resolveNodeBinary } from '../node-resolution.js';
19
23
  import { egressStatus } from '../services/egress-service.js';
@@ -78,6 +82,13 @@ export async function doctorProject(options = {}) {
78
82
  for (const entry of layered.provenance) {
79
83
  notes.push(`Config layer [${entry.source}]: ${entry.path}`);
80
84
  }
85
+ const ignoredShellOverrideLists = [
86
+ ...(loadedConfig.overrides.allow.length > 0 ? ['overrides.allow'] : []),
87
+ ...(loadedConfig.overrides.external.length > 0 ? ['overrides.external'] : []),
88
+ ];
89
+ 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.`);
91
+ }
81
92
  if (loadedConfig.version !== 4) {
82
93
  warnings.push(`Config version is ${loadedConfig.version}; expected 4. Run belay upgrade to migrate.`);
83
94
  }
@@ -290,6 +301,45 @@ export async function doctorProject(options = {}) {
290
301
  warnings.push('Transactional execution is enabled but this directory is not a git repository. Transactional worktrees will be skipped until git is available.');
291
302
  }
292
303
  }
304
+ const fileCheckpoint = loadedConfig.policy.transactional.fileCheckpoint;
305
+ if (fileCheckpoint.enabled) {
306
+ const copyStrategy = await probeFileCloneStrategy();
307
+ notes.push(`File checkpoint: enabled (copyStrategy=${copyStrategy}, non-Git=${fileCheckpoint.allowNonGit}, maxFiles=${fileCheckpoint.maxFiles}, maxSourceBytes=${fileCheckpoint.maxSourceBytes}, maxWorkspaceBytes=${fileCheckpoint.maxWorkspaceBytes}, prepareTimeoutMs=${fileCheckpoint.prepareTimeoutMs}, copyConcurrency=${fileCheckpoint.copyConcurrency}).`);
308
+ if (!loadedConfig.policy.transactional.enabled) {
309
+ warnings.push('File checkpoint is enabled but transactional execution is disabled; dirty Git workspaces cannot use the backend.');
310
+ }
311
+ if (!loadedConfig.policy.transactional.checkpoint?.enabled) {
312
+ warnings.push('File checkpoint is enabled but durable Recovery checkpointing is disabled; backend selection will fail closed.');
313
+ }
314
+ }
315
+ if (loadedConfig.policy.transactional.checkpoint?.enabled) {
316
+ notes.push('Recovery checkpoint: enabled — repo-local pre-images are persisted before observed-safe transactional apply.');
317
+ notes.push(...recoveryApprovalSetupNotes());
318
+ if (!recoveryNotificationConfigured(loadedConfig)) {
319
+ warnings.push(recoveryNotificationSetupWarning());
320
+ }
321
+ const signingKeyPath = approvalSigningKeyPath(loadedConfig.controlPlane.enabled
322
+ ? belayStateDir(loadedConfig, repoLocalDir)
323
+ : (loadedConfig.controlPlane.configDir ?? defaultControlPlaneDir()));
324
+ if (!existsSync(signingKeyPath)) {
325
+ notes.push(`Approval signing key not yet created at ${signingKeyPath}; it will be generated on the first signed recovery approval request.`);
326
+ }
327
+ if (!loadedConfig.policy.transactional.enabled) {
328
+ warnings.push('Recovery checkpoint is enabled but transactional execution is disabled; checkpoints will not be created until transactional execution is enabled.');
329
+ }
330
+ try {
331
+ const stateDir = belayStateDir(loadedConfig, repoLocalDir);
332
+ const checkpoints = await listRecoveryCheckpoints(stateDir, repoRoot);
333
+ if (checkpoints.length > 0) {
334
+ notes.push(`Recovery checkpoints in this repository: ${checkpoints.length}.`);
335
+ }
336
+ const advisories = summarizeRecoveryCheckpointDiagnostics(checkpoints);
337
+ warnings.push(...advisories);
338
+ }
339
+ catch {
340
+ // Checkpoint inspection is advisory only.
341
+ }
342
+ }
293
343
  if (loadedConfig.sandbox.enabled) {
294
344
  const sandbox = await sandboxStatus({ targetDir: repoRoot });
295
345
  notes.push(`Sandbox capability broker: enabled (runtime=${loadedConfig.sandbox.runtime}, fs-scope entries=${sandbox.fsScopeAllowlistCount}, full-isolation=${sandbox.l1FullActive}).`);
@@ -305,6 +355,10 @@ export async function doctorProject(options = {}) {
305
355
  }
306
356
  const boundaryDriver = loadedConfig.capability?.boundaryDriver ??
307
357
  (loadedConfig.sandbox.runtime === 'container' ? 'container' : null);
358
+ if (loadedConfig.policy.transactional.fileCheckpoint.enabled &&
359
+ boundaryDriver !== 'container') {
360
+ warnings.push('File checkpoint requires an attested workspace-isolating boundary driver; configure the container boundary and run belay session start.');
361
+ }
308
362
  if (loadedConfig.capability || boundaryDriver === 'container') {
309
363
  const attestationPath = boundaryAttestationPath(repoRoot, loadedConfig);
310
364
  const attestationFormat = await inspectBoundaryAttestationFile(attestationPath);
@@ -323,6 +377,10 @@ export async function doctorProject(options = {}) {
323
377
  else if (!session.fresh) {
324
378
  warnings.push('Boundary attestation is stale or expired. Run belay session start to refresh.');
325
379
  }
380
+ else if (loadedConfig.policy.transactional.fileCheckpoint.enabled &&
381
+ session.attestation.isolatesWorkspaceMounts !== true) {
382
+ warnings.push('File checkpoint is enabled but the fresh boundary attestation does not prove workspace-mount isolation. Run belay session start with a compatible boundary.');
383
+ }
326
384
  else if (session.attestation.probeSignals.includes('repo-mount-ro-default')) {
327
385
  notes.push('Container boundary defaults to read-only directory mounts; predicted writes use read-write mounts for the working directory only.');
328
386
  }
@@ -45,7 +45,7 @@ async function promoteDogfoodToEnforce(repoRoot, configPath, force, adapter = 'c
45
45
  message: [
46
46
  'Dogfood metrics do not recommend enforce yet.',
47
47
  ...metrics.dogfood.notes,
48
- 'Re-run belay metrics, tune overrides.allow, or pass --force to override.',
48
+ 'Re-run belay metrics, correct EffectPlan semantics or resource scope, or pass --force to override.',
49
49
  ].join(' '),
50
50
  };
51
51
  }
@@ -106,8 +106,8 @@ export function formatExplainReport(report) {
106
106
  report.policy.transactional.enabled
107
107
  ? `Transactional band: [${report.policy.transactional.minConfidence}, ${report.policy.transactional.maxConfidence})`
108
108
  : 'Transactional band: not configured',
109
- `Overrides allow: ${report.overrides.allow.join(', ') || '(none)'}`,
110
- `Overrides external: ${report.overrides.external.join(', ') || '(none)'}`,
109
+ `Legacy overrides allow (deprecated, ignored): ${report.overrides.allow.join(', ') || '(none)'}`,
110
+ `Legacy overrides external (deprecated, ignored): ${report.overrides.external.join(', ') || '(none)'}`,
111
111
  '',
112
112
  `Verdict: ${result.verdict}`,
113
113
  `Reason: ${result.reason}`,
@@ -17,7 +17,6 @@ export declare function harvestListProject(options?: HarvestListOptions): Promis
17
17
  export declare function harvestReportFromRecords(records: AuditRecord[], options?: {
18
18
  since?: string;
19
19
  until?: string;
20
- allowPatterns?: string[];
21
20
  }): HarvestReport;
22
21
  export declare function formatHarvestReport(report: HarvestReport): string;
23
22
  export declare function harvestApplyProject(options: HarvestApplyOptions): Promise<{
@@ -1,6 +1,5 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { loadConfigFile } from '../config-io.js';
4
3
  import { parseAuditNdjson } from '../core/audit-metrics.js';
5
4
  import { toAuditRecord } from '../core/audit-query.js';
6
5
  import { applyHarvestReview, buildHarvestReport, filterRecordsForHarvest, } from '../core/harvest.js';
@@ -8,16 +7,14 @@ import { parseCorpusCases } from '../corpus/types.js';
8
7
  import { loadAuditRecords } from './audit.js';
9
8
  export async function harvestListProject(options = {}) {
10
9
  const repoRoot = path.resolve(options.targetDir ?? process.cwd());
11
- const config = await loadConfigFile(repoRoot);
12
10
  const records = await loadAuditRecords(repoRoot);
13
11
  return harvestReportFromRecords(records, {
14
12
  since: options.since,
15
13
  until: options.until,
16
- allowPatterns: config.overrides.allow,
17
14
  });
18
15
  }
19
16
  export function harvestReportFromRecords(records, options = {}) {
20
- return buildHarvestReport(filterRecordsForHarvest(records, { since: options.since, until: options.until }), { allowPatterns: options.allowPatterns });
17
+ return buildHarvestReport(filterRecordsForHarvest(records, { since: options.since, until: options.until }));
21
18
  }
22
19
  export function formatHarvestReport(report) {
23
20
  const lines = [
@@ -38,6 +38,12 @@ export function formatMetricsReport(report) {
38
38
  if (report.approvalLatency.count > 0) {
39
39
  lines.push(`Approval latency: median ${report.approvalLatency.medianMs ?? 0}ms, p95 ${report.approvalLatency.p95Ms ?? 0}ms (${report.approvalLatency.count} samples)`);
40
40
  }
41
+ if (Object.keys(report.gateEventsByRuntime).length > 0) {
42
+ lines.push('', 'Gate events by runtime:');
43
+ for (const [runtime, count] of Object.entries(report.gateEventsByRuntime).sort((left, right) => right[1] - left[1])) {
44
+ lines.push(`- ${runtime}: ${count}`);
45
+ }
46
+ }
41
47
  if (report.bypassAttemptCount > 0) {
42
48
  lines.push(`Bypass attempts detected: ${report.bypassAttemptCount}`);
43
49
  }
@@ -18,9 +18,7 @@ export async function qualityCheck(options = {}) {
18
18
  mode: config.mode,
19
19
  unknownLocalEffect: config.policy.unknownLocalEffect,
20
20
  });
21
- const harvest = harvestReportFromRecords(auditRecords, {
22
- allowPatterns: config.overrides.allow,
23
- });
21
+ const harvest = harvestReportFromRecords(auditRecords);
24
22
  const notes = [
25
23
  'Overall ok reflects corpus hard gates only; audit and harvest signals are advisory.',
26
24
  'Recursive quality loop: corpus hard gates are the FN/FP safety boundary.',
@@ -0,0 +1,164 @@
1
+ export type RecoveryCheckpointSubcommand = 'status' | 'list' | 'show' | 'apply';
2
+ export declare function recoveryCheckpointCommand(options: {
3
+ targetDir?: string;
4
+ subcommand: RecoveryCheckpointSubcommand;
5
+ checkpointId?: string;
6
+ }): Promise<{
7
+ ok: boolean;
8
+ subcommand: string;
9
+ repoRoot: string;
10
+ backend: string;
11
+ availableBackends: string[];
12
+ backendCounts: {
13
+ [k: string]: number;
14
+ };
15
+ fileCheckpoint: {
16
+ enabled: boolean;
17
+ allowNonGit: boolean;
18
+ isolation: string | null;
19
+ copyStrategy: import("../core/transactional/file-clone.js").FileCloneStrategy | undefined;
20
+ probe: string;
21
+ };
22
+ enabled: boolean;
23
+ checkpointCount: number;
24
+ storageBytes: number;
25
+ recoverableCount: number;
26
+ needsManualRepairCount: number;
27
+ verifiedReceiptCount: number;
28
+ states: {
29
+ [k: string]: number;
30
+ };
31
+ advisories: string[];
32
+ limits: {
33
+ enabled: boolean;
34
+ appliedRetentionHours: number;
35
+ restoredRetentionHours: number;
36
+ maxCheckpoints: number;
37
+ maxBytes: number;
38
+ };
39
+ checkpoints?: undefined;
40
+ manifest?: undefined;
41
+ state?: undefined;
42
+ manifestHash?: undefined;
43
+ receipt?: undefined;
44
+ verdict?: undefined;
45
+ checkpointId?: undefined;
46
+ approvalId?: undefined;
47
+ paths?: undefined;
48
+ auditRecorded?: undefined;
49
+ message?: undefined;
50
+ } | {
51
+ ok: boolean;
52
+ subcommand: string;
53
+ repoRoot: string;
54
+ checkpoints: import("../core/index.js").RecoveryCheckpointSummary[];
55
+ backend?: undefined;
56
+ availableBackends?: undefined;
57
+ backendCounts?: undefined;
58
+ fileCheckpoint?: undefined;
59
+ enabled?: undefined;
60
+ checkpointCount?: undefined;
61
+ storageBytes?: undefined;
62
+ recoverableCount?: undefined;
63
+ needsManualRepairCount?: undefined;
64
+ verifiedReceiptCount?: undefined;
65
+ states?: undefined;
66
+ advisories?: undefined;
67
+ limits?: undefined;
68
+ manifest?: undefined;
69
+ state?: undefined;
70
+ manifestHash?: undefined;
71
+ receipt?: undefined;
72
+ verdict?: undefined;
73
+ checkpointId?: undefined;
74
+ approvalId?: undefined;
75
+ paths?: undefined;
76
+ auditRecorded?: undefined;
77
+ message?: undefined;
78
+ } | {
79
+ ok: boolean;
80
+ subcommand: string;
81
+ manifest: import("../core/index.js").RecoveryCheckpointManifest;
82
+ state: import("../core/recovery/types.js").RecoveryCheckpointStateV1;
83
+ manifestHash: string;
84
+ receipt: import("../core/index.js").RecoveryReceiptV1 | undefined;
85
+ repoRoot?: undefined;
86
+ backend?: undefined;
87
+ availableBackends?: undefined;
88
+ backendCounts?: undefined;
89
+ fileCheckpoint?: undefined;
90
+ enabled?: undefined;
91
+ checkpointCount?: undefined;
92
+ storageBytes?: undefined;
93
+ recoverableCount?: undefined;
94
+ needsManualRepairCount?: undefined;
95
+ verifiedReceiptCount?: undefined;
96
+ states?: undefined;
97
+ advisories?: undefined;
98
+ limits?: undefined;
99
+ checkpoints?: undefined;
100
+ verdict?: undefined;
101
+ checkpointId?: undefined;
102
+ approvalId?: undefined;
103
+ paths?: undefined;
104
+ auditRecorded?: undefined;
105
+ message?: undefined;
106
+ } | {
107
+ ok: boolean;
108
+ subcommand: string;
109
+ verdict: string;
110
+ checkpointId: string;
111
+ approvalId: string;
112
+ manifestHash: string;
113
+ paths: string[];
114
+ auditRecorded: boolean;
115
+ message: string;
116
+ repoRoot?: undefined;
117
+ backend?: undefined;
118
+ availableBackends?: undefined;
119
+ backendCounts?: undefined;
120
+ fileCheckpoint?: undefined;
121
+ enabled?: undefined;
122
+ checkpointCount?: undefined;
123
+ storageBytes?: undefined;
124
+ recoverableCount?: undefined;
125
+ needsManualRepairCount?: undefined;
126
+ verifiedReceiptCount?: undefined;
127
+ states?: undefined;
128
+ advisories?: undefined;
129
+ limits?: undefined;
130
+ checkpoints?: undefined;
131
+ manifest?: undefined;
132
+ state?: undefined;
133
+ receipt?: undefined;
134
+ } | {
135
+ auditRecorded: boolean;
136
+ message: string;
137
+ manifestHash: string;
138
+ changeCount: number;
139
+ ok: boolean;
140
+ subcommand: string;
141
+ verdict: string;
142
+ checkpointId: string;
143
+ repoRoot?: undefined;
144
+ backend?: undefined;
145
+ availableBackends?: undefined;
146
+ backendCounts?: undefined;
147
+ fileCheckpoint?: undefined;
148
+ enabled?: undefined;
149
+ checkpointCount?: undefined;
150
+ storageBytes?: undefined;
151
+ recoverableCount?: undefined;
152
+ needsManualRepairCount?: undefined;
153
+ verifiedReceiptCount?: undefined;
154
+ states?: undefined;
155
+ advisories?: undefined;
156
+ limits?: undefined;
157
+ checkpoints?: undefined;
158
+ manifest?: undefined;
159
+ state?: undefined;
160
+ receipt?: undefined;
161
+ approvalId?: undefined;
162
+ paths?: undefined;
163
+ }>;
164
+ export declare function formatRecoveryCheckpointResult(result: Record<string, unknown>): string;