open-multi-agent-kit 0.97.0 → 0.98.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 (261) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +41 -9
  3. package/dist/cli/args.d.ts +2 -3
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +13 -189
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/help.d.ts +3 -0
  8. package/dist/cli/help.d.ts.map +1 -0
  9. package/dist/cli/help.js +194 -0
  10. package/dist/cli/help.js.map +1 -0
  11. package/dist/cli/initial-message.d.ts.map +1 -1
  12. package/dist/cli/initial-message.js +2 -1
  13. package/dist/cli/initial-message.js.map +1 -1
  14. package/dist/cli/resource-paths.d.ts +4 -0
  15. package/dist/cli/resource-paths.d.ts.map +1 -0
  16. package/dist/cli/resource-paths.js +59 -0
  17. package/dist/cli/resource-paths.js.map +1 -0
  18. package/dist/commands/adaptorch-doctor-cli.d.ts +13 -0
  19. package/dist/commands/adaptorch-doctor-cli.d.ts.map +1 -0
  20. package/dist/commands/adaptorch-doctor-cli.js +115 -0
  21. package/dist/commands/adaptorch-doctor-cli.js.map +1 -0
  22. package/dist/commands/resource-doctor-cli.d.ts +4 -0
  23. package/dist/commands/resource-doctor-cli.d.ts.map +1 -1
  24. package/dist/commands/resource-doctor-cli.js +35 -5
  25. package/dist/commands/resource-doctor-cli.js.map +1 -1
  26. package/dist/core/adaptorch-bridge.d.ts +11 -13
  27. package/dist/core/adaptorch-bridge.d.ts.map +1 -1
  28. package/dist/core/adaptorch-bridge.js +11 -13
  29. package/dist/core/adaptorch-bridge.js.map +1 -1
  30. package/dist/core/agent-session-snapshot.d.ts +16 -0
  31. package/dist/core/agent-session-snapshot.d.ts.map +1 -0
  32. package/dist/core/agent-session-snapshot.js +111 -0
  33. package/dist/core/agent-session-snapshot.js.map +1 -0
  34. package/dist/core/agent-session.d.ts +5 -9
  35. package/dist/core/agent-session.d.ts.map +1 -1
  36. package/dist/core/agent-session.js +27 -219
  37. package/dist/core/agent-session.js.map +1 -1
  38. package/dist/core/agent-tool-settings.d.ts +10 -0
  39. package/dist/core/agent-tool-settings.d.ts.map +1 -1
  40. package/dist/core/agent-tool-settings.js +10 -1
  41. package/dist/core/agent-tool-settings.js.map +1 -1
  42. package/dist/core/compaction/compaction.d.ts +7 -0
  43. package/dist/core/compaction/compaction.d.ts.map +1 -1
  44. package/dist/core/compaction/compaction.js +25 -61
  45. package/dist/core/compaction/compaction.js.map +1 -1
  46. package/dist/core/compaction/index.d.ts +1 -0
  47. package/dist/core/compaction/index.d.ts.map +1 -1
  48. package/dist/core/compaction/index.js +1 -0
  49. package/dist/core/compaction/index.js.map +1 -1
  50. package/dist/core/compaction/knowledge-triage-format.d.ts +9 -0
  51. package/dist/core/compaction/knowledge-triage-format.d.ts.map +1 -0
  52. package/dist/core/compaction/knowledge-triage-format.js +71 -0
  53. package/dist/core/compaction/knowledge-triage-format.js.map +1 -0
  54. package/dist/core/compaction/knowledge-triage-types.d.ts +7 -0
  55. package/dist/core/compaction/knowledge-triage-types.d.ts.map +1 -0
  56. package/dist/core/compaction/knowledge-triage-types.js +2 -0
  57. package/dist/core/compaction/knowledge-triage-types.js.map +1 -0
  58. package/dist/core/compaction/knowledge-triage.d.ts +29 -0
  59. package/dist/core/compaction/knowledge-triage.d.ts.map +1 -0
  60. package/dist/core/compaction/knowledge-triage.js +189 -0
  61. package/dist/core/compaction/knowledge-triage.js.map +1 -0
  62. package/dist/core/compaction/transaction.d.ts.map +1 -1
  63. package/dist/core/compaction/transaction.js +1 -1
  64. package/dist/core/compaction/transaction.js.map +1 -1
  65. package/dist/core/compaction/utils.d.ts +4 -0
  66. package/dist/core/compaction/utils.d.ts.map +1 -1
  67. package/dist/core/compaction/utils.js +37 -0
  68. package/dist/core/compaction/utils.js.map +1 -1
  69. package/dist/core/completion-sound-io.d.ts +14 -0
  70. package/dist/core/completion-sound-io.d.ts.map +1 -0
  71. package/dist/core/completion-sound-io.js +64 -0
  72. package/dist/core/completion-sound-io.js.map +1 -0
  73. package/dist/core/completion-sound.d.ts +4 -10
  74. package/dist/core/completion-sound.d.ts.map +1 -1
  75. package/dist/core/completion-sound.js +28 -58
  76. package/dist/core/completion-sound.js.map +1 -1
  77. package/dist/core/context-budget-relevance.d.ts +15 -2
  78. package/dist/core/context-budget-relevance.d.ts.map +1 -1
  79. package/dist/core/context-budget-relevance.js +21 -9
  80. package/dist/core/context-budget-relevance.js.map +1 -1
  81. package/dist/core/context-budget-system-prompt-items.d.ts +1 -1
  82. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  83. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  84. package/dist/core/context-budget-system-prompt.d.ts +1 -1
  85. package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
  86. package/dist/core/context-budget-system-prompt.js.map +1 -1
  87. package/dist/core/context-file.d.ts +23 -0
  88. package/dist/core/context-file.d.ts.map +1 -0
  89. package/dist/core/context-file.js +17 -0
  90. package/dist/core/context-file.js.map +1 -0
  91. package/dist/core/keybindings.d.ts +1 -1
  92. package/dist/core/keybindings.d.ts.map +1 -1
  93. package/dist/core/keybindings.js +6 -1
  94. package/dist/core/keybindings.js.map +1 -1
  95. package/dist/core/model-prompt-policy.d.ts +1 -1
  96. package/dist/core/model-prompt-policy.d.ts.map +1 -1
  97. package/dist/core/model-prompt-policy.js.map +1 -1
  98. package/dist/core/prompt-settlement.d.ts +6 -3
  99. package/dist/core/prompt-settlement.d.ts.map +1 -1
  100. package/dist/core/prompt-settlement.js +10 -14
  101. package/dist/core/prompt-settlement.js.map +1 -1
  102. package/dist/core/reasoning-router-promotion.d.ts +41 -1
  103. package/dist/core/reasoning-router-promotion.d.ts.map +1 -1
  104. package/dist/core/reasoning-router-promotion.js +31 -2
  105. package/dist/core/reasoning-router-promotion.js.map +1 -1
  106. package/dist/core/reasoning-router-replay-stability.d.ts +65 -0
  107. package/dist/core/reasoning-router-replay-stability.d.ts.map +1 -0
  108. package/dist/core/reasoning-router-replay-stability.js +67 -0
  109. package/dist/core/reasoning-router-replay-stability.js.map +1 -0
  110. package/dist/core/reasoning-router-v4-weights.d.ts +6 -6
  111. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
  112. package/dist/core/reasoning-router-v4-weights.js +3 -3
  113. package/dist/core/reasoning-router-v4-weights.js.map +1 -1
  114. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  115. package/dist/core/reasoning-router-v4.js +6 -6
  116. package/dist/core/reasoning-router-v4.js.map +1 -1
  117. package/dist/core/redaction.d.ts.map +1 -1
  118. package/dist/core/redaction.js +1 -1
  119. package/dist/core/redaction.js.map +1 -1
  120. package/dist/core/resource-admission.d.ts +2 -1
  121. package/dist/core/resource-admission.d.ts.map +1 -1
  122. package/dist/core/resource-admission.js +11 -0
  123. package/dist/core/resource-admission.js.map +1 -1
  124. package/dist/core/resource-governor-format.d.ts +1 -1
  125. package/dist/core/resource-governor-format.d.ts.map +1 -1
  126. package/dist/core/resource-governor-format.js.map +1 -1
  127. package/dist/core/resource-governor-settings.d.ts +6 -12
  128. package/dist/core/resource-governor-settings.d.ts.map +1 -1
  129. package/dist/core/resource-governor-settings.js.map +1 -1
  130. package/dist/core/resource-governor-types.d.ts +24 -0
  131. package/dist/core/resource-governor-types.d.ts.map +1 -0
  132. package/dist/core/resource-governor-types.js +15 -0
  133. package/dist/core/resource-governor-types.js.map +1 -0
  134. package/dist/core/resource-loader.d.ts +1 -7
  135. package/dist/core/resource-loader.d.ts.map +1 -1
  136. package/dist/core/resource-loader.js.map +1 -1
  137. package/dist/core/resource-observation-journal.d.ts +2 -1
  138. package/dist/core/resource-observation-journal.d.ts.map +1 -1
  139. package/dist/core/resource-observation-journal.js +4 -3
  140. package/dist/core/resource-observation-journal.js.map +1 -1
  141. package/dist/core/resource-observation-report-reader.d.ts +7 -0
  142. package/dist/core/resource-observation-report-reader.d.ts.map +1 -0
  143. package/dist/core/resource-observation-report-reader.js +157 -0
  144. package/dist/core/resource-observation-report-reader.js.map +1 -0
  145. package/dist/core/resource-observation-report.d.ts +24 -0
  146. package/dist/core/resource-observation-report.d.ts.map +1 -0
  147. package/dist/core/resource-observation-report.js +101 -0
  148. package/dist/core/resource-observation-report.js.map +1 -0
  149. package/dist/core/router-feedback-collector.d.ts +1 -1
  150. package/dist/core/router-feedback-collector.d.ts.map +1 -1
  151. package/dist/core/router-feedback-collector.js.map +1 -1
  152. package/dist/core/session-failure-cause.d.ts +33 -0
  153. package/dist/core/session-failure-cause.d.ts.map +1 -0
  154. package/dist/core/session-failure-cause.js +141 -0
  155. package/dist/core/session-failure-cause.js.map +1 -0
  156. package/dist/core/session-system-prompt.d.ts +1 -1
  157. package/dist/core/session-system-prompt.d.ts.map +1 -1
  158. package/dist/core/session-system-prompt.js.map +1 -1
  159. package/dist/core/settings-manager.d.ts +1 -1
  160. package/dist/core/settings-manager.d.ts.map +1 -1
  161. package/dist/core/settings-manager.js.map +1 -1
  162. package/dist/core/system-prompt.d.ts +1 -1
  163. package/dist/core/system-prompt.d.ts.map +1 -1
  164. package/dist/core/system-prompt.js.map +1 -1
  165. package/dist/core/tools/bash.d.ts.map +1 -1
  166. package/dist/core/tools/bash.js +7 -7
  167. package/dist/core/tools/bash.js.map +1 -1
  168. package/dist/core/workload-classifier.d.ts +2 -10
  169. package/dist/core/workload-classifier.d.ts.map +1 -1
  170. package/dist/core/workload-classifier.js +17 -0
  171. package/dist/core/workload-classifier.js.map +1 -1
  172. package/dist/core/workload-family-matcher.d.ts +1 -1
  173. package/dist/core/workload-family-matcher.d.ts.map +1 -1
  174. package/dist/core/workload-family-matcher.js.map +1 -1
  175. package/dist/core/workload-shard-execution-types.d.ts +40 -0
  176. package/dist/core/workload-shard-execution-types.d.ts.map +1 -0
  177. package/dist/core/workload-shard-execution-types.js +14 -0
  178. package/dist/core/workload-shard-execution-types.js.map +1 -0
  179. package/dist/core/workload-shard-executor.d.ts +3 -27
  180. package/dist/core/workload-shard-executor.d.ts.map +1 -1
  181. package/dist/core/workload-shard-executor.js.map +1 -1
  182. package/dist/core/workload-shard-runner.d.ts +1 -1
  183. package/dist/core/workload-shard-runner.d.ts.map +1 -1
  184. package/dist/core/workload-shard-runner.js.map +1 -1
  185. package/dist/core/workload-types.d.ts +24 -0
  186. package/dist/core/workload-types.d.ts.map +1 -0
  187. package/dist/core/workload-types.js +15 -0
  188. package/dist/core/workload-types.js.map +1 -0
  189. package/dist/main.d.ts +2 -0
  190. package/dist/main.d.ts.map +1 -1
  191. package/dist/main.js +27 -47
  192. package/dist/main.js.map +1 -1
  193. package/dist/modes/interactive/theme/omk-control-panel.json +1 -1
  194. package/dist/utils/clipboard-image.d.ts.map +1 -1
  195. package/dist/utils/clipboard-image.js +42 -33
  196. package/dist/utils/clipboard-image.js.map +1 -1
  197. package/dist/utils/clipboard.d.ts.map +1 -1
  198. package/dist/utils/clipboard.js +14 -2
  199. package/dist/utils/clipboard.js.map +1 -1
  200. package/docs/adaptorch-preview-spec.md +20 -18
  201. package/docs/adaptorch-preview.md +9 -9
  202. package/docs/compaction.md +24 -3
  203. package/docs/correctness-wall.md +10 -10
  204. package/docs/docs.json +12 -0
  205. package/docs/environment-variables.md +9 -2
  206. package/docs/index.md +3 -0
  207. package/docs/runtime-algorithms.md +359 -0
  208. package/docs/sdk.md +2 -2
  209. package/docs/settings.md +11 -10
  210. package/docs/usage.md +18 -2
  211. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  212. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  213. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  214. package/examples/extensions/gondolin/package-lock.json +2 -2
  215. package/examples/extensions/gondolin/package.json +1 -1
  216. package/examples/extensions/sandbox/package-lock.json +2 -2
  217. package/examples/extensions/sandbox/package.json +1 -1
  218. package/examples/extensions/with-deps/package-lock.json +2 -2
  219. package/examples/extensions/with-deps/package.json +1 -1
  220. package/npm-shrinkwrap.json +18 -18
  221. package/package.json +6 -6
  222. package/dist/core/context-budget-governor.d.ts +0 -67
  223. package/dist/core/context-budget-governor.d.ts.map +0 -1
  224. package/dist/core/context-budget-governor.js +0 -224
  225. package/dist/core/context-budget-governor.js.map +0 -1
  226. package/dist/core/lean-ctx.d.ts +0 -99
  227. package/dist/core/lean-ctx.d.ts.map +0 -1
  228. package/dist/core/lean-ctx.js +0 -225
  229. package/dist/core/lean-ctx.js.map +0 -1
  230. package/dist/core/read-anchors.d.ts +0 -110
  231. package/dist/core/read-anchors.d.ts.map +0 -1
  232. package/dist/core/read-anchors.js +0 -193
  233. package/dist/core/read-anchors.js.map +0 -1
  234. package/dist/core/recovery-checkpoint.d.ts +0 -196
  235. package/dist/core/recovery-checkpoint.d.ts.map +0 -1
  236. package/dist/core/recovery-checkpoint.js +0 -382
  237. package/dist/core/recovery-checkpoint.js.map +0 -1
  238. package/dist/core/sandbox-policy-v2.d.ts +0 -49
  239. package/dist/core/sandbox-policy-v2.d.ts.map +0 -1
  240. package/dist/core/sandbox-policy-v2.js +0 -368
  241. package/dist/core/sandbox-policy-v2.js.map +0 -1
  242. package/dist/guardrails/audit-logger.d.ts +0 -4
  243. package/dist/guardrails/audit-logger.d.ts.map +0 -1
  244. package/dist/guardrails/audit-logger.js +0 -28
  245. package/dist/guardrails/audit-logger.js.map +0 -1
  246. package/dist/guardrails/freedomd-adapter.d.ts +0 -16
  247. package/dist/guardrails/freedomd-adapter.d.ts.map +0 -1
  248. package/dist/guardrails/freedomd-adapter.js +0 -87
  249. package/dist/guardrails/freedomd-adapter.js.map +0 -1
  250. package/dist/orchestration/lane-grant-auditor.d.ts +0 -4
  251. package/dist/orchestration/lane-grant-auditor.d.ts.map +0 -1
  252. package/dist/orchestration/lane-grant-auditor.js +0 -87
  253. package/dist/orchestration/lane-grant-auditor.js.map +0 -1
  254. package/dist/types/guardrails.d.ts +0 -61
  255. package/dist/types/guardrails.d.ts.map +0 -1
  256. package/dist/types/guardrails.js +0 -9
  257. package/dist/types/guardrails.js.map +0 -1
  258. package/dist/types/lane-grant.d.ts +0 -58
  259. package/dist/types/lane-grant.d.ts.map +0 -1
  260. package/dist/types/lane-grant.js +0 -9
  261. package/dist/types/lane-grant.js.map +0 -1
@@ -1,196 +0,0 @@
1
- /**
2
- * `omk.recovery.checkpoint` — pure decision algorithms for native worktree/
3
- * conversation recovery checkpoints.
4
- *
5
- * This module is intentionally side-effect free: it never reads the filesystem,
6
- * never shells out to git, and never mutates a worktree. It validates a
7
- * file-backed {@link RecoveryCheckpoint}, classifies a restore preflight against
8
- * an externally-observed worktree snapshot, decides how OMK-created untracked
9
- * files should be disposed (delete vs quarantine), and produces a
10
- * conversation/code/both restore plan. The caller is responsible for gathering
11
- * the observed state (git status, per-file hashes) and for executing the plan.
12
- *
13
- * Derived clean-room from the lane C plan
14
- * (`.omk/runs/omk-package-hardening-plan/recovery-readseek.md`) and the
15
- * adoption algorithm plan; no legacy package source is imported or copied.
16
- *
17
- * Dependency-free apart from `node:path` (pure path math, no I/O).
18
- */
19
- export declare const RECOVERY_CHECKPOINT_SCHEMA_VERSION = "omk.recovery.checkpoint.v1";
20
- export type RecoveryVcs = "git" | "none";
21
- export type TouchedFileMode = "tracked" | "untracked" | "deleted";
22
- export interface RecoveryCheckpointSession {
23
- readonly sessionFile?: string;
24
- readonly leafId: string | null;
25
- readonly branchPathIds: readonly string[];
26
- readonly contextHash: string;
27
- }
28
- export interface RecoveryTouchedFile {
29
- readonly path: string;
30
- readonly beforeSha256?: string;
31
- readonly afterSha256?: string;
32
- readonly mode: TouchedFileMode;
33
- }
34
- export interface RecoveryCheckpointWorkspace {
35
- readonly repoRoot: string;
36
- readonly vcs: RecoveryVcs;
37
- readonly head?: string;
38
- readonly statusPorcelainSha256: string;
39
- readonly touchedFiles: readonly RecoveryTouchedFile[];
40
- readonly reversePatchArtifact?: string;
41
- }
42
- export interface RecoveryCheckpointTool {
43
- readonly turnIndex: number;
44
- readonly beforeToolCallId?: string;
45
- readonly afterToolCallId?: string;
46
- readonly mutatingTools: readonly string[];
47
- }
48
- export interface RecoveryCheckpointLedger {
49
- readonly eventId: string;
50
- readonly previousEventHash: string;
51
- readonly eventHash: string;
52
- }
53
- export interface RecoveryCheckpoint {
54
- readonly schemaVersion: typeof RECOVERY_CHECKPOINT_SCHEMA_VERSION;
55
- readonly checkpointId: string;
56
- readonly createdAt: string;
57
- readonly session: RecoveryCheckpointSession;
58
- readonly workspace: RecoveryCheckpointWorkspace;
59
- readonly tool?: RecoveryCheckpointTool;
60
- readonly ledger: RecoveryCheckpointLedger;
61
- }
62
- export interface RecoveryCheckpointValidation {
63
- readonly ok: boolean;
64
- readonly errors: readonly string[];
65
- readonly checkpoint?: RecoveryCheckpoint;
66
- }
67
- /** A touched-file path must be repo-relative and must not escape the repo root. */
68
- export declare function isUnsafeTouchedPath(path: unknown): boolean;
69
- /**
70
- * Validate an untrusted value as a {@link RecoveryCheckpoint}. Pure and
71
- * fail-closed: any structural problem yields `ok: false` with explicit errors
72
- * and no `checkpoint` payload.
73
- */
74
- export declare function validateRecoveryCheckpoint(value: unknown): RecoveryCheckpointValidation;
75
- /**
76
- * The ledger anchor as independently re-read from the hash-chained ledger.
77
- * Compared against the checkpoint's embedded anchor before any mutation.
78
- */
79
- export interface ObservedLedgerAnchor {
80
- readonly eventId: string;
81
- readonly previousEventHash: string;
82
- readonly eventHash: string;
83
- }
84
- export interface LedgerAnchorVerification {
85
- readonly ok: boolean;
86
- readonly mismatches: readonly string[];
87
- }
88
- /**
89
- * Compare a checkpoint's embedded ledger anchor with the anchor observed in the
90
- * live ledger. A mismatch means the checkpoint references an event that no
91
- * longer matches the durable audit trail (tamper or divergence) and the restore
92
- * must fail closed.
93
- */
94
- export declare function verifyCheckpointLedgerAnchor(checkpoint: RecoveryCheckpoint, observed: ObservedLedgerAnchor): LedgerAnchorVerification;
95
- export interface ObservedTouchedFile {
96
- readonly path: string;
97
- readonly exists: boolean;
98
- readonly sha256?: string;
99
- readonly isSymlink?: boolean;
100
- /** True when the path (or its symlink target) resolves outside the repo root. */
101
- readonly escapesRepo?: boolean;
102
- }
103
- export interface ObservedWorktree {
104
- readonly head?: string;
105
- readonly statusPorcelainSha256?: string;
106
- /** Current dirty paths (repo-relative) from `git status --porcelain`. */
107
- readonly dirtyPaths: readonly string[];
108
- readonly touchedFiles: readonly ObservedTouchedFile[];
109
- }
110
- export interface RestorePreflightOptions {
111
- /** Allow restoring even though HEAD moved since the checkpoint. */
112
- readonly allowNonHeadRestore?: boolean;
113
- }
114
- export type RestorePreflightVerdict = "clean" | "blocked";
115
- export type RestorePreflightIssueCode = "unrelated-dirty-path" | "touched-hash-mismatch" | "missing-touched-file" | "symlink-escape" | "head-mismatch";
116
- export interface RestorePreflightIssue {
117
- readonly code: RestorePreflightIssueCode;
118
- readonly path?: string;
119
- readonly detail: string;
120
- }
121
- export interface RestorePreflightResult {
122
- readonly verdict: RestorePreflightVerdict;
123
- readonly issues: readonly RestorePreflightIssue[];
124
- readonly unrelatedDirtyPaths: readonly string[];
125
- readonly hashMismatches: readonly string[];
126
- readonly symlinkEscapes: readonly string[];
127
- readonly headChanged: boolean;
128
- }
129
- /**
130
- * Classify whether a code restore can proceed against the live worktree.
131
- *
132
- * Fail-closed rules (from the lane C restore algorithm):
133
- * - any dirty path outside the recorded touched set blocks the restore;
134
- * - any touched file whose current hash differs from the recorded `afterSha256`
135
- * blocks the restore;
136
- * - any symlink escaping the repo root blocks the restore;
137
- * - a moved HEAD blocks the restore unless `allowNonHeadRestore` is set.
138
- */
139
- export declare function classifyRestorePreflight(checkpoint: RecoveryCheckpoint, observed: ObservedWorktree, options?: RestorePreflightOptions): RestorePreflightResult;
140
- export type UntrackedDisposition = "delete" | "quarantine" | "skip";
141
- export interface UntrackedDispositionDecision {
142
- readonly disposition: UntrackedDisposition;
143
- readonly reason: string;
144
- }
145
- export interface ObservedUntrackedFile {
146
- readonly exists: boolean;
147
- readonly sha256?: string;
148
- }
149
- /**
150
- * Decide how an OMK-created untracked file should be handled on restore.
151
- *
152
- * - `skip`: the file is already gone, nothing to undo.
153
- * - `delete`: the file still exactly matches what OMK created, safe to remove.
154
- * - `quarantine`: the file changed since OMK created it; move it under the
155
- * recovery quarantine directory rather than destroying user edits.
156
- */
157
- export declare function decideUntrackedDisposition(touched: RecoveryTouchedFile, observed: ObservedUntrackedFile): UntrackedDispositionDecision;
158
- export type RestoreMode = "conversation" | "code" | "both";
159
- export type RestorePhaseKind = "conversation" | "code";
160
- export type RestorePhaseAction = "navigate-tree" | "apply-reverse-patch";
161
- export interface RestorePlanPhase {
162
- readonly phase: RestorePhaseKind;
163
- readonly action: RestorePhaseAction;
164
- readonly targetLeafId?: string | null;
165
- readonly recordedPaths?: readonly string[];
166
- }
167
- export interface RestorePlan {
168
- readonly mode: RestoreMode;
169
- readonly status: "ready" | "blocked";
170
- readonly phases: readonly RestorePlanPhase[];
171
- readonly blockers: readonly string[];
172
- /** For `both`: the leaf to keep if the conversation phase fails after code restore. */
173
- readonly preservePreviousLeafId?: string | null;
174
- readonly inDoubtPolicy?: "preserve-previous-leaf";
175
- }
176
- export interface PlanRestoreInput {
177
- readonly mode: RestoreMode;
178
- readonly checkpoint: RecoveryCheckpoint;
179
- /** Required for `code` and `both` modes. */
180
- readonly preflight?: RestorePreflightResult;
181
- /** The current session leaf, preserved for manual recovery in `both` mode. */
182
- readonly currentLeafId?: string | null;
183
- }
184
- /**
185
- * Build a restore plan for the requested mode.
186
- *
187
- * - `conversation`: navigate the session tree to the checkpoint leaf; no file
188
- * writes, so no worktree preflight is required.
189
- * - `code`: apply the recorded reverse patch path-by-path; requires a clean
190
- * preflight.
191
- * - `both`: restore code first, then navigate the tree; requires a clean
192
- * preflight. If the second (conversation) phase fails at execution time the
193
- * caller marks the operation `in_doubt` and keeps `preservePreviousLeafId`.
194
- */
195
- export declare function planRestore(input: PlanRestoreInput): RestorePlan;
196
- //# sourceMappingURL=recovery-checkpoint.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"recovery-checkpoint.d.ts","sourceRoot":"","sources":["../../src/core/recovery-checkpoint.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `omk.recovery.checkpoint` — pure decision algorithms for native worktree/\n * conversation recovery checkpoints.\n *\n * This module is intentionally side-effect free: it never reads the filesystem,\n * never shells out to git, and never mutates a worktree. It validates a\n * file-backed {@link RecoveryCheckpoint}, classifies a restore preflight against\n * an externally-observed worktree snapshot, decides how OMK-created untracked\n * files should be disposed (delete vs quarantine), and produces a\n * conversation/code/both restore plan. The caller is responsible for gathering\n * the observed state (git status, per-file hashes) and for executing the plan.\n *\n * Derived clean-room from the lane C plan\n * (`.omk/runs/omk-package-hardening-plan/recovery-readseek.md`) and the\n * adoption algorithm plan; no legacy package source is imported or copied.\n *\n * Dependency-free apart from `node:path` (pure path math, no I/O).\n */\n\nimport { isAbsolute } from \"node:path\";\n\nexport const RECOVERY_CHECKPOINT_SCHEMA_VERSION = \"omk.recovery.checkpoint.v1\";\n\nexport type RecoveryVcs = \"git\" | \"none\";\nexport type TouchedFileMode = \"tracked\" | \"untracked\" | \"deleted\";\n\nexport interface RecoveryCheckpointSession {\n\treadonly sessionFile?: string;\n\treadonly leafId: string | null;\n\treadonly branchPathIds: readonly string[];\n\treadonly contextHash: string;\n}\n\nexport interface RecoveryTouchedFile {\n\treadonly path: string;\n\treadonly beforeSha256?: string;\n\treadonly afterSha256?: string;\n\treadonly mode: TouchedFileMode;\n}\n\nexport interface RecoveryCheckpointWorkspace {\n\treadonly repoRoot: string;\n\treadonly vcs: RecoveryVcs;\n\treadonly head?: string;\n\treadonly statusPorcelainSha256: string;\n\treadonly touchedFiles: readonly RecoveryTouchedFile[];\n\treadonly reversePatchArtifact?: string;\n}\n\nexport interface RecoveryCheckpointTool {\n\treadonly turnIndex: number;\n\treadonly beforeToolCallId?: string;\n\treadonly afterToolCallId?: string;\n\treadonly mutatingTools: readonly string[];\n}\n\nexport interface RecoveryCheckpointLedger {\n\treadonly eventId: string;\n\treadonly previousEventHash: string;\n\treadonly eventHash: string;\n}\n\nexport interface RecoveryCheckpoint {\n\treadonly schemaVersion: typeof RECOVERY_CHECKPOINT_SCHEMA_VERSION;\n\treadonly checkpointId: string;\n\treadonly createdAt: string;\n\treadonly session: RecoveryCheckpointSession;\n\treadonly workspace: RecoveryCheckpointWorkspace;\n\treadonly tool?: RecoveryCheckpointTool;\n\treadonly ledger: RecoveryCheckpointLedger;\n}\n\nexport interface RecoveryCheckpointValidation {\n\treadonly ok: boolean;\n\treadonly errors: readonly string[];\n\treadonly checkpoint?: RecoveryCheckpoint;\n}\n\nconst HEX64 = /^[0-9a-f]{64}$/i;\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n\treturn typeof value === \"string\" && value.length > 0;\n}\n\nfunction isHex64(value: unknown): value is string {\n\treturn typeof value === \"string\" && HEX64.test(value);\n}\n\nfunction isIsoTimestamp(value: unknown): value is string {\n\treturn typeof value === \"string\" && value.length > 0 && !Number.isNaN(Date.parse(value));\n}\n\n/** A touched-file path must be repo-relative and must not escape the repo root. */\nexport function isUnsafeTouchedPath(path: unknown): boolean {\n\tif (typeof path !== \"string\" || path.length === 0) return true;\n\tif (isAbsolute(path)) return true;\n\tconst normalized = path.replace(/\\\\/g, \"/\");\n\treturn normalized.split(\"/\").some((segment) => segment === \"..\");\n}\n\nfunction normalizeRelPath(path: string): string {\n\treturn path.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n}\n\nfunction validateTouchedFile(value: unknown, index: number, errors: string[]): void {\n\tconst label = `workspace.touchedFiles[${index}]`;\n\tif (!isPlainObject(value)) {\n\t\terrors.push(`${label} must be an object`);\n\t\treturn;\n\t}\n\tif (isUnsafeTouchedPath(value.path)) {\n\t\terrors.push(`${label}.path must be a repo-relative path without '..' segments`);\n\t}\n\tconst mode = value.mode;\n\tif (mode !== \"tracked\" && mode !== \"untracked\" && mode !== \"deleted\") {\n\t\terrors.push(`${label}.mode must be one of tracked|untracked|deleted`);\n\t\treturn;\n\t}\n\tif (value.beforeSha256 !== undefined && !isHex64(value.beforeSha256)) {\n\t\terrors.push(`${label}.beforeSha256 must be a 64-char hex digest when present`);\n\t}\n\tif (value.afterSha256 !== undefined && !isHex64(value.afterSha256)) {\n\t\terrors.push(`${label}.afterSha256 must be a 64-char hex digest when present`);\n\t}\n\tif (mode === \"untracked\") {\n\t\tif (!isHex64(value.afterSha256)) errors.push(`${label}.afterSha256 is required for untracked files`);\n\t\tif (value.beforeSha256 !== undefined) errors.push(`${label}.beforeSha256 must be absent for untracked files`);\n\t}\n\tif (mode === \"deleted\") {\n\t\tif (!isHex64(value.beforeSha256)) errors.push(`${label}.beforeSha256 is required for deleted files`);\n\t\tif (value.afterSha256 !== undefined) errors.push(`${label}.afterSha256 must be absent for deleted files`);\n\t}\n\tif (mode === \"tracked\" && !isHex64(value.afterSha256)) {\n\t\terrors.push(`${label}.afterSha256 is required for tracked files`);\n\t}\n}\n\nfunction validateSession(value: unknown, errors: string[]): void {\n\tif (!isPlainObject(value)) {\n\t\terrors.push(\"session must be an object\");\n\t\treturn;\n\t}\n\tif (value.leafId !== null && typeof value.leafId !== \"string\") {\n\t\terrors.push(\"session.leafId must be a string or null\");\n\t}\n\tif (!Array.isArray(value.branchPathIds) || value.branchPathIds.some((id) => typeof id !== \"string\")) {\n\t\terrors.push(\"session.branchPathIds must be an array of strings\");\n\t}\n\tif (!isHex64(value.contextHash)) {\n\t\terrors.push(\"session.contextHash must be a 64-char hex digest\");\n\t}\n\tif (value.sessionFile !== undefined && typeof value.sessionFile !== \"string\") {\n\t\terrors.push(\"session.sessionFile must be a string when present\");\n\t}\n}\n\nfunction validateWorkspace(value: unknown, errors: string[]): void {\n\tif (!isPlainObject(value)) {\n\t\terrors.push(\"workspace must be an object\");\n\t\treturn;\n\t}\n\tif (!isNonEmptyString(value.repoRoot)) errors.push(\"workspace.repoRoot must be a non-empty string\");\n\tif (value.vcs !== \"git\" && value.vcs !== \"none\") errors.push(\"workspace.vcs must be one of git|none\");\n\tif (value.head !== undefined && typeof value.head !== \"string\") {\n\t\terrors.push(\"workspace.head must be a string when present\");\n\t}\n\tif (!isHex64(value.statusPorcelainSha256)) {\n\t\terrors.push(\"workspace.statusPorcelainSha256 must be a 64-char hex digest\");\n\t}\n\tif (value.reversePatchArtifact !== undefined && typeof value.reversePatchArtifact !== \"string\") {\n\t\terrors.push(\"workspace.reversePatchArtifact must be a string when present\");\n\t}\n\tif (!Array.isArray(value.touchedFiles)) {\n\t\terrors.push(\"workspace.touchedFiles must be an array\");\n\t\treturn;\n\t}\n\tvalue.touchedFiles.forEach((entry, index) => {\n\t\tvalidateTouchedFile(entry, index, errors);\n\t});\n}\n\nfunction validateTool(value: unknown, errors: string[]): void {\n\tif (value === undefined) return;\n\tif (!isPlainObject(value)) {\n\t\terrors.push(\"tool must be an object when present\");\n\t\treturn;\n\t}\n\tif (typeof value.turnIndex !== \"number\" || !Number.isInteger(value.turnIndex) || value.turnIndex < 0) {\n\t\terrors.push(\"tool.turnIndex must be a non-negative integer\");\n\t}\n\tif (!Array.isArray(value.mutatingTools) || value.mutatingTools.some((t) => typeof t !== \"string\")) {\n\t\terrors.push(\"tool.mutatingTools must be an array of strings\");\n\t}\n\tif (value.beforeToolCallId !== undefined && typeof value.beforeToolCallId !== \"string\") {\n\t\terrors.push(\"tool.beforeToolCallId must be a string when present\");\n\t}\n\tif (value.afterToolCallId !== undefined && typeof value.afterToolCallId !== \"string\") {\n\t\terrors.push(\"tool.afterToolCallId must be a string when present\");\n\t}\n}\n\nfunction validateLedger(value: unknown, errors: string[]): void {\n\tif (!isPlainObject(value)) {\n\t\terrors.push(\"ledger must be an object\");\n\t\treturn;\n\t}\n\tif (!isNonEmptyString(value.eventId)) errors.push(\"ledger.eventId must be a non-empty string\");\n\tif (!isHex64(value.previousEventHash)) errors.push(\"ledger.previousEventHash must be a 64-char hex digest\");\n\tif (!isHex64(value.eventHash)) errors.push(\"ledger.eventHash must be a 64-char hex digest\");\n}\n\n/**\n * Validate an untrusted value as a {@link RecoveryCheckpoint}. Pure and\n * fail-closed: any structural problem yields `ok: false` with explicit errors\n * and no `checkpoint` payload.\n */\nexport function validateRecoveryCheckpoint(value: unknown): RecoveryCheckpointValidation {\n\tconst errors: string[] = [];\n\tif (!isPlainObject(value)) {\n\t\treturn { ok: false, errors: [\"checkpoint must be an object\"] };\n\t}\n\tif (value.schemaVersion !== RECOVERY_CHECKPOINT_SCHEMA_VERSION) {\n\t\terrors.push(`schemaVersion must be ${RECOVERY_CHECKPOINT_SCHEMA_VERSION}`);\n\t}\n\tif (!isNonEmptyString(value.checkpointId)) errors.push(\"checkpointId must be a non-empty string\");\n\tif (!isIsoTimestamp(value.createdAt)) errors.push(\"createdAt must be an ISO-8601 timestamp\");\n\tvalidateSession(value.session, errors);\n\tvalidateWorkspace(value.workspace, errors);\n\tvalidateTool(value.tool, errors);\n\tvalidateLedger(value.ledger, errors);\n\n\tif (errors.length > 0) return { ok: false, errors };\n\treturn { ok: true, errors: [], checkpoint: value as unknown as RecoveryCheckpoint };\n}\n\n/**\n * The ledger anchor as independently re-read from the hash-chained ledger.\n * Compared against the checkpoint's embedded anchor before any mutation.\n */\nexport interface ObservedLedgerAnchor {\n\treadonly eventId: string;\n\treadonly previousEventHash: string;\n\treadonly eventHash: string;\n}\n\nexport interface LedgerAnchorVerification {\n\treadonly ok: boolean;\n\treadonly mismatches: readonly string[];\n}\n\n/**\n * Compare a checkpoint's embedded ledger anchor with the anchor observed in the\n * live ledger. A mismatch means the checkpoint references an event that no\n * longer matches the durable audit trail (tamper or divergence) and the restore\n * must fail closed.\n */\nexport function verifyCheckpointLedgerAnchor(\n\tcheckpoint: RecoveryCheckpoint,\n\tobserved: ObservedLedgerAnchor,\n): LedgerAnchorVerification {\n\tconst mismatches: string[] = [];\n\tif (checkpoint.ledger.eventId !== observed.eventId) {\n\t\tmismatches.push(`eventId mismatch: checkpoint=${checkpoint.ledger.eventId} ledger=${observed.eventId}`);\n\t}\n\tif (checkpoint.ledger.previousEventHash !== observed.previousEventHash) {\n\t\tmismatches.push(\"previousEventHash mismatch between checkpoint and ledger\");\n\t}\n\tif (checkpoint.ledger.eventHash !== observed.eventHash) {\n\t\tmismatches.push(\"eventHash mismatch between checkpoint and ledger\");\n\t}\n\treturn { ok: mismatches.length === 0, mismatches };\n}\n\nexport interface ObservedTouchedFile {\n\treadonly path: string;\n\treadonly exists: boolean;\n\treadonly sha256?: string;\n\treadonly isSymlink?: boolean;\n\t/** True when the path (or its symlink target) resolves outside the repo root. */\n\treadonly escapesRepo?: boolean;\n}\n\nexport interface ObservedWorktree {\n\treadonly head?: string;\n\treadonly statusPorcelainSha256?: string;\n\t/** Current dirty paths (repo-relative) from `git status --porcelain`. */\n\treadonly dirtyPaths: readonly string[];\n\treadonly touchedFiles: readonly ObservedTouchedFile[];\n}\n\nexport interface RestorePreflightOptions {\n\t/** Allow restoring even though HEAD moved since the checkpoint. */\n\treadonly allowNonHeadRestore?: boolean;\n}\n\nexport type RestorePreflightVerdict = \"clean\" | \"blocked\";\n\nexport type RestorePreflightIssueCode =\n\t| \"unrelated-dirty-path\"\n\t| \"touched-hash-mismatch\"\n\t| \"missing-touched-file\"\n\t| \"symlink-escape\"\n\t| \"head-mismatch\";\n\nexport interface RestorePreflightIssue {\n\treadonly code: RestorePreflightIssueCode;\n\treadonly path?: string;\n\treadonly detail: string;\n}\n\nexport interface RestorePreflightResult {\n\treadonly verdict: RestorePreflightVerdict;\n\treadonly issues: readonly RestorePreflightIssue[];\n\treadonly unrelatedDirtyPaths: readonly string[];\n\treadonly hashMismatches: readonly string[];\n\treadonly symlinkEscapes: readonly string[];\n\treadonly headChanged: boolean;\n}\n\n/**\n * Classify whether a code restore can proceed against the live worktree.\n *\n * Fail-closed rules (from the lane C restore algorithm):\n * - any dirty path outside the recorded touched set blocks the restore;\n * - any touched file whose current hash differs from the recorded `afterSha256`\n * blocks the restore;\n * - any symlink escaping the repo root blocks the restore;\n * - a moved HEAD blocks the restore unless `allowNonHeadRestore` is set.\n */\nexport function classifyRestorePreflight(\n\tcheckpoint: RecoveryCheckpoint,\n\tobserved: ObservedWorktree,\n\toptions: RestorePreflightOptions = {},\n): RestorePreflightResult {\n\tconst issues: RestorePreflightIssue[] = [];\n\tconst touchedByPath = new Map<string, RecoveryTouchedFile>();\n\tfor (const file of checkpoint.workspace.touchedFiles) {\n\t\ttouchedByPath.set(normalizeRelPath(file.path), file);\n\t}\n\n\tconst unrelatedDirtyPaths: string[] = [];\n\tfor (const dirty of observed.dirtyPaths) {\n\t\tif (!touchedByPath.has(normalizeRelPath(dirty))) {\n\t\t\tunrelatedDirtyPaths.push(dirty);\n\t\t\tissues.push({\n\t\t\t\tcode: \"unrelated-dirty-path\",\n\t\t\t\tpath: dirty,\n\t\t\t\tdetail: \"dirty path is outside the checkpoint's recorded touched files\",\n\t\t\t});\n\t\t}\n\t}\n\n\tconst observedByPath = new Map<string, ObservedTouchedFile>();\n\tfor (const file of observed.touchedFiles) {\n\t\tobservedByPath.set(normalizeRelPath(file.path), file);\n\t}\n\n\tconst hashMismatches: string[] = [];\n\tconst symlinkEscapes: string[] = [];\n\tfor (const expected of checkpoint.workspace.touchedFiles) {\n\t\tconst key = normalizeRelPath(expected.path);\n\t\tconst current = observedByPath.get(key);\n\t\tif (!current) {\n\t\t\tissues.push({\n\t\t\t\tcode: \"missing-touched-file\",\n\t\t\t\tpath: expected.path,\n\t\t\t\tdetail: \"no observed state was provided for a recorded touched file\",\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (current.escapesRepo === true || (current.isSymlink === true && current.escapesRepo !== false)) {\n\t\t\tsymlinkEscapes.push(expected.path);\n\t\t\tissues.push({\n\t\t\t\tcode: \"symlink-escape\",\n\t\t\t\tpath: expected.path,\n\t\t\t\tdetail: \"touched path is a symlink resolving outside the repo root\",\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (expected.mode === \"deleted\") {\n\t\t\tif (current.exists) {\n\t\t\t\thashMismatches.push(expected.path);\n\t\t\t\tissues.push({\n\t\t\t\t\tcode: \"touched-hash-mismatch\",\n\t\t\t\t\tpath: expected.path,\n\t\t\t\t\tdetail: \"expected a deleted file to be absent but it is present\",\n\t\t\t\t});\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (!current.exists) {\n\t\t\tissues.push({\n\t\t\t\tcode: \"missing-touched-file\",\n\t\t\t\tpath: expected.path,\n\t\t\t\tdetail: \"recorded touched file is missing from the worktree\",\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (current.sha256 !== expected.afterSha256) {\n\t\t\thashMismatches.push(expected.path);\n\t\t\tissues.push({\n\t\t\t\tcode: \"touched-hash-mismatch\",\n\t\t\t\tpath: expected.path,\n\t\t\t\tdetail: \"current file hash differs from the recorded post-edit hash\",\n\t\t\t});\n\t\t}\n\t}\n\n\tconst headChanged =\n\t\tcheckpoint.workspace.vcs === \"git\" &&\n\t\tcheckpoint.workspace.head !== undefined &&\n\t\tobserved.head !== undefined &&\n\t\tcheckpoint.workspace.head !== observed.head;\n\tif (headChanged && options.allowNonHeadRestore !== true) {\n\t\tissues.push({\n\t\t\tcode: \"head-mismatch\",\n\t\t\tdetail: \"HEAD moved since the checkpoint; pass allowNonHeadRestore to override\",\n\t\t});\n\t}\n\n\treturn {\n\t\tverdict: issues.length === 0 ? \"clean\" : \"blocked\",\n\t\tissues,\n\t\tunrelatedDirtyPaths,\n\t\thashMismatches,\n\t\tsymlinkEscapes,\n\t\theadChanged,\n\t};\n}\n\nexport type UntrackedDisposition = \"delete\" | \"quarantine\" | \"skip\";\n\nexport interface UntrackedDispositionDecision {\n\treadonly disposition: UntrackedDisposition;\n\treadonly reason: string;\n}\n\nexport interface ObservedUntrackedFile {\n\treadonly exists: boolean;\n\treadonly sha256?: string;\n}\n\n/**\n * Decide how an OMK-created untracked file should be handled on restore.\n *\n * - `skip`: the file is already gone, nothing to undo.\n * - `delete`: the file still exactly matches what OMK created, safe to remove.\n * - `quarantine`: the file changed since OMK created it; move it under the\n * recovery quarantine directory rather than destroying user edits.\n */\nexport function decideUntrackedDisposition(\n\ttouched: RecoveryTouchedFile,\n\tobserved: ObservedUntrackedFile,\n): UntrackedDispositionDecision {\n\tif (touched.mode !== \"untracked\") {\n\t\treturn { disposition: \"skip\", reason: \"disposition only applies to untracked files\" };\n\t}\n\tif (!observed.exists) {\n\t\treturn { disposition: \"skip\", reason: \"untracked file is already absent\" };\n\t}\n\tif (isHex64(touched.afterSha256) && observed.sha256 === touched.afterSha256) {\n\t\treturn { disposition: \"delete\", reason: \"current hash matches the OMK-created hash\" };\n\t}\n\treturn { disposition: \"quarantine\", reason: \"current hash differs from the OMK-created hash\" };\n}\n\nexport type RestoreMode = \"conversation\" | \"code\" | \"both\";\n\nexport type RestorePhaseKind = \"conversation\" | \"code\";\nexport type RestorePhaseAction = \"navigate-tree\" | \"apply-reverse-patch\";\n\nexport interface RestorePlanPhase {\n\treadonly phase: RestorePhaseKind;\n\treadonly action: RestorePhaseAction;\n\treadonly targetLeafId?: string | null;\n\treadonly recordedPaths?: readonly string[];\n}\n\nexport interface RestorePlan {\n\treadonly mode: RestoreMode;\n\treadonly status: \"ready\" | \"blocked\";\n\treadonly phases: readonly RestorePlanPhase[];\n\treadonly blockers: readonly string[];\n\t/** For `both`: the leaf to keep if the conversation phase fails after code restore. */\n\treadonly preservePreviousLeafId?: string | null;\n\treadonly inDoubtPolicy?: \"preserve-previous-leaf\";\n}\n\nexport interface PlanRestoreInput {\n\treadonly mode: RestoreMode;\n\treadonly checkpoint: RecoveryCheckpoint;\n\t/** Required for `code` and `both` modes. */\n\treadonly preflight?: RestorePreflightResult;\n\t/** The current session leaf, preserved for manual recovery in `both` mode. */\n\treadonly currentLeafId?: string | null;\n}\n\nfunction codePhase(checkpoint: RecoveryCheckpoint): RestorePlanPhase {\n\treturn {\n\t\tphase: \"code\",\n\t\taction: \"apply-reverse-patch\",\n\t\trecordedPaths: checkpoint.workspace.touchedFiles.map((file) => file.path),\n\t};\n}\n\nfunction conversationPhase(checkpoint: RecoveryCheckpoint): RestorePlanPhase {\n\treturn { phase: \"conversation\", action: \"navigate-tree\", targetLeafId: checkpoint.session.leafId };\n}\n\nfunction collectPreflightBlockers(preflight: RestorePreflightResult | undefined): string[] {\n\tif (!preflight) return [\"code restore requires a preflight classification\"];\n\tif (preflight.verdict === \"clean\") return [];\n\treturn preflight.issues.map((issue) => (issue.path ? `${issue.code}: ${issue.path}` : issue.code));\n}\n\n/**\n * Build a restore plan for the requested mode.\n *\n * - `conversation`: navigate the session tree to the checkpoint leaf; no file\n * writes, so no worktree preflight is required.\n * - `code`: apply the recorded reverse patch path-by-path; requires a clean\n * preflight.\n * - `both`: restore code first, then navigate the tree; requires a clean\n * preflight. If the second (conversation) phase fails at execution time the\n * caller marks the operation `in_doubt` and keeps `preservePreviousLeafId`.\n */\nexport function planRestore(input: PlanRestoreInput): RestorePlan {\n\tconst { mode, checkpoint, preflight } = input;\n\tif (mode === \"conversation\") {\n\t\treturn { mode, status: \"ready\", phases: [conversationPhase(checkpoint)], blockers: [] };\n\t}\n\n\tconst blockers = collectPreflightBlockers(preflight);\n\tif (mode === \"code\") {\n\t\tif (blockers.length > 0) return { mode, status: \"blocked\", phases: [], blockers };\n\t\treturn { mode, status: \"ready\", phases: [codePhase(checkpoint)], blockers: [] };\n\t}\n\n\t// mode === \"both\"\n\tconst preservePreviousLeafId = input.currentLeafId ?? null;\n\tif (blockers.length > 0) {\n\t\treturn {\n\t\t\tmode,\n\t\t\tstatus: \"blocked\",\n\t\t\tphases: [],\n\t\t\tblockers,\n\t\t\tpreservePreviousLeafId,\n\t\t\tinDoubtPolicy: \"preserve-previous-leaf\",\n\t\t};\n\t}\n\treturn {\n\t\tmode,\n\t\tstatus: \"ready\",\n\t\tphases: [codePhase(checkpoint), conversationPhase(checkpoint)],\n\t\tblockers: [],\n\t\tpreservePreviousLeafId,\n\t\tinDoubtPolicy: \"preserve-previous-leaf\",\n\t};\n}\n"]}
@@ -1,382 +0,0 @@
1
- /**
2
- * `omk.recovery.checkpoint` — pure decision algorithms for native worktree/
3
- * conversation recovery checkpoints.
4
- *
5
- * This module is intentionally side-effect free: it never reads the filesystem,
6
- * never shells out to git, and never mutates a worktree. It validates a
7
- * file-backed {@link RecoveryCheckpoint}, classifies a restore preflight against
8
- * an externally-observed worktree snapshot, decides how OMK-created untracked
9
- * files should be disposed (delete vs quarantine), and produces a
10
- * conversation/code/both restore plan. The caller is responsible for gathering
11
- * the observed state (git status, per-file hashes) and for executing the plan.
12
- *
13
- * Derived clean-room from the lane C plan
14
- * (`.omk/runs/omk-package-hardening-plan/recovery-readseek.md`) and the
15
- * adoption algorithm plan; no legacy package source is imported or copied.
16
- *
17
- * Dependency-free apart from `node:path` (pure path math, no I/O).
18
- */
19
- import { isAbsolute } from "node:path";
20
- export const RECOVERY_CHECKPOINT_SCHEMA_VERSION = "omk.recovery.checkpoint.v1";
21
- const HEX64 = /^[0-9a-f]{64}$/i;
22
- function isPlainObject(value) {
23
- return typeof value === "object" && value !== null && !Array.isArray(value);
24
- }
25
- function isNonEmptyString(value) {
26
- return typeof value === "string" && value.length > 0;
27
- }
28
- function isHex64(value) {
29
- return typeof value === "string" && HEX64.test(value);
30
- }
31
- function isIsoTimestamp(value) {
32
- return typeof value === "string" && value.length > 0 && !Number.isNaN(Date.parse(value));
33
- }
34
- /** A touched-file path must be repo-relative and must not escape the repo root. */
35
- export function isUnsafeTouchedPath(path) {
36
- if (typeof path !== "string" || path.length === 0)
37
- return true;
38
- if (isAbsolute(path))
39
- return true;
40
- const normalized = path.replace(/\\/g, "/");
41
- return normalized.split("/").some((segment) => segment === "..");
42
- }
43
- function normalizeRelPath(path) {
44
- return path.replace(/\\/g, "/").replace(/^\.\//, "");
45
- }
46
- function validateTouchedFile(value, index, errors) {
47
- const label = `workspace.touchedFiles[${index}]`;
48
- if (!isPlainObject(value)) {
49
- errors.push(`${label} must be an object`);
50
- return;
51
- }
52
- if (isUnsafeTouchedPath(value.path)) {
53
- errors.push(`${label}.path must be a repo-relative path without '..' segments`);
54
- }
55
- const mode = value.mode;
56
- if (mode !== "tracked" && mode !== "untracked" && mode !== "deleted") {
57
- errors.push(`${label}.mode must be one of tracked|untracked|deleted`);
58
- return;
59
- }
60
- if (value.beforeSha256 !== undefined && !isHex64(value.beforeSha256)) {
61
- errors.push(`${label}.beforeSha256 must be a 64-char hex digest when present`);
62
- }
63
- if (value.afterSha256 !== undefined && !isHex64(value.afterSha256)) {
64
- errors.push(`${label}.afterSha256 must be a 64-char hex digest when present`);
65
- }
66
- if (mode === "untracked") {
67
- if (!isHex64(value.afterSha256))
68
- errors.push(`${label}.afterSha256 is required for untracked files`);
69
- if (value.beforeSha256 !== undefined)
70
- errors.push(`${label}.beforeSha256 must be absent for untracked files`);
71
- }
72
- if (mode === "deleted") {
73
- if (!isHex64(value.beforeSha256))
74
- errors.push(`${label}.beforeSha256 is required for deleted files`);
75
- if (value.afterSha256 !== undefined)
76
- errors.push(`${label}.afterSha256 must be absent for deleted files`);
77
- }
78
- if (mode === "tracked" && !isHex64(value.afterSha256)) {
79
- errors.push(`${label}.afterSha256 is required for tracked files`);
80
- }
81
- }
82
- function validateSession(value, errors) {
83
- if (!isPlainObject(value)) {
84
- errors.push("session must be an object");
85
- return;
86
- }
87
- if (value.leafId !== null && typeof value.leafId !== "string") {
88
- errors.push("session.leafId must be a string or null");
89
- }
90
- if (!Array.isArray(value.branchPathIds) || value.branchPathIds.some((id) => typeof id !== "string")) {
91
- errors.push("session.branchPathIds must be an array of strings");
92
- }
93
- if (!isHex64(value.contextHash)) {
94
- errors.push("session.contextHash must be a 64-char hex digest");
95
- }
96
- if (value.sessionFile !== undefined && typeof value.sessionFile !== "string") {
97
- errors.push("session.sessionFile must be a string when present");
98
- }
99
- }
100
- function validateWorkspace(value, errors) {
101
- if (!isPlainObject(value)) {
102
- errors.push("workspace must be an object");
103
- return;
104
- }
105
- if (!isNonEmptyString(value.repoRoot))
106
- errors.push("workspace.repoRoot must be a non-empty string");
107
- if (value.vcs !== "git" && value.vcs !== "none")
108
- errors.push("workspace.vcs must be one of git|none");
109
- if (value.head !== undefined && typeof value.head !== "string") {
110
- errors.push("workspace.head must be a string when present");
111
- }
112
- if (!isHex64(value.statusPorcelainSha256)) {
113
- errors.push("workspace.statusPorcelainSha256 must be a 64-char hex digest");
114
- }
115
- if (value.reversePatchArtifact !== undefined && typeof value.reversePatchArtifact !== "string") {
116
- errors.push("workspace.reversePatchArtifact must be a string when present");
117
- }
118
- if (!Array.isArray(value.touchedFiles)) {
119
- errors.push("workspace.touchedFiles must be an array");
120
- return;
121
- }
122
- value.touchedFiles.forEach((entry, index) => {
123
- validateTouchedFile(entry, index, errors);
124
- });
125
- }
126
- function validateTool(value, errors) {
127
- if (value === undefined)
128
- return;
129
- if (!isPlainObject(value)) {
130
- errors.push("tool must be an object when present");
131
- return;
132
- }
133
- if (typeof value.turnIndex !== "number" || !Number.isInteger(value.turnIndex) || value.turnIndex < 0) {
134
- errors.push("tool.turnIndex must be a non-negative integer");
135
- }
136
- if (!Array.isArray(value.mutatingTools) || value.mutatingTools.some((t) => typeof t !== "string")) {
137
- errors.push("tool.mutatingTools must be an array of strings");
138
- }
139
- if (value.beforeToolCallId !== undefined && typeof value.beforeToolCallId !== "string") {
140
- errors.push("tool.beforeToolCallId must be a string when present");
141
- }
142
- if (value.afterToolCallId !== undefined && typeof value.afterToolCallId !== "string") {
143
- errors.push("tool.afterToolCallId must be a string when present");
144
- }
145
- }
146
- function validateLedger(value, errors) {
147
- if (!isPlainObject(value)) {
148
- errors.push("ledger must be an object");
149
- return;
150
- }
151
- if (!isNonEmptyString(value.eventId))
152
- errors.push("ledger.eventId must be a non-empty string");
153
- if (!isHex64(value.previousEventHash))
154
- errors.push("ledger.previousEventHash must be a 64-char hex digest");
155
- if (!isHex64(value.eventHash))
156
- errors.push("ledger.eventHash must be a 64-char hex digest");
157
- }
158
- /**
159
- * Validate an untrusted value as a {@link RecoveryCheckpoint}. Pure and
160
- * fail-closed: any structural problem yields `ok: false` with explicit errors
161
- * and no `checkpoint` payload.
162
- */
163
- export function validateRecoveryCheckpoint(value) {
164
- const errors = [];
165
- if (!isPlainObject(value)) {
166
- return { ok: false, errors: ["checkpoint must be an object"] };
167
- }
168
- if (value.schemaVersion !== RECOVERY_CHECKPOINT_SCHEMA_VERSION) {
169
- errors.push(`schemaVersion must be ${RECOVERY_CHECKPOINT_SCHEMA_VERSION}`);
170
- }
171
- if (!isNonEmptyString(value.checkpointId))
172
- errors.push("checkpointId must be a non-empty string");
173
- if (!isIsoTimestamp(value.createdAt))
174
- errors.push("createdAt must be an ISO-8601 timestamp");
175
- validateSession(value.session, errors);
176
- validateWorkspace(value.workspace, errors);
177
- validateTool(value.tool, errors);
178
- validateLedger(value.ledger, errors);
179
- if (errors.length > 0)
180
- return { ok: false, errors };
181
- return { ok: true, errors: [], checkpoint: value };
182
- }
183
- /**
184
- * Compare a checkpoint's embedded ledger anchor with the anchor observed in the
185
- * live ledger. A mismatch means the checkpoint references an event that no
186
- * longer matches the durable audit trail (tamper or divergence) and the restore
187
- * must fail closed.
188
- */
189
- export function verifyCheckpointLedgerAnchor(checkpoint, observed) {
190
- const mismatches = [];
191
- if (checkpoint.ledger.eventId !== observed.eventId) {
192
- mismatches.push(`eventId mismatch: checkpoint=${checkpoint.ledger.eventId} ledger=${observed.eventId}`);
193
- }
194
- if (checkpoint.ledger.previousEventHash !== observed.previousEventHash) {
195
- mismatches.push("previousEventHash mismatch between checkpoint and ledger");
196
- }
197
- if (checkpoint.ledger.eventHash !== observed.eventHash) {
198
- mismatches.push("eventHash mismatch between checkpoint and ledger");
199
- }
200
- return { ok: mismatches.length === 0, mismatches };
201
- }
202
- /**
203
- * Classify whether a code restore can proceed against the live worktree.
204
- *
205
- * Fail-closed rules (from the lane C restore algorithm):
206
- * - any dirty path outside the recorded touched set blocks the restore;
207
- * - any touched file whose current hash differs from the recorded `afterSha256`
208
- * blocks the restore;
209
- * - any symlink escaping the repo root blocks the restore;
210
- * - a moved HEAD blocks the restore unless `allowNonHeadRestore` is set.
211
- */
212
- export function classifyRestorePreflight(checkpoint, observed, options = {}) {
213
- const issues = [];
214
- const touchedByPath = new Map();
215
- for (const file of checkpoint.workspace.touchedFiles) {
216
- touchedByPath.set(normalizeRelPath(file.path), file);
217
- }
218
- const unrelatedDirtyPaths = [];
219
- for (const dirty of observed.dirtyPaths) {
220
- if (!touchedByPath.has(normalizeRelPath(dirty))) {
221
- unrelatedDirtyPaths.push(dirty);
222
- issues.push({
223
- code: "unrelated-dirty-path",
224
- path: dirty,
225
- detail: "dirty path is outside the checkpoint's recorded touched files",
226
- });
227
- }
228
- }
229
- const observedByPath = new Map();
230
- for (const file of observed.touchedFiles) {
231
- observedByPath.set(normalizeRelPath(file.path), file);
232
- }
233
- const hashMismatches = [];
234
- const symlinkEscapes = [];
235
- for (const expected of checkpoint.workspace.touchedFiles) {
236
- const key = normalizeRelPath(expected.path);
237
- const current = observedByPath.get(key);
238
- if (!current) {
239
- issues.push({
240
- code: "missing-touched-file",
241
- path: expected.path,
242
- detail: "no observed state was provided for a recorded touched file",
243
- });
244
- continue;
245
- }
246
- if (current.escapesRepo === true || (current.isSymlink === true && current.escapesRepo !== false)) {
247
- symlinkEscapes.push(expected.path);
248
- issues.push({
249
- code: "symlink-escape",
250
- path: expected.path,
251
- detail: "touched path is a symlink resolving outside the repo root",
252
- });
253
- continue;
254
- }
255
- if (expected.mode === "deleted") {
256
- if (current.exists) {
257
- hashMismatches.push(expected.path);
258
- issues.push({
259
- code: "touched-hash-mismatch",
260
- path: expected.path,
261
- detail: "expected a deleted file to be absent but it is present",
262
- });
263
- }
264
- continue;
265
- }
266
- if (!current.exists) {
267
- issues.push({
268
- code: "missing-touched-file",
269
- path: expected.path,
270
- detail: "recorded touched file is missing from the worktree",
271
- });
272
- continue;
273
- }
274
- if (current.sha256 !== expected.afterSha256) {
275
- hashMismatches.push(expected.path);
276
- issues.push({
277
- code: "touched-hash-mismatch",
278
- path: expected.path,
279
- detail: "current file hash differs from the recorded post-edit hash",
280
- });
281
- }
282
- }
283
- const headChanged = checkpoint.workspace.vcs === "git" &&
284
- checkpoint.workspace.head !== undefined &&
285
- observed.head !== undefined &&
286
- checkpoint.workspace.head !== observed.head;
287
- if (headChanged && options.allowNonHeadRestore !== true) {
288
- issues.push({
289
- code: "head-mismatch",
290
- detail: "HEAD moved since the checkpoint; pass allowNonHeadRestore to override",
291
- });
292
- }
293
- return {
294
- verdict: issues.length === 0 ? "clean" : "blocked",
295
- issues,
296
- unrelatedDirtyPaths,
297
- hashMismatches,
298
- symlinkEscapes,
299
- headChanged,
300
- };
301
- }
302
- /**
303
- * Decide how an OMK-created untracked file should be handled on restore.
304
- *
305
- * - `skip`: the file is already gone, nothing to undo.
306
- * - `delete`: the file still exactly matches what OMK created, safe to remove.
307
- * - `quarantine`: the file changed since OMK created it; move it under the
308
- * recovery quarantine directory rather than destroying user edits.
309
- */
310
- export function decideUntrackedDisposition(touched, observed) {
311
- if (touched.mode !== "untracked") {
312
- return { disposition: "skip", reason: "disposition only applies to untracked files" };
313
- }
314
- if (!observed.exists) {
315
- return { disposition: "skip", reason: "untracked file is already absent" };
316
- }
317
- if (isHex64(touched.afterSha256) && observed.sha256 === touched.afterSha256) {
318
- return { disposition: "delete", reason: "current hash matches the OMK-created hash" };
319
- }
320
- return { disposition: "quarantine", reason: "current hash differs from the OMK-created hash" };
321
- }
322
- function codePhase(checkpoint) {
323
- return {
324
- phase: "code",
325
- action: "apply-reverse-patch",
326
- recordedPaths: checkpoint.workspace.touchedFiles.map((file) => file.path),
327
- };
328
- }
329
- function conversationPhase(checkpoint) {
330
- return { phase: "conversation", action: "navigate-tree", targetLeafId: checkpoint.session.leafId };
331
- }
332
- function collectPreflightBlockers(preflight) {
333
- if (!preflight)
334
- return ["code restore requires a preflight classification"];
335
- if (preflight.verdict === "clean")
336
- return [];
337
- return preflight.issues.map((issue) => (issue.path ? `${issue.code}: ${issue.path}` : issue.code));
338
- }
339
- /**
340
- * Build a restore plan for the requested mode.
341
- *
342
- * - `conversation`: navigate the session tree to the checkpoint leaf; no file
343
- * writes, so no worktree preflight is required.
344
- * - `code`: apply the recorded reverse patch path-by-path; requires a clean
345
- * preflight.
346
- * - `both`: restore code first, then navigate the tree; requires a clean
347
- * preflight. If the second (conversation) phase fails at execution time the
348
- * caller marks the operation `in_doubt` and keeps `preservePreviousLeafId`.
349
- */
350
- export function planRestore(input) {
351
- const { mode, checkpoint, preflight } = input;
352
- if (mode === "conversation") {
353
- return { mode, status: "ready", phases: [conversationPhase(checkpoint)], blockers: [] };
354
- }
355
- const blockers = collectPreflightBlockers(preflight);
356
- if (mode === "code") {
357
- if (blockers.length > 0)
358
- return { mode, status: "blocked", phases: [], blockers };
359
- return { mode, status: "ready", phases: [codePhase(checkpoint)], blockers: [] };
360
- }
361
- // mode === "both"
362
- const preservePreviousLeafId = input.currentLeafId ?? null;
363
- if (blockers.length > 0) {
364
- return {
365
- mode,
366
- status: "blocked",
367
- phases: [],
368
- blockers,
369
- preservePreviousLeafId,
370
- inDoubtPolicy: "preserve-previous-leaf",
371
- };
372
- }
373
- return {
374
- mode,
375
- status: "ready",
376
- phases: [codePhase(checkpoint), conversationPhase(checkpoint)],
377
- blockers: [],
378
- preservePreviousLeafId,
379
- inDoubtPolicy: "preserve-previous-leaf",
380
- };
381
- }
382
- //# sourceMappingURL=recovery-checkpoint.js.map