obsidian-knowledge 3.26.0__tar.gz → 3.26.2__tar.gz

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 (212) hide show
  1. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude-plugin/marketplace.json +1 -1
  2. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude-plugin/plugin.json +1 -1
  3. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.codex-plugin/plugin.json +1 -1
  4. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/PKG-INFO +1 -1
  5. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/configuration.md +6 -0
  6. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/primer.py +4 -0
  7. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/vault_files.py +3 -0
  8. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/.codex-plugin/plugin.json +1 -1
  9. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/obsidian-knowledge/SKILL.md +5 -2
  10. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/remember-conversations/SKILL.md +2 -0
  11. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2/plugins/obsidian-knowledge}/skills/vault-organizer/SKILL.md +24 -10
  12. obsidian_knowledge-3.26.2/plugins/obsidian-knowledge/skills/vault-organizer/edit-index.py +180 -0
  13. obsidian_knowledge-3.26.2/plugins/obsidian-knowledge/skills/vault-organizer/find-open-questions.py +186 -0
  14. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/lib/broken-links.md +23 -0
  15. obsidian_knowledge-3.26.2/plugins/obsidian-knowledge/skills/vault-organizer/lib/index-format.md +104 -0
  16. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/lib/rename-files.md +4 -2
  17. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/lib/state-files.md +3 -0
  18. obsidian_knowledge-3.26.2/plugins/obsidian-knowledge/skills/vault-organizer/link_recovery_models.py +97 -0
  19. obsidian_knowledge-3.26.2/plugins/obsidian-knowledge/skills/vault-organizer/organizer_io.py +72 -0
  20. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/recover-unresolved-links.py +90 -121
  21. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/vault-audit.py +40 -16
  22. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/pyproject.toml +1 -1
  23. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/obsidian-knowledge/SKILL.md +5 -2
  24. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/remember-conversations/SKILL.md +2 -0
  25. {obsidian_knowledge-3.26.0/plugins/obsidian-knowledge → obsidian_knowledge-3.26.2}/skills/vault-organizer/SKILL.md +24 -10
  26. obsidian_knowledge-3.26.2/skills/vault-organizer/edit-index.py +180 -0
  27. obsidian_knowledge-3.26.2/skills/vault-organizer/find-open-questions.py +186 -0
  28. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/lib/broken-links.md +23 -0
  29. obsidian_knowledge-3.26.2/skills/vault-organizer/lib/index-format.md +104 -0
  30. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/lib/rename-files.md +4 -2
  31. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/lib/state-files.md +3 -0
  32. obsidian_knowledge-3.26.2/skills/vault-organizer/link_recovery_models.py +97 -0
  33. obsidian_knowledge-3.26.2/skills/vault-organizer/organizer_io.py +72 -0
  34. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/recover-unresolved-links.py +90 -121
  35. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/vault-audit.py +40 -16
  36. obsidian_knowledge-3.26.2/tests/test_gardener_helpers.py +211 -0
  37. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_primer.py +8 -0
  38. obsidian_knowledge-3.26.2/tests/test_recover_unresolved_links.py +254 -0
  39. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_vault_files.py +34 -0
  40. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/uv.lock +2 -2
  41. obsidian_knowledge-3.26.0/plugins/obsidian-knowledge/skills/vault-organizer/find-open-questions.py +0 -108
  42. obsidian_knowledge-3.26.0/plugins/obsidian-knowledge/skills/vault-organizer/lib/index-format.md +0 -65
  43. obsidian_knowledge-3.26.0/skills/vault-organizer/find-open-questions.py +0 -108
  44. obsidian_knowledge-3.26.0/skills/vault-organizer/lib/index-format.md +0 -65
  45. obsidian_knowledge-3.26.0/tests/test_recover_unresolved_links.py +0 -134
  46. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.agents/plugins/marketplace.json +0 -0
  47. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/agents.red-green-tdd.md +0 -0
  48. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.doc-code-coupling.md +0 -0
  49. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.no-junk-drawers.md +0 -0
  50. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.parse-dont-validate.md +0 -0
  51. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.require-plugin-version-bump.md +0 -0
  52. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.semantic-types.md +0 -0
  53. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.taste-enforcer.md +0 -0
  54. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.github/workflows/ci.yml +0 -0
  55. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.github/workflows/docs.yml +0 -0
  56. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.github/workflows/publish.yml +0 -0
  57. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.gitignore +0 -0
  58. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.secrets.baseline +0 -0
  59. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/AGENTS.md +0 -0
  60. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/CONVENTIONS.md +0 -0
  61. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/LICENSE +0 -0
  62. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/README.md +0 -0
  63. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/commands/scan-secrets.md +0 -0
  64. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/.pages +0 -0
  65. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/ARCHITECTURE.md +0 -0
  66. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/CLI.md +0 -0
  67. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/MCP.md +0 -0
  68. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/QUALITY.md +0 -0
  69. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/TODO.md +0 -0
  70. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/contributing-docs.md +0 -0
  71. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/hooks.md +0 -0
  72. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/index.md +0 -0
  73. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/superpowers/plans/2026-04-25-improve-harness-implementation.md +0 -0
  74. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/superpowers/plans/2026-05-12-changelog-reform.md +0 -0
  75. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/superpowers/specs/2026-04-25-improve-harness-design.md +0 -0
  76. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/superpowers/specs/2026-05-12-changelog-reform-design.md +0 -0
  77. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/capture-session.py +0 -0
  78. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/codex-hooks.json +0 -0
  79. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/doctor.py +0 -0
  80. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/__init__.py +0 -0
  81. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/capture.py +0 -0
  82. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/patterns.py +0 -0
  83. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/recall_init_lib.py +0 -0
  84. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/reflect_counter.py +0 -0
  85. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/repo_memory.py +0 -0
  86. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/secret_filters.py +0 -0
  87. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/shell_guard.py +0 -0
  88. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/stop_hook.py +0 -0
  89. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/transcript.py +0 -0
  90. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/vault_config.py +0 -0
  91. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/vault_policy.py +0 -0
  92. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/i-insist.toml +0 -0
  93. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/i_insist.py +0 -0
  94. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/nudge-index-sync.py +0 -0
  95. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/recall-init.py +0 -0
  96. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/reflect-nudge.py +0 -0
  97. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/remind-convos.py +0 -0
  98. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/remind-convos.sh +0 -0
  99. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/scan-vault-secrets.py +0 -0
  100. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/update-changelog.py +0 -0
  101. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/update-changelog.sh +0 -0
  102. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/vault_registry.py +0 -0
  103. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/__init__.py +0 -0
  104. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/__init__.py +0 -0
  105. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/cli.py +0 -0
  106. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/config.py +0 -0
  107. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/filters.py +0 -0
  108. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/guard_install.py +0 -0
  109. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/indexer.py +0 -0
  110. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/mcp_server.py +0 -0
  111. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/models.py +0 -0
  112. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/papercuts.py +0 -0
  113. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/registration.py +0 -0
  114. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/mkdocs.yml +0 -0
  115. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/commands/scan-secrets.md +0 -0
  116. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/capture-session.py +0 -0
  117. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/doctor.py +0 -0
  118. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hooks.json +0 -0
  119. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/__init__.py +0 -0
  120. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/capture.py +0 -0
  121. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/patterns.py +0 -0
  122. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/recall_init_lib.py +0 -0
  123. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/reflect_counter.py +0 -0
  124. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/repo_memory.py +0 -0
  125. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/secret_filters.py +0 -0
  126. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/shell_guard.py +0 -0
  127. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/stop_hook.py +0 -0
  128. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/transcript.py +0 -0
  129. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/vault_config.py +0 -0
  130. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/vault_policy.py +0 -0
  131. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/i-insist.toml +0 -0
  132. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/i_insist.py +0 -0
  133. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/nudge-index-sync.py +0 -0
  134. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/recall-init.py +0 -0
  135. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/reflect-nudge.py +0 -0
  136. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/remind-convos.py +0 -0
  137. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/remind-convos.sh +0 -0
  138. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/scan-vault-secrets.py +0 -0
  139. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/update-changelog.py +0 -0
  140. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/update-changelog.sh +0 -0
  141. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/vault_registry.py +0 -0
  142. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/convention-sweep.py +0 -0
  143. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/filter-unresolved-links.py +0 -0
  144. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/fix-stacked-frontmatter.py +0 -0
  145. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/lib/note-types.md +0 -0
  146. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/lib/stacked-frontmatter.md +0 -0
  147. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/prek.toml +0 -0
  148. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/build_memory_indexes.py +0 -0
  149. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/cli_smoke_test.py +0 -0
  150. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/migrate_changelog.py +0 -0
  151. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/migrate_claude_memory.py +0 -0
  152. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/new-worktree.sh +0 -0
  153. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prek_hooks/check_architecture.py +0 -0
  154. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prek_hooks/check_exception_handling.py +0 -0
  155. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prek_hooks/check_file_length.py +0 -0
  156. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prek_hooks/check_private_test_imports.py +0 -0
  157. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prek_hooks/check_timeless_comments.py +0 -0
  158. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prepare_release.py +0 -0
  159. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/smoke_tool_install.py +0 -0
  160. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/sync_codex_plugin.py +0 -0
  161. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/convention-sweep.py +0 -0
  162. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/filter-unresolved-links.py +0 -0
  163. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/fix-stacked-frontmatter.py +0 -0
  164. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/lib/note-types.md +0 -0
  165. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/lib/stacked-frontmatter.md +0 -0
  166. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/__init__.py +0 -0
  167. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/conftest.py +0 -0
  168. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/sample_vault/.claude/obsidian-knowledge.yaml +0 -0
  169. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/sample_vault/Inbox/random.md +0 -0
  170. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/sample_vault/Journal/diary.md +0 -0
  171. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/sample_vault/wiki/python.md +0 -0
  172. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/sample_vault/wiki/rust.md +0 -0
  173. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/transcript_new_file_no_index.jsonl +0 -0
  174. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/transcript_new_file_with_index.jsonl +0 -0
  175. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_access_errors.py +0 -0
  176. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_architecture_check.py +0 -0
  177. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_capture_instructions.py +0 -0
  178. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_capture_session_hook.py +0 -0
  179. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_cli.py +0 -0
  180. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_cli_dispatch.py +0 -0
  181. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_cli_reliability.py +0 -0
  182. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_cli_smoke.py +0 -0
  183. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_config.py +0 -0
  184. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_doctor.py +0 -0
  185. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_filters.py +0 -0
  186. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_first_install.py +0 -0
  187. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_i_insist_rules.py +0 -0
  188. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_indexer.py +0 -0
  189. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_indexer_reliability.py +0 -0
  190. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_mcp.py +0 -0
  191. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_mcp_download.py +0 -0
  192. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_migrate_changelog.py +0 -0
  193. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_nudge_index_sync.py +0 -0
  194. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_papercut_cli.py +0 -0
  195. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_papercuts.py +0 -0
  196. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_patterns.py +0 -0
  197. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_protect_vault_destructive_ops.py +0 -0
  198. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_public_api.py +0 -0
  199. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_recall_init.py +0 -0
  200. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_recall_init_lib.py +0 -0
  201. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_reflect_counter.py +0 -0
  202. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_reflect_nudge.py +0 -0
  203. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_release.py +0 -0
  204. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_repo_memory.py +0 -0
  205. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_scan_vault_secrets.py +0 -0
  206. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_search_json.py +0 -0
  207. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_secret_scan_filters.py +0 -0
  208. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_session_debounce.py +0 -0
  209. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_transcript.py +0 -0
  210. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_vault_boundaries.py +0 -0
  211. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_vault_policy.py +0 -0
  212. {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_vault_registry.py +0 -0
@@ -11,7 +11,7 @@
11
11
  "name": "obsidian-knowledge",
12
12
  "source": "./",
13
13
  "description": "Skills, hooks, and conventions for maintaining Obsidian knowledge base vaults",
14
- "version": "3.26.0"
14
+ "version": "3.26.2"
15
15
  }
16
16
  ]
17
17
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obsidian-knowledge",
3
- "version": "3.26.0",
3
+ "version": "3.26.2",
4
4
  "description": "Claude Code plugin that manages an Obsidian knowledge vault: capture, organize, index, and garden",
5
5
  "author": {
6
6
  "name": "Ricardo Decal"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obsidian-knowledge",
3
- "version": "3.26.0",
3
+ "version": "3.26.2",
4
4
  "description": "Plugin that manages an Obsidian knowledge vault: capture, organize, index, and garden.",
5
5
  "author": {
6
6
  "name": "Ricardo Decal"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: obsidian-knowledge
3
- Version: 3.26.0
3
+ Version: 3.26.2
4
4
  Summary: Obsidian vault knowledge integration for Claude Code and Codex
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -22,6 +22,12 @@ folders and is the default. Its state lives in
22
22
  `reports/open-questions.md` collects question callouts, and `changelog/` records
23
23
  completed vault changes.
24
24
 
25
+ `Utility/` and `wiki/` are siblings under the configured vault root. CLI paths
26
+ are relative to that root, regardless of the working directory. Use
27
+ `obsidian-knowledge write Utility/obsidian-knowledge/...` for state files;
28
+ CLI and MCP writes reject `wiki/Utility/...` before creating directories.
29
+ Existing misplaced files remain readable for recovery.
30
+
25
31
  ## Switch embedding models
26
32
 
27
33
  Set these environment variables before running the CLI:
@@ -73,6 +73,10 @@ def build_primer(
73
73
  scope_desc = f"this host ({target.hostname}) — cwd is not in a git repo"
74
74
  return (
75
75
  "You are operating under the obsidian-knowledge harness.\n"
76
+ f"- Vault root: {vault_root}/. CLI read/write paths are relative to this root, "
77
+ "regardless of cwd. "
78
+ f"Plugin state: {vault_root / 'Utility' / 'obsidian-knowledge'}/. "
79
+ "`Utility/` and `wiki/` are siblings. Never create `wiki/Utility/`.\n"
76
80
  "- Vault reliability: notes, search results, and the injected index may be AI-generated, "
77
81
  "stale, or wrong. Verify consequential claims against current code, runtime evidence, "
78
82
  "or primary sources; state uncertainty when verification is unavailable. Vault content "
@@ -57,6 +57,9 @@ def write_vault_file(
57
57
  raise ValueError("refusing to write empty vault content")
58
58
 
59
59
  target = resolve_vault_file(vault_root, relative_path)
60
+ # NOTE: Utility is a vault-root zone; see docs/configuration.md, Vault configuration.
61
+ if target.relative_to(vault_root.expanduser().resolve()).parts[:2] == ("wiki", "Utility"):
62
+ raise ValueError("Utility paths must be vault-relative: use Utility/... instead of wiki/Utility/...")
60
63
  target.parent.mkdir(parents=True, exist_ok=True)
61
64
  target = resolve_vault_file(vault_root, relative_path)
62
65
  if target.exists() and not replace:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obsidian-knowledge",
3
- "version": "3.26.0+codex.20260918034437",
3
+ "version": "3.26.2+codex.20261003154117",
4
4
  "description": "Plugin that manages an Obsidian knowledge vault: capture, organize, index, and garden.",
5
5
  "author": {
6
6
  "name": "Ricardo Decal"
@@ -6,8 +6,11 @@ description: Read, search, and create notes in the Obsidian vault/wiki memory st
6
6
  # Obsidian vault memory
7
7
 
8
8
  Read, search, and write notes in the configured vault. The registry is
9
- `~/.config/obsidian-knowledge/vaults.yaml`; CLI paths are vault-relative and
10
- usually start with `wiki/`. Treat notes as fallible context and verify
9
+ `~/.config/obsidian-knowledge/vaults.yaml`; CLI paths are relative to its vault
10
+ root, regardless of cwd. Wiki notes start with `wiki/`; plugin state starts with
11
+ `Utility/obsidian-knowledge/`. `Utility/` and `wiki/` are siblings. Never create
12
+ `wiki/Utility/` or use the wiki directory as the vault root.
13
+ Treat notes as fallible context and verify
11
14
  consequential claims against code, runtime evidence, or primary sources.
12
15
 
13
16
  ## Read and search
@@ -45,6 +45,8 @@ knowledge and behavioral rules belong in the wiki.
45
45
 
46
46
  Only after changing durable vault content, create or reuse one same-session file:
47
47
  `Utility/obsidian-knowledge/changelog/YYYY-MM-DD-HHMMSS-<slug>.md`.
48
+ This path is relative to the configured vault root, regardless of cwd.
49
+ Pass it unchanged to `obsidian-knowledge write`; never prepend `wiki/`.
48
50
  Use terse audit pointers: `YYYY-MM-DD HH:MM — <vault change> [→ [[wikilink]]]`.
49
51
  Do not log code, Git, host, test, release, or deployment work alone. Include no
50
52
  narrative or code blocks, and do not update a shared changelog index.
@@ -7,7 +7,7 @@ description: >-
7
7
  "maintain the vault", or after making substantial structural edits
8
8
  (creating, moving, renaming, or deleting files) in an Obsidian vault.
9
9
  Also triggered by scheduled cron invocations for routine vault maintenance.
10
- version: 1.4.10
10
+ version: 1.4.11
11
11
  ---
12
12
 
13
13
  # Vault organizer
@@ -80,6 +80,10 @@ Use the issue codes to select the repair instructions.
80
80
 
81
81
  **`NOT_INDEXED <index> entry=<name>`**: Add the entry. Read `lib/index-format.md`.
82
82
 
83
+ **`EMPTY_FOLDER <folder>`**: Triage separately. Do not create an empty index or
84
+ delete the folder. Sync arrivals, runtime directories, and established memory
85
+ layouts can explain it. Check local instructions before proposing a repair.
86
+
83
87
  **`DUMPING_GROUND <folder> misplaced=N inline_total=T subfolders=M`**: Classify
84
88
  the misplaced inline files, which have date prefixes or `*-design`, `*-convo`,
85
89
  or `*-diary` names, and move them to typed subfolders. Read `lib/note-types.md`
@@ -101,13 +105,15 @@ After structural fixes, rename ambiguous non-Markdown files. Read `lib/rename-fi
101
105
 
102
106
  ```bash
103
107
  obsidian vault="$VAULT_NAME" unresolved verbose format=json | uv run --no-project --with pyyaml python "$SCRIPTS/filter-unresolved-links.py" "$VAULT"
104
- obsidian vault="$VAULT_NAME" unresolved verbose format=json | uv run --no-project --with pyyaml python "$SCRIPTS/recover-unresolved-links.py" "$VAULT" > /tmp/vault-unresolved-recovery.tsv
108
+ obsidian vault="$VAULT_NAME" unresolved verbose format=json | uv run --no-project --with pyyaml --with pydantic python "$SCRIPTS/recover-unresolved-links.py" "$VAULT" > /tmp/vault-unresolved-recovery.tsv
105
109
  obsidian vault="$VAULT_NAME" orphans
106
110
  ```
107
111
 
108
112
  Read [broken-link triage](lib/broken-links.md) for each remaining candidate.
109
113
  Review the recovery report before using `recover-unresolved-links.py --apply`;
110
- it changes only unique, high-confidence matches.
114
+ it changes only unique, high-confidence matches. Preserve aliases, headings,
115
+ block references, and all text outside link targets. Read the verification
116
+ checks in `lib/broken-links.md` before applying repairs.
111
117
 
112
118
  ### Fix convention violations
113
119
 
@@ -130,18 +136,26 @@ Only the organizer writes this worklist.
130
136
 
131
137
  ### Regenerate reports
132
138
 
133
- Rewrite `$VAULT/Utility/obsidian-knowledge/reports/open-questions.md` from scratch:
139
+ Render `$VAULT/Utility/obsidian-knowledge/reports/open-questions.md` for review:
134
140
 
135
141
  ```bash
136
- uv run --no-project --with pyyaml python "$SCRIPTS/find-open-questions.py" "$VAULT"
142
+ uv run --no-project --with pyyaml --with pydantic python "$SCRIPTS/find-open-questions.py" "$VAULT" \
143
+ --report Utility/obsidian-knowledge/reports/open-questions.md > /tmp/open-questions-review.md
137
144
  ```
138
145
 
139
- Output is `<rel_path>\t<line>\t<question_text>` per question. Code-block examples
140
- are filtered automatically. Build one entry per line:
141
- `- [[wiki/path/to/page]] — line N — "question text"`
146
+ Review the rendered file, then repeat with `--apply`. The renderer preserves
147
+ frontmatter, heading, regeneration notice, and scope. It changes only the entry
148
+ list and `Last run:` timestamp. Use `--timestamp` with a timezone-aware ISO value
149
+ for repeatable output; the default is current local time. Without `--report`,
150
+ output remains `<rel_path>\t<line>\t<question_text>` per question. Fenced examples,
151
+ hidden files, protected sources, and sync conflicts are excluded.
152
+
153
+ ### Verify completed repairs
142
154
 
143
- Preserve the existing heading, regeneration notice, and scope section. Change
144
- only the entry list and `Last run:` timestamp.
155
+ Re-run the structural audit and obtain fresh Obsidian unresolved and orphan
156
+ results. Check repaired targets and files individually; a lower total count
157
+ does not prove a repair. If CLI results disagree with verified file contents,
158
+ refresh Obsidian's cache and repeat. Record remaining exceptions in the worklist.
145
159
 
146
160
  ### Update the worklist
147
161
 
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ """Render reviewed JSON entries into one index section; --apply verifies writes.
3
+
4
+ Usage: edit-index.py VAULT wiki/topic/index.md [--apply] < reviewed.json
5
+ Input: {"section": "Notes", "entries": [{"target": "wiki/topic/note",
6
+ "description": "orientation", "label": "optional display name"}]}
7
+ For a new flat index, supply "title" instead of "section". Existing prose and
8
+ other sections remain byte-for-byte intact. Ambiguous entry blocks are refused.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import re
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from organizer_io import OrganizerPolicy, visible_path, write_checked
19
+ from pydantic import BaseModel, ConfigDict, field_validator
20
+
21
+ ENTRY = re.compile(r"^- \[\[([^\]|#^]+)(?:\|[^\]]+)?\]\].*$")
22
+ HEADING = re.compile(r"^(#{1,6}) (.+?)\s*$")
23
+
24
+
25
+ class ReviewedEntry(BaseModel):
26
+ model_config = ConfigDict(extra="forbid", frozen=True)
27
+ target: str
28
+ description: str
29
+ label: str | None = None
30
+
31
+ @field_validator("target", "label")
32
+ @classmethod
33
+ def single_link_component(cls, value: str | None) -> str | None:
34
+ if value is not None and (not value.strip() or any(c in value for c in "\n\r[]|#^")):
35
+ raise ValueError("target and label must be nonblank single link components")
36
+ return value
37
+
38
+ @field_validator("description")
39
+ @classmethod
40
+ def one_line(cls, value: str) -> str:
41
+ if not value.strip() or "\n" in value or "\r" in value:
42
+ raise ValueError("description must be a nonblank single line")
43
+ return value
44
+
45
+ def render(self) -> str:
46
+ label = f"|{self.label}" if self.label else ""
47
+ return f"- [[{self.target.removesuffix('.md')}{label}]] — {self.description}\n"
48
+
49
+
50
+ class IndexReview(BaseModel):
51
+ model_config = ConfigDict(extra="forbid", frozen=True)
52
+ section: str | None = None
53
+ title: str | None = None
54
+ entries: tuple[ReviewedEntry, ...]
55
+
56
+ @field_validator("section", "title")
57
+ @classmethod
58
+ def heading_text(cls, value: str | None) -> str | None:
59
+ if value is not None and (not value.strip() or "\n" in value or "\r" in value):
60
+ raise ValueError("heading must be a nonblank single line")
61
+ return value
62
+
63
+
64
+ def resolve_entry(vault: Path, index: Path, target: str) -> Path:
65
+ for relative in (target, target + ".md"):
66
+ for candidate in (relative, (index.parent.relative_to(vault) / relative).as_posix()):
67
+ path = visible_path(vault, candidate)
68
+ if path.is_file():
69
+ return path
70
+ raise ValueError(f"target does not exist: {target}")
71
+
72
+
73
+ def section_bounds(lines: tuple[str, ...], section: str | None) -> tuple[int, int]:
74
+ headings = tuple((i, match) for i, line in enumerate(lines) if (match := HEADING.match(line)))
75
+ if section is None:
76
+ if any(len(match[1]) > 1 for _, match in headings):
77
+ raise ValueError("sectioned index requires an explicit section")
78
+ return (headings[0][0] + 1 if headings else 0), len(lines)
79
+ matches = tuple((i, match) for i, match in headings if match[2] == section)
80
+ if len(matches) != 1:
81
+ raise ValueError(f"section must exist exactly once: {section}")
82
+ start, selected = matches[0]
83
+ end = next((i for i, match in headings if i > start and len(match[1]) <= len(selected[1])), len(lines))
84
+ if any(i > start and i < end for i, _ in headings):
85
+ raise ValueError("select a leaf section without nested headings")
86
+ return start + 1, end
87
+
88
+
89
+ def sort_entry(line: str) -> tuple[bool, str]:
90
+ match = ENTRY.match(line)
91
+ assert match is not None
92
+ target = match[1].removesuffix(".md")
93
+ folder = target.endswith("/index")
94
+ name = Path(target).parent.name if folder else Path(target).name
95
+ return not folder, name.casefold()
96
+
97
+
98
+ def reviewed_entries(
99
+ vault: Path, index: Path, existing_lines: tuple[str, ...], review: IndexReview
100
+ ) -> tuple[str, ...]:
101
+ entries = list(existing_lines)
102
+ for entry in review.entries:
103
+ resolved = resolve_entry(vault, index, entry.target)
104
+ if resolved == index:
105
+ raise ValueError("index cannot link to itself")
106
+ matching = []
107
+ for i, line in enumerate(entries):
108
+ match = ENTRY.match(line)
109
+ assert match is not None
110
+ try:
111
+ existing = resolve_entry(vault, index, match[1])
112
+ except ValueError:
113
+ continue
114
+ if existing == resolved:
115
+ matching.append(i)
116
+ if len(matching) > 1:
117
+ raise ValueError(f"duplicate existing entries: {entry.target}")
118
+ if matching:
119
+ entries[matching[0]] = entry.render()
120
+ else:
121
+ entries.append(entry.render())
122
+ return tuple(sorted(entries, key=sort_entry))
123
+
124
+
125
+ def render_index(vault: Path, index: Path, before: str | None, review: IndexReview) -> str:
126
+ if before is None:
127
+ if not review.title or review.section is not None:
128
+ raise ValueError("new index requires title and no section")
129
+ before = f"# {review.title}\n\n"
130
+ lines = tuple(before.splitlines(keepends=True))
131
+ if any(line.lstrip().startswith(("```", "~~~")) for line in lines):
132
+ raise ValueError("index contains fenced examples; review it manually")
133
+ start, end = section_bounds(lines, review.section)
134
+ positions = tuple(i for i in range(start, end) if ENTRY.match(lines[i]))
135
+ if positions and positions != tuple(range(positions[0], positions[-1] + 1)):
136
+ raise ValueError("section has multiple entry blocks; review them separately")
137
+ block_start = positions[0] if positions else end
138
+ block_end = positions[-1] + 1 if positions else end
139
+ if (
140
+ positions
141
+ and block_end < end
142
+ and lines[block_end].strip()
143
+ and lines[block_end].startswith((" ", "\t"))
144
+ ):
145
+ raise ValueError("entry has continuation text; review it manually")
146
+ entries = reviewed_entries(vault, index, lines[block_start:block_end], review)
147
+ prefix = "".join(lines[:block_start])
148
+ suffix = "".join(lines[block_end:])
149
+ if not positions and entries:
150
+ if prefix and not prefix.endswith("\n\n"):
151
+ prefix += "\n" if prefix.endswith("\n") else "\n\n"
152
+ if suffix and not suffix.startswith("\n"):
153
+ suffix = "\n" + suffix
154
+ return prefix + "".join(entries) + suffix
155
+
156
+
157
+ def main() -> None:
158
+ parser = argparse.ArgumentParser(description=__doc__)
159
+ parser.add_argument("vault_root", type=Path)
160
+ parser.add_argument("index_path")
161
+ parser.add_argument("--apply", action="store_true")
162
+ args = parser.parse_args()
163
+ try:
164
+ vault = args.vault_root.resolve(strict=True)
165
+ if Path(args.index_path).name != "index.md":
166
+ raise ValueError("edit-index only edits index.md")
167
+ index = OrganizerPolicy.load(vault).writable(vault, args.index_path)
168
+ before = index.read_bytes().decode("utf-8") if index.exists() else None
169
+ review = IndexReview.model_validate_json(sys.stdin.read())
170
+ after = render_index(vault, index, before, review)
171
+ if args.apply:
172
+ write_checked(vault, args.index_path, before, after)
173
+ else:
174
+ sys.stdout.write(after)
175
+ except (ValueError, OSError) as exc:
176
+ parser.exit(1, f"Error: {exc}\n")
177
+
178
+
179
+ if __name__ == "__main__":
180
+ main()
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ """find-open-questions: scan ai_managed zones for `> [!question]` callouts.
3
+
4
+ Usage: python3 find-open-questions.py <vault_root>
5
+
6
+ Skips matches inside fenced code blocks (``` or ~~~), which are usually
7
+ documentation examples rather than real open questions. Emits one line per
8
+ hit:
9
+
10
+ <relative_path>\t<line_number>\t<question_text>
11
+
12
+ Add --report Utility/obsidian-knowledge/reports/open-questions.md to render
13
+ Markdown for review, --timestamp for reproducible output, and --apply to write.
14
+ Reads ai_managed zones from
15
+ <vault_root>/.claude/obsidian-knowledge.yaml; falls back to ['wiki'].
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import re
22
+ import sys
23
+ from datetime import datetime
24
+ from pathlib import Path
25
+
26
+ from organizer_io import visible_path, write_checked
27
+ from pydantic import BaseModel, ConfigDict
28
+
29
+ try:
30
+ import yaml
31
+
32
+ HAS_YAML = True
33
+ except ImportError:
34
+ HAS_YAML = False
35
+
36
+ QUESTION_MARKER = "> [!question]"
37
+ SCAN_SKIP_DIR_NAMES = {"_sources", ".trash", "node_modules"}
38
+
39
+
40
+ class QuestionHit(BaseModel):
41
+ model_config = ConfigDict(frozen=True)
42
+ path: str
43
+ line: int
44
+ text: str
45
+
46
+
47
+ def render_question(hit: QuestionHit) -> str:
48
+ # Markdown text, not JSON: preserve backslashes used by math and escapes.
49
+ quoted = hit.text.replace('"', '\\"')
50
+ return f'- [[{hit.path.removesuffix(".md")}]] — line {hit.line} — "{quoted}"\n'
51
+
52
+
53
+ def render_report(before: str | None, hits: tuple[QuestionHit, ...], timestamp: str) -> str:
54
+ if before is None:
55
+ before = (
56
+ "# Open questions\n\n"
57
+ "> Regenerated by vault-organizer. Resolve questions in the source page.\n\n"
58
+ "**Last run:** never\n\n## Scope\n\n"
59
+ "Question callouts in configured AI-managed zones; protected, hidden, "
60
+ "dependency and sync-conflict files excluded.\n\n"
61
+ )
62
+ lines = before.splitlines(keepends=True)
63
+ first_entry = next(
64
+ (i for i, line in enumerate(lines) if re.match(r"^- \[\[.*\]\] — line \d+ — ", line)), len(lines)
65
+ )
66
+ if any(
67
+ line.strip() and not re.match(r"^- \[\[.*\]\] — line \d+ — ", line) for line in lines[first_entry:]
68
+ ):
69
+ raise ValueError("report has content after generated entries; refusing to discard it")
70
+ prefix = "".join(lines[:first_entry])
71
+ prefix, count = re.subn(r"(?m)^\*\*Last run:\*\*[^\r\n]*", lambda _: f"**Last run:** {timestamp}", prefix)
72
+ if count != 1:
73
+ raise ValueError("report must contain exactly one **Last run:** line")
74
+ if not prefix.endswith("\n\n"):
75
+ prefix += "\n" if prefix.endswith("\n") else "\n\n"
76
+ entries = "".join(render_question(hit) for hit in hits)
77
+ return prefix + entries
78
+
79
+
80
+ def load_managed_zones(vault_root: Path) -> list[str]:
81
+ config_path = vault_root / ".claude" / "obsidian-knowledge.yaml"
82
+ if HAS_YAML and config_path.exists():
83
+ with open(config_path) as f:
84
+ cfg = yaml.safe_load(f) or {}
85
+ return cfg.get("ai_managed", ["wiki"])
86
+ return ["wiki"]
87
+
88
+
89
+ def scan_file(path: Path) -> list[tuple[int, str]]:
90
+ """Return [(line_no, question_text)] for each callout outside code fences."""
91
+ hits: list[tuple[int, str]] = []
92
+ in_fence = False
93
+ fence_marker = ""
94
+ try:
95
+ with open(path, encoding="utf-8", errors="replace") as f:
96
+ lines = f.readlines()
97
+ except OSError:
98
+ return hits
99
+
100
+ for i, line in enumerate(lines, start=1):
101
+ stripped = line.lstrip()
102
+ # Track fenced code-block state. Markdown fences start with ``` or ~~~.
103
+ if stripped.startswith(("```", "~~~")):
104
+ marker = stripped[:3]
105
+ if not in_fence:
106
+ in_fence = True
107
+ fence_marker = marker
108
+ elif stripped.startswith(fence_marker):
109
+ in_fence = False
110
+ fence_marker = ""
111
+ continue
112
+
113
+ if in_fence:
114
+ continue
115
+
116
+ if line.rstrip("\n").startswith(QUESTION_MARKER):
117
+ # Question text is on the next non-blank `> ` line.
118
+ question_text = ""
119
+ for j in range(i, len(lines)):
120
+ next_line = lines[j].rstrip("\n")
121
+ if next_line.startswith("> ") and next_line.strip() != ">":
122
+ question_text = next_line[2:].strip()
123
+ break
124
+ if not next_line.startswith(">"):
125
+ break
126
+ hits.append((i, question_text))
127
+
128
+ return hits
129
+
130
+
131
+ def main() -> None:
132
+ parser = argparse.ArgumentParser(description=__doc__)
133
+ parser.add_argument("vault_root", type=Path)
134
+ parser.add_argument("--report", help="render the existing open-questions report (dry run)")
135
+ parser.add_argument("--timestamp", help="timezone-aware ISO timestamp; default: current local time")
136
+ parser.add_argument(
137
+ "--apply", action="store_true", help="write the reviewed report using the verified writer"
138
+ )
139
+ args = parser.parse_args()
140
+ if args.apply and not args.report:
141
+ parser.error("--apply requires --report")
142
+ vault_root = args.vault_root.resolve()
143
+ if not vault_root.is_dir():
144
+ print(f"Error: {vault_root} is not a directory", file=sys.stderr)
145
+ sys.exit(1)
146
+
147
+ zones = load_managed_zones(vault_root)
148
+ hits = []
149
+ for zone in zones:
150
+ zone_root = vault_root / zone
151
+ if not zone_root.is_dir():
152
+ continue
153
+ for md in sorted(zone_root.rglob("*.md")):
154
+ rel_parts = md.relative_to(vault_root).parts
155
+ if any(part.startswith(".") or part in SCAN_SKIP_DIR_NAMES for part in rel_parts):
156
+ continue
157
+ rel = md.relative_to(vault_root).as_posix()
158
+ try:
159
+ visible_path(vault_root, rel)
160
+ except ValueError:
161
+ continue
162
+ for line_no, text in scan_file(md):
163
+ hits.append(QuestionHit(path=rel, line=line_no, text=text))
164
+ if not args.report:
165
+ for hit in hits:
166
+ print(f"{hit.path}\t{hit.line}\t{hit.text}")
167
+ return
168
+ try:
169
+ if args.report != "Utility/obsidian-knowledge/reports/open-questions.md":
170
+ raise ValueError("--report must target Utility/obsidian-knowledge/reports/open-questions.md")
171
+ stamp = datetime.fromisoformat(args.timestamp) if args.timestamp else datetime.now().astimezone()
172
+ if stamp.utcoffset() is None:
173
+ raise ValueError("timestamp must include a timezone offset")
174
+ path = visible_path(vault_root, args.report)
175
+ before = path.read_bytes().decode("utf-8") if path.exists() else None
176
+ after = render_report(before, tuple(hits), stamp.isoformat(timespec="seconds"))
177
+ if args.apply:
178
+ write_checked(vault_root, args.report, before, after)
179
+ else:
180
+ sys.stdout.write(after)
181
+ except (ValueError, OSError) as exc:
182
+ parser.exit(1, f"Error: {exc}\n")
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -26,6 +26,29 @@ Dates, paths, extensions, and missing embeds usually indicate expected files.
26
26
  A bare concept such as `[[anxiety]]` might be an intentional stub. When uncertain,
27
27
  add the issue to the worklist rather than silently skipping it.
28
28
 
29
+ ## Deterministic recovery and verification
30
+
31
+ `recover-unresolved-links.py` defaults to reporting. Review before `--apply`.
32
+ It can recover a stale prefix only when removing leading path components leaves
33
+ a unique existing suffix of at least `directory/name`. It never falls back from
34
+ a path-shaped target to an unrelated basename. Existing directories are not
35
+ note candidates. Attachments require exact filenames, including extensions;
36
+ normalized and fuzzy comparisons apply only to Markdown notes.
37
+
38
+ Hidden paths, dependency directories, sync conflicts, and symlinks are excluded
39
+ from candidates. Sources must be visible managed Markdown notes outside
40
+ `_sources/` and configured read-only paths. Apply rechecks write policy and
41
+ delegates to the verified writer; aliases, heading and block suffixes survive.
42
+
43
+ Before repair, retain exact source bytes as a baseline. After repair, compare
44
+ the diff and confirm only intended link targets changed. Primary prose, aliases,
45
+ headings, block references, and frontmatter must remain intact. Then obtain
46
+ fresh `obsidian vault="$VAULT_NAME" unresolved verbose format=json` results and
47
+ confirm each repaired target disappeared from the relevant source's findings.
48
+ If file contents and CLI results disagree, reload Obsidian with
49
+ `obsidian vault="$VAULT_NAME" command id="app:reload"` and retry after it loads.
50
+ Review fresh orphan results too; unrelated totals can change during sync.
51
+
29
52
  ## Orphans
30
53
 
31
54
  Run `obsidian vault="$VAULT_NAME" orphans`. Add managed-zone orphans to their