@imunitic/synapse 0.4.2 → 0.4.4

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.
@@ -27,7 +27,7 @@ reinventing a parallel view — this skill has no separate list/continue mode of
27
27
 
28
28
  ## Prerequisites
29
29
 
30
- - Requires a matching Obsidian design note (`designs/`) with `Status: Ready`.
30
+ - Requires a matching design note (`designs/`) with `Status: Ready`.
31
31
  - No matching note → "No Ready design note found for '{topic}'. Start or continue that design note
32
32
  first." Never generate a checklist from scratch.
33
33
  - Matching note but `Status: Discussing` → "Design note for '{topic}' is still in Discussing. Finish
@@ -123,7 +123,7 @@ skill doesn't set or watch that field itself.
123
123
  ```
124
124
  User: let's compile a task note for "rollup direct storage"
125
125
 
126
- Codex: [reads designs/{PROJECT}Rollup direct storage.md, Status: Ready]
126
+ Codex: [reads designs/{project}/Rollup direct storage.md, Status: Ready]
127
127
  [no existing linked task note]
128
128
 
129
129
  Compiled checklist:
@@ -134,8 +134,8 @@ Codex: [reads designs/{PROJECT} — Rollup direct storage.md, Status: Ready]
134
134
 
135
135
  Project already known: {PROJECT} → {prefix}-005.
136
136
 
137
- Created: tasks/{PROJECT}/Rollup direct storage implementation.md (task_id: {prefix}-005)
138
- Linked back from designs/{PROJECT}Rollup direct storage.md.
137
+ Created: tasks/{project}/Rollup direct storage implementation.md (task_id: {prefix}-005)
138
+ Linked back from designs/{project}/Rollup direct storage.md.
139
139
 
140
140
  Status transitions happen automatically via the synapse-task skill once you start work.
141
141
  ```
@@ -30,14 +30,14 @@ there is no argument to parse, every run produces the same one-pass sweep.
30
30
  ## Prerequisites
31
31
 
32
32
  Requires the `synapse` CLI on `PATH`, resolving a vault with a working link graph
33
- (`synapse vault-backlinks`/`vault-links`/`vault-unresolved`/`vault-orphans`/`vault-deadends`). Both
34
- real coding-vault backends have one `disk` (the default) always local, `obsidian` reaching a
35
- running app when reachable and falling back to `disk`'s own implementation otherwise. If
36
- `SYNAPSE_VAULT_INTEGRATIONS` ever resolves to a backend with no link graph at all, these commands exit 1
37
- saying so; stop and report that rather than falling back to anything else.
33
+ (`synapse vault-backlinks`/`vault-links`/`vault-unresolved`/`vault-orphans`/`vault-deadends`).
34
+ `disk`, the one real backend, always has one -- case-insensitive wikilink resolution over the vault
35
+ folder directly, no external dependency. If `SYNAPSE_VAULT_INTEGRATIONS` ever resolves to a backend
36
+ with no link graph at all, these commands exit 1 saying so; stop and report that rather than
37
+ falling back to anything else.
38
38
 
39
39
  This skill reaches the vault store only through the `synapse` CLI's `vault-*` subcommands, the same
40
- door every other skill uses — no MCP tool, no direct `ObsidianStore` call. `vault-links`/
40
+ door every other skill uses — no MCP tool, no direct store call. `vault-links`/
41
41
  `vault-backlinks` each answer for one note at a time, so Step 1's inventory sweep runs one pair per
42
42
  note in scope: `2N` process spawns for an `N`-note vault. Acceptable for an on-demand, rare command.
43
43
  Step 3's broken-link history check is a plain `git log` call (via the shell, not a compiled tool)
@@ -87,10 +87,9 @@ derivable without guessing:
87
87
  - Missing `created` → `stat.ctime`, formatted `YYYY-MM-DD HH:MM` to match every other note's
88
88
  convention.
89
89
 
90
- Apply via read-modify-write on the whole file (`synapse vault-read` → edit the one frontmatter line
91
- in the returned content `synapse vault-write` the whole file back) never `vault-patch` with
92
- `--frontmatter`, which re-serializes the entire YAML block and silently reformats unrelated fields,
93
- the same hazard the vault and task-status skills already document.
90
+ Apply with `synapse frontmatter set <path> <key> <value>` (or `vault-patch --frontmatter`, which
91
+ delegates to the same byte-preserving mechanism) either writes exactly this one field and leaves
92
+ every other line untouched, the way the vault and task-status skills already document.
94
93
 
95
94
  ## Step 3: Note-health findings (reported, not fixed)
96
95
 
@@ -199,4 +198,4 @@ Print a short summary directly in the response, not left only in tool-call outpu
199
198
  asked, or on a recurring cadence the user sets up themselves.
200
199
  - Every step above goes through the `synapse` CLI's `vault-*` subcommands, or (Step 3's broken-link
201
200
  history check only) a plain `git log` via the shell against the vault's own local repo; this skill
202
- never calls an `mcp__obsidian__*` tool or `ObsidianStore` directly.
201
+ never calls a store implementation directly.
@@ -105,6 +105,12 @@ const EDIT_TOOLS = new Set(["write", "edit"])
105
105
  // yet -- delivered on that session's next `chat.message`.
106
106
  const pendingNudge = new Map()
107
107
 
108
+ // sessionID -> queued `staleness` output, same reason and same delivery as
109
+ // `pendingNudge` above: `tool.execute.after` has no message `output` to push
110
+ // a part onto, so a drift/grounding warning from it would otherwise be
111
+ // silently discarded instead of just reaching the next turn late.
112
+ const pendingStaleness = new Map()
113
+
108
114
  export const Synapse = async ({ directory, client }) => {
109
115
  return {
110
116
  "chat.message": async (input, output) => {
@@ -123,6 +129,12 @@ export const Synapse = async ({ directory, client }) => {
123
129
  newParts.push(textPart(sessionID, output, `[SYNAPSE-STOP-NUDGE]\n${queuedNudge}`))
124
130
  }
125
131
 
132
+ const queuedStaleness = pendingStaleness.get(sessionID)
133
+ if (queuedStaleness) {
134
+ pendingStaleness.delete(sessionID)
135
+ newParts.push(textPart(sessionID, output, `[SYNAPSE-STALENESS]\n${queuedStaleness}`))
136
+ }
137
+
126
138
  const promptText = (output.parts || [])
127
139
  .filter((p) => p.type === "text")
128
140
  .map((p) => p.text)
@@ -136,10 +148,11 @@ export const Synapse = async ({ directory, client }) => {
136
148
  "tool.execute.after": async (input) => {
137
149
  const filePath = input.args?.filePath
138
150
  if (EDIT_TOOLS.has(input.tool) && filePath) {
139
- runHook("staleness", {
151
+ const text = runHook("staleness", {
140
152
  session_id: input.sessionID,
141
153
  tool_input: { file_path: filePath },
142
154
  })
155
+ if (text) pendingStaleness.set(input.sessionID, text)
143
156
  }
144
157
  },
145
158
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@imunitic/synapse",
3
- "version": "0.4.2",
4
- "description": "Memory for Claude Code, Codex CLI, and OpenCode: a durable Obsidian vault plus a per-repo code graph.",
3
+ "version": "0.4.4",
4
+ "description": "Memory for Claude Code, Codex CLI, and OpenCode: a durable markdown vault plus a per-repo code graph.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/imunitic/synapse"
@@ -23,9 +23,9 @@
23
23
  "*.conf.template"
24
24
  ],
25
25
  "optionalDependencies": {
26
- "@imunitic/synapse-darwin-arm64": "0.4.2",
27
- "@imunitic/synapse-linux-x64": "0.4.2",
28
- "@imunitic/synapse-linux-arm64": "0.4.2"
26
+ "@imunitic/synapse-darwin-arm64": "0.4.4",
27
+ "@imunitic/synapse-linux-x64": "0.4.4",
28
+ "@imunitic/synapse-linux-arm64": "0.4.4"
29
29
  },
30
30
  "license": "SEE LICENSE IN LICENSE"
31
31
  }
@@ -21,7 +21,7 @@ needs one.
21
21
 
22
22
  ## Procedure
23
23
 
24
- 1. **Verify the whole project once, with the script.** Run `~/.synapse query stale` from
24
+ 1. **Verify the whole project once, with the script.** Run `synapse query stale` from
25
25
  inside the repo. It prints one `{node title}\t{reason}` line per stale node and nothing at all
26
26
  when everything is current, so its output is the complete stale set for the project.
27
27
 
@@ -50,7 +50,7 @@ needs one.
50
50
  So skip the frontmatter entirely:
51
51
 
52
52
  ```sh
53
- ~/.synapse query body "{Node title}"
53
+ synapse query body "{Node title}"
54
54
  ```
55
55
 
56
56
  That prints only what is between the generated fences — so it excludes `## Notes` as well as the
@@ -71,7 +71,7 @@ needs one.
71
71
  4. **Regeneration (only if step 1 or 2 found the node stale).** You re-author the prose; a script
72
72
  writes the file. **The node contract itself — frontmatter fields, the crux pointer, `## Links`,
73
73
  `grounded_in` — is the `synapse-node-format` skill**, shared with `/synapse-init` and
74
- `/synapse-rebuild`. What follows here is only what differs when *re*-authoring an existing
74
+ `/synapse-rebuild-diff`. What follows here is only what differs when *re*-authoring an existing
75
75
  node rather than writing a new one. Everything mechanical — hashes, `sources_digest`, the `## Sources` mirror,
76
76
  `built_at`, `commit`, `stale: false`, and preserving `## Notes` — belongs to
77
77
  `synapse write-node`, because a hub node's `sources` can no more be *emitted* into a tool call
@@ -81,11 +81,11 @@ needs one.
81
81
  - **Get the node's path list into a file, never into context:**
82
82
 
83
83
  ```sh
84
- ~/.synapse query sources "{Node title}" > "$W/paths.txt"
84
+ synapse query sources "{Node title}" > "$W/paths.txt"
85
85
  ```
86
86
 
87
87
  If `$W/manifest.tsv` exists (or the namespace has `_manifest.tsv`), prefer re-running
88
- `~/.synapse build-lists` instead and use the regenerated `lists/NN.txt`: it
88
+ `synapse build-lists` instead and use the regenerated `lists/NN.txt`: it
89
89
  re-derives every list from the clustering patterns, so files *added* since the last build are
90
90
  picked up automatically rather than sitting in `_unassigned`. `synapse query sources` can only
91
91
  return what the node already claims.
@@ -101,11 +101,11 @@ needs one.
101
101
  and discards findings the diff has nothing to say about. Project the diff as carefully as
102
102
  `sources`: names first, `--stat` to size it, hunks only for the selection.
103
103
  - Fall back to reading the files when patching cannot be justified — a large fraction changed, the
104
- `crux` file is gone, or the baseline is unusable. Then try `~/.synapse tags {path}`
104
+ `crux` file is gone, or the baseline is unusable. Then try `synapse tags {path}`
105
105
  first (exit 0 use the tags, exit 1 fall back to reading the file, exit 2 run the discovery
106
106
  procedure `/synapse-init` documents, then retry), and read the load-bearing files in full — the
107
107
  tags signal informs regrouping, it never substitutes for reading a file before rewriting its prose.
108
- If this is happening across many nodes at once, stop and run `/synapse-rebuild` instead: that is
108
+ If this is happening across many nodes at once, stop and run `/synapse-rebuild-diff` instead: that is
109
109
  the instrument for major drift, and it triages node by node rather than paying full cost for each.
110
110
  - Re-author `## Summary`, `## Crux` and `## Links` to match what the files contain now, into
111
111
  `$W/body.md`. Re-check the node's one-line `summary` as well; keep the existing one with
@@ -127,22 +127,22 @@ needs one.
127
127
  - **Write it back with the script:**
128
128
 
129
129
  ```sh
130
- ~/.synapse write-node --title "{Node title}" --summary "{one line}" \
130
+ synapse write-node --title "{Node title}" --summary "{one line}" \
131
131
  --paths "$W/paths.txt" --body "$W/body.md"
132
132
  ```
133
133
 
134
134
  It replaces only the generated region and re-emits everything after the closing fence verbatim,
135
135
  which is what makes the `## Notes` guarantee enforceable rather than a promise.
136
136
  - **Never hand-write the frontmatter**, with `vault_patch` at `targetType: frontmatter` or
137
- otherwise. Two reasons, both load-bearing: that patch re-serialises the whole YAML block and
138
- YAML-coerces values (an all-digit `hash` becomes `1.1111111111111112e+39`), and
139
- enumerating fields by hand is how `summary` and `commit` get silently dropped — which then breaks
140
- the next `synapse build-project-index` run, far from the cause.
137
+ otherwise. `--frontmatter` only ever writes a single scalar value, so it can't represent
138
+ `sources:`/`grounded_in:` at all, and enumerating the rest of a node's fields by hand one call
139
+ at a time is how `summary` and `commit` get silently dropped — which then breaks the next
140
+ `synapse build-project-index` run, far from the cause.
141
141
  - **`## Notes` is human-authored only.** Never write into it — not at regeneration, not to record
142
142
  what you just did. (Task notes in `tasks/` are a different artifact: the `synapse-task` skill *does*
143
143
  append there. Do not carry that habit into a Synapse node.)
144
144
  - If the node's `summary` or title changed, rebuild the index so the map matches:
145
- `~/.synapse build-project-index`.
145
+ `synapse build-project-index`.
146
146
  - **Say out loud that a regeneration happened** — e.g. "Node '{title}' was stale, regenerated
147
147
  before use." This has real latency and token cost, unlike Tier 1/2's detection; it must never
148
148
  be absorbed silently into the read.
@@ -150,13 +150,13 @@ needs one.
150
150
  - Read the bucket with a shell command, not into context — the index runs to tens of megabytes:
151
151
 
152
152
  ```sh
153
- ~/.synapse index unassigned
153
+ synapse index unassigned
154
154
  ```
155
155
 
156
156
  Empty → nothing to do, skip silently (an empty sweep isn't worth announcing).
157
157
  - Otherwise read `synapse/{project}/Index.md` for the current node list (titles + summaries).
158
158
  - Tag the whole bucket in **one** call — write the paths to a list and run
159
- `~/.synapse tags --paths {list}`, whose output is attributable (an unindented
159
+ `synapse tags --paths {list}`, whose output is attributable (an unindented
160
160
  line is a path, the tab-indented lines under it are its tags). A per-file loop costs ~33× more
161
161
  for the same answer. Fall back to a full read for ambiguous cases, then classify against that
162
162
  node list. **The judgment is which cluster a path belongs to; the bookkeeping is not yours to
@@ -167,7 +167,7 @@ needs one.
167
167
  immediately. If there is no manifest, add the path to that node's list file instead.
168
168
  - **Fits nothing** → leave it unassigned. A genuinely new subsystem wants its own manifest line
169
169
  and its own node, which is `/synapse-init` work, not a sweep.
170
- - Then rebuild the projection with `~/.synapse build-index`. **Never hand-edit
170
+ - Then rebuild the projection with `synapse build-index`. **Never hand-edit
171
171
  `_index.bin`** — it is derived, binary, and tens of megabytes; there is nothing to
172
172
  hand-edit.
173
173
  - **Announce every outcome**, same transparency rule as regeneration: which file, and which
@@ -126,8 +126,8 @@ candidate list, not something to copy verbatim.
126
126
  Never hard-wrap. Write each paragraph as one single unbroken line and let the editor soft-wrap
127
127
  it — a newline exists only where a real break is intended (between paragraphs, list items,
128
128
  headings). This is a vault-wide rule, not specific to this node, and it is not optional: a
129
- hard-wrapped paragraph renders as a ragged stack of short lines in Obsidian instead of flowing
130
- text.
129
+ hard-wrapped paragraph renders as a ragged stack of short lines in a soft-wrapping viewer instead of
130
+ flowing text.
131
131
  ```
132
132
 
133
133
  Same model as the orchestrating session, no override — matches the constraint that "a
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: synapse-node-format
3
- description: The contract for a Synapse code-graph node — frontmatter fields, the crux pointer, `## Links`, `grounded_in`, `## Sources`, and what `synapse write-node` adds or refuses. Load before authoring or regenerating any node, whether from /synapse-init's first build, /synapse-rebuild's triage, or the synapse-node skill's lazy regeneration. Not for reading the graph (that is synapse-query) or for task notes in the vault (that is synapse-task).
3
+ description: The contract for a Synapse code-graph node — frontmatter fields, the crux pointer, `## Links`, `grounded_in`, `## Sources`, and what `synapse write-node` adds or refuses. Load before authoring or regenerating any node, whether from /synapse-init's first build, /synapse-rebuild-diff's triage, or the synapse-node skill's lazy regeneration. Not for reading the graph (that is synapse-query) or for task notes in the vault (that is synapse-task).
4
4
  ---
5
5
 
6
6
  # What a node is, and how to author one
7
7
 
8
8
  Every component that writes a node loads this: `/synapse-init` (first build), the `synapse-node`
9
- skill (Tier 2 lazy regeneration), and `/synapse-rebuild` (reseat, patch, re-orient).
9
+ skill (Tier 2 lazy regeneration), and `/synapse-rebuild-diff` (reseat, patch, re-orient).
10
10
 
11
11
  All three write the same artifact, so the format belongs in one place rather than being restated
12
12
  wherever it is used. What stays with each caller is what is genuinely specific to it — the skill's
@@ -42,15 +42,15 @@ to orient someone already inside. A node without one is an error, not a default.
42
42
 
43
43
  - **Filename/title:** short, senior-engineer-style description of the concept (e.g. "World —
44
44
  entity/component/resource core"). Filesystem-illegal characters (`/ : * ? " < > |`) are
45
- sanitized — but **reword the title instead of relying on that**, because Obsidian resolves a
46
- wikilink by *filename*, so `[[World — entity/component/resource core]]` silently resolves to
45
+ sanitized — but **reword the title instead of relying on that**, because a wikilink resolves
46
+ by *filename*, so `[[World — entity/component/resource core]]` silently resolves to
47
47
  nothing once the file becomes `...entity_component_resource core.md`. A broken wikilink is a
48
48
  valid link to a not-yet-existing note, so it fails quietly. The writer warns when a title needs
49
49
  sanitizing; treat that warning as "rename this node". Same trap when you *retitle* a node
50
50
  mid-build: inbound links already written keep pointing at the old name.
51
51
  - **`sources`:** **every** file the node covers — repo-relative path plus that file's
52
52
  `git hash-object <path>` output, run from the repo root at the moment of writing. Exhaustive,
53
- not a sample: this is a **machine** field, and it is what makes Obsidian's search able to reach
53
+ not a sample: this is a **machine** field, and it is what makes a path-based lookup able to reach
54
54
  a node from any file it covers (searching a class name that appears in no node's prose still
55
55
  finds its node via this list). Do **not** trim it to a handful of "representative" files —
56
56
  doing so silently destroys that lookup, leaves the node unable to answer "which files am I
@@ -65,17 +65,15 @@ to orient someone already inside. A node without one is an error, not a default.
65
65
  someone through this subsystem), `crux` (the few lines that carry the actual logic — **authored
66
66
  as line numbers, stored as text**: you point, the writer slices, so composing is impossible at
67
67
  authoring time and nothing decays afterwards the way a stored line number would), `links` (typed
68
- Obsidian wikilinks to other nodes in this same namespace: `depends_on`, `part_of`, `uses`, or
68
+ wikilinks to other nodes in this same namespace: `depends_on`, `part_of`, `uses`, or
69
69
  another type that fits better if one doesn't — for `depends_on`/`uses` specifically, `/synapse-init`
70
70
  computes candidates before any node exists via `synapse link-graph`; read that node's rows from
71
71
  `links.tsv` rather than guessing which siblings it relates to, `part_of` stays a judgement call
72
72
  with nothing mechanical behind it), a `## Sources` section, and an empty `## Notes` section.
73
73
  - **Break a "does N things" enumeration into real bullets, not inline `(1)/(2)/(3)`.** A sentence
74
74
  enumerating three or more parallel sub-points reads as a wall of text once each item carries its
75
- own clause or parenthetical -- the node is read by a human skimming it in Obsidian as much as by
76
- an agent (see the design note this format came from: hosting the graph as vault-readable markdown
77
- was chosen specifically so it stays "just as readable by a human directly in Obsidian as it is by
78
- Claude"), and a dense inline run-on defeats that. Use a markdown bullet list under the sentence
75
+ own clause or parenthetical -- the node is read by a human skimming it as much as by an agent,
76
+ and a dense inline run-on defeats that. Use a markdown bullet list under the sentence
79
77
  introducing them instead. This is narrow, not a general "prefer bullets" rule: an aside of one or
80
78
  two items, or connected causal narrative ("X, because Y, which is why Z"), stays flowing prose --
81
79
  over-bulleting ordinary narrative just trades one readability problem for another.
@@ -138,10 +136,10 @@ to orient someone already inside. A node without one is an error, not a default.
138
136
  per owning directory or module with a file count, `LC_ALL=C` sorted. A node covering 941 files
139
137
  would otherwise put 75 KB of paths in front of a reader who wants to know which modules are
140
138
  involved — and the frontmatter already carries every path for search, so the mirror doesn't
141
- need to repeat them. Rewritten from `sources` on every write, never hand-edited. (Obsidian's
142
- Properties panel flattens the raw `sources` field into a truncated one-line string, which is
143
- why a mirror exists at all — but that is an argument for aggregating *the mirror*, not for
144
- trimming the field.)
139
+ need to repeat them. Rewritten from `sources` on every write, never hand-edited. (A raw YAML
140
+ list in frontmatter renders as a flattened, truncated one-line string in typical note-viewer
141
+ UI, which is why a mirror exists at all — but that is an argument for aggregating *the mirror*,
142
+ not for trimming the field.)
145
143
  - **`## Notes` is human-authored only.** Claude never writes into it — not at build time, not at
146
144
  regeneration. It is created empty and preserved verbatim forever after.
147
145
  - **Fence the generated region.** Everything the generator owns sits between
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: synapse-orientation
3
- description: How to work out where meaning lives in a codebase you have not seen before — first mechanically, from the repo's own symbol vocabulary, and where no grammar exists, by four questions in order with the cheap commands that answer each. Includes tree-sitter grammar discovery. Use when clustering a repo into Synapse graph nodes for the first time, when re-deriving a node's premises in /synapse-rebuild's re-orient class, or any time you need to orient in an unfamiliar tree before making claims about it.
3
+ description: How to work out where meaning lives in a codebase you have not seen before — first mechanically, from the repo's own symbol vocabulary, and where no grammar exists, by four questions in order with the cheap commands that answer each. Includes tree-sitter grammar discovery. Use when clustering a repo into Synapse graph nodes for the first time, when re-deriving a node's premises in /synapse-rebuild-diff's re-orient class, or any time you need to orient in an unfamiliar tree before making claims about it.
4
4
  ---
5
5
 
6
6
  # Orienting in an unfamiliar repo
7
7
 
8
- Loaded by `/synapse-init` at its orientation step, and by `/synapse-rebuild` when a node lands in
8
+ Loaded by `/synapse-init` at its orientation step, and by `/synapse-rebuild-diff` when a node lands in
9
9
  the *re-orient* class and its premises have to be re-derived rather than patched. Useful on its own
10
10
  terms too: nothing here is Synapse-specific except what you do with the answers.
11
11
 
@@ -116,8 +116,8 @@ script fetched from a notes vault. Begin any re-run by reading it, and improve i
116
116
  re-deriving from scratch. Nothing like it ships, because which aggregations carry signal depends
117
117
  on the codebase — a distributed one would encode the wrong ecosystem's conventions.
118
118
 
119
- `.txt`, with markdown formatting inside, for a measured reason: Obsidian indexes `.md` files as
120
- notes, so a `_profile.md` turns up in search, Quick Switcher and the graph, where it is pure noise
119
+ `.txt`, with markdown formatting inside, for a measured reason: a vault viewer indexes `.md` files
120
+ as notes, so a `_profile.md` would turn up in search and note-browsing UI, where it is pure noise
121
121
  to a human reading notes. A non-`.md` extension is invisible to all of those and still perfectly
122
122
  readable. Note the `_` prefix does *nothing* mechanically — it is only a hint to a human who sees
123
123
  the file, matching `_manifest.tsv`. Record **negative results** here too ("this abbreviation has no
@@ -32,10 +32,10 @@ already named, to locate a specific detail inside it — never as an unscoped re
32
32
  instead of asking Synapse first. If you catch yourself about to grep the whole repo before checking
33
33
  `synapse/{project}/Index.md`, stop and check the index instead.
34
34
 
35
- **Why the cost difference is real, not just tidiness.** `synapse query body <node>` never goes
36
- through Obsidian's API — it's a direct disk read that extracts only the prose between the generated
37
- fences, skipping the node's `sources` list entirely. On a hub node, going through the API instead
38
- would move that node's entire frontmatter — megabytes — to print a few hundred words. On a large
35
+ **Why the cost difference is real, not just tidiness.** `synapse query body <node>` is a direct disk
36
+ read that extracts only the prose between the generated fences, skipping the node's `sources` list
37
+ entirely. Reading the whole note instead would move that node's entire frontmatter megabytes — to
38
+ print a few hundred words. On a large
39
39
  repo (dozens to hundreds of thousands of tracked files), that difference is the entire reason a
40
40
  query stays cheap instead of dominating the turn.
41
41
 
@@ -73,9 +73,9 @@ into `status:` either — always go through this skill, which caps at
73
73
  back what you read.
74
74
 
75
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
76
+ delegates to the same field-local mechanism `frontmatter set` uses internally) but it only ever
77
+ writes a plain scalar, and it's a full read-apply-write round trip through the patch layer for
78
+ one field. `frontmatter set` stays the
79
79
  right tool for this step: narrower, and the one call that exists specifically for it.
80
80
  6. **For completion only:** append implementation bullets to the existing
81
81
  `## Notes` section with `synapse vault-patch`:
@@ -95,6 +95,29 @@ On a `SYNAPSE_VAULT_INTEGRATIONS=git` vault, every edit commits automatically, s
95
95
  calls, not merely a record of intentional edits. The default `disk` backend keeps no history at all,
96
96
  so this safety net only exists once that backend is chosen.
97
97
 
98
+ ## Creating notes: authoring commands, not freeform writes
99
+
100
+ Every new note is created through an authoring command, never by freeforming a file at
101
+ `vault-write`:
102
+
103
+ - `/synapse-note` (bare mode) creates plain notes in any category -- `research/`,
104
+ `scratchpad/`, `inbox/`, anything that is not a tracked task or a design note. It mints the
105
+ frontmatter (`schema`, `title`, `note_id`, `created`, `updated`, `tags`) to the note schema's
106
+ contract, including full-timestamp format.
107
+ - `/synapse-note --task` creates tracked task notes under `tasks/{project}/`.
108
+ - `/synapse-design-note` and `/synapse-task-note` handle design discussions and compiled tasks --
109
+ both delegate the file-minting mechanics to `/synapse-note` internally.
110
+
111
+ `vault-write` is for *editing existing notes*, or the rare read-modify-write round trip. If a
112
+ direct creation is ever genuinely unavoidable, follow bare mode's frontmatter contract exactly
113
+ (`schema`/`title`/`note_id`/`created`/`updated`/`tags`, full `%Y-%m-%d %H:%M:%S %Z` timestamps)
114
+ rather than improvising a shape.
115
+
116
+ Schemas are opt-in by declaration: a note whose frontmatter declares `schema:` is held to that
117
+ schema's contract on every `vault-write`/`vault-patch`; a note without the field is never
118
+ validated or failed. Legacy notes predate the feature and stay valid as they are -- no backfill
119
+ is wanted, and "helpfully" adding `schema:` to old notes is not a service to anyone.
120
+
98
121
  ## Tagging is part of writing a note, not a separate pass
99
122
 
100
123
  Every note-authoring command (`synapse-note`, `synapse-design-note`, `synapse-task-note`) applies tags as one of the steps in creating or substantially updating a note. There is no separate tagging pass or command — `/synapse-vault-tidy`'s recategorization signal reads tag data, it never writes it.
package/synapse-claude.md CHANGED
@@ -100,27 +100,20 @@ a real yes/no answer, not a formality to wave past.
100
100
  The vault is reached through the `synapse` CLI — `synapse vault-read`/`vault-write`/`vault-list`/
101
101
  `vault-search`/`vault-search-text`/`vault-doc-map`/`vault-patch`/`vault-backlinks`/`vault-links`/
102
102
  `vault-unresolved`/`vault-orphans`/`vault-deadends`/`vault-ambiguous`/`vault-rename` — for reads
103
- *and* for writes, never by resolving a vault path or calling an `mcp__obsidian__*` tool directly.
104
- Which concrete store the CLI talks to (`SYNAPSE_VAULT_INTEGRATIONS unset`, the default; `obsidian`, opted
105
- into for a running Obsidian app's own live search relevance and graph data; or `git`, opted into for
106
- the vault to own its own version control -- commit on every write, push/pull in the background) is
107
- resolved once,
108
- inside the compiled binary, from `SYNAPSE_VAULT_INTEGRATIONS`/`SYNAPSE_VAULT_DIR` never something a skill
109
- or an agent turn needs to know or branch on. By default that means no Obsidian dependency
110
- whatsoever: `read`/`write`/`list`/`search`/the link graph/rename are all plain disk I/O and direct
111
- computation against the vault folder. Under the opted-in `obsidian` backend, `search`/the link
112
- graph/rename go through Obsidian's own CLI over its local socket instead when Obsidian is running,
113
- falling back to the same disk-backed behavior automatically and silently (a one-line stderr note,
114
- nothing an agent turn needs to react to) whenever it isn't — no precondition to check or fail on
115
- either way.
103
+ *and* for writes, never by resolving a vault path and reading or writing the note file directly.
104
+ Which concrete store the CLI talks to (`SYNAPSE_VAULT_INTEGRATIONS unset`, the default; or `git`,
105
+ opted into for the vault to own its own version control -- commit on every write, push/pull in the
106
+ background) is resolved once, inside the compiled binary, from `SYNAPSE_VAULT_INTEGRATIONS`/
107
+ `SYNAPSE_VAULT_DIR` — never something a skill or an agent turn needs to know or branch on. `read`/
108
+ `write`/`list`/`search`/the link graph/rename are all plain disk I/O and direct computation against
109
+ the vault folder, in every configuration.
116
110
 
117
111
  **Every write to a note goes through `synapse vault-write` or `vault-patch`. Never the `Write`/`Edit`
118
- tools on the on-disk path, and never a raw `mcp__obsidian__*` tool call either** not for a one-line
119
- change, and least of all when `Write`/`Edit` are already in hand from editing code earlier in the
120
- same turn, because that proximity is precisely what causes this to be violated. The vault being an
121
- ordinary directory means the wrong path *works*: Obsidian's file watcher converges, the auto-commit
122
- hook matches `Write|Edit`/`Bash` running `vault-write`/`vault-patch`, and nothing visibly breaks
123
- which is why the habit never self-corrects on its own. The reason is not a
112
+ tools on the on-disk path** not for a one-line change, and least of all when `Write`/`Edit` are
113
+ already in hand from editing code earlier in the same turn, because that proximity is precisely
114
+ what causes this to be violated. The vault being an ordinary directory means the wrong path
115
+ *works*: the auto-commit hook matches `Write|Edit`/`Bash` running `vault-write`/`vault-patch`, and
116
+ nothing visibly breaks which is why the habit never self-corrects on its own. The reason is not a
124
117
  failure mode to dodge; it is that an invariant upheld only when convenient is worth nothing. Nothing
125
118
  else in the system can rely on it, and every note then has to be re-checked by hand instead of
126
119
  trusted. Synapse's own tooling holds this line — `synapse write-node` goes through the same `Store`
@@ -129,19 +122,18 @@ differ. If the CLI itself ever fails (not installed, no vault configured), that'
129
122
  failure to report and stop on, never a reason to fall back to a raw file edit.
130
123
 
131
124
  Every shipped command and skill, `/synapse-vault-tidy` included, reaches the vault only through
132
- the `synapse` CLI's `vault-*` subcommands — none of them calls an `mcp__obsidian__*` tool.
125
+ the `synapse` CLI's `vault-*` subcommands — none of them writes to the vault folder directly.
133
126
 
134
127
  - You may create and edit notes in this vault **without asking for
135
128
  permission first**, as long as each note is placed in the folder
136
129
  matching its category per `Index.md`.
137
- - Filenames are human-readable titles (not timestamp-prefixed — Obsidian's
138
- sidebar/graph display the filename directly, so a timestamp prefix reads
139
- poorly there). Sanitize filesystem-illegal characters (`/ : * ? " < > |`)
130
+ - Filenames are human-readable titles (not timestamp-prefixed — the filename is the display title,
131
+ so a timestamp prefix reads poorly). Sanitize filesystem-illegal characters (`/ : * ? " < > |`)
140
132
  but otherwise keep the title as-is.
141
133
  - Frontmatter carries what the filename no longer does: `title`, `created`
142
134
  (real timestamp at creation time), and for task notes `task_id` /
143
135
  `status` (`TODO`/`IN-PROGRESS`/`REVIEW`/`DONE`/`CANCELED`).
144
- - Link with Obsidian wikilinks: `[[filename]]` or `[[filename|display
136
+ - Link with wikilinks: `[[filename]]` or `[[filename|display
145
137
  text]]` (no extension, exact filename minus `.md`).
146
138
  - **Never hard-wrap note bodies. A newline exists if and only if a break is
147
139
  intended in the output** — source line structure mirrors the output's block
@@ -156,16 +148,16 @@ the `synapse` CLI's `vault-*` subcommands — none of them calls an `mcp__obsidi
156
148
  makes the text **renderer-independent**: if a newline never appears where
157
149
  no break is wanted, then "does a single newline render as `<br>` or as a
158
150
  space?" never arises, and strict CommonMark, non-strict CommonMark,
159
- Obsidian, pandoc and GitHub all produce the same result — the ambiguous
151
+ pandoc and GitHub all produce the same result — the ambiguous
160
152
  input case is simply gone. This is HTML's content model applied to plain
161
153
  text: a newline is markup meaning "break here", not cosmetic formatting of
162
154
  the source file.
163
155
 
164
156
  It also keeps **line length a view decision rather than a content one**.
165
157
  Hard-wrapping is the author asserting a measure, baking one viewport into
166
- the text; unwrapped, the same bytes are correct at every width — Obsidian's
167
- "Readable line length" on or off, a narrow split pane, a wide monitor,
168
- mobile, print. Hard-wrapped prose fails both ways and is unfixable at read
158
+ the text; unwrapped, the same bytes are correct at every width — any
159
+ reader's readable-line-length setting on or off, a narrow split pane, a
160
+ wide monitor, mobile, print. Hard-wrapped prose fails both ways and is unfixable at read
169
161
  time: at 80 columns it double-wraps raggedly in a narrow pane, and sits as a
170
162
  fixed narrow ribbon in a wide one. Wanting a ~66-character measure is right
171
163
  (it's a real typographic optimum, which is why that setting exists) — put it
@@ -1,5 +1,9 @@
1
- # Copy this to ~/.claude/synapse-ignore-files.conf (setup.sh does this for you).
2
- # Never overwritten once installed -- edit or extend any time.
1
+ # Read live from the Synapse package as a fallback (tier 3) -- never copied or
2
+ # seeded anywhere, and this template file is never written to. To customize,
3
+ # create your own synapse-ignore-files.conf at $XDG_CONFIG_HOME/synapse/, else
4
+ # ~/.config/synapse/ (the legacy ~/.claude/ also works); your own copy always
5
+ # wins and is never touched by Synapse.
6
+ # Edit or extend your own copy any time.
3
7
  #
4
8
  # Extra path patterns dropped at enumeration by synapse build-lists, on top
5
9
  # of the built-in exclusions (compiled objects, archives, media, model weights,
@@ -1,6 +1,9 @@
1
- # Copy this to ~/.claude/synapse-module-boilerplate.conf (setup.sh does this
2
- # for you). Never overwritten once installed -- add your own ecosystem's
3
- # conventions any time.
1
+ # Read live from the Synapse package as a fallback (tier 3) -- never copied or
2
+ # seeded anywhere, and this template file is never written to. To customize,
3
+ # create your own synapse-module-boilerplate.conf at $XDG_CONFIG_HOME/synapse/, else
4
+ # ~/.config/synapse/ (the legacy ~/.claude/ also works); your own copy always
5
+ # wins and is never touched by Synapse.
6
+ # Add your own ecosystem's conventions any time.
4
7
  #
5
8
  # Boilerplate path-segment chains that module_of() (synapse query,
6
9
  # synapse write-node's ## Sources mirror) strips through wholesale when
@@ -1,4 +1,8 @@
1
- # Copy this to ~/.claude/synapse-projects.conf (setup.sh does this for you).
1
+ # Read live from the Synapse package as a fallback (tier 3) -- never copied or
2
+ # seeded anywhere, and this template file is never written to. To customize,
3
+ # create your own synapse-projects.conf at $XDG_CONFIG_HOME/synapse/, else
4
+ # ~/.config/synapse/ (the legacy ~/.claude/ also works); your own copy always
5
+ # wins and is never touched by Synapse.
2
6
  #
3
7
  # Known project -> task-prefix mappings for THIS machine only. Deliberately
4
8
  # separate from the portable Synapse package (never committed
@@ -1,5 +1,9 @@
1
- # Copy this to ~/.claude/synapse-prompt-stopwords.conf (setup.sh does this for
2
- # you). Never overwritten once installed -- edit or extend any time.
1
+ # Read live from the Synapse package as a fallback (tier 3) -- never copied or
2
+ # seeded anywhere, and this template file is never written to. To customize,
3
+ # create your own synapse-prompt-stopwords.conf at $XDG_CONFIG_HOME/synapse/, else
4
+ # ~/.config/synapse/ (the legacy ~/.claude/ also works); your own copy always
5
+ # wins and is never touched by Synapse.
6
+ # Edit or extend your own copy any time.
3
7
  #
4
8
  # Common English function words, filtered out by `synapse vocab` before
5
9
  # building a search pattern from a raw prompt -- without this, an ordinary
@@ -5,15 +5,12 @@ SYNAPSE_VAULT_DIR="$HOME/Vault/YourVault"
5
5
  # Unset by default: reads/writes the vault folder directly, no external
6
6
  # dependency at all -- search and the link graph
7
7
  # (backlinks/links/unresolved/orphans/deadends/ambiguous) are computed by
8
- # DiskStore's own scan. A comma-separated, outer-to-inner list of
9
- # integrations layers extra behavior on top instead: "obsidian" prefers a
10
- # running external app's own live search relevance and graph data, falling
11
- # back to the same disk-backed behavior automatically whenever that app
12
- # isn't reachable (see docs/synapse/synapse-extended-store.md); "git" owns
13
- # the vault's own git lifecycle (commit on write, push once enough pile up).
14
- # "git,obsidian" combines both. The disk store itself is never named -- it's
15
- # always the implicit innermost element.
16
- #SYNAPSE_VAULT_INTEGRATIONS="obsidian"
8
+ # DiskStore's own scan. The one optional integration, "git", layers extra
9
+ # behavior on top instead: it owns the vault's own git lifecycle (commit on
10
+ # write, push once enough pile up) -- see
11
+ # docs/synapse/synapse-extended-store.md. The disk store itself is never
12
+ # named -- it's always the implicit innermost element.
13
+ #SYNAPSE_VAULT_INTEGRATIONS="git"
17
14
 
18
15
  # Where Synapse clones/builds tree-sitter grammars (shared across every
19
16
  # project -- not per-repo). Safe to leave at the default.