@imunitic/synapse 0.1.2 → 0.2.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.
@@ -7,12 +7,12 @@ description: Print a read-only report of what in Synapse Vault currently needs a
7
7
 
8
8
  A read-only sweep over Synapse Vault answering one question: what currently needs a human decision?
9
9
  Five categories, one pass, printed as plain chat text -- an org-agenda-style check-in, not a document
10
- to hand to someone else and not a live dashboard (a published Artifact has no route to the local
11
- Obsidian REST API, so nothing here is ever presented that way). Run it whenever the user asks for a
12
- vault status check, or on a recurring schedule the user has set up themselves -- never wired into
13
- session start: two of the five categories need a per-note body read, not just a frontmatter check,
14
- and unlike the vault's own index note this report is a periodic human check-in, not something the
15
- agent needs injected every session to behave correctly.
10
+ to hand to someone else and not a live dashboard (a published Artifact has no route to the vault, so
11
+ nothing here is ever presented that way). Run it whenever the user asks for a vault status check, or
12
+ on a recurring schedule the user has set up themselves -- never wired into session start: some of the
13
+ five categories need a per-note content read, not just a frontmatter check, and unlike the vault's
14
+ own index note this report is a periodic human check-in, not something the agent needs injected every
15
+ session to behave correctly.
16
16
 
17
17
  Scoped to `designs/`/`tasks/` only -- the two folders design-note creation and task-note creation
18
18
  structurally require, so every Synapse install has them in the same shape. The free-form taxonomy
@@ -28,78 +28,91 @@ produces the same five-category sweep.
28
28
 
29
29
  ## Prerequisites
30
30
 
31
- Requires the `obsidian` MCP server (`mcp__obsidian__*` tools). If unreachable, say so and stop --
32
- there is no local-file fallback.
31
+ Requires the `synapse` CLI on `PATH`. Backend-agnostic -- every query below goes through `synapse
32
+ vault-search`, so it works the same whether the vault is Obsidian-backed or plain-disk-backed. If the
33
+ CLI errors (no vault configured), say so and stop -- there is no other fallback.
33
34
 
34
35
  ## Producing the report
35
36
 
36
- Run all five queries in parallel where the tool call shape allows it; none depends on another's
37
- result.
37
+ Run each query below via `synapse vault-search --fields <fields>`, with the JsonLogic query on stdin.
38
+ `--fields` asks for exactly the columns the query needs back -- no separate read per match required,
39
+ since a matched row already carries them.
38
40
 
39
41
  **1. Design notes still `Discussing`.** Design notes carry status in-body under `## Status`, not in
40
- frontmatter (unlike task notes) -- a content match, scoped to `designs/`:
42
+ frontmatter (unlike task notes) -- a content match, scoped to `designs/`. Needs only the title:
41
43
 
42
44
  ```
45
+ synapse vault-search --fields frontmatter.title <<'EOF'
43
46
  {"and": [
44
47
  {"glob": ["designs/*", {"var": "path"}]},
45
48
  {"regexp": ["## Status\\nDiscussing", {"var": "content"}]}
46
49
  ]}
50
+ EOF
47
51
  ```
48
52
 
49
53
  **2. `Ready` design notes with no compiled task yet.** The design-compilation skill's own "Linking
50
54
  back" step patches a compiled design note with a `> Compiled task: [[...]]` line right after its
51
55
  title -- "`Ready` and missing that line" is a direct signal, not fuzzy title-matching against
52
- `tasks/`:
56
+ `tasks/`. Needs only the title:
53
57
 
54
58
  ```
59
+ synapse vault-search --fields frontmatter.title <<'EOF'
55
60
  {"and": [
56
61
  {"glob": ["designs/*", {"var": "path"}]},
57
62
  {"regexp": ["## Status\\nReady", {"var": "content"}]},
58
63
  {"!": [{"regexp": ["Compiled task:", {"var": "content"}]}]}
59
64
  ]}
65
+ EOF
60
66
  ```
61
67
 
62
68
  **3. Design notes (any status) with a non-empty `## Open Questions`.** Match the heading followed by
63
69
  at least one bullet -- a heading with nothing under it (fully pruned, per the design-note skill's own
64
70
  Ready-gate convention) doesn't count as open. Since this section spans every status, each line in the
65
- composed report also shows *which* status the note is currently in:
71
+ composed report also shows *which* status the note is currently in -- request `content` too, and pull
72
+ the status directly out of the returned text rather than reading the note again:
66
73
 
67
74
  ```
75
+ synapse vault-search --fields frontmatter.title,content <<'EOF'
68
76
  {"and": [
69
77
  {"glob": ["designs/*", {"var": "path"}]},
70
78
  {"regexp": ["## Open Questions\\n- ", {"var": "content"}]}
71
79
  ]}
80
+ EOF
72
81
  ```
73
82
 
74
- `regexp` is boolean-only (no captured groups), so getting each match's actual status needs a second
75
- pass. Run the same query three more times, `and`-ed with `{"regexp": ["## Status\\nDiscussing", ...]}`
76
- / `Ready` / `Reference` respectively, to sort the matches from the first query into the three known
77
- statuses without a body read. **A design note written before `## Status` was standardized on those
78
- exact three words can carry free text there instead** (e.g. `Superseded by [[...]]`) -- it matches
79
- the first query but none of the three status-scoped ones. Whatever's left over after removing the
80
- Discussing/Ready/Reference matches from the first query's full result set is exactly this case:
81
- report those under a fourth bucket, "Other", rather than silently dropping them -- surfacing an odd
82
- note beats losing it, the same reasoning behind reporting a 0-unchecked task instead of hiding it
83
- (see Query 4 below).
83
+ Do not chain further `and`/`regexp` conditions onto this query to sort matches by status -- compound
84
+ `regexp` conditions against `content` have been observed to return results that are logically
85
+ impossible (a query with strictly more AND-ed conditions returning *more* matches than one with
86
+ fewer), so any query beyond a single `glob` + single `regexp` pair is unverified and not to be
87
+ trusted here. Instead, take the line following `## Status` directly out of each row's `content`
88
+ column -- normally `Discussing`/`Ready`/`Reference`, but a design note written before that
89
+ three-word convention was standardized can carry free text there instead (e.g. `Superseded by
90
+ [[...]]`); report that verbatim rather than forcing it into a bucket, surfacing an odd note beats
91
+ losing it, the same reasoning behind reporting a 0-unchecked task instead of hiding it (see Query 4
92
+ below).
84
93
 
85
94
  **4. Open task notes with at least one unchecked item.** Task notes carry `status:` in frontmatter,
86
- unlike design notes -- filter there first:
95
+ unlike design notes -- filter there first, and request `content` to count `- [ ]` lines directly from
96
+ the row rather than reading each match again:
87
97
 
88
98
  ```
99
+ synapse vault-search --fields frontmatter.title,content <<'EOF'
89
100
  {"in": [{"var": "frontmatter.status"}, ["TODO", "IN-PROGRESS"]]}
101
+ EOF
90
102
  ```
91
103
 
92
- Then `mcp__obsidian__vault_read` each match and count `- [ ]` lines in the body. A match with zero
93
- unchecked lines (a checklist that's fully checked but hasn't been promoted to `REVIEW` yet) is still
94
- worth surfacing -- report it under this section with its count shown as 0, rather than silently
95
- dropping it, since that state itself is worth a human noticing.
104
+ A match with zero unchecked lines (a checklist that's fully checked but hasn't been promoted to
105
+ `REVIEW` yet) is still worth surfacing -- report it under this section with its count shown as 0,
106
+ rather than silently dropping it, since that state itself is worth a human noticing.
96
107
 
97
- **5. Task notes stuck in `REVIEW`.** Frontmatter-only, no body read needed -- a fully-checked
98
- checklist waiting specifically on human sign-off, since the task-status skill deliberately never
99
- promotes a note past `REVIEW` on its own:
108
+ **5. Task notes stuck in `REVIEW`.** Frontmatter-only, no body needed -- a fully-checked checklist
109
+ waiting specifically on human sign-off, since the task-status skill deliberately never promotes a
110
+ note past `REVIEW` on its own:
100
111
 
101
112
  ```
113
+ synapse vault-search --fields frontmatter.title <<'EOF'
102
114
  {"==": [{"var": "frontmatter.status"}, "REVIEW"]}
115
+ EOF
103
116
  ```
104
117
 
105
118
  ## Composing the report
@@ -108,8 +121,7 @@ One section per category, in the order above. Each line names the note (title, o
108
121
  `title` frontmatter) plus the one identifying detail that category needs. The Open Questions section
109
122
  is the one place a note's status also belongs on the line -- every other section's heading already
110
123
  implies it (the "Discussing" section only ever holds `Discussing` notes), but Open Questions spans
111
- every status (`Discussing`/`Ready`/`Reference`/the "Other" catch-all from Query 3 above), so put the
112
- status first, before the title, so it's the first thing scanned:
124
+ every status, so put the status first, before the title, so it's the first thing scanned:
113
125
 
114
126
  ```
115
127
  ## Discussing
@@ -138,9 +150,8 @@ go dig for.
138
150
 
139
151
  ## Constraints
140
152
 
141
- - Read-only end to end. Never calls `vault_write`/`vault_patch`/`vault_move`/`vault_delete`/
142
- `vault_copy` -- if a step here ever seems to need one, that step is out of scope, not a case to
143
- special-case around.
153
+ - Read-only end to end. Never calls `vault-write`/`vault-patch` -- if a step here ever seems to need
154
+ one, that step is out of scope, not a case to special-case around.
144
155
  - No Artifact/web-UI output. No session-start wiring. Not a new binary CLI subcommand -- every
145
- category above is a plain mechanical query already reachable through `mcp__obsidian__*` tools.
156
+ category above is a plain mechanical query already reachable through `synapse vault-search`.
146
157
  - Scoped to `designs/`/`tasks/` only -- never `inbox/`/`research/`/`scratchpad/`.
@@ -89,12 +89,19 @@ don't duplicate that scaffolding here, just supply its inputs:
89
89
 
90
90
  ## Linking back
91
91
 
92
- After creation, patch the design note: add `> Compiled task: [[{task note title}]]` near the top
93
- (via `mcp__obsidian__vault_patch`) a small annotation line, same idea as any other cross-reference
94
- you'd drop near a note's title. This is a one-time link; the design note's `Status` itself does not
95
- change and does not need to track the task's progress — Obsidian's own backlinks panel already
96
- surfaces the live task note from the design note, and the task's `status:` frontmatter is the
97
- authoritative progress signal from here on.
92
+ After creation, patch the design note: add `> Compiled task: [[{task note title}]]` near the top
93
+ right after the `# {title}` heading, before `## Status`:
94
+
95
+ ```
96
+ printf '> Compiled task: [[%s]]\n\n' "{task note title}" | \
97
+ synapse vault-patch "{design note path}" --heading "{design note title}" --prepend
98
+ ```
99
+
100
+ A small annotation line, same idea as any other cross-reference you'd drop near a note's title. This
101
+ is a one-time link; the design note's `Status` itself does not change and does not need to track the
102
+ task's progress — a plain `synapse vault-search-text` for the task note's title already surfaces the
103
+ live task note from the design note, and the task's `status:` frontmatter is the authoritative
104
+ progress signal from here on.
98
105
 
99
106
  ## Confirm
100
107
 
@@ -90,7 +90,7 @@ instead of a silent edit:
90
90
  local git history to say *why* it's broken instead of leaving that to guesswork — the vault is
91
91
  usually a git repo (a db-sync hook auto-commits every agent-driven edit into it, opt-in per vault,
92
92
  same precondition as that hook). Resolve the vault's filesystem path the same way its own conf
93
- file already does (`OBSIDIAN_VAULT_DIR`), skip this sub-step entirely if `{vault}/.git` doesn't
93
+ file already does (`SYNAPSE_VAULT_DIR`), skip this sub-step entirely if `{vault}/.git` doesn't
94
94
  exist, and never let a missing/unreachable git repo block the rest of the finding — worst case
95
95
  it's reported with no history context, same as today.
96
96
  - `git -C {vault} log --all --diff-filter=A --name-only --pretty=format: -- "**/{target}.md"` — a
@@ -19,12 +19,21 @@
19
19
  // edit: `write` (`args.filePath`, `args.content`) and `edit` (`args.filePath`,
20
20
  // `args.oldString`, `args.newString`) -- both share `filePath`, matching
21
21
  // Claude Code's `Write`/`Edit` sharing `tool_input.file_path`. `db_sync.zig`
22
- // itself reads no payload at all (a blind `git add -A && commit` against the
23
- // vault's own repo), so it only needs a trigger, not a translated payload --
24
- // fired on the same write/edit tools, plus the two vault-mutating MCP tools
25
- // (`obsidian_vault_write`/`obsidian_vault_patch`, the same OpenCode
22
+ // itself is a blind `git add -A && commit` against the vault's own repo, so
23
+ // this plugin only needs to decide *whether* to fire it, not translate a
24
+ // payload -- fired on the same write/edit tools, the two vault-mutating MCP
25
+ // tools (`obsidian_vault_write`/`obsidian_vault_patch`, the same OpenCode
26
26
  // tool-naming transform `mcp__obsidian__vault_write`/`vault_patch` go
27
- // through elsewhere in this plugin's own skill/command prose).
27
+ // through elsewhere in this plugin's own skill/command prose), and `bash`
28
+ // (`args.command`, confirmed against `packages/core/src/tool/bash.ts` in
29
+ // the real `sst/opencode` source) when the command actually names `synapse
30
+ // vault-write`/`vault-patch` -- the CLI door skills use instead of calling
31
+ // the MCP tools directly (see `sb — Vault store backend selection`). Unlike
32
+ // Claude Code's/Codex's `hooks.json`, which can only match a tool *name* and
33
+ // so must widen their own matcher to `Bash` and let `db_sync.zig` filter the
34
+ // command text itself, this plugin has the real args in hand before ever
35
+ // spawning the hook, so it filters right here instead -- no spawn at all for
36
+ // an unrelated `bash` call.
28
37
  //
29
38
  // `stop-nudge`'s Claude Code trigger (`Stop`, once per turn) maps to
30
39
  // `session.idle` -- live-verified as firing exactly once, after every tool
@@ -104,6 +113,15 @@ async function alreadyInjected(client, sessionID) {
104
113
  const EDIT_TOOLS = new Set(["write", "edit"])
105
114
  const VAULT_WRITE_TOOLS = new Set(["obsidian_vault_write", "obsidian_vault_patch"])
106
115
 
116
+ // A `bash` call whose command names a `synapse vault-write`/`vault-patch`
117
+ // invocation -- the substring check a real shell quoting/path prefix can't
118
+ // evade in the cases that matter (`synapse vault-write ...`, `/path/to/synapse
119
+ // vault-patch ...`), matching `db_sync.zig`'s own `shouldSkipBash` filter for
120
+ // the harnesses that can't pre-filter on args the way this plugin can.
121
+ function isVaultWriteCommand(command) {
122
+ return typeof command === "string" && (command.includes("vault-write") || command.includes("vault-patch"))
123
+ }
124
+
107
125
  // sessionID -> nudge text from a `stop-nudge` call that had nowhere to land
108
126
  // yet -- delivered on that session's next `chat.message`.
109
127
  const pendingNudge = new Map()
@@ -146,6 +164,8 @@ export const Synapse = async ({ directory, client }) => {
146
164
  runHook("db-sync", {});
147
165
  } else if (VAULT_WRITE_TOOLS.has(input.tool)) {
148
166
  runHook("db-sync", {});
167
+ } else if (input.tool === "bash" && isVaultWriteCommand(input.args?.command)) {
168
+ runHook("db-sync", {});
149
169
  }
150
170
  },
151
171
 
@@ -130,7 +130,7 @@ function confExpand(raw) {
130
130
 
131
131
  // Mirrors core/conf.zig's `resolveExisting` (tiers 1-2 only -- this hook,
132
132
  // like the shell script it replaces, never falls back to the plugin's own
133
- // bundled template: that template's OBSIDIAN_VAULT_DIR is a placeholder
133
+ // bundled template: that template's SYNAPSE_VAULT_DIR is a placeholder
134
134
  // path, not a real vault, and silently trying it would be worse than doing
135
135
  // nothing).
136
136
  function resolveConfPath(name) {
@@ -150,7 +150,7 @@ function resolveConfPath(name) {
150
150
  // conf file name in turn (synapse.conf, then the pre-rename second-brain.conf
151
151
  // as a fallback), each resolved through the same tier order.
152
152
  function resolveVaultDir() {
153
- const override = process.env.OBSIDIAN_VAULT_DIR;
153
+ const override = process.env.SYNAPSE_VAULT_DIR;
154
154
  if (override) return override;
155
155
 
156
156
  for (const name of ["synapse.conf", "second-brain.conf"]) {
@@ -162,7 +162,7 @@ function resolveVaultDir() {
162
162
  } catch {
163
163
  continue;
164
164
  }
165
- const raw = confGet(text, "OBSIDIAN_VAULT_DIR");
165
+ const raw = confGet(text, "SYNAPSE_VAULT_DIR");
166
166
  if (raw === null) continue;
167
167
  const expanded = confExpand(raw);
168
168
  if (expanded) return expanded;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imunitic/synapse",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Memory for Claude Code, Codex CLI, and OpenCode: a durable Obsidian vault plus a per-repo code graph.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,9 +22,9 @@
22
22
  "*.conf.template"
23
23
  ],
24
24
  "optionalDependencies": {
25
- "@imunitic/synapse-darwin-arm64": "0.1.2",
26
- "@imunitic/synapse-linux-x64": "0.1.2",
27
- "@imunitic/synapse-linux-arm64": "0.1.2"
25
+ "@imunitic/synapse-darwin-arm64": "0.2.0",
26
+ "@imunitic/synapse-linux-x64": "0.2.0",
27
+ "@imunitic/synapse-linux-arm64": "0.2.0"
28
28
  },
29
29
  "license": "SEE LICENSE IN LICENSE"
30
30
  }
@@ -38,7 +38,7 @@ needs one.
38
38
  namespace for this repo, or a `remote:` mismatch. Do not treat that as a passing verification —
39
39
  either fix the cause or proceed knowing the graph is unverified, and say which.
40
40
 
41
- 2. **Also honour the Tier 1 flag.** `rg -m1 '^stale:' "$OBSIDIAN_VAULT_DIR/synapse/{project}/{Node}.md"`
41
+ 2. **Also honour the Tier 1 flag.** `rg -m1 '^stale:' "$SYNAPSE_VAULT_DIR/synapse/{project}/{Node}.md"`
42
42
  — one line, negligible cost. The two tiers catch different things: Tier 1 flags edits made through
43
43
  this Claude Code session the moment they happen, the script catches everything including changes
44
44
  the hook never saw (`git pull`, branch switch, rebase, an IDE edit). **Either one saying stale
@@ -55,13 +55,13 @@ needs one.
55
55
 
56
56
  That prints only what is between the generated fences — so it excludes `## Notes` as well as the
57
57
  frontmatter, which a raw offset read would not — and costs ~500 tokens whether the node covers 5
58
- files or 941. **A full `mcp__obsidian__vault_read`
58
+ files or 941. **A full `synapse vault-read`
59
59
  of a hub node is a mistake, not merely expensive** — it spends tens of thousands of tokens on a
60
- path list you are not going to use. Use `vault_read` only when you specifically need the
61
- frontmatter or the links/backlinks metadata it returns.
60
+ path list you are not going to use. Use `vault-read` only when you specifically need the
61
+ frontmatter itself.
62
62
 
63
63
  Finding *which* node to read is a separate job, and search does it: because `sources` is
64
- exhaustive, `mcp__obsidian__search_simple` on a class or file name locates the owning node even
64
+ exhaustive, `synapse vault-search-text` on a class or file name locates the owning node even
65
65
  when that name appears nowhere in any node's prose, and returns snippets rather than whole files.
66
66
 
67
67
  For the other questions about a node, use the same tool rather than reading frontmatter:
@@ -47,7 +47,7 @@ required:
47
47
 
48
48
  | Need | Reach for | Why |
49
49
  |---|---|---|
50
- | "Where does X live?" (ranked, natural-language) | `mcp__obsidian__search_simple`/`search_query` over the vault, plus a first read of `synapse/{project}/Index.md` | Full-text, relevance-ranked. Not semantic ranking, but genuinely comparable for locating a concept. |
50
+ | "Where does X live?" (ranked, natural-language) | `synapse vault-search-text`/`vault-search` over the vault, plus a first read of `synapse/{project}/Index.md` | Full-text, relevance-ranked. Not semantic ranking, but genuinely comparable for locating a concept. |
51
51
  | Every occurrence of a pattern | native `grep`/`rg`, **scoped to a file Synapse already named** | Not a repo-wide first move — the deterred, last-resort case. See "Why this exists" above. |
52
52
  | A file's API surface | read the file directly | Claude already has direct, cheap filesystem access — no separate view needed. |
53
53
  | Who depends on a subsystem, or what it depends on | `synapse query links "{Node}" --inbound` / `--closure` | Real transitive-closure traversal over the typed relations, at node granularity. |
@@ -63,7 +63,7 @@ required:
63
63
  |---|---|---|
64
64
  | Orienting on an unfamiliar repo | `synapse/{project}/Index.md`, then the relevant node's `body` | grepping around to build a mental map by hand |
65
65
  | Understanding a flow ("how does X work") | `synapse query body "{Node}"` for the node that covers it | reading every file the flow touches, cold |
66
- | Finding where a change belongs | `search_simple`/`Index.md` to find the owning node, then that node's `sources`/`crux_path` for the exact file(s) | a repo-wide grep for a guessed symbol name |
66
+ | Finding where a change belongs | `vault-search-text`/`Index.md` to find the owning node, then that node's `sources`/`crux_path` for the exact file(s) | a repo-wide grep for a guessed symbol name |
67
67
  | Judging blast radius before an edit | `synapse query links "{Node}" --inbound` (or `--closure` for transitive) | assuming nothing else depends on it |
68
68
  | You already know the exact file and line range | just fetch it (`sed`, or a direct file read) | asking Synapse a question you can already answer |
69
69
  | Finding every occurrence of a literal pattern | native `grep`/`rg`, scoped to files Synapse already named | an unscoped repo-wide grep before consulting Synapse at all |
@@ -52,10 +52,11 @@ into `status:` either — always go through this skill, which caps at
52
52
 
53
53
  ## Procedure
54
54
 
55
- 1. Find the task note: `mcp__obsidian__search_query` with
56
- `{"==": [{"var": "frontmatter.task_id"}, "<task-id>"]}`, where `<task-id>`
55
+ 1. Find the task note: `synapse vault-search --fields content` with
56
+ `{"==": [{"var": "frontmatter.task_id"}, "<task-id>"]}` on stdin, where `<task-id>`
57
57
  is the specific task's ID (whatever prefix it uses — `proj-035`, `sb-008`,
58
- etc.), then `mcp__obsidian__vault_read` the matched file.
58
+ etc.) one call returns the matched note's path and full body together, no
59
+ separate read needed.
59
60
  2. Inspect its checklist items (`- [ ]` / `- [x]`).
60
61
  3. Determine the new `status:` value: `IN-PROGRESS` if any unchecked,
61
62
  `REVIEW` if all checked.
@@ -67,44 +68,38 @@ into `status:` either — always go through this skill, which caps at
67
68
  changes exactly that one line and nothing else, entirely inside the
68
69
  compiled binary — the note's body never enters your context at all.
69
70
  When the command isn't available, fall back to **read-modify-write**:
70
- `vault_read` the file, replace the one line in the returned content,
71
- `vault_write` the whole file back — byte-preserving, because you write
71
+ `synapse vault-read` the file, replace the one line in the returned content,
72
+ `synapse vault-write` the whole file back — byte-preserving, because you write
72
73
  back what you read.
73
74
 
74
- **Never use `vault_patch` with `targetType: frontmatter`.** It is *not*
75
- field-local, despite reading that way: two patches
76
- (one for `status`, one for `last_updated`) re-serialise the entire
77
- frontmatter block every quoted value loses its quotes (`created:
78
- "2024-01-01 15:57"` `created: 2024-01-01 15:57`) and a long `title:`
79
- is folded across two lines. Both are valid YAML, so nothing breaks
80
- loudly, but any tool string-matching `^title: "` stops matching, and
81
- every status transition silently reformats the note. Since transitions
82
- are this skill's main job, that reformatting would land on every task
83
- note in the vault. `frontmatter set` exists specifically so this never
84
- has to be reached for.
75
+ `synapse vault-patch <path> --frontmatter <key> --replace` is also byte-preserving now (it
76
+ delegates to the same field-local mechanism `frontmatter set` uses internally), unlike the old
77
+ Obsidian MCP tool of the same shape but it only ever writes a plain scalar, and it's a full
78
+ read-apply-write round trip through the patch layer for one field. `frontmatter set` stays the
79
+ right tool for this step: narrower, and the one call that exists specifically for it.
85
80
  6. **For completion only:** append implementation bullets to the existing
86
- `## Notes` section with `mcp__obsidian__vault_patch`
87
- (`targetType: heading`, `target: "{H1 title}::Notes"`, `operation:
88
- append`) — append is safe here too. **The target must be the full
89
- nested path** (`H1::Notes`), not just `"Notes"`: since `## Notes` is
90
- nested under the top-level `# {title}` heading, the
91
- plugin's heading lookup fails with "target not found in document" on
92
- the bare leaf name and requires the `::`-joined path from the tool's
93
- own docs. If no notes section exists, same call with
94
- `createTargetIfMissing: true`.
95
-
96
- The vault_patch hazards below are the task-note-specific instance of a general rule; the
97
- `synapse-vault` skill carries the full list (H1 replace, nested heading paths, frontmatter
98
- re-serialisation) for every note, not just task notes.
99
-
100
- **Do not use `vault_patch` with `operation: replace` on the top-level
101
- heading to edit checklist items.** "Content beneath
102
- heading" for a top-level (`#`) heading extends through *all* nested
103
- subheadings (including `## Notes`), not just the leading paragraph/checklist
104
- directly under it — a replace there silently deletes everything past the
105
- checklist, including the Notes section. To check off checklist items,
106
- instead `vault_read` the full file, edit the `- [ ]` → `- [x]` lines in the
107
- returned content, and `vault_write` the whole file back.
81
+ `## Notes` section with `synapse vault-patch`:
82
+
83
+ ```
84
+ printf '%s\n' "- bullet one" "- bullet two" | \
85
+ synapse vault-patch "{path}" --heading "{H1 title}::Notes" --append --create
86
+ ```
87
+
88
+ Append is safe here too. **The target must be the full nested path** (`H1::Notes`), not just
89
+ `"Notes"`: since `## Notes` is nested under the top-level `# {title}` heading, a bare leaf name
90
+ fails with "target not found in {path}" -- check `synapse vault-doc-map <path>` rather than
91
+ guessing. `--create` creates the section (and any missing parent) if it doesn't exist yet.
92
+
93
+ The vault-patch hazards below are the task-note-specific instance of a general rule; the
94
+ `synapse-vault` skill carries the full list (H1 replace, nested heading paths, frontmatter) for
95
+ every note, not just task notes.
96
+
97
+ **Do not use `vault-patch --heading "{H1 title}" --replace` to edit checklist items.** A top-level
98
+ heading's own section extends through *all* nested subheadings (including `## Notes`), not just
99
+ the leading paragraph/checklist directly under it — a replace there silently deletes everything
100
+ past the checklist, including the Notes section. To check off checklist items,
101
+ instead `synapse vault-read` the full file, edit the `- [ ]` → `- [x]` lines in the
102
+ returned content, and `synapse vault-write` the whole file back.
108
103
 
109
104
  ## Notes format
110
105
 
@@ -245,13 +240,13 @@ repos/<owner>/<repo>/issues/<n>/comments`).
245
240
  commit/push — a prior "commit and push" is not a standing license for the
246
241
  next one, and setting `DONE` is itself a human action this skill never
247
242
  takes (see "Never write `DONE`" above).
248
- - **Never create a task note via a bare `vault_write`.** Always
243
+ - **Never create a task note via a bare `vault-write`.** Always
249
244
  `/synapse-note --task`, or `/synapse-task-note` when compiling one from a
250
245
  `Ready` design note. A freeform write skips both the skeleton (checklist
251
246
  items plus a single `## Notes` section) and every guardrail in this file —
252
247
  there is no partial-credit version of following this skill.
253
- - **Never restructure an existing task note's shape via `vault_write`/
254
- `vault_patch` outside this skill's own procedure.** Ad hoc edits that add
248
+ - **Never restructure an existing task note's shape via `vault-write`/
249
+ `vault-patch` outside this skill's own procedure.** Ad hoc edits that add
255
250
  new headings, drop the checklist, or otherwise diverge from the skeleton
256
251
  are what this guardrail exists to prevent — not just wrong `status:`
257
252
  values.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: synapse-vault
3
- description: Synapse Vault is durable memory, and only its Index.md is ever auto-injected — every other note is pull-only. Load this BEFORE answering from your own reasoning about tooling behaviour, a past decision, a convention, a gotcha, or anything a previous session might have written down; and before creating or editing any note in the vault. Covers what to search with, and the vault_patch operations that silently destroy a note.
3
+ description: Synapse Vault is durable memory, and only its Index.md is ever auto-injected — every other note is pull-only. Load this BEFORE answering from your own reasoning about tooling behaviour, a past decision, a convention, a gotcha, or anything a previous session might have written down; and before creating or editing any note in the vault. Covers what to search with, and the vault-patch operations that silently destroy a note.
4
4
  ---
5
5
 
6
6
  # Synapse Vault: search it first, and do not destroy it
@@ -27,12 +27,18 @@ Search before you answer when the question is about:
27
27
  - anything that starts "I think it works like…"
28
28
 
29
29
  ```
30
- mcp__obsidian__search_simple full-text, relevance-ranked, with match context
31
- mcp__obsidian__search_query JsonLogic over frontmatter, tags, links, backlinks, path globs
32
- mcp__obsidian__vault_list when you already know roughly where it is
33
- mcp__obsidian__vault_read with targetType/target to pull one section, not the whole note
30
+ synapse vault-search-text <query> full-text, relevance-ranked, with match context
31
+ synapse vault-search --fields <f1,f2,...> JsonLogic over frontmatter, tags, content, path globs
32
+ synapse vault-list when you already know roughly where it is
33
+ synapse vault-read <path> the whole note -- no partial/targeted read exists
34
34
  ```
35
35
 
36
+ `vault-search`'s JsonLogic filter and `--fields` projection both stop at frontmatter/tags/content/
37
+ path -- there is no `links`/`backlinks`/`unresolvedLinks` equivalent (no whole-vault wikilink index
38
+ exists yet), and `vault-read` always returns the full body, never one section on its own -- request
39
+ exactly what you need with `--fields` instead of over-fetching, and pull a specific section out of
40
+ `vault-read`'s output locally when that's all you actually need.
41
+
36
42
  A search that returns nothing is a real result and worth one line in the note you then write —
37
43
  a negative result cannot be rediscovered by searching for it.
38
44
 
@@ -40,37 +46,39 @@ a negative result cannot be rediscovered by searching for it.
40
46
 
41
47
  Every item below returns success.
42
48
 
43
- - **`vault_patch` + `operation: replace` targeting the H1 replaces the entire document.**
44
- `targetScope` defaults to `content` = "everything below this heading until one of the same or
45
- higher level", and below the H1 there is no higher level, so that is the whole note. To insert
46
- between the title and the first H2 — a status or metadata blockquote, the usual reason to touch
47
- that region — use **`operation: prepend` against the H1**. Never `replace`.
48
- - **Heading targets are nested paths.** An H2 is `H1 title::H2 title`; a bare `H2 title` fails
49
- with "target not found in document", which reads like the heading is missing rather than like
50
- the path is wrong. `mcp__obsidian__vault_get_document_map` returns the exact `::` paths.
51
- - **`operation: append` against the bare H1 can land one heading level too deep.** When the
52
- document's last section has a subheading of its own (e.g. `## Approach` ending in
53
- `### Alternatives considered`), an appended `## Notes` can come back as `### Notes`, nested
54
- under that last subsection instead of sitting as a sibling top-level heading. Target the
55
- specific preceding heading directly, or check `mcp__obsidian__vault_get_document_map` after
56
- it shows the real heading tree, so a wrong level is visible immediately rather than assumed
57
- from the call's own success response.
58
- - **Never `vault_patch` frontmatter.** `targetType: frontmatter` reads as field-local and is not:
59
- it re-serialises the whole YAML block, so unrelated values lose their quotes, long lines get
60
- re-wrapped, and an array value comes back as a quoted string instead of a real YAML list. Use
61
- `synapse frontmatter set <path> <key> <value>` (or `--add-tag`/`--remove-tag` for `tags`
62
- specifically; `synapse frontmatter get <path> <key>` reads one back) it changes one field in
63
- the compiled binary, byte-preserving, without ever
64
- pulling the note's body into your context. It only handles a flat scalar or comma-separated
65
- array field; for anything it doesn't cover (a block-style value, or an edit outside
66
- frontmatter), fall back to reading the file, changing the one line, and writing the whole file
67
- back.
68
- - **Do not patch for whitespace-precise edits.** A blank line between a heading and the block
69
- under it is exactly what a patch may normalise away. A disk-level file edit with a unique anchor
70
- is more predictable; Obsidian picks up external changes automatically.
71
- - **Never hardcode the vault path.** The REST API targets whichever vault the running headless
72
- Obsidian has open. If you must reach disk, the path comes from `OBSIDIAN_VAULT_DIR` in
73
- `~/.claude/synapse.conf`.
49
+ - **`vault-patch --heading "{title}" --replace` targeting the H1 replaces the entire document.**
50
+ A heading target's `content` scope is "everything below this heading's own line, until the next
51
+ heading at the same or shallower level" -- below a top-level H1 there is nothing shallower, so
52
+ that scope is the whole note. To insert between the title and the first H2 — a status or
53
+ metadata blockquote, the usual reason to touch that region — use **`--prepend` against the H1**.
54
+ Never `--replace` there.
55
+ - **Heading targets are nested paths.** An H2 under the H1 is `"{H1 title}::{H2 title}"`; a bare
56
+ `"{H2 title}"` fails with "target not found in {path}", which reads like the heading is missing
57
+ rather than like the path is wrong. `synapse vault-doc-map <path>` returns the exact `::` paths
58
+ (and every block id and frontmatter key too), so check it rather than guessing a path.
59
+ - **`--append`/`--prepend`/`--replace` splice `content` in exactly as given, with no relative
60
+ heading-level translation.** Unlike some patch tools, a `#` in `content` is never adjusted
61
+ relative to the target -- if you write `## Notes` in `content`, `## Notes` is what lands, at
62
+ whatever nesting level you typed. `--append` always lands as the last thing in the target's own
63
+ section (right before the next heading at the same level or shallower, or end of document),
64
+ regardless of how deeply nested the immediately preceding content happens to be -- so a wrong
65
+ heading level in your own `content` string is the only way to get this wrong, not an artifact of
66
+ where in the section you're inserting.
67
+ - **`vault-patch --frontmatter <key>` only ever writes a scalar.** It delegates to
68
+ `core.frontmatter.set` internally, so unlike a patch tool that re-serialises the whole YAML
69
+ block, it *is* byte-preserving every other line survives untouched. But its `content` is
70
+ always written as a plain scalar, never a list, so it's the wrong tool for `tags` specifically.
71
+ Prefer `synapse frontmatter set <path> <key> <value>` (or `--add-tag`/`--remove-tag` for `tags`;
72
+ `synapse frontmatter get <path> <key>` reads one back) regardless it's the narrower, one-field
73
+ tool this exists for, without a read-then-write round trip through `vault-patch` for a single
74
+ value. Neither tool handles a block-style value; fall back to reading the file, changing the one
75
+ line, and writing the whole file back for that.
76
+ - **Block targets (`--block <id>`) only recognize a single-line block** — a line ending in
77
+ ` ^{id}`. A multi-line block has no target grammar here at all; fall back to a heading target
78
+ around it, or a disk-level edit.
79
+ - **The vault path is never hardcoded in a skill.** Every `vault-*` subcommand resolves it itself
80
+ (`SYNAPSE_VAULT_DIR`, same tiered lookup `synapse.conf` uses) — pass a vault-relative path, never
81
+ an absolute one you constructed by hand.
74
82
 
75
83
  ## Verify with a structural invariant, not by re-reading what you wrote
76
84
 
@@ -94,7 +102,7 @@ The vocabulary is `synapse-tag-vocabulary.conf` — one tag per line, `#`-commen
94
102
 
95
103
  Before adding a tag to a note, read the conf and prefer an existing entry over minting a new one. Add a new entry only for a genuinely new concept the existing vocabulary doesn't cover, and append it to the conf when you do. A note's tag set is agent-owned working content: a command may add or remove a tag freely, including one a human added by hand — there is no special protection that would block reconsidering it.
96
104
 
97
- Tagging many existing notes at once (a backfill) should be rare to the point of not recurring — every note created through the authoring commands above is tagged inline as part of being written, so a vault only ever needs a backfill once, for notes that predate tagging being wired in at all. If one is ever needed anyway, resolve tags mechanically first: title text, folder path, and frontmatter (`task_id` prefix, `project` field) matched against the vocabulary settle the large majority of notes with no content read at all. Read a note's body only when those signals don't already resolve to at least one vocabulary tag. A full `vault_read` on every note in a large backfill spends session context on a decision cheap signals usually already make.
105
+ Tagging many existing notes at once (a backfill) should be rare to the point of not recurring — every note created through the authoring commands above is tagged inline as part of being written, so a vault only ever needs a backfill once, for notes that predate tagging being wired in at all. If one is ever needed anyway, resolve tags mechanically first: title text, folder path, and frontmatter (`task_id` prefix, `project` field) matched against the vocabulary settle the large majority of notes with no content read at all. Read a note's body only when those signals don't already resolve to at least one vocabulary tag. A full `vault-read` on every note in a large backfill spends session context on a decision cheap signals usually already make.
98
106
 
99
107
  ## The graph side of the vault
100
108