oh-my-second-brain 0.19.0 → 0.20.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 (259) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/CHANGELOG-assets.md +6 -0
  5. package/CHANGELOG-cli.md +16 -0
  6. package/CHANGELOG-kernel.md +38 -0
  7. package/CHANGELOG-mcp.md +15 -0
  8. package/CHANGELOG-vendors.md +7 -0
  9. package/CHANGELOG.md +4 -0
  10. package/README.ko.md +10 -10
  11. package/README.md +9 -9
  12. package/assets/claude/hooks/oms-guard.mjs +17 -2
  13. package/assets/hermes-manifest.json +1 -1
  14. package/assets/skills/doctor/SKILL.md +8 -3
  15. package/assets/skills/interview/SKILL.md +3 -4
  16. package/assets/skills/search/SKILL.md +2 -2
  17. package/assets/skills/setup/SKILL.md +13 -18
  18. package/assets/skills/write/SKILL.md +1 -1
  19. package/dist/cli/contract-command.d.ts +9 -0
  20. package/dist/cli/contract-command.js +117 -73
  21. package/dist/cli/contract-command.js.map +1 -1
  22. package/dist/cli/doctor-command.js +35 -2
  23. package/dist/cli/doctor-command.js.map +1 -1
  24. package/dist/cli/evolution-approve.d.ts +64 -0
  25. package/dist/cli/evolution-approve.js +138 -0
  26. package/dist/cli/evolution-approve.js.map +1 -0
  27. package/dist/cli/interview-command.js +2 -2
  28. package/dist/cli/lineage-command.d.ts +16 -0
  29. package/dist/cli/lineage-command.js +113 -0
  30. package/dist/cli/lineage-command.js.map +1 -0
  31. package/dist/cli/search.js +11 -0
  32. package/dist/cli/search.js.map +1 -1
  33. package/dist/cli/setup-command.js +5 -5
  34. package/dist/cli/setup-command.js.map +1 -1
  35. package/dist/cli/status-command.js +9 -0
  36. package/dist/cli/status-command.js.map +1 -1
  37. package/dist/cli/usage.js +6 -3
  38. package/dist/cli/usage.js.map +1 -1
  39. package/dist/cli/write-command.d.ts +2 -2
  40. package/dist/cli/write-command.js +19 -4
  41. package/dist/cli/write-command.js.map +1 -1
  42. package/dist/kernel/contract/audit.d.ts +1 -1
  43. package/dist/kernel/contract/audit.js +8 -4
  44. package/dist/kernel/contract/audit.js.map +1 -1
  45. package/dist/kernel/contract/contradiction.d.ts +26 -0
  46. package/dist/kernel/contract/contradiction.js +40 -0
  47. package/dist/kernel/contract/contradiction.js.map +1 -0
  48. package/dist/kernel/contract/digest.d.ts +15 -0
  49. package/dist/kernel/contract/digest.js +24 -0
  50. package/dist/kernel/contract/digest.js.map +1 -0
  51. package/dist/kernel/contract/gap-ledger.d.ts +132 -0
  52. package/dist/kernel/contract/gap-ledger.js +398 -0
  53. package/dist/kernel/contract/gap-ledger.js.map +1 -0
  54. package/dist/kernel/contract/gaps-report.d.ts +44 -0
  55. package/dist/kernel/contract/gaps-report.js +49 -0
  56. package/dist/kernel/contract/gaps-report.js.map +1 -0
  57. package/dist/kernel/contract/generation-snapshot.d.ts +56 -0
  58. package/dist/kernel/contract/generation-snapshot.js +245 -0
  59. package/dist/kernel/contract/generation-snapshot.js.map +1 -0
  60. package/dist/kernel/contract/guard-events.d.ts +4 -0
  61. package/dist/kernel/contract/guard-events.js +3 -1
  62. package/dist/kernel/contract/guard-events.js.map +1 -1
  63. package/dist/kernel/contract/interview-log.d.ts +23 -4
  64. package/dist/kernel/contract/interview-log.js +39 -10
  65. package/dist/kernel/contract/interview-log.js.map +1 -1
  66. package/dist/kernel/contract/interview-resume.d.ts +12 -3
  67. package/dist/kernel/contract/interview-resume.js +25 -11
  68. package/dist/kernel/contract/interview-resume.js.map +1 -1
  69. package/dist/kernel/contract/interview.d.ts +29 -56
  70. package/dist/kernel/contract/interview.js +95 -265
  71. package/dist/kernel/contract/interview.js.map +1 -1
  72. package/dist/kernel/contract/judge-write.d.ts +54 -14
  73. package/dist/kernel/contract/judge-write.js +65 -33
  74. package/dist/kernel/contract/judge-write.js.map +1 -1
  75. package/dist/kernel/contract/judge.d.ts +14 -2
  76. package/dist/kernel/contract/judge.js +39 -104
  77. package/dist/kernel/contract/judge.js.map +1 -1
  78. package/dist/kernel/contract/legacy.d.ts +15 -0
  79. package/dist/kernel/contract/legacy.js +9 -0
  80. package/dist/kernel/contract/legacy.js.map +1 -0
  81. package/dist/kernel/contract/lineage-health.d.ts +20 -0
  82. package/dist/kernel/contract/lineage-health.js +71 -0
  83. package/dist/kernel/contract/lineage-health.js.map +1 -0
  84. package/dist/kernel/contract/lineage.d.ts +164 -0
  85. package/dist/kernel/contract/lineage.js +304 -0
  86. package/dist/kernel/contract/lineage.js.map +1 -0
  87. package/dist/kernel/contract/loosening.d.ts +15 -11
  88. package/dist/kernel/contract/loosening.js +13 -102
  89. package/dist/kernel/contract/loosening.js.map +1 -1
  90. package/dist/kernel/contract/mutation.d.ts +56 -0
  91. package/dist/kernel/contract/mutation.js +83 -0
  92. package/dist/kernel/contract/mutation.js.map +1 -0
  93. package/dist/kernel/contract/redact.js +0 -9
  94. package/dist/kernel/contract/redact.js.map +1 -1
  95. package/dist/kernel/contract/revision.d.ts +10 -0
  96. package/dist/kernel/contract/revision.js +14 -0
  97. package/dist/kernel/contract/revision.js.map +1 -0
  98. package/dist/kernel/contract/scripted-interview.d.ts +1 -1
  99. package/dist/kernel/contract/scripted-interview.js +1 -1
  100. package/dist/kernel/contract/scripted-interview.js.map +1 -1
  101. package/dist/kernel/contract/state-dir.d.ts +3 -3
  102. package/dist/kernel/contract/state-dir.js +3 -3
  103. package/dist/kernel/contract/state-dir.js.map +1 -1
  104. package/dist/kernel/contract/status.d.ts +20 -7
  105. package/dist/kernel/contract/status.js +44 -8
  106. package/dist/kernel/contract/status.js.map +1 -1
  107. package/dist/kernel/contract/store.d.ts +131 -14
  108. package/dist/kernel/contract/store.js +237 -53
  109. package/dist/kernel/contract/store.js.map +1 -1
  110. package/dist/kernel/contract/types.d.ts +67 -18
  111. package/dist/kernel/contract/types.js +53 -12
  112. package/dist/kernel/contract/types.js.map +1 -1
  113. package/dist/kernel/contract/vault-id.d.ts +5 -1
  114. package/dist/kernel/contract/vault-id.js +10 -3
  115. package/dist/kernel/contract/vault-id.js.map +1 -1
  116. package/dist/kernel/conventions/frontmatter.js +25 -0
  117. package/dist/kernel/conventions/frontmatter.js.map +1 -1
  118. package/dist/kernel/conventions/note-exclude.d.ts +6 -9
  119. package/dist/kernel/conventions/note-exclude.js +29 -117
  120. package/dist/kernel/conventions/note-exclude.js.map +1 -1
  121. package/dist/kernel/doctor/evolution-ops.d.ts +48 -0
  122. package/dist/kernel/doctor/evolution-ops.js +173 -0
  123. package/dist/kernel/doctor/evolution-ops.js.map +1 -0
  124. package/dist/kernel/doctor/evolution-status.d.ts +31 -0
  125. package/dist/kernel/doctor/evolution-status.js +38 -0
  126. package/dist/kernel/doctor/evolution-status.js.map +1 -0
  127. package/dist/kernel/doctor/service.d.ts +23 -3
  128. package/dist/kernel/doctor/service.js +146 -2
  129. package/dist/kernel/doctor/service.js.map +1 -1
  130. package/dist/kernel/engine/retrieval/template-source.js +21 -16
  131. package/dist/kernel/engine/retrieval/template-source.js.map +1 -1
  132. package/dist/kernel/evolution/convergence.d.ts +28 -0
  133. package/dist/kernel/evolution/convergence.js +82 -0
  134. package/dist/kernel/evolution/convergence.js.map +1 -0
  135. package/dist/kernel/evolution/decision.d.ts +14 -0
  136. package/dist/kernel/evolution/decision.js +2 -0
  137. package/dist/kernel/evolution/decision.js.map +1 -0
  138. package/dist/kernel/evolution/evaluator.d.ts +47 -0
  139. package/dist/kernel/evolution/evaluator.js +34 -0
  140. package/dist/kernel/evolution/evaluator.js.map +1 -0
  141. package/dist/kernel/evolution/events.d.ts +28 -0
  142. package/dist/kernel/evolution/events.js +108 -0
  143. package/dist/kernel/evolution/events.js.map +1 -0
  144. package/dist/kernel/evolution/evolution-lock.d.ts +48 -0
  145. package/dist/kernel/evolution/evolution-lock.js +133 -0
  146. package/dist/kernel/evolution/evolution-lock.js.map +1 -0
  147. package/dist/kernel/evolution/evolve.d.ts +34 -0
  148. package/dist/kernel/evolution/evolve.js +80 -0
  149. package/dist/kernel/evolution/evolve.js.map +1 -0
  150. package/dist/kernel/evolution/human-approval.d.ts +37 -0
  151. package/dist/kernel/evolution/human-approval.js +37 -0
  152. package/dist/kernel/evolution/human-approval.js.map +1 -0
  153. package/dist/kernel/evolution/maker.d.ts +42 -0
  154. package/dist/kernel/evolution/maker.js +116 -0
  155. package/dist/kernel/evolution/maker.js.map +1 -0
  156. package/dist/kernel/evolution/mutation-direction.d.ts +31 -0
  157. package/dist/kernel/evolution/mutation-direction.js +113 -0
  158. package/dist/kernel/evolution/mutation-direction.js.map +1 -0
  159. package/dist/kernel/evolution/policy.d.ts +51 -0
  160. package/dist/kernel/evolution/policy.js +92 -0
  161. package/dist/kernel/evolution/policy.js.map +1 -0
  162. package/dist/kernel/evolution/rate-limit.d.ts +36 -0
  163. package/dist/kernel/evolution/rate-limit.js +59 -0
  164. package/dist/kernel/evolution/rate-limit.js.map +1 -0
  165. package/dist/kernel/evolution/request-state.d.ts +149 -0
  166. package/dist/kernel/evolution/request-state.js +374 -0
  167. package/dist/kernel/evolution/request-state.js.map +1 -0
  168. package/dist/kernel/evolution/revert.d.ts +30 -0
  169. package/dist/kernel/evolution/revert.js +98 -0
  170. package/dist/kernel/evolution/revert.js.map +1 -0
  171. package/dist/kernel/evolution/seal-gate-human.d.ts +29 -0
  172. package/dist/kernel/evolution/seal-gate-human.js +71 -0
  173. package/dist/kernel/evolution/seal-gate-human.js.map +1 -0
  174. package/dist/kernel/evolution/seal-gate.d.ts +70 -0
  175. package/dist/kernel/evolution/seal-gate.js +248 -0
  176. package/dist/kernel/evolution/seal-gate.js.map +1 -0
  177. package/dist/kernel/evolution/snapshot-contract.d.ts +20 -0
  178. package/dist/kernel/evolution/snapshot-contract.js +21 -0
  179. package/dist/kernel/evolution/snapshot-contract.js.map +1 -0
  180. package/dist/kernel/evolution/stage-consensus.d.ts +104 -0
  181. package/dist/kernel/evolution/stage-consensus.js +185 -0
  182. package/dist/kernel/evolution/stage-consensus.js.map +1 -0
  183. package/dist/kernel/evolution/stage-mechanical.d.ts +23 -0
  184. package/dist/kernel/evolution/stage-mechanical.js +84 -0
  185. package/dist/kernel/evolution/stage-mechanical.js.map +1 -0
  186. package/dist/kernel/evolution/stage-semantic.d.ts +35 -0
  187. package/dist/kernel/evolution/stage-semantic.js +80 -0
  188. package/dist/kernel/evolution/stage-semantic.js.map +1 -0
  189. package/dist/kernel/search/read-exact.d.ts +4 -2
  190. package/dist/kernel/search/read-exact.js +46 -6
  191. package/dist/kernel/search/read-exact.js.map +1 -1
  192. package/dist/kernel/write/ambiguity.d.ts +91 -0
  193. package/dist/kernel/write/ambiguity.js +147 -0
  194. package/dist/kernel/write/ambiguity.js.map +1 -0
  195. package/dist/kernel/write/coerce.d.ts +42 -0
  196. package/dist/kernel/write/coerce.js +254 -0
  197. package/dist/kernel/write/coerce.js.map +1 -0
  198. package/dist/kernel/write/conform.d.ts +9 -6
  199. package/dist/kernel/write/conform.js +71 -36
  200. package/dist/kernel/write/conform.js.map +1 -1
  201. package/dist/kernel/write/frame.d.ts +9 -5
  202. package/dist/kernel/write/frame.js +13 -14
  203. package/dist/kernel/write/frame.js.map +1 -1
  204. package/dist/kernel/write/live-templates.d.ts +63 -0
  205. package/dist/kernel/write/live-templates.js +104 -0
  206. package/dist/kernel/write/live-templates.js.map +1 -0
  207. package/dist/kernel/write/payload.d.ts +22 -9
  208. package/dist/kernel/write/payload.js +16 -4
  209. package/dist/kernel/write/payload.js.map +1 -1
  210. package/dist/kernel/write/pipeline.d.ts +55 -4
  211. package/dist/kernel/write/pipeline.js +146 -27
  212. package/dist/kernel/write/pipeline.js.map +1 -1
  213. package/dist/kernel/write/receipt.d.ts +38 -4
  214. package/dist/kernel/write/receipt.js +9 -4
  215. package/dist/kernel/write/receipt.js.map +1 -1
  216. package/dist/mcp/server.d.ts +1 -0
  217. package/dist/mcp/server.js +12 -8
  218. package/dist/mcp/server.js.map +1 -1
  219. package/dist/mcp/tools/doctor.js +18 -0
  220. package/dist/mcp/tools/doctor.js.map +1 -1
  221. package/dist/mcp/tools/interview.js +25 -22
  222. package/dist/mcp/tools/interview.js.map +1 -1
  223. package/dist/mcp/tools/search.d.ts +5 -2
  224. package/dist/mcp/tools/search.js +8 -3
  225. package/dist/mcp/tools/search.js.map +1 -1
  226. package/dist/mcp/tools/status.js +16 -5
  227. package/dist/mcp/tools/status.js.map +1 -1
  228. package/dist/mcp/tools/write.d.ts +3 -2
  229. package/dist/mcp/tools/write.js +5 -4
  230. package/dist/mcp/tools/write.js.map +1 -1
  231. package/dist/vendors/claude/hook/pre-tool-use.d.ts +19 -2
  232. package/dist/vendors/claude/hook/pre-tool-use.js +69 -18
  233. package/dist/vendors/claude/hook/pre-tool-use.js.map +1 -1
  234. package/docs/architecture.md +7 -7
  235. package/docs/cli-map.md +23 -15
  236. package/docs/conventions.md +7 -8
  237. package/docs/migration-0.19.md +1 -1
  238. package/docs/verified-target.md +1 -1
  239. package/package.json +3 -2
  240. package/skills/doctor/SKILL.md +8 -3
  241. package/skills/interview/SKILL.md +3 -4
  242. package/skills/search/SKILL.md +2 -2
  243. package/skills/setup/SKILL.md +13 -18
  244. package/skills/write/SKILL.md +1 -1
  245. package/dist/kernel/contract/contract-vault-fixture.d.ts +0 -45
  246. package/dist/kernel/contract/contract-vault-fixture.js +0 -53
  247. package/dist/kernel/contract/contract-vault-fixture.js.map +0 -1
  248. package/dist/kernel/contract/drift.d.ts +0 -6
  249. package/dist/kernel/contract/drift.js +0 -27
  250. package/dist/kernel/contract/drift.js.map +0 -1
  251. package/dist/kernel/contract/interpretation-fixture.d.ts +0 -5
  252. package/dist/kernel/contract/interpretation-fixture.js +0 -97
  253. package/dist/kernel/contract/interpretation-fixture.js.map +0 -1
  254. package/dist/kernel/contract/interpretation.d.ts +0 -102
  255. package/dist/kernel/contract/interpretation.js +0 -211
  256. package/dist/kernel/contract/interpretation.js.map +0 -1
  257. package/dist/kernel/search/morning-test-fixtures.d.ts +0 -7
  258. package/dist/kernel/search/morning-test-fixtures.js +0 -41
  259. package/dist/kernel/search/morning-test-fixtures.js.map +0 -1
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "oms",
12
12
  "description": "Oh My Second Brain convention layer for Obsidian vaults — capture, retrieve, and validate knowledge under a declared semantic convention.",
13
- "version": "0.19.0",
13
+ "version": "0.20.0",
14
14
  "author": {
15
15
  "name": "gobeumsu",
16
16
  "email": "gobeumsu@gmail.com"
@@ -37,5 +37,5 @@
37
37
  ]
38
38
  }
39
39
  ],
40
- "version": "0.19.0"
40
+ "version": "0.20.0"
41
41
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oms",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Oh My Second Brain convention layer for Obsidian vaults — six shared skills and four MCP tools under a user-owned contract.",
5
5
  "author": {
6
6
  "name": "gobeumsu"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oms",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Oh My Second Brain convention layer for Obsidian vaults — Codex native rules, skills, and MCP adapter.",
5
5
  "_note": "oms setup host install writes Codex MCP config and provenance, installs ~/.codex/rules/oms.md, and installs the six shared skills under ~/.codex/skills/oms-*.",
6
6
  "skills": "./assets/skills/",
@@ -4,6 +4,12 @@ Skills, agents, templates, and host guidance changes belong here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.20.0] - 2026-09-30
8
+
9
+ - **The doctor skill covers contract evolution.** It describes `evolve`, `evolve-verdict` and `revert-propose`, requires the maker's session on `evolve` and keeps the maker from judging its own request, explains the stage-2 refusal of an overlapping or drifting candidate, says a revert always requires owner approval, says the quorum is host-attested and cannot be verified by OMS, which is why autonomy is off by default, tells a host without independent evaluator subagents to stop and send the owner to `oms setup`, and sends `reclaim-evolution-lock` and `lineage-reanchor` to the owner's terminal.
10
+ - **The setup and interview skills cover folders and properties only.** The setup skill drops the template-interpretation steps (`--interpretations`, `observedHash`, `interpretation-required`, `interpretation-rejected`) and describes `oms setup extract --template <name>` as a scaffold preview; the interview skill drops the `interpretations` parameter. The write, search and doctor skills describe templates as live files in the template folder that scaffold new notes and are never judged.
11
+ - **The setup skill no longer describes a `template-tightened` refusal.** Reseal no longer refuses a template answered more strictly, because the judge never reads a template.
12
+
7
13
  ## [0.19.0] - 2026-09-28
8
14
 
9
15
  - **Breaking: six shared skills.** `write`, `search`, `interview`, `distill`, `setup`, and `doctor`. The `link` skill is folded into `search` (suggest) and `doctor` (check), the `status` skill into `doctor` `op: "status"`, and the new `interview` skill reads the pending questions without sealing. Every skill and host guidance file uses the 0.19 command spellings.
package/CHANGELOG-cli.md CHANGED
@@ -4,6 +4,22 @@ Changes to the `oms` command surface belong here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.20.0] - 2026-09-30
8
+
9
+ - **The `oms doctor` evolution usage text is built from one flag table.** Each leaf's flag and its placeholder (`<id>`, `<file|->`, `<digest>`) now live together in `src/cli/lineage-command.ts`, and the printed usage is unchanged.
10
+ - **`oms doctor evolve`, `evolve-verdict`, `revert-propose` and `reclaim-evolution-lock` drive contract evolution.** `evolve --maker-session <id>` opens one evolution request from the open gaps and seals nothing; the maker session is required. `evolve-verdict --verdict <file|->` submits one evaluator verdict as JSON and runs the seal-gate. `revert-propose --target <digest>` proposes a kept generation as a new forward candidate; a revert always requires owner approval, so it is approved or rejected at `oms setup`. `reclaim-evolution-lock` releases a stale evolution lock after the owner confirms at a terminal; `lineage-reanchor` now also asks the owner and is refused without a terminal. `oms doctor status` prints the `evolution` section.
11
+ - **`oms setup` reviews pending contract evolutions, and `oms setup --autonomy on|off` controls autonomous sealing.** Before the interview, each request awaiting the owner is shown with its loosening changes marked and is sealed only on an explicit approve; a reject closes it. Autonomy is off by default and turned on only at a terminal.
12
+ - **`oms doctor contract` suggests setting `templateFolder` for an older vault.** When an older (version 1 or 2) generation sealed templates and `.oms/settings.json` names no `templateFolder`, it lists `template-folder-unset: set "templateFolder" to "<folder>" in .oms/settings.json so templates scaffold new notes`, naming the folder only when every sealed template shares it. It is a suggestion only: nothing is written, and it appears only beside `legacy-template-constraints-ignored`, which already exits 1, so no vault's exit code changes.
13
+ - **`oms setup` / `oms interview` seal folders and properties only.** Neither lists, asks about, previews or seals templates any more; templates in the `templateFolder` scaffold new notes and are never judged. A sealed result's JSON carries `warnings` when the seal raised any, such as `CONTRACT_LEGACY_TEMPLATES_DROPPED` for a reseal that drops an older generation's templates, and no longer has a `templates` field. `--interpretations` is removed and is now an unknown argument (`CONTRACT_ARGS_INVALID`), and the `interpretation-required` and `interpretation-rejected` statuses are gone. `oms setup extract --template <name>` now previews what a live template would scaffold, printing `{status: "scaffold", name, source, folder, properties, headings}`, or `{status: "missing", template, remediation}` with exit 1 for an unknown name; it no longer prints a source hash.
14
+ - **`oms doctor contract` reports `legacyTemplates` instead of `templates`.** The status payload's `templates` field is replaced by `legacyTemplates`, the number of template constraints an older (version 1 or 2) contract generation still carries and the judge now ignores; a new contract generation reports `0`. A non-zero count is also listed as the finding `legacy-template-constraints-ignored: N` with `oms setup` as guidance.
15
+ - **`oms write` prints warnings and fixes.** After the JSON result, a saved write prints `[oms] warnings: [{field, kind}]` and `[oms] fixed: [{field, kind}]` to stderr when either is non-empty, and the receipt's `fixes` now lists the lossless fixes the write applied instead of dropped keys. A write with an unknown key or an out-of-rule value is saved as written rather than drafted; only unparseable frontmatter is drafted.
16
+ - **`oms search` stops looking for `--vault` after a `--` terminator.** A query such as `oms search --vault notes -- --vault elsewhere` searches `notes` for the text `--vault elsewhere` instead of reading the second `--vault` as the flag. `oms search --link <note>` refuses a `--` terminator with `SEARCH_ARGS_INVALID` rather than forwarding the tokens after it to link suggestion as flags.
17
+ - **`oms doctor gaps` reports open contract gaps and contradictions.** It prints the same read-only report as the MCP `doctor` `op: gaps`. It exits 1 only when the contract contradicts itself, the ledger cannot be read, or a ledger line is corrupt; open gaps are the ledger doing its job, and `ledger: "truncated"` is a warning that leaves the exit code alone. `oms write` follows the new write pipeline, so a write that only adds keys the frame has no place for is saved without them and reports the dropped fields in its receipt. `oms write --check` prints the `resolution` the write would apply.
18
+ - **`oms write` saves or drafts where it used to refuse.** Only a safety refusal denies a write, reported as `status: "denied"` with `refusals`. In a sealed vault, only a note whose frontmatter does not parse is kept as a draft and printed as `status: "drafted"` with a `draftRef`; like a denial it exits 1. Every other write is saved, with lossless fixes applied and the rest kept as written. A saved note's receipt lists the saved note's `warnings` and, as `fixes`, the `{field, kind}` of each lossless fix it applied, and `--check` prints `refusals`, `warnings` and `fixes`. `oms doctor contract` prints a `hint` with a transport failure when the installed guard needs `oms setup host sync`.
19
+ - **`oms doctor lineage-recover` and `oms doctor lineage-reanchor` repair the contract lineage.** They print the same result as the MCP `doctor` ops of the same name, and they take `--vault <path>` or resolve the vault the usual way. A `cwd`-inferred vault is rejected. A refused gap, a vault that is not sealed, or bad arguments exit 1. `oms doctor contract` now reports a `lineage` field: events, snapshots, snapshot bytes, and findings, each naming the repair it needs. It exits 1 when a finding needs attention. A store sealed before the lineage existed, a snapshot kept from a crashed seal, and a cut-short last line are reported but do not fail the check.
20
+ - **Lineage repair failures no longer leak paths or unknown codes.** When `oms doctor lineage-recover` or `oms doctor lineage-reanchor` fails, a diagnostic keeps the error's code only when it is a real `CODE:` prefix; any other failure, including a thrown non-Error value, reports `CONTRACT_LINEAGE_REPAIR_FAILED`. Absolute paths in the message, quoted ones with spaces included, are printed as `<path>`.
21
+ - **`oms search --context` refuses a `--` terminator.** Context retrieval takes only flags, and the terminator used to reach its flag parser as the token `--`, which failed with the unclear `unknown context flag --`. It now fails with `--context does not accept a -- terminator`, the same way `--link` does.
22
+
7
23
  ## [0.19.0] - 2026-09-28
8
24
 
9
25
  - **`oms write` takes `--if-match sha256:<rev>` and `--check`.** Overwriting an existing note without `--if-match` exits 1 with `WRITE_IF_MATCH_REQUIRED` and leaves the file unchanged; a stale revision reports the retryable `WRITE_TARGET_CHANGED`, and `--if-match` for a note that does not exist reports the retryable `WRITE_TARGET_ABSENT`. `--check` prints the frame, the note's current revision and any violations and writes nothing, not even an index row. Each flag may be given once, and `--if-match` requires a value. The receipt is the one MCP `write` returns.
@@ -4,6 +4,44 @@ Domain logic changes belong here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.20.0] - 2026-09-30
8
+
9
+ - **The vault contract can evolve from its write gaps, and only one seal-gate may seal it.** `src/kernel/evolution/` turns the open gaps into one maker-drafted evolution request (`evolve.ts`, `maker.ts`) with a pinned candidate, a nonce and three evaluator slots, held under an evolution lock with its own journal (`events.ts`). `seal-gate.ts` is the only path that seals an evolved candidate; the only other product caller of `sealContract` is the interview. Stage 1 (`stage-mechanical.ts`) judges the vault's existing notes against the candidate: any new refusal rejects it, and the warning delta is its score, reported on every request. A candidate that raises warnings, or that loosens the contract (an added allowed value or folder, a widened type, a removed `required`, as classified by `mutation-direction.ts`), waits for the owner. Stage 2 (`stage-semantic.ts`) is a hard gate too: a touched entry whose meaning overlaps an existing one (similarity 0.9 or more) or a candidate that drifts more than 0.3 from the first sealed generation is rejected, so `evolve` issues nothing (`EVOLUTION_STAGE2_REFUSED`) and the router and the autonomous seal-gate refuse it and journal `request.rejected`. Stage 3 is a 2-of-3 quorum of evaluator verdicts from an injected `VerdictProvider`, waiting for all three bound verdicts, and its reject is effective: the same request never seals on a rerun. A provider without subagents is `EVALUATOR_CONSENSUS_UNAVAILABLE` before anything is asked or written. `evolve` requires the maker's session id (`EVOLUTION_MAKER_SESSION_REQUIRED`), and a verdict whose session repeats another evaluator's or the maker's is discarded. The quorum is host-attested: OMS cannot verify that the evaluators were independent subagents, so autonomy is off by default and an autonomous seal records `quorum: "host-attested"` in its journal event and lineage event. An autonomous seal needs the opt-in policy (`policy.ts`, off by default, turned on only interactively, limits of 1 a day and 3 a week that can only be lowered), the quorum, a warning delta of 0 or less (a negative delta qualifies) and the rate limit (`rate-limit.ts`). The owner decides approve or reject through `human-approval.ts` and `seal-gate-human.ts`, which seal through the same gate. `revert.ts` proposes a kept generation as a new forward candidate, read only from its snapshot and projected to the version 3 form. A revert always requires owner approval: it has no maker, so no host quorum can bind to it, and every revert (tightening, neutral or loosening, with autonomy on or off) goes to `awaiting-human` for the owner to approve at `oms setup`; a verdict on it is refused with `EVOLUTION_REQUEST_CLOSED`, and the owner's seal records the proposer as `owner`. Stage 1 and stage 2 both run when a revert is proposed, so a revert that adds a refusal (`EVOLUTION_REVERT_REFUSED`) or overlaps or drifts past 0.3 (`EVOLUTION_STAGE2_REFUSED`) proposes nothing. The `maker-unknown` discard applies only to `evolve` requests. The judge is unchanged.
10
+ - **`doctor` runs the evolution ops and reports evolution in status.** `src/kernel/doctor/evolution-ops.ts` runs `evolve`, `evolve-verdict`, `revert-propose` and `reclaim-evolution-lock` on a verified target, each checking a postcondition read back from the store (the request state, the pinned candidate digest, the slot used exactly once, the linked generation unmoved or equal to the sealed candidate, the lock gone). A postcondition that does not hold throws `EVOLUTION_POSTCONDITION_FAILED`, or `EVOLUTION_POSTCONDITION_FAILED_AFTER_SEAL` naming the sealed digest and generation when the seal already happened. `reclaim-evolution-lock` needs the owner at a terminal. `src/kernel/doctor/evolution-status.ts` gives `doctor status` an `evolution` section: journal counters, requests awaiting the owner, the autonomous budget left today and this week, and whether the lineage has a gap, and `quorum: "host-attested"`, how an autonomous seal's quorum is attested. It is read-only and `null` for an unsealed vault.
11
+ - **The seal boundary is enforced.** `test/architecture/write-evolution-boundary.test.ts` now fails when a new module names `sealContract`, a test fixture is reachable from an entrypoint, anything but `src/cli/evolution-approve.ts` imports the approval modules, the approval module reads stdin or seals, the maker or evaluator stages reach the write kernel, or `kernel/contract` imports `kernel/evolution`. Non-literal dynamic imports in `src/` fail the gate.
12
+ - **An empty or comment-only frontmatter block is empty frontmatter, not malformed YAML.** `parseNote` now reads `---` followed directly by `---`, or a block that holds only comments, as a note with no properties instead of a `yaml-syntax` draft. A missing required property whose rule fixes its value, a new note's date defaults and a template's frontmatter defaults are now filled into such a block, and the inserted lines sit on their own line before the closing fence. Other readers change with it: building the axes store no longer fails on such a note, a live template whose frontmatter is `---` then `---` now loads and can add a template choice for its folder, and `oms doctor audit` no longer reports these notes as `yaml-syntax`. The block closes at the first fence, so in `---`, `---`, `k: v`, `---` the `k: v` line is body text, as Obsidian reads it; a test pins this.
13
+ - **A discarded coerce pass no longer uses up a fix pass.** `resolveTiers` in `src/kernel/write/ambiguity.ts` counts only accepted passes against its three fix passes, and a separate hard cap of twelve attempts still bounds the loop.
14
+ - **The interview types a template key Obsidian has not typed from its value.** A key set only by the live templates is offered as a `list` (a `-` item block or `[...]`), a `date` (`YYYY-MM-DD` or `{{date}}`) or a `datetime` (`YYYY-MM-DDTHH:mm` or its `{{date:...}}` format) when its value clearly is one, and as `text` otherwise or when templates disagree. An Obsidian property type still wins.
15
+ - **Scaffolded headings keep the note's line endings.** Conform appends a template's missing headings to a CRLF note with CRLF, not LF.
16
+ - **Scaffolding skips Templater tags, and the interview offers the keys templates set.** Conform never runs Templater, so a template value or heading that holds a `<% ... %>` tag is left out of a new note, the same as an unfilled `{{...}}`, instead of being saved as literal tag text. A multi-line template value is inserted with the note's own line endings, so a CRLF note stays CRLF. The interview now also offers each frontmatter key the templates in the template folder set as a property to register, typed as Obsidian types it or else `text`, so a vault whose `status` appears only in templates is asked about it; templates themselves are still never asked about or sealed. `oms doctor contract` adds the finding `template-folder-unset` when an older generation sealed templates but `.oms/settings.json` names no `templateFolder`, suggesting the folder those templates share; it writes nothing.
17
+ - **New contract generations no longer store templates; older generations still load and their template constraints are reported, not enforced.** The store now writes manifest version 3, which lists only `folders.json`, `properties.json` and an optional `declined.json` (`{version: 2, folders, properties}`); no `templates/` directory is written. Version 1 and 2 generations stay readable permanently and their digests are unchanged: their templates are projected into `view.legacy.templates` on read and never persisted. `VaultContract` no longer carries templates (the old shape is `LegacyTemplateContract`), the judge never enforces them, and template drift detection (`drift.ts`) is removed. `oms doctor contract` reports `legacy-template-constraints-ignored: N` for a head that still carries them, and a revert or reseal writes a new version 3 head rather than rewriting an old generation. Neither the judge nor the write decision reads an older generation's templates: their required properties, narrowed rules and required headings no longer shape a verdict, a fix or a contradiction. A newly sealed vault carries no templates; templates scaffold from `templateFolder` instead (see below).
18
+ - **Unknown keys and out-of-rule values are saved as written and recorded; lossless type and spelling fixes are applied with the original kept in the ledger.** `src/kernel/write/coerce.ts` fixes a new warning only when the contract reads the value the same way: a number or checkbox written as a string, a scalar for a list, a midnight datetime for a date, a number for text, an allowed value that differs only in Unicode form, spacing or case and matches exactly one allowed value, and a missing property whose rule fixes its value (or, on a new note, a date or datetime default). The fixed note is judged again, `decideWrite` returns it as `fixedContent`, and each fix is recorded as a `fixed` gap whose `wanted` is the value as written. Every other new warning, including an unknown key, an unmatched allowed value, a pattern, range or count miss, and an unregistered folder, is saved as written and recorded as a `kept` gap. Only frontmatter that does not parse is kept as a draft. The key-dropping repair is removed (`DROPPABLE`, `droppableKeys`, `dropFrontmatterKeys`, `requiredBy`), so nothing a note says is silently deleted. Coerce fills a required property whose rule fixes its value when it is missing, and conform adds a non-required default whose only rule fixes its value on a new note; an untyped property takes that value as a single value, not a list. A fix changes only the bytes of the fixed value, so every other key keeps its exact spelling, and a number is fixed to text only when its source text is exactly that number (`01234`, `1.0`, `0x1F` and integers past 2^53 are kept, and the ledger records them as written). A tagged or anchored value is kept as written, and a fix counts only when the judged note no longer reports it. The guard hook never rewrites a tool call: a fix it would make stays a warning and is recorded as `kept`.
19
+ - **A reseal drops legacy templates with a warning.** Resealing a version 1 or 2 head, interactively or through the agent (`nonLoosening`) path, is not refused over its templates: it seals, shows and returns the warning `CONTRACT_LEGACY_TEMPLATES_DROPPED: N template(s) sealed by an older generation are not carried forward; templates now scaffold new notes from the template folder and are never judged`, and records `droppedLegacyTemplates: N` on the `proposed` interview event. The seal guard and the unsafe-pattern prompt look only at properties, so a template never refuses a seal or asks a question.
20
+ - **Templates scaffold new notes from `templateFolder`, and are never sealed or judged.** `src/kernel/write/live-templates.ts` reads the Markdown templates in the `templateFolder` recorded in `.oms/settings.json` at write time, so an edited template takes effect on the next write without a reseal. The write pipeline picks the template the write names; otherwise the one template whose basename or `folder:` key matches the target folder; when two or more match it records a template choice and scaffolds nothing; when none match it scaffolds nothing. A named template that is not there scaffolds nothing and is reported as a `template-missing` conform entry, and the write proceeds. On a new note, conform adds the template's frontmatter defaults where the note has no value and appends its missing headings; the note's own values win, and a scaffolded key outside the property pool is saved and warned as `unknown-property` like any other. The frame's `required` comes only from the property contract. The interview asks about folders and properties only; `templateFolder` stays in settings. Retrieval's template axis and template-source paths now come from the live templates of a sealed vault. The template-interpretation input (`interpretations`, `observedHash`) and its modules are removed, and the sealed result no longer carries `templates` or `removedTemplates`.
21
+ - **The judge no longer compares a note with its previous version or a selected template; `template-mismatch`, `folder-mismatch` and `heading-missing` are retired.** `judge` in `src/kernel/contract/judge.ts` now depends only on the contract, the note path and its frontmatter: `JudgeInput` drops `selectedTemplate` and `previousContent`, and `decideWrite` drops its template argument for judging. A `template:` frontmatter key is judged like any other property. The unsubstituted-variable check no longer reads the note body; only frontmatter values are checked. What an edit newly breaks is still decided in `decideWrite`, which judges the note on disk as a baseline and records only warnings the edit added. Reseal loosening follows: a template answered more strictly or more loosely, or a template whose apply folder changes or overlaps another, is no longer reported, since the judge never reads a template. Removing a sealed template or moving its source is no longer refused as loosening either: a reseal writes a version 3 head, which stores no templates, so every legacy template is dropped with a warning (see below).
22
+ - **An exact read refuses a file larger than 16 MiB, and a directory is `READ_EXACT_NOT_FILE` on every platform.** `readExact` in `src/kernel/search/read-exact.ts` checks the size of the opened handle and refuses a file over `READ_EXACT_MAX_BYTES` with `READ_EXACT_TOO_LARGE` before reading it. The read itself stops at the same cap, so a file that grows after the check cannot exhaust memory. An `EISDIR` raised when opening or reading, as Windows does for a directory, is now `READ_EXACT_NOT_FILE` instead of an I/O error.
23
+ - **A write that only misses the frame is saved, and the gap it exposed is recorded.** `src/kernel/write/ambiguity.ts` sorts a judged write into tiers. An accepted note is saved as written. When more than one sealed template applies to the folder and none was chosen, the recommended one is recorded as a `choice` gap. A new warning is fixed when the fix is lossless and recorded as a `fixed` gap; every other new warning, including a missing property, an unregistered folder and a field the contract contradicts itself about, is saved as written and recorded as a `kept` gap. Only frontmatter that does not parse is kept as a draft beside the ledger, recorded as a `no-fit` gap. The judge is never bypassed: a fixed note is judged again, and nothing is saved that it refuses.
24
+ - **Gaps live in an append-only ledger outside the vault.** `src/kernel/contract/gap-ledger.ts` appends `{id, at, notePath, noteRevision, contractRevision, axis, kind, chosen, wanted, reason, draftRef?}` events to `<root>/.<id>.state/gaps/events.jsonl` through the state-dir helpers, so it inherits their symlink, ownership and 0600 checks. A cut-short or malformed line is skipped, reported by line number and never rewritten, and the next append starts on a fresh line. The trailing-newline check reads one byte, so an append costs the same however large the ledger grows. A resolved gap stays closed, and replaying the ledger always gives the same open set. A template choice gap (②) has a deterministic id derived from the note path, the sorted candidates and the contract revision, and is appended once per ledger: repeating the same ambiguous write records nothing new, and a resolved choice stays closed until the contract or the candidates change. The `.seen` markers that remember a choice are named after the current ledger by a hash of its first line, so after the ledger is moved aside the next repeat of a choice lands in the fresh ledger, and the first append to it deletes the old markers. A marker also records where its event starts, so a choice that has fallen out of the 16 MB read window is appended again on its next repeat. Markers written before this change use the old `<id>.seen` name and no longer count, so each open choice is appended once more on its next repeat; a resolved choice stays closed because the fold keeps its `resolved` event, and the old markers are deleted at the next rotation. Writing a marker is best effort, the same as pruning one: it runs only after its event is fsynced, so a failed write (a state-dir security check, a full disk) no longer rejects `recordGaps` and never loses the event, only re-appending that choice once more on its next repeat. Appends never refuse on size. A read past 16 MB reads only the newest 16 MB from its first whole line and returns `truncated: true`, keeping corrupt line numbers counted from the start of the file; to rotate, move `events.jsonl` aside and the next append starts a fresh ledger. Drafts are 0600 files named `draft-<uuid>.md`, and a ref of any other shape is refused.
25
+ - **The write pipeline reads the seal state once.** The judge, every gap a write records and the receipt all name the one `contractRevision` derived from that read, so a seal that lands mid-write cannot split a write across two revisions. `src/kernel/contract/revision.ts` computes the revision; it is the one place to switch to the seal manifest digest later. A written receipt adds `gaps: [{id?, axis, kind, field}]`. When the note was saved but its gaps were not recorded, the receipt still lists every dropped field without an id and adds `gapLedger: "failed"` (the append failed) or `gapLedger: "unavailable"` (the vault has no id to key a ledger). `check` now runs the same ambiguity resolution and reports `resolution: {action, gaps, wouldDraft, precondition?}`, fields only, while still writing no note, draft or ledger entry. The `ifMatch` check runs before a draft is kept, so a stale or missing `ifMatch` keeps no draft and records no gap. `check` folds that in: it reports `precondition: "if-match-required"`, `"changed"` or `"absent"` with `wouldDraft: false` when the write would stop there first, and a draft whose gaps could not be recorded still returns its `draftRef`. `oms doctor gaps` reports `ledger: "truncated"` as a warning when it read only the newest window.
26
+ - **A `count` rule bounds how many values a property holds.** `{kind: "count", min?, max?}` counts list members, and a scalar counts as one. An empty value counts as absent, so only `required` reports it. Narrowing a count is a tightening; widening, unbounding or removing one is reported as loosening. The contract store accepts a `count` rule whose bounds are whole numbers of zero or more and that carries no other key; any other shape still reads as unreadable and fails closed.
27
+ - **Contract mutations and contradictions are pure functions.** `src/kernel/contract/mutation.ts` applies `ADD`, `MODIFY` and `REMOVE` mutations on the folder, property, template and rule axes without changing its input. It refuses the whole list at the first conflict with `CONTRACT_MUTATION_CONFLICT`, naming the index, axis, key and kind, including when `before` does not match the parent. `src/kernel/contract/contradiction.ts` finds rules no value can satisfy: inverted count or range bounds, an empty allowed set, a fixed value outside the allowed set, and a template that requires an unregistered property. It reports each one by field and kind, never by value.
28
+ - **Only safety refuses a write; every other finding is a warning.** The judge's `Verdict` now splits findings into `refusals`, `warnings` and `fixes`, and `ok` means no refusals (`violations` still lists the refusals for older callers). Refusals are control paths, unsafe paths, paths outside the vault, unsupported input, and a tampered contract. A tampered contract is now its own kind, `contract-tampered`, and is reported only when `.oms/settings.json` names a vault id that differs from this machine's index. A missing or invalid `.oms/settings.json`, an index without a store, a damaged store, a seal that cannot be resolved, or an existing note that cannot be read no longer refuses: the write goes ahead with a `contract-unreadable` warning whose guidance is `oms interview`. An open vault writes with a `contract-open` warning. The shared `decideWrite` in `src/kernel/contract/judge-write.ts` decides MCP and hook writes alike. A warning counts as new only when the note's previous content did not already have it; in a sealed vault a write with new warnings on the frame is kept as a draft and its gaps recorded, a write that can only drop keys it added is still saved without them, and a contradiction or a draft that cannot be kept saves the note as written and records the gap. A written receipt adds `warnings` (the full set on the note as saved) and `fixes` (the keys a repair dropped). `oms doctor contract` adds a `hint` to its transport failures when the guard dropped a malformed judge answer, telling the user to run `oms setup host sync`.
29
+ - **Every seal is recorded in a contract lineage, and every sealed generation is kept as a snapshot.** `src/kernel/contract/lineage.ts` appends one event per seal to `<root>/.<id>.state/lineage/events.jsonl` under the seal lock. Each event names the generation it replaced (`parentDigest`) and the one it installed (`digest`). Both are the sha256 of the generation's exact `manifest.json` bytes (`src/kernel/contract/digest.ts`), so neither depends on the store sequence. `src/kernel/contract/generation-snapshot.ts` writes each sealed generation once to `generations/<hex>/` and never changes or removes it. Two generations with the same manifest share one snapshot. The store still keeps only N and N-1, and the snapshots are how an older generation is read back. A seal now publishes its snapshot, swaps the link, appends its lineage event, and only then writes the index and runs the GC. A failed append throws `CONTRACT_LINEAGE_APPEND_FAILED` and skips both. A caller can pass `expectedParentDigest`: if the linked generation no longer has that digest, the seal aborts with `CONTRACT_SEAL_CHANGED`, which also catches a replacement at the same sequence. When the lineage does not end at the linked generation, the seal records an anchor event before its own. That anchor is `bootstrap`, `unrecorded-seal`, `seq-restart` or `gap-anchor`. Under `lineageGapPolicy: "refuse"` a gap instead throws `CONTRACT_LINEAGE_GAP` and nothing is written. The first seal on an existing store bootstraps the lineage from the retained N and N-1. **Generations N-2 and older that the GC removed before that first seal cannot be recovered**: the lineage starts at the oldest generation still on disk.
30
+ - **A lineage whose last line was cut short is repaired on the next append.** The cut line is reported as `lineage-truncated-tail`, and a read ignores it. The next append truncates the file back to the last whole line before it writes, so the partial line is dropped instead of being joined to the new event. Before this fix, the new event was glued to the fragment and both became unreadable. `src/kernel/contract/lineage-health.ts` reports the lineage read-only: events, snapshots, snapshot bytes, and findings, each with the repair it needs or none.
31
+ - **The contract revision is now the seal's manifest digest.** `contractRevision` returns the manifest digest of the generation the contract was read from, which is the digest the lineage records and the name of its snapshot, so a receipt or gap names a revision you can look up in the lineage. A resealed but unchanged contract keeps its revision. A sealed view built in memory, with no generation behind it, still falls back to the digest of the contract itself.
32
+ - **`lineage-recover` finishes a first seal whose lineage append failed.** When the append fails on a vault's first seal, the link is already swapped but `index.json` is never written, so the vault reads as `store-without-index` (or `vault-moved`). Recovery used to refuse that vault as not sealed. It now appends the missing lineage events and then writes the index entry, both under the seal lock, so the vault reads as sealed again. A vault whose store link is missing is still refused with `CONTRACT_NOT_SEALED` and gets no index entry. A `vault-moved` vault whose original path still exists is a copy sharing the vault id, so recovery refuses it with `CONTRACT_VAULT_ID_SHARED` instead of claiming the id, as the interview already does. The recovery postcondition, that no lineage finding needing repair remains and the vault reads as sealed, is now checked under the same lock, so a concurrent seal cannot change what it verifies. When it fails, the repair throws instead of returning a receipt.
33
+ - **The interview keeps the template folder when the lineage append fails.** The seal itself landed, so `.oms/settings.json` still records `templateFolder` before `CONTRACT_LINEAGE_APPEND_FAILED` reaches the caller (if that settings write also fails, the append failure is still what is thrown), and `lineage-recover` completes the rest. A seal that fails before the link swap still writes nothing.
34
+ - **A seal lists snapshots by name alone.** `snapshotDigests` reads `generations/` with one `readdir` and no per-snapshot size walk, so seal cost no longer grows with snapshot bytes. `snapshotInventory`, with sizes, is used only by the doctor.
35
+ - **A pending interview log is read in the order its move gives it.** When a vault has an id and a pending log is still beside the id log, `vaultLog` in `src/kernel/contract/interview-resume.ts` used to put the pending events first, while `migrateInterviewLog` appends them after the id log. Both now put the id log first and the pending events after it, so resuming an interview replays the same events before and after the move. When a cut-short move already copied some pending events, `vaultLog` lists them once, as the retried move will leave them.
36
+ - **Retrying a cut-short pending-log move no longer copies events twice.** `migrateInterviewLog` in `src/kernel/contract/interview-log.ts` copies the pending events and then removes the pending log. A crash between the two used to copy every event again on the next run. It now skips the pending events that already end the id log. It matches on their time, type, question and payload, not on `seq`, because the move renumbers `seq`.
37
+ - **The proposal records the template folder chosen in its run.** The `proposed` interview event now carries `templateFolder` when this run chose one, so a retried seal can save a folder the first seal did not reach.
38
+ - **Correction to the 0.19.0 notes on the interview log.** Those notes say readers order interview events by line, not by `seq`. Events are read in line order, but `confirmedProposal` compares `seq` to decide whether a confirm came after the latest proposal. So `seq` is trusted within one writer's run and not across concurrent writers. The log's header comment now states this. It also lists the log's known limits: each append rereads the whole file (O(n²) over a run), a log over 16 MiB is refused on read and append and nothing rotates it, and there is no cross-process lock for appends or for the pending-log move.
39
+ - **Interview log line numbers now point into the right file.** `vaultLog` reports unparsable lines per file: `corrupt` for the log under the vault id and `pendingCorrupt` for a pending log kept before the first seal, each numbered as in its own file, the way `oms doctor contract` reports them. Before, both files' line numbers were merged into one `corrupt` list, so a number could name a line in either file. `resumableIO` passes both lists through, and the unreadable-line warning of `oms interview` and `oms setup` still counts both.
40
+ - **A refused revert says how to restore a drifted generation.** When `revert-propose` refuses a target that drifts more than 0.3 from the first sealed generation, the message now ends `nothing was proposed. A revert cannot restore it, so reseal that contract with \`oms setup\``. Only a human seal skips stage 2, so such a generation can only come back through the interview. The refusal code is unchanged.
41
+ - **A makerless evolve no longer seals autonomously.** An evolve request recorded before the maker session became required names no maker, so its quorum cannot exclude the proposer and the lineage could not name who proposed it. An autonomous seal of such a request is now refused with `EVOLUTION_MAKER_SESSION_REQUIRED`, nothing is sealed, and the request stays open; an owner can still approve it in a terminal, where it is recorded as proposed by `owner`. Reverts and human seals behave as before.
42
+ - **Evolution internals are simpler, with no change in behaviour.** `runEvolutionOp` in `src/kernel/doctor/evolution-ops.ts` dispatches its four operations through a `switch` instead of a nested ternary, and its test-only hooks (`afterSeal`, `afterPropose`) are declared in their own `EvolutionOpsTestHooks` interface, and its internal dispatcher takes one object instead of seven positional arguments. The seal-gate's proposer is the maker's session id or `owner`, without a fallback no request can reach. ADR-007 §5 has an amendment naming the 0.19 `write` input (`ifMatch`, `check`) and its receipt, pointing to `docs/cli-map.md`.
43
+ - **Test fixtures no longer ship in the package.** `legacy-store-fixture`, `contract-vault-fixture` and `morning-test-fixtures` moved from `src/kernel/` to `test/fixtures/`, so they are no longer compiled into `dist/` or published. An architecture test fails if a fixture module appears under `src/` again, and `npm run lint` now typechecks `test/fixtures/*.ts` through `tsconfig.fixtures.json`, since those modules left the `src/` build.
44
+
7
45
  ## [0.19.0] - 2026-09-28
8
46
 
9
47
  - **One write pipeline frames, conforms, judges, writes and indexes a note.** `runWritePipeline` in `src/kernel/write/pipeline.ts` replaces `verified-write`: it admits and resolves the target, builds the frame the note is judged in (`frame.ts`: sealed folder meaning, properties, the chosen template and its defaults), applies only mechanical fixes (`conform.ts`: `{{title}}`, `{{date}}` and `{{time}}` variables, date and datetime defaults on a new note, and the chosen template's missing headings), then runs the unchanged judge. Conform adds only that mechanical skeleton and those defaults: it never changes a value and never supplies a property the contract or a template requires (a date default is skipped for any name a template lists in `requiredProperties`, every template's when none is chosen), so a missing required property is still refused. The heading skeleton does, by design, satisfy a template's required headings, and inserted lines keep the note's CRLF or LF line endings. Overwriting an existing note needs `ifMatch`, the `sha256:` revision of its current bytes; without it the outcome is `if-match-required`, and a revision that no longer matches, a note that vanished, or an `ifMatch` sent for a note that does not exist (`absent`, retried without `ifMatch` to create it) is a retryable `retry`. `check` stops after the judge and reports the frame, the revision and the violations without touching disk. A written note returns the receipt built by `receipt.ts`, `{ok, path, revision, contractRevision, index: {keyword, vector}, conformed, missingDefaults}`, and `writePayload` in `payload.ts` shapes every outcome for both surfaces. `src/kernel/engine/index-update.ts` replaces the note's keyword rows in an existing engine store in the same call and queues its vectors in `engine_dirty`; it never creates a store, and doctor `sync-embeddings` drains the queue, reporting `queue: {drained, pending}`.
package/CHANGELOG-mcp.md CHANGED
@@ -4,6 +4,21 @@ MCP server tools and resources belong here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.20.0] - 2026-09-30
8
+
9
+ - **The `status` write posture is chosen by a plain `if` chain.** `writePosture` in `src/mcp/tools/status.ts` replaces a nested ternary and returns the same four strings.
10
+ - **The MCP server's schema validator no longer pulls a vulnerable `fast-uri`.** The lockfile moves the transitive `fast-uri` (via `@modelcontextprotocol/sdk` → `ajv`) from 4.1.4 to 4.2.1, fixing GHSA-hrr3-gc8f-f4qj and GHSA-jvvf-x445-j334. No behaviour changes.
11
+ - **`doctor` gains `op: "evolve"`, `"evolve-verdict"`, `"revert-propose"` and `"reclaim-evolution-lock"`.** They are repair ops, so each requires a verified target and returns a receipt whose postcondition is read back from the store. `evolve` requires `makerSessionId` in its schema (`EVOLUTION_MAKER_SESSION_REQUIRED` without it) and returns the request id, nonce, digests and evaluator slots; `evolve-verdict` takes `requestId`, `nonce`, `slotToken`, `candidateDigest`, `parentDigest`, `evaluatorSessionId`, `verdict` (`approve` or `reject`), `rubricScores` and `reasons`; `revert-propose` takes `targetDigest`, and a revert always requires owner approval, so `evolve-verdict` on it is refused with `EVOLUTION_REQUEST_CLOSED`. `reclaim-evolution-lock` and `lineage-reanchor` need the owner at a terminal and are refused over MCP with `EVOLUTION_RECLAIM_REQUIRES_TTY` and `LINEAGE_REANCHOR_REQUIRES_TTY`. No tool is added. `status` carries an `evolution` field, `null` for an unsealed vault and `{unavailable}` when it cannot be read.
12
+ - **`interview` `op: "seal"` reports dropped templates as a warning instead of refusing.** Resealing an older contract generation seals and returns `CONTRACT_LEGACY_TEMPLATES_DROPPED: N ...` under `result.warnings` rather than refusing the removal as loosening. A template-only issue no longer refuses the seal or asks an unsafe-pattern question.
13
+ - **`interview` covers folders and properties only.** The `interpretations` parameter and the `interpretation-required` status are removed, no question asks about a template, and a sealed result carries no `templates` or `removedTemplates`. `write` scaffolds a new note from the live template in the vault's template folder (the named one, or the one matching the target folder) and reports a named template that does not exist as a `template-missing` conform entry; `search` `op: "templates"` lists the live templates.
14
+ - **The `oms_doctor` contract status payload reports `legacyTemplates` instead of `templates`.** The count is the number of template constraints an older contract generation still carries, which the write judge no longer enforces; a non-zero count also appears as the finding `legacy-template-constraints-ignored: N`.
15
+ - **`write` saves with warnings instead of dropping keys or drafting.** A write with an unknown key or an out-of-rule value is saved as written, and its receipt lists the finding under `warnings`. `fixes` now means lossless value fixes, each as `{field, kind}`: a number or checkbox written as a string, a scalar for a list, a midnight datetime for a date, a number for text whose source is exactly that number, an allowed value that differs only in spelling, and a missing value whose rule fixes it. A fix changes only that value's bytes. No key is ever dropped, and only frontmatter that does not parse is drafted.
16
+ - **`search {path}` accepts echoed schema defaults.** Some clients send every field with its default on each call. A path read now accepts `limit: 10`, `rerank: false` and `minScore: 0` alongside `path`, and the tool schema advertises them in the path branch. Any other value, and any field without a default such as `mode`, is still refused with the same `SEARCH_ARGS_INVALID` error.
17
+ - **`doctor` gains `op: "gaps"`, and `write` saves a repaired form instead of refusing a gap.** `op: gaps` is read-only. It reports the sealed contract revision, the open gaps in the ledger by id, note path, axis, kind and field (never the wanted value), whether each gap is drafted or stale, the corrupt ledger lines, and the contradictions in the sealed contract. It creates no store, state directory or ledger. A `write` that only misses the frame is now saved, with lossless fixes applied and every other finding kept as written. The receipt lists each finding; a gap the ledger could not record has no `id` and the receipt carries `gapLedger: "failed"` or `"unavailable"`. A denied write whose note was kept as a draft carries an opaque `draftRef`. `check: true` adds `resolution: {action, gaps, wouldDraft, precondition?}`, the same resolution the write would apply, and still writes nothing; `precondition` names a missing or stale `ifMatch` that would stop the write first. `op: gaps` reports `ledger: "truncated"` when the ledger outgrew the read window.
18
+ - **`write` denies only on a safety refusal, and a kept draft is no longer an error.** A denied write returns `{ok: false, status: "denied", refusals, violations, reason}`, and only control paths, unsafe paths, paths outside the vault, unsupported input, a tampered contract or a stale `ifMatch` deny it. Other findings ride on the result as `warnings`: a saved note's receipt carries the saved note's `warnings` and, as `fixes`, the `{field, kind}` of each lossless fix applied. In a sealed vault, only a note whose frontmatter does not parse is kept as a draft and returns `{ok: false, status: "drafted", draftRef, warnings}` without `isError`; every other write, including a contradiction and a draft that cannot be kept, is saved. `check: true` reports `refusals`, `warnings` and `fixes`. `status` reports `writeTools` as `write-disabled-contract-tampered` for a tampered contract and `write-unverified-contract` for a broken one, and `contract.reason` says which.
19
+ - **`doctor` gains `op: "lineage-recover"` and `op: "lineage-reanchor"`.** They are repair ops, so both require a verified target and reject a `cwd`-inferred vault. Each runs under the seal lock. It snapshots the retained generations the lineage is missing and appends the events the chain can account for: a seal that crashed before its event, a lost `<id>` link, or a store sealed before the lineage existed. `lineage-recover` refuses a gap it cannot explain with `CONTRACT_LINEAGE_GAP` and writes nothing. `lineage-reanchor` records that gap as a `gap-anchor` event, so the chain continues from the linked generation. Both return the anchors they recorded and a receipt. The receipt's postcondition gives the event and snapshot counts read back after the write. A vault that is not sealed returns `CONTRACT_NOT_SEALED`, and an unsafe state directory returns `STATE_DIR_UNSAFE` without touching it. On a vault whose first seal lost its lineage append, so it reads as `store-without-index` or `vault-moved`, `lineage-recover` also writes the vault's `index.json` entry, even when the lineage itself is already current, and the receipt lists `index.json`. A `vault-moved` vault whose original path still exists is a copy, and recovery refuses it with `CONTRACT_VAULT_ID_SHARED`. On an indexed vault with a current lineage, either op writes nothing.
20
+ - **A retried `interview` `op: "seal"` saves a template folder the first seal did not.** The seal records the template folder in `.oms/settings.json` after it seals the generation. When a seal stopped between the two and the retry found the contract already sealed, the folder was lost. The retry now saves the folder recorded with the confirmed proposal if the settings do not name one. The logged folder is checked as an interview answer is checked (it must be an existing, visible folder inside the vault) and is not saved otherwise. A folder that fails that check, or a settings write that fails, is returned as the warning `INTERVIEW_TEMPLATE_FOLDER_UNRECORDED` under `result.warnings`, and the seal is still reported. Settings that already name a folder are left as they are, and a vault with no settings is not treated as already sealed. Declined templates need no repair, because they are stored in the sealed generation.
21
+
7
22
  ## [0.19.0] - 2026-09-28
8
23
 
9
24
  - **Breaking: `write` refuses to overwrite an existing note without `ifMatch`.** The input is `{path, content, template?, ifMatch?, check?}`. Replacing a note needs `ifMatch`, the `sha256:` revision from the previous receipt or a `check`; without it the call returns `WRITE_IF_MATCH_REQUIRED` and the file is unchanged, and a stale revision returns the retryable `WRITE_TARGET_CHANGED` (or `WRITE_TARGET_VANISHED`); an `ifMatch` for a note that does not exist returns the retryable `WRITE_TARGET_ABSENT`, and retrying without `ifMatch` creates it. `check: true` judges the note and returns `status: "checked"` with the frame, the current revision and any violations, writing nothing. A written note now returns the receipt `{ok, path, revision, contractRevision, index: {keyword, vector}, conformed, missingDefaults}` instead of `{ok, path, missingDefaults}`; when the vault has an engine store the note is keyword-searchable in the next call.
@@ -4,6 +4,13 @@ Per-host adapter and installer changes belong here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.20.0] - 2026-09-30
8
+
9
+ - **The Claude guard reads the live templates only for a new note.** `oms hook pre` loads the template folder only when the target does not exist yet, the only case in which a template choice is recorded, so an edit of an existing note no longer reads every template. New notes get the same decision and template choice gap as MCP `write`.
10
+ - **The Claude guard records an open template choice as MCP `write` does.** `oms hook pre` now reads the live templates, so a new note in a folder that two or more templates match records the same template choice gap. The allow or deny decision and its warnings are unchanged.
11
+ - **The Claude guard warns with the whole verdict on an edit.** Since the judge is stateless, `oms hook pre` returns every warning the edited note carries, while the gap ledger still records only the warnings the edit added.
12
+ - **The Claude guard allows a write with warnings instead of denying it.** `oms hook pre` now denies only on a safety refusal (control path, unsafe path, tampered contract, unparseable or truncated input). Any other finding allows the tool call and returns the warnings as `{systemMessage, hookSpecificOutput: {hookEventName: "PreToolUse", additionalContext}}`, without a `permissionDecision`, so the normal permission flow still applies and Claude sees what the note breaks. A broken contract, an unreadable note, or an edit whose result cannot be rebuilt warns instead of denying. Warnings the write adds are recorded as kept gaps; a ledger that cannot be written never denies. `oms-guard.mjs` forwards exactly that warning shape verbatim and still treats any other output as `malformed-output`. After upgrading, run `oms setup host sync` so the installed guard understands warnings; an older guard allows the write but drops the warning and logs `malformed-output`.
13
+
7
14
  ## [0.19.0] - 2026-09-28
8
15
 
9
16
  - **An ambiguous Codex managed block names the 0.19 commands.** The error now tells the user to rerun `oms setup host install` or `oms setup host remove` after the managed blocks are removed by hand.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,10 @@ This aggregate changelog contains changes that span multiple layers.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.20.0] - 2026-09-30
14
+
15
+ - **`ip-address` no longer carries its `<=10.5.0` SSRF advisories.** `npm run audit` started failing again on the transitive `@modelcontextprotocol/sdk` → `express-rate-limit` → `ip-address` chain (GHSA-rpw4-54j3-4h4q, GHSA-2vr4-cq9g-pvrc). `npm audit fix` (no `--force`) resolved it to `10.7.2`, still within `express-rate-limit`'s own `^10.2.0` range, so only `package-lock.json` changed — no `package.json` edit or `overrides` entry was needed. `npm run audit` reports zero vulnerabilities again.
16
+
13
17
  ## [0.19.0] - 2026-09-28
14
18
 
15
19
  - **Upgrading from 0.18: run `oms setup host sync` first.** Installed host assets (the Claude guard hook, skills, and MCP wiring) still point at the 0.18 surface until they are re-synced. Then read [Migrating to 0.19](./docs/migration-0.19.md) for the full command map.
package/README.ko.md CHANGED
@@ -40,7 +40,7 @@ Obsidian은 사령탑으로 남는다. OMS가 꺼져 있어도 노트는 사람
40
40
  </td>
41
41
  <td width="50%" valign="top">
42
42
  <h3>내 볼트의 언어 그대로</h3>
43
- 폴더, 속성, 템플릿의 의미는 내가 정한다. OMS가 제시하는 체계를 따르는 대신, 내 컨벤션을 기록한다.
43
+ 폴더와 속성의 의미는 내가 정한다. OMS가 제시하는 체계를 따르는 대신, 내 컨벤션을 기록한다.
44
44
  </td>
45
45
  </tr>
46
46
  <tr>
@@ -78,7 +78,7 @@ oms --help
78
78
 
79
79
  ### 2. 볼트 규약 정의
80
80
 
81
- 터미널에서 setup을 실행한다. 폴더, 속성, 템플릿을 인터뷰한 뒤 계약을 봉인한다. 기존 노트는 수정하지 않는다.
81
+ 터미널에서 setup을 실행한다. 폴더와 속성을 인터뷰한 뒤 계약을 봉인한다. 기존 노트는 수정하지 않는다.
82
82
 
83
83
  ```bash
84
84
  oms setup --vault /path/to/vault
@@ -132,10 +132,10 @@ oms search "프로젝트 결정" --vault /path/to/vault
132
132
  <details>
133
133
  <summary><strong>볼트 계약 자세히 보기</strong></summary>
134
134
 
135
- - **의미는 사용자 소유다.** 폴더, 속성 pool, 템플릿을 함께 인터뷰한다. 속성 이름·폴더·페르소나를 하드코딩하지 않고 Inbox fallback도 없다.
135
+ - **의미는 사용자 소유다.** 폴더와 속성 pool을 함께 인터뷰한다. 속성 이름·폴더·페르소나를 하드코딩하지 않고 Inbox fallback도 없다.
136
136
  - **볼트 안의 제어 파일은 하나다.** `.oms/settings.json`에 `version`, `vaultId`, `templateFolder`, `embedding`, `agentRepair`를 둔다. 다른 `.oms/` 항목은 무시하고 `oms doctor contract`가 예상하지 않은 제어 파일로 보고한다. `.obsidian/types.json`은 읽기 전용 관측값이며 봉인을 덮어쓰지 않는다.
137
- - **템플릿은 원본으로 남는다.** OMS는 템플릿이 선언하는 것을 기록한다. 템플릿 파일을 다시 쓰거나 복사하지 않으며, Templater·JavaScript·전용 token 언어를 해석하거나 실행하지 않는다. 쓰기는 작성 중인 노트의 기계적인 부분만 채운다. `{{title}}`·`{{date}}`·`{{time}}` 변수, 새 노트의 date·datetime 기본값, 선택한 템플릿에서 빠진 heading이다. 필수 값을 대신 채우지는 않는다.
138
- - **변경은 드러난다.** `oms setup status`는 템플릿 상태를 `active`, `drift`, `missing`으로 보고한다. 변경된 템플릿을 조용히 재봉인하지 않는다.
137
+ - **템플릿은 원본으로 남는다.** 템플릿은 `templateFolder`에 있으며 봉인하거나 판정하지 않는다. 새 노트는 살아 있는 템플릿으로 뼈대를 채운다. 쓰기가 이름을 준 템플릿, 없으면 basename이나 `folder:` 키가 대상 폴더와 맞는 유일한 템플릿이다. 템플릿 파일을 다시 쓰거나 복사하지 않으며, Templater·JavaScript·전용 token 언어를 해석하거나 실행하지 않는다. 쓰기는 작성 중인 노트의 기계적인 부분만 채운다. `{{title}}`·`{{date}}`·`{{time}}` 변수, 새 노트의 date·datetime 기본값, 선택한 템플릿의 frontmatter 기본값과 빠진 heading이다. 노트에 이미 있는 값이 우선한다. 필수 값을 대신 채우지는 않는다.
138
+ - **이전 봉인도 읽힌다.** 새 봉인은 폴더와 속성만 저장한다. 이전 릴리스가 만든 봉인도 그대로 읽히며, `oms setup status`는 그 템플릿 제약을 `legacyTemplates` 개수로 보고할 뿐 강제하지 않는다.
139
139
  - **판정자는 하나다.** 거부 시 `{field, kind}` 위반과 안내 명령 하나만 반환한다. 규칙 값, 저장소 경로, 계약 본문은 반환하지 않는다.
140
140
  - **봉인 증거가 맞지 않으면 쓰기를 거부한다.** 이 기기의 증거가 볼트와 어긋나면 `contract-unreadable`로 거부하고 소유자가 `oms setup`을 다시 실행해야 한다. 봉인이 아예 없는 기기에서는 판정하지 않는 것과 구별한다.
141
141
 
@@ -144,13 +144,13 @@ oms search "프로젝트 결정" --vault /path/to/vault
144
144
  </details>
145
145
 
146
146
  <details>
147
- <summary><strong>설정, 템플릿 해석, 복구</strong></summary>
147
+ <summary><strong>설정, 템플릿 뼈대, 복구</strong></summary>
148
148
 
149
149
  터미널에서 `oms setup`을 실행하면 대화형 인터뷰를 거쳐 계약을 봉인한다. `oms interview`는 같은 터미널 인터뷰를 독립 명령으로 제공하며, 터미널이 필요하고 `OMS_NON_INTERACTIVE=1`이면 실행을 거부한다.
150
150
 
151
- `setup` 스킬은 `oms setup --questions`로 질문을 받아 소유자에게 하나씩 묻고, `oms setup --answers <file|->`로 답을 제출한다. 이 경로는 첫 봉인이나 더 엄격한 계약만 봉인하며, 계약을 느슨하게 하는 재봉인은 소유자의 터미널에서 한다. MCP `interview` 도구는 질문과 봉인 상태만 보여 주며 아무것도 봉인하지 않는다.
151
+ `setup` 스킬은 `oms setup --questions`로 질문을 받아 소유자에게 하나씩 묻고, `oms setup --answers <file|->`로 답을 제출한다. 이 경로는 첫 봉인이나 더 엄격한 계약만 봉인하며, 계약을 느슨하게 하는 재봉인은 소유자의 터미널에서 한다. MCP `interview` 도구는 여러 호출에 걸쳐 인터뷰를 이어 간다. `op: questions`는 읽기 전용이고, `answer`, `confirm`, `seal`은 검증된 대상의 인터뷰 로그에 기록한다. 소유자가 확인한 제안만 봉인하며 오래된 seal lock을 회수하지 않는다.
152
152
 
153
- `oms setup extract --template <path>`는 템플릿 원문과 계산한 hash를 반환한다. 에이전트는 각 템플릿을 읽어 `oms setup --interpretations <file>`로 해석을 제출한다. 소유자가 그 해석을 확인한 다음 인터뷰에 사용한다.
153
+ setup과 인터뷰는 폴더와 속성만 묻는다. `oms setup extract --template <name>`은 `templateFolder`의 템플릿이 채울 뼈대(원본 경로, `folder:` 선택자, 속성 이름, heading)를 미리 보여 준다. 템플릿을 고치면 재봉인 없이 다음 쓰기부터 반영된다.
154
154
 
155
155
  `oms doctor contract`는 봉인, 오래된 lock, 고아 generation, 예상하지 않은 제어 파일, hook 전송 실패를 진단한다. `--fix`는 이동했거나 색인되지 않은 볼트를 다시 색인할 뿐이다. 다른 봉인 문제는 `oms setup`으로 복구한다.
156
156
 
@@ -223,7 +223,7 @@ oms search --path|--context|--link 노트 하나 읽기, 맥락 조
223
223
  oms interview 터미널에서 볼트 소유자를 인터뷰하고 봉인
224
224
  oms write <path> 계약이 허용하면 stdin의 노트를 저장
225
225
  oms setup 계약 봉인 (에이전트는 --questions/--answers)
226
- oms setup extract|status 템플릿 원문 또는 계약 상태 표시
226
+ oms setup extract|status 템플릿 뼈대 미리보기 또는 계약 상태 표시
227
227
  oms setup host install|remove|sync|status 호스트 asset과 MCP 등록 관리
228
228
  oms setup model install|select|waive|status 로컬 모델 선택 관리
229
229
  oms setup package check|update OMS 패키지 확인 또는 갱신
@@ -240,7 +240,7 @@ oms hook pre Claude 쓰기를 계약으로
240
240
 
241
241
  인식되는 모든 명령은 `--help`와 `-h`를 받으며 exit 0, 부작용 없음으로 끝난다. 알 수 없는 명령과 `--help`를 함께 쓰면 exit 1이다. 0.19에서 제거된 family는 exit 1로 끝나며 대체 명령을 알려 준다.
242
242
 
243
- `oms doctor audit`는 노트를 다시 쓰지 않고 `{path, field, kind}` 항목을 보고한다. 노트는 `oms write <path> < note.md` 또는 MCP `write {path, content, template?, ifMatch?, check?}`로 전체 내용을 쓴다. 둘 다 같은 쓰기 파이프라인을 거친다. 선택 사항인 `template`은 따르는 봉인된 템플릿 이름이다. 기존 노트를 덮어쓰려면 현재 `sha256:` revision을 `ifMatch`(`--if-match`)로 넘겨야 하고, `check`(`--check`)는 디스크를 건드리지 않고 판정만 한다. 허용된 쓰기는 새 revision이 담긴 receipt를 돌려주고, 엔진 저장소가 있으면 같은 호출에서 키워드 인덱스를 갱신하므로 노트를 바로 검색할 수 있다. 완료 호출이나 리뷰어 대화는 없다.
243
+ `oms doctor audit`는 노트를 다시 쓰지 않고 `{path, field, kind}` 항목을 보고한다. 노트는 `oms write <path> < note.md` 또는 MCP `write {path, content, template?, ifMatch?, check?}`로 전체 내용을 쓴다. 둘 다 같은 쓰기 파이프라인을 거친다. 선택 사항인 `template`은 새 노트의 뼈대를 채울 `templateFolder`의 템플릿 이름이다. 기존 노트를 덮어쓰려면 현재 `sha256:` revision을 `ifMatch`(`--if-match`)로 넘겨야 하고, `check`(`--check`)는 디스크를 건드리지 않고 판정만 한다. 허용된 쓰기는 새 revision이 담긴 receipt를 돌려주고, 엔진 저장소가 있으면 같은 호출에서 키워드 인덱스를 갱신하므로 노트를 바로 검색할 수 있다. 완료 호출이나 리뷰어 대화는 없다.
244
244
 
245
245
  `oms doctor cleanup`은 제거 가능한 파생 상태를 지운다. `oms doctor build-graph`는 노트 그래프를 다시 만든다.
246
246
 
package/README.md CHANGED
@@ -40,7 +40,7 @@ Search your existing notes with lexical retrieval. Choose vector, HyDE, query ex
40
40
  </td>
41
41
  <td width="50%" valign="top">
42
42
  <h3>Your vault, your vocabulary</h3>
43
- Define the meaning of folders, properties, and templates. OMS records your conventions instead of shipping a system you have to adopt.
43
+ Define the meaning of folders and properties. OMS records your conventions instead of shipping a system you have to adopt.
44
44
  </td>
45
45
  </tr>
46
46
  <tr>
@@ -78,7 +78,7 @@ oms --help
78
78
 
79
79
  ### 2. Define your vault's conventions
80
80
 
81
- Run setup in your terminal. The interview covers folders, properties, and templates, then seals the contract. Existing notes are not modified.
81
+ Run setup in your terminal. The interview covers folders and properties, then seals the contract. Existing notes are not modified.
82
82
 
83
83
  ```bash
84
84
  oms setup --vault /path/to/vault
@@ -132,10 +132,10 @@ These are example requests, not captured run results. Available workflows and wr
132
132
  <details>
133
133
  <summary><strong>The vault contract, in detail</strong></summary>
134
134
 
135
- - **Meaning is user-owned.** The interview covers folders, the property pool, and templates together. OMS hardcodes no property names, folders, or personas and has no Inbox fallback.
135
+ - **Meaning is user-owned.** The interview covers folders and the property pool together. OMS hardcodes no property names, folders, or personas and has no Inbox fallback.
136
136
  - **One control file inside the vault.** `.oms/settings.json` holds `version`, `vaultId`, `templateFolder`, `embedding`, and `agentRepair`. Other `.oms/` entries are ignored and reported as unexpected control files by `oms doctor contract`. `.obsidian/types.json` is a read-only observation, not an override of the seal.
137
- - **Templates stay yours.** OMS records what each template declares. It never rewrites or copies a template file, and does not parse or execute Templater, JavaScript, or a private token language. A write only fills what is mechanical in the note being written: `{{title}}`, `{{date}}` and `{{time}}` variables, date and datetime defaults on a new note, and the chosen template's missing headings. It never supplies a required value.
138
- - **Drift is visible.** `oms setup status` reports templates as `active`, `drift`, or `missing`. It never silently re-seals a changed template.
137
+ - **Templates stay yours.** Templates live in your `templateFolder` and are never sealed or judged. A new note is scaffolded from the live template: the one the write names, or else the one template whose basename or `folder:` key matches the target folder. OMS never rewrites or copies a template file, and does not parse or execute Templater, JavaScript, or a private token language. A write only fills what is mechanical in the note being written: `{{title}}`, `{{date}}` and `{{time}}` variables, date and datetime defaults on a new note, and the chosen template's frontmatter defaults and missing headings. The note's own values win. It never supplies a required value.
138
+ - **Old seals stay readable.** A new seal stores folders and properties only. A seal made by an older release still loads; `oms setup status` counts its template constraints as `legacyTemplates`, and they are reported, never enforced.
139
139
  - **One judge, bounded feedback.** Denied writes return `{field, kind}` violations and one guidance command, not rule values, store paths, or the contract body.
140
140
  - **Mismatched seal evidence blocks writes.** When this machine's evidence no longer matches the vault, writes fail with `contract-unreadable` until the owner runs `oms setup` again. A machine with no seal is a different case: its vault is not contract-judged.
141
141
 
@@ -144,13 +144,13 @@ See [architecture](./docs/architecture.md), [conventions](./docs/conventions.md)
144
144
  </details>
145
145
 
146
146
  <details>
147
- <summary><strong>Setup, template interpretation, and recovery</strong></summary>
147
+ <summary><strong>Setup, template scaffolds, and recovery</strong></summary>
148
148
 
149
149
  `oms setup` in a terminal runs the interactive interview and seals the contract. `oms interview` is the same terminal interview on its own command; it requires a terminal and refuses to run under `OMS_NON_INTERACTIVE=1`.
150
150
 
151
151
  The `setup` skill asks the owner each question via `oms setup --questions` and submits answers with `oms setup --answers <file|->`. This path can seal a first or stricter contract; a loosening reseal stays with the owner's terminal. The MCP `interview` tool continues the interview across calls: `op: questions` is read-only, and `answer`, `confirm`, and `seal` record to the interview log on a verified target. It seals only the proposal the owner confirmed and never reclaims a stale seal lock.
152
152
 
153
- `oms setup extract --template <path>` returns a template source and its computed hash. The agent reads each template and submits its interpretation with `oms setup --interpretations <file>`. The owner confirms that interpretation before it drives the interview.
153
+ Setup and the interview ask about folders and properties only. `oms setup extract --template <name>` previews what a template in `templateFolder` would scaffold: its source, `folder:` selector, property names, and headings. Editing a template takes effect on the next write without a reseal.
154
154
 
155
155
  `oms doctor contract` diagnoses seal problems, stale locks, orphaned generations, unexpected control files, and hook transport failures. Its `--fix` only re-indexes a moved or unindexed vault. Other broken seals are recovered through `oms setup`.
156
156
 
@@ -223,7 +223,7 @@ oms search --path|--context|--link Read one note, gather context, o
223
223
  oms interview Interview the vault owner in a terminal and seal
224
224
  oms write <path> Save a note from stdin when the contract allows it
225
225
  oms setup Seal the contract (--questions/--answers for agents)
226
- oms setup extract|status Show a template source or the contract posture
226
+ oms setup extract|status Preview a template scaffold or the contract posture
227
227
  oms setup host install|remove|sync|status Manage host assets and MCP registrations
228
228
  oms setup model install|select|waive|status Manage local model selection
229
229
  oms setup package check|update Check or update the OMS package
@@ -240,7 +240,7 @@ oms hook pre Judge a Claude write against the
240
240
 
241
241
  Every recognized command accepts `--help` and `-h`, exits 0, and has no side effects. An unknown command combined with `--help` exits 1. A family removed in 0.19 exits 1 and names its replacement.
242
242
 
243
- `oms doctor audit` reports `{path, field, kind}` entries without rewriting notes. Notes are written as whole content through `oms write <path> < note.md` or MCP `write {path, content, template?, ifMatch?, check?}`; both take the same write pipeline, and `template` optionally names the sealed template being followed. Overwriting an existing note needs `ifMatch` (`--if-match`) with its current `sha256:` revision; `check` (`--check`) judges without touching disk. An allowed write returns a receipt with the new revision and updates the keyword index of an existing engine store in the same call, so the note is searchable at once. There is no completion call or reviewer conversation.
243
+ `oms doctor audit` reports `{path, field, kind}` entries without rewriting notes. Notes are written as whole content through `oms write <path> < note.md` or MCP `write {path, content, template?, ifMatch?, check?}`; both take the same write pipeline, and `template` optionally names the template in `templateFolder` that scaffolds a new note. Overwriting an existing note needs `ifMatch` (`--if-match`) with its current `sha256:` revision; `check` (`--check`) judges without touching disk. An allowed write returns a receipt with the new revision and updates the keyword index of an existing engine store in the same call, so the note is searchable at once. There is no completion call or reviewer conversation.
244
244
 
245
245
  `oms doctor cleanup` removes eligible derived state. `oms doctor build-graph` rebuilds the note graph.
246
246
 
@@ -12,7 +12,8 @@
12
12
  * OMS_VAULT — primary vault path
13
13
  * OMS_AGENT_VAULT — agent vault path
14
14
  *
15
- * The judge's deny is forwarded as is. When the judge cannot be reached (spawn failure,
15
+ * The judge's deny, and its allow with warnings (a `systemMessage` plus PreToolUse
16
+ * `additionalContext`, no `permissionDecision`), are forwarded as is. When the judge cannot be reached (spawn failure,
16
17
  * non-zero exit, timeout, malformed output) the write is allowed with one stderr line and
17
18
  * the failure kind is recorded in `~/.oms/guard-events.jsonl` for `oms doctor contract`.
18
19
  * A payload that cannot be parsed (or exceeds the stdin cap) is denied when its raw text
@@ -177,13 +178,16 @@ function transportFailure(kind) {
177
178
  }
178
179
  }
179
180
 
180
- /** Accepts exactly the allow shape or the deny shape `oms hook pre` prints. */
181
+ const WARNING_PREFIX = "[oms] write allowed with warnings: ";
182
+
183
+ /** Accepts exactly the allow, allow-with-warnings or deny shape `oms hook pre` prints. */
181
184
  function validJudgeOutput(stdout) {
182
185
  let value;
183
186
  try { value = JSON.parse(stdout); } catch { return null; }
184
187
  if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
185
188
  const keys = Object.keys(value).sort().join(",");
186
189
  if (keys === "continue,suppressOutput" && value.continue === true && value.suppressOutput === true) return ALLOW;
190
+ if (keys === "hookSpecificOutput,systemMessage") return validWarning(value);
187
191
  if (keys !== "hookSpecificOutput") return null;
188
192
  const out = value.hookSpecificOutput;
189
193
  if (out === null || typeof out !== "object" || Array.isArray(out)) return null;
@@ -193,6 +197,17 @@ function validJudgeOutput(stdout) {
193
197
  return JSON.stringify(value);
194
198
  }
195
199
 
200
+ /** The allow-with-warnings shape: no permission decision, so Claude's normal permission flow applies. */
201
+ function validWarning(value) {
202
+ if (typeof value.systemMessage !== "string" || !value.systemMessage.startsWith(WARNING_PREFIX)) return null;
203
+ const out = value.hookSpecificOutput;
204
+ if (out === null || typeof out !== "object" || Array.isArray(out)) return null;
205
+ if (Object.keys(out).sort().join(",") !== "additionalContext,hookEventName") return null;
206
+ if (out.hookEventName !== "PreToolUse") return null;
207
+ if (typeof out.additionalContext !== "string" || !out.additionalContext.startsWith(WARNING_PREFIX)) return null;
208
+ return JSON.stringify(value);
209
+ }
210
+
196
211
  const GLOB_META = /[*?[\]{}\\,!\s]/;
197
212
 
198
213
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oms",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Oh My Second Brain convention layer for Obsidian vaults — Hermes skill bundle and MCP adapter.",
5
5
  "_note": "oms setup host install writes ~/.hermes/config.yaml mcp_servers.oms and installs the six shared skills, prefixed `oms-`, with provenance under ~/.hermes/skills/knowledge-management/oms/."
6
6
  }
@@ -11,16 +11,21 @@ mcp_args:
11
11
  Report vault health, diagnose the seal and derived indexes, then run only the repair the user named. Do not backfill notes, rewrite note bodies, or edit OMS files by hand. Unknown note values go back to `/write`, not to an invented repair.
12
12
 
13
13
  ```text
14
- /doctor <status|link-check|validate|audit|build-graph|cleanup|sync-embeddings>
14
+ /doctor <status|link-check|validate|audit|build-graph|cleanup|sync-embeddings|evolve|evolve-verdict|revert-propose>
15
15
  ```
16
16
 
17
- - `status` is read-only vault health: the seal posture with each sealed template as `active`, `drift`, or `missing`, template counts, the generation digest and diagnostics, graph status, the runtime history for this host and vault, whether writes are enabled, and `readTools` (the read-only MCP tools: `search`). It creates nothing and writes nothing, with or without an engine store. `oms doctor status` is the CLI counterpart. A drifted or missing template is reported, never resealed here.
17
+ - `status` is read-only vault health: the seal posture, the count of live templates in the template folder, the generation digest and diagnostics, graph status, the runtime history for this host and vault, the contract `evolution` summary (journal counters, requests awaiting the owner, the autonomous budget left, whether the lineage has a gap), whether writes are enabled, and `readTools` (the read-only MCP tools: `search`). It creates nothing and writes nothing, with or without an engine store. `oms doctor status` is the CLI counterpart.
18
18
  - `link-check` validates one note's `[[wikilinks]]` (`notePath`, optional `folder`) without writing. `oms doctor link-check` is the CLI counterpart and can check the whole vault without a path.
19
- - `validate` is read-only seal diagnosis. It returns the vault, the contract posture with its findings and template states, the `cause` and `recovery` when the seal cannot be read, stale locks, orphaned generations, how many unexpected files sit in the vault's `.oms` folder, and counted hook transport failures. It repairs nothing and prints no rule value or store path.
19
+ - `validate` is read-only seal diagnosis. It returns the vault, the contract posture with its findings, the `cause` and `recovery` when the seal cannot be read, stale locks, orphaned generations, how many unexpected files sit in the vault's `.oms` folder, and counted hook transport failures. It repairs nothing and prints no rule value or store path.
20
20
  - `audit` reports which notes would fail the sealed contract, as `{path, field, kind}` entries. It rewrites nothing. `oms doctor audit` is the CLI counterpart.
21
21
  - `build-graph` and `cleanup` repair the derived graph or semantic index the user named.
22
22
  - `sync-embeddings` takes exactly one `mode`: `sync`, `embed`, or `repair`. `repair` also requires `repairMode: "rebuild"` or `"drop"` and may set `dryRun`. It backs up the engine store and checks the rebuilt or absent result. It is not forced embedding. Do not send retired boolean `embed` or `force` switches, and do not send repair-only fields with `sync` or `embed`.
23
23
 
24
+ - `evolve` turns the open write gaps into one contract evolution request and returns its request id, nonce, digests and evaluator slots. It seals nothing. `makerSessionId` is required (`EVOLUTION_MAKER_SESSION_REQUIRED` without it): you are the maker, so do not judge your own request. A candidate whose meaning overlaps an existing entry or that drifts more than 0.3 from the first sealed generation is refused with `EVOLUTION_STAGE2_REFUSED` and nothing is issued.
25
+ - `evolve-verdict` submits one evaluator verdict (`approve` or `reject`, with `rubricScores` and `reasons`) bound to a request slot. Each verdict must come from a separate evaluator subagent that did not draft the request. A host that cannot run independent evaluator subagents must not submit verdicts at all: stop and tell the user to review the request at `oms setup`. Evaluator session ids must differ from each other and from the maker, or the verdict is discarded. Nothing is sealed without three bound verdicts. A candidate that adds a refusal, overlaps an existing meaning or drifts past 0.3 is rejected, and a quorum reject stays rejected; one that loosens the contract or raises warnings waits for the owner; only a candidate with no new refusal and a warning delta of 0 or less (a negative delta counts) can seal on its own, and only when the owner turned autonomy on. The quorum is host-attested: OMS cannot verify that the three evaluators were independent subagents, which is why autonomy is off by default, and an autonomous seal is recorded with `quorum: "host-attested"`.
26
+ - `revert-propose` (`targetDigest`) proposes a kept generation's contract as a new forward candidate. It never rewrites history. A revert always requires owner approval: it has no maker, so every revert, tightening, neutral or loosening, waits for the owner at `oms setup` whatever the autonomy policy says, and `evolve-verdict` is refused on it (`EVOLUTION_REQUEST_CLOSED`). Stage 1 and stage 2 run when it is proposed: a revert that would add a refusal is refused with `EVOLUTION_REVERT_REFUSED`, and one that overlaps in meaning or drifts past 0.3 from the first sealed generation with `EVOLUTION_STAGE2_REFUSED`; nothing is proposed.
27
+ - `reclaim-evolution-lock` and `lineage-reanchor` belong to the owner at a terminal; over MCP they are refused. Tell the user to run `oms doctor reclaim-evolution-lock` or `oms doctor lineage-reanchor` themselves. Requests awaiting the owner are approved or rejected only at `oms setup`, and autonomy is turned on only with `oms setup --autonomy on`.
28
+
24
29
  A broken or missing seal is recovered by the user running `oms setup` at a terminal; recovery is never done through the `setup` skill. The only automatic seal repair is `oms doctor contract --fix`, which re-indexes a moved or unindexed vault and nothing else. `oms doctor contract` also names the unexpected `.oms` entries for the person at the CLI.
25
30
 
26
31
  Index repairs run only when explicitly requested and do not edit notes. There is no default-value backfill: OMS never rewrites a note.
@@ -13,11 +13,11 @@ Continue the vault interview where it stopped. Answers are kept in the interview
13
13
  /interview [--reask]
14
14
  ```
15
15
 
16
- MCP `interview { op?, answers?, proposed?, reask?, interpretations? }`. `op` defaults to `questions`, which is read-only. `answer`, `confirm`, and `seal` record to the log and need a verified vault target; on a vault inferred from the working directory they return `{ok: false, status: "rejected", rejection}` and write nothing.
16
+ MCP `interview { op?, answers?, proposed?, reask? }`. `op` defaults to `questions`, which is read-only. `answer`, `confirm`, and `seal` record to the log and need a verified vault target; on a vault inferred from the working directory they return `{ok: false, status: "rejected", rejection}` and write nothing.
17
17
 
18
- 1. `op: "questions"` returns the vault, the contract posture, and a `status`.
18
+ 1. `op: "questions"` returns the vault, the contract posture, and a `status`. The questions cover folders and properties only; templates scaffold new notes from the template folder and are never sealed.
19
19
  2. Ask the owner each listed question, then `op: "answer"` with `answers` keyed by question id. Earlier answers stay; only unanswered questions come back.
20
- 3. When every question is answered, the status is `proposed` with a `proposed` digest and a preview in `notes`. Show the owner the preview.
20
+ 3. When every question is answered, the status is `proposed` with a `proposed` digest and a preview in `notes`. Show the owner the preview, including any `CONTRACT_LEGACY_TEMPLATES_DROPPED` line, which means templates an older seal held are not carried forward.
21
21
  4. Only when the owner agrees, `op: "confirm"` with that `proposed` digest, then `op: "seal"`.
22
22
 
23
23
  Statuses:
@@ -25,7 +25,6 @@ Statuses:
25
25
  - `questions`: `questions` lists `{id, prompt, kind, choices?, default?}` and `notes` holds interview lines worth showing the owner. `drift` lists earlier answers dropped because their question changed.
26
26
  - `proposed`: every question is answered; confirm with the owner before `confirm` and `seal`.
27
27
  - `sealed`: the confirmed contract is sealed.
28
- - `interpretation-required`: the vault has templates. `sources` lists each one with its `sourceHash`. The `setup` skill covers reading each template; pass the interpretations as `interpretations` on every interview call.
29
28
  - `loosening`: the sealed contract would loosen. `changes` names each field and kind of change, never a value. Only the owner can loosen a contract, by running `oms interview` in a terminal.
30
29
  - `refused`: `reasons` names why the interview cannot run. Tell the owner to run `oms doctor contract`, then `oms interview` in a terminal.
31
30
  - `rejected`: `rejection.code` says why. `INTERVIEW_CONFIRM_REQUIRED` and `INTERVIEW_CONFIRM_STALE` mean the owner has not confirmed the current proposal. `INTERVIEW_SEAL_LOCK_STALE` means an earlier seal left its lock; the tool never reclaims it, and the owner runs `oms interview` in a terminal. `CONTRACT_SEAL_BUSY` is retryable.
@@ -20,7 +20,7 @@ Retrieve vault knowledge without changing the vault. Search does not depend on c
20
20
 
21
21
  - `query` accepts three shapes. `mode: "query" | "search" | "vsearch"` with a `query` string; a bare `query` string with no `mode`; or typed retrieval with `searches`, `vec`, or `hyde` and no `mode` or `query`. `mode` never combines with `searches`. Lexical retrieval reads no contract and stays available when no embedding provider is configured.
22
22
  - `context` retrieves the declared search context.
23
- - `templates` lists the sealed templates and their declared axes, or shows one template.
23
+ - `templates` lists the templates in the vault's template folder and their declared axes, or shows one template.
24
24
  - `index-status` requires `view: "status" | "collections" | "contexts"`.
25
25
  - `get-document` requires exactly one of `target`, `targets`, or `notePath` with its window.
26
26
  - `link` suggests `[[wikilinks]]` for one note (`notePath`, optional `folder`). Suggestions are anchored to a term note's basename or alias, cover the first occurrence of each target only, and report an ambiguous span instead of resolving it. `oms search --link <path>` is the CLI counterpart.
@@ -31,7 +31,7 @@ Template source files are never returned as notes. Expansion is explicit only fo
31
31
 
32
32
  Typed queries intersect these axes:
33
33
 
34
- - `axes.template` selects one sealed template.
34
+ - `axes.template` selects one template from the template folder.
35
35
  - `axes.field.<key>` filters a field declared by that template; values may be scalars, scalar lists, or supported predicate objects.
36
36
  - `axes.folder` scopes physical placement.
37
37
  - `axes.link` follows observed wikilinks.