claude-dev-env 1.24.0 → 1.25.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.
@@ -1,15 +1,22 @@
1
1
  """Tests for magic value detection."""
2
2
 
3
3
  import ast
4
+ import tempfile
5
+ from pathlib import Path
4
6
 
5
7
  import pytest
6
8
 
7
9
  from magic_value_checks import (
8
10
  check_magic_values,
11
+ validate_file,
9
12
  )
10
13
  from validator_base import Violation
11
14
 
12
15
 
16
+ MAGIC_NUMBER_SOURCE = "x = 42\n"
17
+ NON_TEST_TEMPDIR_PREFIX = "src_"
18
+
19
+
13
20
  GOOD_NAMED_CONSTANTS = '''
14
21
  API_TIMEOUT_MS = 5000
15
22
  HASH_DELIMITER = "__"
@@ -29,8 +36,23 @@ ALLOWED_SMALL_NUMBERS = '''
29
36
  def process():
30
37
  count = 0
31
38
  increment = 1
32
- doubled = count * 2
33
- return count + 1
39
+ negative = -1
40
+ return count + increment + negative
41
+ '''
42
+
43
+ ALLOWED_NEGATIVE_ONE_IN_BINARY_EXPRESSION = '''
44
+ def process(total):
45
+ return total * -1
46
+ '''
47
+
48
+ ALLOWED_NEGATIVE_ONE_IN_RETURN = '''
49
+ def process():
50
+ return -1
51
+ '''
52
+
53
+ BAD_NEGATIVE_LITERAL_TWO = '''
54
+ def process():
55
+ return total * -2
34
56
  '''
35
57
 
36
58
  ALLOWED_EMPTY_STRING = '''
@@ -39,6 +61,114 @@ def process():
39
61
  return result
40
62
  '''
41
63
 
64
+ BAD_LITERAL_TWO = '''
65
+ def process():
66
+ doubled = something * 2
67
+ return doubled
68
+ '''
69
+
70
+ BAD_LITERAL_ONE_HUNDRED = '''
71
+ def process():
72
+ percentage = fraction * 100
73
+ return percentage
74
+ '''
75
+
76
+ ALLOWED_NEGATIVE_UPPER_CONSTANT_ASSIGNMENT = '''
77
+ LIMIT = -100
78
+ OFFSET = -5
79
+ '''
80
+
81
+ ALLOWED_TYPED_NEGATIVE_UPPER_CONSTANT = '''
82
+ LIMIT: int = -100
83
+ OFFSET: int = -5
84
+ '''
85
+
86
+ DOUBLE_NEGATED_LITERAL = '''
87
+ def process():
88
+ return --5
89
+ '''
90
+
91
+ DICT_VALUED_CONSTANT = '''
92
+ SETTINGS = {"timeout": 30, "retries": 5}
93
+ '''
94
+
95
+ TUPLE_VALUED_CONSTANT = '''
96
+ RETRY_DELAYS = (2, 4, 8)
97
+ '''
98
+
99
+ LIST_VALUED_CONSTANT = '''
100
+ PORTS = [8080, 8443, 9000]
101
+ '''
102
+
103
+ NESTED_DICT_VALUED_CONSTANT = '''
104
+ CONFIG = {"db": {"port": 5432}}
105
+ '''
106
+
107
+ ANNOTATED_SCALAR_CONSTANT = '''
108
+ TIMEOUT_MS: int = 5000
109
+ '''
110
+
111
+ ANNOTATED_DICT_VALUED_CONSTANT = '''
112
+ SETTINGS: dict[str, int] = {"timeout": 30}
113
+ '''
114
+
115
+ NON_CONSTANT_ASSIGNMENT_IN_FUNCTION = '''
116
+ def configure():
117
+ delay = 30
118
+ return delay
119
+ '''
120
+
121
+ SCALAR_MAGIC_NUMBER_OUTSIDE_CONSTANT = '''
122
+ def compute():
123
+ return 30 + 5000
124
+ '''
125
+
126
+ LEADING_UNDERSCORE_TARGET = '''
127
+ _PRIVATE = {"timeout": 30}
128
+ '''
129
+
130
+ DOUBLE_UNDERSCORE_TARGET = '''
131
+ __PRIVATE = {"timeout": 30}
132
+ '''
133
+
134
+ NON_CONTAINER_RHS_CALL = '''
135
+ CONFIG = some_factory(30, retries=5)
136
+ '''
137
+
138
+ NON_CONTAINER_RHS_BINOP = '''
139
+ WINDOW = base + 5000
140
+ '''
141
+
142
+ TUPLE_TARGET_ASSIGNMENT = '''
143
+ A, B = 1, 30
144
+ '''
145
+
146
+ AUG_ASSIGN_UPPER_SNAKE = '''
147
+ COUNTER = 0
148
+ COUNTER += 30
149
+ '''
150
+
151
+ CLASS_BODY_UPPER_SNAKE_CONSTANT = '''
152
+ class Foo:
153
+ TIMEOUT = 30
154
+ '''
155
+
156
+ ANNOTATED_LITERAL_TYPE = '''
157
+ X: Literal[42] = 42
158
+ '''
159
+
160
+ TRUE_LITERAL_IN_FUNCTION = '''
161
+ def configure():
162
+ is_enabled = True
163
+ return is_enabled
164
+ '''
165
+
166
+ FALSE_LITERAL_IN_FUNCTION = '''
167
+ def configure():
168
+ is_disabled = False
169
+ return is_disabled
170
+ '''
171
+
42
172
 
43
173
  class TestMagicValues:
44
174
  def test_named_constants_pass(self) -> None:
@@ -57,7 +187,231 @@ class TestMagicValues:
57
187
  violations = check_magic_values(tree, "test.py")
58
188
  assert violations == []
59
189
 
190
+ def test_negative_one_allowed_in_binary_expression(self) -> None:
191
+ tree = ast.parse(ALLOWED_NEGATIVE_ONE_IN_BINARY_EXPRESSION)
192
+ violations = check_magic_values(tree, "test.py")
193
+ assert violations == []
194
+
195
+ def test_negative_one_allowed_in_return_expression(self) -> None:
196
+ tree = ast.parse(ALLOWED_NEGATIVE_ONE_IN_RETURN)
197
+ violations = check_magic_values(tree, "test.py")
198
+ assert violations == []
199
+
200
+ def test_negative_literal_two_is_flagged_with_signed_value(self) -> None:
201
+ tree = ast.parse(BAD_NEGATIVE_LITERAL_TWO)
202
+ violations = check_magic_values(tree, "test.py")
203
+ assert len(violations) == 1
204
+ assert "-2" in violations[0].message
205
+
60
206
  def test_empty_string_allowed(self) -> None:
61
207
  tree = ast.parse(ALLOWED_EMPTY_STRING)
62
208
  violations = check_magic_values(tree, "test.py")
63
209
  assert violations == []
210
+
211
+ def test_check_magic_values_should_flag_literal_two_in_function_body(self) -> None:
212
+ tree = ast.parse(BAD_LITERAL_TWO)
213
+ violations = check_magic_values(tree, "test.py")
214
+ assert len(violations) == 1
215
+ assert "2" in violations[0].message
216
+
217
+ def test_check_magic_values_should_flag_literal_one_hundred_in_function_body(self) -> None:
218
+ tree = ast.parse(BAD_LITERAL_ONE_HUNDRED)
219
+ violations = check_magic_values(tree, "test.py")
220
+ assert len(violations) == 1
221
+ assert "100" in violations[0].message
222
+
223
+ def test_negative_upper_constant_assignment_allowed(self) -> None:
224
+ tree = ast.parse(ALLOWED_NEGATIVE_UPPER_CONSTANT_ASSIGNMENT)
225
+ violations = check_magic_values(tree, "test.py")
226
+ assert violations == []
227
+
228
+ def test_typed_negative_upper_constant_allowed(self) -> None:
229
+ tree = ast.parse(ALLOWED_TYPED_NEGATIVE_UPPER_CONSTANT)
230
+ violations = check_magic_values(tree, "test.py")
231
+ assert violations == []
232
+
233
+ def test_double_negation_reports_collapsed_positive_value(self) -> None:
234
+ tree = ast.parse(DOUBLE_NEGATED_LITERAL)
235
+ violations = check_magic_values(tree, "test.py")
236
+ assert len(violations) == 1
237
+ assert "5" in violations[0].message
238
+ assert "-5" not in violations[0].message
239
+
240
+ def test_should_exempt_numbers_inside_dict_valued_constant(self) -> None:
241
+ tree = ast.parse(DICT_VALUED_CONSTANT)
242
+ violations = check_magic_values(tree, "test.py")
243
+ assert violations == []
244
+
245
+ def test_should_exempt_numbers_inside_tuple_valued_constant(self) -> None:
246
+ tree = ast.parse(TUPLE_VALUED_CONSTANT)
247
+ violations = check_magic_values(tree, "test.py")
248
+ assert violations == []
249
+
250
+ def test_should_exempt_numbers_inside_list_valued_constant(self) -> None:
251
+ tree = ast.parse(LIST_VALUED_CONSTANT)
252
+ violations = check_magic_values(tree, "test.py")
253
+ assert violations == []
254
+
255
+ def test_should_exempt_numbers_inside_nested_dict_valued_constant(self) -> None:
256
+ tree = ast.parse(NESTED_DICT_VALUED_CONSTANT)
257
+ violations = check_magic_values(tree, "test.py")
258
+ assert violations == []
259
+
260
+ def test_should_exempt_numbers_inside_annotated_upper_snake_constant(self) -> None:
261
+ tree = ast.parse(ANNOTATED_SCALAR_CONSTANT)
262
+ violations = check_magic_values(tree, "test.py")
263
+ assert violations == []
264
+
265
+ def test_should_exempt_numbers_inside_annotated_dict_valued_constant(self) -> None:
266
+ tree = ast.parse(ANNOTATED_DICT_VALUED_CONSTANT)
267
+ violations = check_magic_values(tree, "test.py")
268
+ assert violations == []
269
+
270
+ def test_should_still_flag_numbers_in_function_body_assignments(self) -> None:
271
+ tree = ast.parse(NON_CONSTANT_ASSIGNMENT_IN_FUNCTION)
272
+ violations = check_magic_values(tree, "test.py")
273
+ assert len(violations) == 1
274
+ assert "30" in violations[0].message
275
+
276
+ def test_should_still_flag_scalar_magic_number_outside_constant(self) -> None:
277
+ tree = ast.parse(SCALAR_MAGIC_NUMBER_OUTSIDE_CONSTANT)
278
+ violations = check_magic_values(tree, "test.py")
279
+ flagged_numbers = {violation.message for violation in violations}
280
+ assert any("30" in message for message in flagged_numbers)
281
+ assert any("5000" in message for message in flagged_numbers)
282
+
283
+ def test_should_flag_literal_under_single_leading_underscore_target(self) -> None:
284
+ tree = ast.parse(LEADING_UNDERSCORE_TARGET)
285
+ violations = check_magic_values(tree, "test.py")
286
+ assert any("30" in violation.message for violation in violations)
287
+
288
+ def test_should_flag_literal_under_double_leading_underscore_target(self) -> None:
289
+ tree = ast.parse(DOUBLE_UNDERSCORE_TARGET)
290
+ violations = check_magic_values(tree, "test.py")
291
+ assert any("30" in violation.message for violation in violations)
292
+
293
+ def test_should_flag_literal_under_non_container_call_rhs(self) -> None:
294
+ tree = ast.parse(NON_CONTAINER_RHS_CALL)
295
+ violations = check_magic_values(tree, "test.py")
296
+ flagged_numbers = {violation.message for violation in violations}
297
+ assert any("30" in message for message in flagged_numbers)
298
+ assert any("5" in message for message in flagged_numbers)
299
+
300
+ def test_should_flag_literal_under_non_container_binop_rhs(self) -> None:
301
+ tree = ast.parse(NON_CONTAINER_RHS_BINOP)
302
+ violations = check_magic_values(tree, "test.py")
303
+ assert any("5000" in violation.message for violation in violations)
304
+
305
+ def test_tuple_target_assignment_flags_literal_when_no_upper_snake_target(
306
+ self,
307
+ ) -> None:
308
+ tree = ast.parse(TUPLE_TARGET_ASSIGNMENT)
309
+ violations = check_magic_values(tree, "test.py")
310
+ assert any("30" in violation.message for violation in violations)
311
+
312
+ def test_aug_assign_on_upper_snake_target_is_not_exempt(self) -> None:
313
+ tree = ast.parse(AUG_ASSIGN_UPPER_SNAKE)
314
+ violations = check_magic_values(tree, "test.py")
315
+ assert any("30" in violation.message for violation in violations)
316
+
317
+ def test_class_body_upper_snake_constant_is_exempt(self) -> None:
318
+ tree = ast.parse(CLASS_BODY_UPPER_SNAKE_CONSTANT)
319
+ violations = check_magic_values(tree, "test.py")
320
+ assert violations == []
321
+
322
+ def test_annotated_literal_type_flags_literal_inside_subscript_annotation(
323
+ self,
324
+ ) -> None:
325
+ tree = ast.parse(ANNOTATED_LITERAL_TYPE)
326
+ violations = check_magic_values(tree, "test.py")
327
+ assert any("42" in violation.message for violation in violations)
328
+
329
+ def test_check_magic_values_should_not_flag_True_literal(self) -> None:
330
+ tree = ast.parse(TRUE_LITERAL_IN_FUNCTION)
331
+ violations = check_magic_values(tree, "test.py")
332
+ assert violations == []
333
+
334
+ def test_check_magic_values_should_not_flag_False_literal(self) -> None:
335
+ tree = ast.parse(FALSE_LITERAL_IN_FUNCTION)
336
+ violations = check_magic_values(tree, "test.py")
337
+ assert violations == []
338
+
339
+
340
+ class TestValidateFilePathExemptions:
341
+ def test_validate_file_should_skip_test_underscore_py_files(
342
+ self, tmp_path: Path
343
+ ) -> None:
344
+ tests_directory = tmp_path / "tests"
345
+ tests_directory.mkdir()
346
+ test_module_path = tests_directory / "test_foo.py"
347
+ test_module_path.write_text(MAGIC_NUMBER_SOURCE, encoding="utf-8")
348
+
349
+ assert validate_file(test_module_path) == []
350
+
351
+ def test_validate_file_should_skip_config_directory(
352
+ self, tmp_path: Path
353
+ ) -> None:
354
+ config_directory = tmp_path / "config"
355
+ config_directory.mkdir()
356
+ constants_path = config_directory / "constants.py"
357
+ constants_path.write_text(MAGIC_NUMBER_SOURCE, encoding="utf-8")
358
+
359
+ assert validate_file(constants_path) == []
360
+
361
+ def test_validate_file_should_skip_settings_py(self, tmp_path: Path) -> None:
362
+ settings_path = tmp_path / "settings.py"
363
+ settings_path.write_text(MAGIC_NUMBER_SOURCE, encoding="utf-8")
364
+
365
+ assert validate_file(settings_path) == []
366
+
367
+ def test_validate_file_should_skip_uppercase_config_directory(
368
+ self, tmp_path: Path
369
+ ) -> None:
370
+ """Case-insensitive match so Config/ on case-preserving filesystems skips."""
371
+ uppercase_config_directory = tmp_path / "Config"
372
+ uppercase_config_directory.mkdir()
373
+ constants_path = uppercase_config_directory / "constants.py"
374
+ constants_path.write_text(MAGIC_NUMBER_SOURCE, encoding="utf-8")
375
+
376
+ assert validate_file(constants_path) == []
377
+
378
+ def test_validate_file_should_skip_mixed_case_tests_directory(
379
+ self, tmp_path: Path
380
+ ) -> None:
381
+ """Case-insensitive match so src/Tests/ short-circuits the same way src/tests/ does."""
382
+ mixed_case_tests_directory = tmp_path / "Tests"
383
+ mixed_case_tests_directory.mkdir()
384
+ test_module_path = mixed_case_tests_directory / "test_foo.py"
385
+ test_module_path.write_text(MAGIC_NUMBER_SOURCE, encoding="utf-8")
386
+
387
+ assert validate_file(test_module_path) == []
388
+
389
+ def test_validate_file_should_skip_conftest_helpers(
390
+ self, tmp_path: Path
391
+ ) -> None:
392
+ """Naked ``conftest`` substring matches conftest_helpers.py and similar."""
393
+ conftest_helpers_path = tmp_path / "conftest_helpers.py"
394
+ conftest_helpers_path.write_text(MAGIC_NUMBER_SOURCE, encoding="utf-8")
395
+
396
+ assert validate_file(conftest_helpers_path) == []
397
+
398
+ def test_validate_file_should_still_scan_production_py_files(self) -> None:
399
+ """Use tempfile with a non-test prefix instead of the pytest tmp_path fixture.
400
+
401
+ pytest's tmp_path produces directories like ``pytest-of-<user>/pytest-N/
402
+ test_validate_file_should_still_scan_production_py_files0/`` whose names
403
+ contain ``test_`` and ``pytest-`` -- both match TEST_PATH_PATTERNS, so
404
+ validate_file would short-circuit via is_test_file and make this
405
+ regression a false green.
406
+ """
407
+ with tempfile.TemporaryDirectory(
408
+ prefix=NON_TEST_TEMPDIR_PREFIX
409
+ ) as temporary_root:
410
+ source_directory = Path(temporary_root) / "src"
411
+ source_directory.mkdir()
412
+ production_path = source_directory / "app.py"
413
+ production_path.write_text(MAGIC_NUMBER_SOURCE, encoding="utf-8")
414
+
415
+ violations = validate_file(production_path)
416
+ assert len(violations) == 1
417
+ assert "42" in violations[0].message
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,17 +33,26 @@ subprocess.run(["gh", "pr", "create", "--title", "My PR", "--body-file", body_pa
33
33
 
34
34
  ### PowerShell (Windows — preferred for this repo)
35
35
 
36
+ `Set-Content -Encoding utf8` writes UTF-8-**with-BOM** on Windows PowerShell 5.1,
37
+ which causes `gh` to treat the leading BOM as part of the first heading character
38
+ and can corrupt rendering. Use the BOM-free pattern below — it works on both
39
+ Windows PowerShell 5.1 and PowerShell 7+.
40
+
36
41
  ```powershell
37
42
  $bodyPath = [System.IO.Path]::ChangeExtension((New-TemporaryFile).FullName, '.md')
38
- @'
43
+ $body = @'
39
44
  ## Summary
40
45
 
41
46
  Fixes `foo` by updating `bar`.
42
- '@ | Set-Content -Path $bodyPath -Encoding utf8
47
+ '@
48
+ [IO.File]::WriteAllText($bodyPath, $body, [Text.UTF8Encoding]::new($false))
43
49
 
44
50
  gh pr create --title "My PR" --body-file $bodyPath
45
51
  ```
46
52
 
53
+ On PowerShell 7+ you can alternatively use `Set-Content -Encoding utf8NoBOM`,
54
+ but the `[IO.File]::WriteAllText` pattern above is version-agnostic.
55
+
47
56
  ### Bash
48
57
 
49
58
  Safe Bash patterns are intentionally omitted from this rule file. This repo
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: searching-obsidian-vault
3
+ description: >-
4
+ Retrieves Obsidian vault context from %USERPROFILE%/SessionLog/ via the
5
+ configured mcp__obsidian__* server. Searches sessions/, decisions/, and
6
+ Research/. Use when the user says "yesterday", "last night", "the other
7
+ day", "earlier today", "this morning", "a few days ago", "last week",
8
+ "this past week", "a while back", "recently", "recent session", "last
9
+ session", or "previous session"; when the user asks why existing code
10
+ was built a certain way or whether an approach was tried before; when
11
+ starting a session in a git repo whose name matches a project folder
12
+ under sessions/; or when the user names a specific component, decision,
13
+ gotcha, or prior research note.
14
+ ---
15
+
16
+ # Obsidian Vault
17
+
18
+ ## Overview
19
+
20
+ Invoke this skill when prior session history, decisions, or research from
21
+ the Obsidian vault would change how the current task is executed. Load it
22
+ on demand rather than keeping the full vault policy always-on.
23
+
24
+ The retrieval *mechanics* live in `/recall`. The
25
+ `vault_context_retrieved` frontmatter bookkeeping lives in `/session-log`
26
+ Step 2. This skill's job is to make the vault search happen at the right
27
+ moments so the downstream flag can be set honestly.
28
+
29
+ ## Trigger Conditions
30
+
31
+ Invoke automatically when any of the following holds:
32
+
33
+ - The user says "yesterday", "last night", "the other day", "earlier
34
+ today", "this morning", "a few days ago", "last week", "this past
35
+ week", "a while back", "recently", "recent session", "last session",
36
+ or "previous session".
37
+ - The user asks why existing code was built a certain way, or whether an
38
+ approach was tried before.
39
+ - A new session starts in a git repo whose name matches a project folder
40
+ under `sessions/`.
41
+ - The user names a specific component, feature, or architectural decision
42
+ that a session or decision note might exist for.
43
+ - The user mentions "session", "decision", "gotcha", "superseded", or
44
+ "prior research" by name.
45
+
46
+ Skip the skill for isolated lookups with no project history (e.g. one-off
47
+ utility scripts, pure syntax questions, fresh repos with no
48
+ `sessions/[project]/` folder).
49
+
50
+ ## Search Algorithm
51
+
52
+ 1. **Search by frontmatter first.** Call `mcp__obsidian__search_notes` with
53
+ `searchFrontmatter: true` and the project name (inferred from the git
54
+ remote, working directory, or topic under discussion). Then search by
55
+ content keywords such as `blocked`, `superseded`, `decision`, `gotcha`,
56
+ plus task-specific terms (component names, error messages, library names).
57
+
58
+ Concrete example:
59
+
60
+ ```xml
61
+ <invoke name="mcp__obsidian__search_notes">
62
+ <parameter name="query">claude-code-config</parameter>
63
+ <parameter name="searchFrontmatter">true</parameter>
64
+ </invoke>
65
+
66
+ <invoke name="mcp__obsidian__search_notes">
67
+ <parameter name="query">superseded decision themes-pr-stack</parameter>
68
+ </invoke>
69
+ ```
70
+
71
+ 2. **Scope to the three vault folders.**
72
+ - `sessions/` — session reports (`type: session-report`, `project`,
73
+ `session`, `date`, `status`, `blocked`, `tags`).
74
+ - `decisions/` — decision notes (`type: decision|procedural|fact|gotcha`,
75
+ `project`, `date`, `status: Active|Superseded`, `tags`).
76
+ - `Research/` — deep research documents.
77
+
78
+ 3. **Read the top 3–5 hits.** Use `mcp__obsidian__read_note` for single notes
79
+ or `mcp__obsidian__read_multiple_notes` for several at once. Prefer recent
80
+ notes over older ones, and prefer decision notes and session summaries
81
+ over raw research.
82
+
83
+ 4. **Summarise the relevant prior context** inline before proceeding with
84
+ the work, so the user can see what prior history shaped the current
85
+ approach. Call out any decision marked `status: Superseded`.
86
+
87
+ 5. **Record the outcome for `/session-log`.** Set
88
+ `vault_context_retrieved: true` if any `mcp__obsidian__*` read or search
89
+ tool was used productively (at least one relevant note surfaced and
90
+ informed the work). Set it to `false` if the vault was unreachable or no
91
+ relevant notes were found.
92
+
93
+ ## Session-End Integration
94
+
95
+ At the end of substantive sessions, offer to run `/session-log`. Step 2 of
96
+ `/session-log` already auto-detects vault MCP calls and writes the
97
+ `vault_context_retrieved` field into frontmatter. This skill's contribution
98
+ is to ensure those MCP calls actually happen when they should, so the flag
99
+ ends up `true` whenever genuine prior context exists.
100
+
101
+ ## Frontmatter Requirement
102
+
103
+ Any session note written via `/session-log` after invoking this skill must
104
+ include:
105
+
106
+ ```yaml
107
+ vault_context_retrieved: true # or false
108
+ ```
109
+
110
+ `true` means a `mcp__obsidian__*` read or search tool was used productively
111
+ during the session. `false` means the vault was unreachable or no relevant
112
+ notes existed.
113
+
114
+ ## Vault Location
115
+
116
+ The vault lives at `%USERPROFILE%/SessionLog/` on this workspace
117
+ (expanding to the user's Windows profile directory). It is accessed
118
+ entirely through the `mcp__obsidian__*` MCP server, whose own
119
+ configuration holds the concrete path; `OBSIDIAN_VAULT_PATH` serves the
120
+ same role on systems that resolve the vault outside the MCP server. This
121
+ skill does not read the filesystem directly and must not hard-code a
122
+ user-specific path like `C:/Users/<name>/SessionLog/` in code it
123
+ produces — expand `%USERPROFILE%` (or read `OBSIDIAN_VAULT_PATH`) at run
124
+ time.
125
+
126
+ ## Related Skills
127
+
128
+ - `/recall` performs the retrieval mechanics end-to-end with user-facing
129
+ output.
130
+ - `/session-log` consumes the `vault_context_retrieved` flag this skill is
131
+ responsible for keeping honest.