@imunitic/synapse 0.0.1-test.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/Index.md.template +23 -0
  2. package/bin/synapse-hook.cjs +19 -0
  3. package/bin/synapse-setup.cjs +420 -0
  4. package/bin/synapse.cjs +20 -0
  5. package/commands/synapse-design-note.md +229 -0
  6. package/commands/synapse-init.md +354 -0
  7. package/commands/synapse-note.md +196 -0
  8. package/commands/synapse-rebuild-diff.md +314 -0
  9. package/commands/synapse-rebuild-full.md +152 -0
  10. package/commands/synapse-status.md +144 -0
  11. package/commands/synapse-task-note.md +133 -0
  12. package/commands/synapse-vault-tidy.md +187 -0
  13. package/harness/claude/hooks.json +54 -0
  14. package/harness/codex/hooks.json +54 -0
  15. package/harness/codex/skills/synapse-design-note/SKILL.md +236 -0
  16. package/harness/codex/skills/synapse-init/SKILL.md +354 -0
  17. package/harness/codex/skills/synapse-note/SKILL.md +212 -0
  18. package/harness/codex/skills/synapse-rebuild-diff/SKILL.md +315 -0
  19. package/harness/codex/skills/synapse-rebuild-full/SKILL.md +149 -0
  20. package/harness/codex/skills/synapse-status/SKILL.md +146 -0
  21. package/harness/codex/skills/synapse-task-note/SKILL.md +133 -0
  22. package/harness/codex/skills/synapse-vault-tidy/SKILL.md +187 -0
  23. package/harness/opencode/plugin/synapse.js +164 -0
  24. package/lib/obsidian-mcp-refresh.cjs +303 -0
  25. package/lib/resolve-binaries.cjs +54 -0
  26. package/package.json +26 -0
  27. package/skills/synapse-node/SKILL.md +211 -0
  28. package/skills/synapse-node-authoring/SKILL.md +188 -0
  29. package/skills/synapse-node-format/SKILL.md +205 -0
  30. package/skills/synapse-orientation/SKILL.md +468 -0
  31. package/skills/synapse-query/SKILL.md +99 -0
  32. package/skills/synapse-task/SKILL.md +261 -0
  33. package/skills/synapse-vault/SKILL.md +107 -0
  34. package/synapse-claude.md +220 -0
  35. package/synapse-fence-languages.conf.template +24 -0
  36. package/synapse-ignore-files.conf.template +45 -0
  37. package/synapse-module-boilerplate.conf.template +24 -0
  38. package/synapse-projects.conf.template +14 -0
  39. package/synapse-prompt-stopwords.conf.template +594 -0
  40. package/synapse.conf.template +23 -0
@@ -0,0 +1,354 @@
1
+ ---
2
+ name: synapse-init
3
+ description: Build a repo's Synapse Graph namespace from scratch — enumerate tracked files, orient into the repo's own symbol vocabulary, cluster into manifest.tsv, gate cluster quality, compute the link graph, author each node's prose, and write the two derived projections (_index.bin, Index.md). This is the only way a project gets a Synapse namespace in the first place — nothing else in the system creates one unprompted. Also handles the lighter re-run case for an already-initialized project (sweeping unassigned files into existing nodes, never re-clustering). Use whenever the user wants to set up Synapse for a repo for the first time ("init synapse here", "build the code graph", "set up the namespace") or asks to sweep newly-unassigned files into an existing graph. Not for repairing drift in an existing namespace (that's the synapse-rebuild-diff skill) or a full wipe-and-rebuild (that's the synapse-rebuild-full skill).
4
+ ---
5
+
6
+ # Synapse Init: Build or Refresh a Repo's Code-Graph Namespace
7
+
8
+ Builds a repo's Synapse Graph namespace in Synapse Vault — a small set of LLM-authored node
9
+ notes (summary + crux + typed links per subsystem/concept) plus the two derived projections that
10
+ keep it cheap to consult and keep stale (`_index.bin`, `synapse/{repo}@{branch}/Index.md`).
11
+
12
+ This is the **only** way a project gets a Synapse namespace in the first place — nothing else in
13
+ this system creates one unprompted, matching the "zero cost for projects that never opt in"
14
+ constraint. Run it once per repo to bootstrap; running it again later is a lighter operation (see
15
+ "Already initialized" below), not a full rebuild.
16
+
17
+ ## When this runs
18
+
19
+ Invoked whenever the user wants Synapse set up for a repo for the first time ("init synapse here",
20
+ "build the code graph", "set up the namespace") or asks to sweep newly-unassigned files into an
21
+ existing graph — no arguments to parse, always operates on the repo containing the current working
22
+ directory.
23
+
24
+ ## Prerequisites
25
+
26
+ - Requires the `obsidian` MCP server (`mcp__obsidian__*` tools). If unreachable, say so and stop.
27
+ - Must be run from inside a git repository. Synapse assumes git throughout (source hashing uses
28
+ `git hash-object`, file enumeration uses `git ls-files`) — if `git rev-parse --show-toplevel`
29
+ fails, stop and say this only works inside a git repo.
30
+ - **Tree-sitter acceleration (optional, never blocking):** check once, up front, whether a C
31
+ compiler is available (`command -v cc`, falling back to `gcc`/`clang`). Missing → print one clear,
32
+ friendly note ("no C compiler found; Synapse will use its full-read behavior for this project, no
33
+ tree-sitter acceleration") and proceed with every step below exactly as if this section didn't
34
+ exist — never let a raw `cc`/build error surface later from inside a grammar build. This check
35
+ gates whether "Tree-sitter acceleration" below is attempted at all for this run; nothing else in
36
+ this skill depends on its result.
37
+
38
+ ## Resolving repo context
39
+
40
+ Every step below needs the same three facts, resolved once up front:
41
+
42
+ 1. **Repo root:** `git rev-parse --show-toplevel`.
43
+ 2. **Namespace key:** `{repo}@{branch}`, resolved by `synapse_namespace` in
44
+ `synapse namespace` — never derived by hand here, since every component resolves it the same way
45
+ from that one place and a second derivation is how they start disagreeing. The repo half comes
46
+ from the *remote's* basename, not the directory: a linked worktree's directory name differs from
47
+ its parent's, and that difference is exactly what must not matter. The branch half is
48
+ `git symbolic-ref --short HEAD`, with `/` and other filename-hostile characters translated.
49
+
50
+ A namespace describes **one branch**. That is the point: it keeps `commit`, the per-file hashes
51
+ and `stale` describing a single tree, and it means a branch switch leaves the old graph intact
52
+ rather than invalidating it wholesale.
53
+
54
+ **On a detached HEAD, stop.** There is no branch, so there is no key — `synapse_namespace` exits
55
+ 1 and says so. Do not invent one, and do not fall back to the directory name: every detached
56
+ checkout everywhere would collide on the same value. Tell the user to check out a branch first.
57
+
58
+ Distinct from the short task-prefix scheme (`project-name=prefix`) used by the synapse-note and
59
+ synapse-design-note skills — unrelated conventions that happen to both involve the word
60
+ "project."
61
+ 3. **Remote:** `git remote get-url origin` (or any configured remote if `origin` doesn't exist —
62
+ pick the first one `git remote` lists). If the repo has no remote at all, fall back to the
63
+ repo root's absolute path. This is the verification field written into the per-project
64
+ `Index.md` and checked at session start before it ever injects a pointer.
65
+
66
+ ## Already initialized?
67
+
68
+ Check whether `synapse/{repo}@{branch}/Index.md` exists (`mcp__obsidian__vault_list` on
69
+ `synapse/{repo}@{branch}/`, or a direct `vault_read` attempt).
70
+
71
+ - **Doesn't exist** → this is a first-time build. Go to "First-time build" below.
72
+ - **Exists, `remote` frontmatter matches** the resolved remote/path → this namespace already
73
+ belongs to this repo. Nothing here needs a full rebuild (regeneration is handled lazily at read
74
+ time — see the design note's Generation & Regeneration section); the only thing this skill still
75
+ does for an already-initialized project is the manual "process it now" sweep of `_unassigned` —
76
+ go to "Re-running on an initialized project" below.
77
+ - **Exists, `remote` mismatches** → `synapse/{repo}@{branch}/` belongs to a *different* repo that
78
+ happens to share this key. Do not touch it. Stop and tell the user plainly: "A Synapse
79
+ namespace already exists at `synapse/{repo}@{branch}/` for a different remote/path
80
+ (`{existing remote}`) — this repo's remote is `{resolved remote}`. Refusing to overwrite; rename
81
+ one of the two repos, or pick a different resolution, before initializing here." This is the
82
+ same detect-and-flag asymmetry the session-start injection uses — contaminating one project's
83
+ graph with another's is worse than a blocked skill.
84
+
85
+ ## First-time build
86
+
87
+ **Two kinds of work, and the seam between them.** Everything here is either *mechanics* — fixed,
88
+ language-agnostic, and already implemented as a tested script — or *interpretation*, which is
89
+ yours and cannot be scripted because what counts as signal differs per codebase.
90
+
91
+ - **Mechanics (do not reimplement inline):** `synapse vocab` (repo → per-group symbol
92
+ vocabulary), `synapse build-lists` (enumerate + expand a manifest + prove coverage),
93
+ `synapse gate` (flag clusters that own no vocabulary), `synapse build-refs` (project the
94
+ tags cache into a def/ref index), `synapse link-graph` (candidate `## Links` edges from that
95
+ index), `synapse rank` (which files are worth reading), `synapse brief` (bundle a node's
96
+ ranked pools and edges into one data file, for pooled authoring), `synapse write-node`
97
+ (hash, digest, `## Sources` mirror, PUT), `synapse push-nodes`, `synapse build-index`,
98
+ `synapse build-project-index`.
99
+
100
+ **The work directory** defaults to `~/.claude/synapse-work/{repo}@{branch}/`, created on demand, and
101
+ holds `manifest.tsv`, `all.txt`, `lists/`, the authored `b-NN.md` bodies and the coverage files. Override with `$SYNAPSE_WORK_DIR` if you need to. Two things never to do: point it
102
+ at the repo (`synapse` runs from inside the repo, so its working files would land in the user's
103
+ checkout) or at the vault (Obsidian would index a file list that runs to six figures of lines).
104
+ It is deliberately persistent rather than a temp dir, so a later run finds the previous manifest
105
+ instead of re-deriving the clustering.
106
+ - **Interpretation (only you can do this):** deciding what the nodes *are*, and writing their prose.
107
+
108
+ The seam is **`manifest.tsv`** — `title <TAB> include-ERE <TAB> exclude-ERE`, one line per node.
109
+ Your judgment goes in as a few dozen regexes; everything downstream of that file is mechanical and
110
+ verifiable. Note the practical consequence: a node's `sources` is exhaustive by construction
111
+ because a script expands it, so the "never a context read" rule holds in **both** directions — a
112
+ 125k-file namespace is ~10 MB of frontmatter plus a ~10 MB `_index.bin`, which you can no more
113
+ emit into tool calls than read into a window. Never hand-author those.
114
+
115
+ 1. **Enumerate files** — mechanics, run `synapse build-lists` (it does this step and step 4's
116
+ expansion together, and reports coverage). It enumerates `git ls-files` from the repo root —
117
+ tracked files only, which gets
118
+ `.gitignore` exclusion for free and matches what's actually worth summarizing (build output,
119
+ dependencies, etc. are never tracked), and it drops binary/generated files — images, compiled
120
+ objects, packages, archives, media, model weights, lockfiles, minified bundles and source maps.
121
+ Those lists are grouped by *what a file is* rather than by ecosystem, so they are not JVM- or
122
+ web-specific; add repo-specific noise through `$SYNAPSE_EXTRA_EXCLUDE_RE` (it appends to the
123
+ defaults) rather than editing the script.
124
+
125
+ **Submodule gitlinks are skipped for you**, but know why, because it explains a failure you will
126
+ otherwise meet: `git ls-files` reports a submodule as a single entry, but it is a directory on
127
+ disk — `git hash-object` fails on it and takes the whole batch down with it. Its contents belong
128
+ to another repo, which can have its own namespace, so it never belongs in `sources`. The
129
+ detection is a plain "is this a regular file" test rather than parsing `.gitmodules`, and a hash
130
+ is never synthesised from `git ls-files -s`: that would leave the writer and
131
+ `synapse query stale` using different commands for one entry, which is exactly the kind of
132
+ asymmetry that produces a permanent false positive.
133
+ 2. **Read hint files, if present:** `CLAUDE.md` and `README.md` at the repo root. These bias the
134
+ clustering pass in step 4 — they are never treated as authoritative structure, and the pass can
135
+ and should diverge from them if the files themselves disagree. No other project-specific doc
136
+ convention (e.g. a `docs/design/` folder) gets this treatment — see the design note's
137
+ Alternatives for why that was rejected.
138
+ 3. **Orientation pass — the evidence is mechanical, the reading of it is yours.** Run
139
+ `synapse vocab`. It writes six tables into the work directory, covering every file that has a
140
+ grammar — the whole of a large repo, 125,351 files, in ~51 seconds: `groupwords.tsv`
141
+ (`group ⇥ word ⇥ count`), `counts.tsv` (`group ⇥ file count`), `groupexts.tsv`, `namespaces.tsv`,
142
+ `parseable.tsv` and `distinctive.tsv`. Read these instead of exploring the tree.
143
+
144
+ What is *not fully* mechanical, and is still the actual work: deciding which words are
145
+ **distinctive** rather than merely frequent. `distinctive.tsv` (`group ⇥ distinctive ⇥
146
+ considered`) gives a first answer — how many of a group's top terms clear 0.5 on a saturation
147
+ curve, not the old "appears in every group" cliff — but it says how many, not which ones or why.
148
+ A word in every group is background; a word in two is a concept; seeing that by reading
149
+ `groupwords.tsv` across groups, not down one, is what turns the count into a cluster.
150
+
151
+ An empty `groupwords.tsv` means no file here had a usable grammar. That is a supported state, not
152
+ an error — fall back to the four questions in the skill below.
153
+
154
+ **Load the `synapse-orientation` skill** for how to read the vocabulary, the four questions that
155
+ cover a tree with no grammar, and the grammar-discovery procedure. It is shared with the
156
+ synapse-rebuild-diff skill's re-orient class, which needs the same technique.
157
+ 4. **Cluster into nodes — write `manifest.tsv`, the seam.** Group what you learned into a few dozen
158
+ readable nodes, not one per file — same density Graft aims for. A node is a subsystem or concept,
159
+ not a file; a file may legitimately belong to more than one node's `sources` when it's genuinely
160
+ load-bearing for two concepts (many-to-many is intentional, not an oversight). Use the
161
+ `CLAUDE.md`/`README.md` content read in step 2 as a bias on grouping and naming, never as a
162
+ boundary the files themselves don't support.
163
+
164
+ Express each cluster as one line in `$SYNAPSE_WORK_DIR/manifest.tsv`:
165
+
166
+ ```
167
+ title <TAB> include-ERE <TAB> exclude-ERE
168
+ ```
169
+
170
+ Then run `synapse build-lists` and **read the coverage report it prints.** `covered` +
171
+ `unassigned` must account for `enumerated`; anything unclaimed lands in `unassigned.txt` and
172
+ flows into the index's unassigned list. Iterate the manifest until the split is deliberate
173
+ rather than accidental — a regex slip like `config$` (which matches only a file literally named
174
+ `config`, not the directory) shows up here as a count, which is the entire reason this step is a
175
+ file plus a script instead of a judgement you make silently.
176
+
177
+ Keep the manifest: it is the reviewable record of a judgment call, and re-running or extending
178
+ the namespace later should start from it rather than re-deriving the clustering. Copying it to
179
+ `synapse/{repo}@{branch}/_manifest.tsv` is worth doing for any repo you
180
+ expect to revisit.
181
+ 5. **Gate the clusters — before paying to author any prose.** Coverage was already provable in step
182
+ 4; cluster *quality* was not, and a bad cluster used to be discovered only when someone tried to
183
+ write its summary and found there was nothing to say.
184
+
185
+ ```
186
+ synapse vocab --lists "$SYNAPSE_WORK_DIR/lists" # re-key the vocabulary by CLUSTER
187
+ synapse gate --vocab "$SYNAPSE_WORK_DIR/groupwords.tsv" \
188
+ --parseable "$SYNAPSE_WORK_DIR/parseable.tsv"
189
+ ```
190
+
191
+ The second run of `synapse vocab` is not redundant. A cluster is generally *not* a union of
192
+ directories, so cluster vocabulary cannot be derived from the directory-keyed table of step 3;
193
+ this re-keys it by cluster instead. It is not a second tagging pass — step 3 already left
194
+ `_tags_cache.bin` current for every file, so this run reads it rather than re-parsing anything.
195
+ Note it overwrites `groupwords.tsv`/`counts.tsv`/`namespaces.tsv`/`parseable.tsv` — pass `--out`
196
+ if you want to keep the directory-keyed set.
197
+
198
+ Empty output means every cluster is differentiated; go on to step 6. Each line printed is a
199
+ cluster whose top eight terms are nearly all corpus-common, i.e. it owns no vocabulary of its
200
+ own. **Re-cluster or disperse those before authoring** — merge into a neighbour, split along a
201
+ distinction the vocabulary actually shows, or drop the line and let its files land in a better
202
+ node. Then re-run `synapse build-lists` and the gate.
203
+
204
+ A flag is advice, never a hard stop. `--parseable` handles the fully-unparseable case
205
+ automatically now: a cluster whose code is in a language with **no tree-sitter grammar at all**
206
+ produces no vocabulary, which used to be indistinguishable from owning none — with
207
+ `parseable.tsv` passed, the gate reports it `unparseable` instead of `flagged` and leaves it out
208
+ of the default listing on its own. What is still a manual call is the partial case, a cluster
209
+ *mostly* but not entirely unparseable: the rare-term count still means something there, so the
210
+ gate still judges it, and if a flag on one of those turns out to be about the mixed language
211
+ rather than a real generic cluster, override it and say
212
+ so.
213
+ 6. **Compute the link graph — before any node exists.** A node's `## Links` section is typed
214
+ relations, not prose, and node titles already exist in `manifest.tsv`/`lists/` at this point, so
215
+ this needs no summary to exist first.
216
+
217
+ ```
218
+ synapse build-refs
219
+ synapse link-graph --refs "$SYNAPSE_WORK_DIR/_refs.tsv" --lists "$SYNAPSE_WORK_DIR/lists"
220
+ ```
221
+
222
+ `build-refs` projects the tags cache into `$SYNAPSE_WORK_DIR/_refs.tsv` — cheap here, since step 3
223
+ already left the cache current for every file, so this reads it rather than re-parsing anything.
224
+ `link-graph` joins that against the path lists: a `ref` in one node's file to a name whose `def`
225
+ sits in another node's file is a candidate edge, weighted by how many distinct symbols support
226
+ it that are *rare* — referenced from few enough nodes to be informative, not a generic utility
227
+ name every node calls into. Writes `$SYNAPSE_WORK_DIR/links.tsv`
228
+ (`node ⇥ target ⇥ weight ⇥ symbols`), strongest edges first per node.
229
+
230
+ The edges are fact; `depends_on` and `uses` are both covered by what this computes, and which
231
+ word reads right for a given pair is judgement, made when a node's prose is written in step 7.
232
+ `part_of` is containment rather than reference and is never computed here — it stays entirely a
233
+ judgement call.
234
+ 7. **Write each node.** Author the prose only — put each node's content in
235
+ `$SYNAPSE_WORK_DIR/b-NN.md` (matching its `lists/NN.txt`), then run `synapse push-nodes`,
236
+ which calls `synapse write-node` per node.
237
+
238
+ **Do not choose which files to read by judgment, and do not decide how the writing itself
239
+ happens by habit.** Both are decided by the `synapse-node-authoring` skill — **load it
240
+ before writing the first node.** It resolves `SYNAPSE_AUTHOR_POOL` (env var, then
241
+ `~/.claude/synapse.conf`, default 0) and either walks you through authoring every node
242
+ yourself in one continuous pass (`rank --sources` per node, `## Links` candidates from
243
+ step 6's `links.tsv`, reading order only — `sources` stays exhaustive either way), or fans
244
+ out to a configurable pool of concurrent subagents, each handed a self-contained
245
+ `synapse brief` and verified on completion. Same outcome either way: a summary authored
246
+ from a small `sources` subset matches a hand-written one just as well as one authored
247
+ from every source file.
248
+
249
+ **Load the `synapse-node-format` skill too, before writing the first one** — it is the
250
+ single description of the node contract itself (summary, the crux *pointer*, `## Links`,
251
+ `grounded_in`, what the writer adds and what it refuses), shared with the `synapse-node`
252
+ skill and the synapse-rebuild-diff skill, which write the same artifact. `synapse-node-authoring`
253
+ covers *how* nodes get written; this covers *what* one is. Do not re-derive either from an
254
+ existing node: a node you are reading may predate a change to its format.
255
+ 8. **Write `_index.bin`** — mechanics, run `synapse build-index`. It emits
256
+ `$SYNAPSE_WORK_DIR/_index.bin`, mapping every source path used
257
+ above to the list of node **filenames, including the `.md` extension** (matching the design
258
+ note's schema exactly, since the file-edit staleness hook and the read-time procedure both use
259
+ this value directly as a vault path with no extension-handling of their own) that claim it, plus
260
+ an `_unassigned` array for any enumerated file that didn't end up in any node's `sources` (e.g. a
261
+ file judged not worth its own concept but not discardable either — leave it here rather than
262
+ forcing a bad fit). This file is derived and machine-only — nothing edits it directly except
263
+ this skill and the file-edit staleness hook.
264
+
265
+ ```json
266
+ {
267
+ "acme_ecs/world.ml": ["World — entity_component_resource core.md"],
268
+ "acme_ecs/world.mli": ["World — entity_component_resource core.md"],
269
+ "_unassigned": []
270
+ }
271
+ ```
272
+
273
+ 9. **Write `synapse/{repo}@{branch}/Index.md`** — mechanics, run `synapse build-project-index`. It
274
+ takes no prose from you at all: each bullet's headline is read back from that node's `summary`
275
+ frontmatter field, and the script computes the exact file count, the sanitized wikilink filename
276
+ and the `remote` field. Bullets come out sorted by title. Run it only after the nodes exist — it
277
+ fails loudly on a node that is missing or has no `summary`, both of which mean the namespace is
278
+ incomplete.
279
+
280
+ The result is the per-project map, and nothing more: **the index carries no repo-specific prose.**
281
+ That is not a limitation to work around. A convention worth explaining — a module-name/package-name
282
+ divergence, a layering rule, an overlay mechanism found during the orientation pass — is a
283
+ *concept*, and concepts are **nodes**. Written as a node it gets `sources` (so it is reachable by
284
+ searching any file that evidences it), staleness tracking when that evidence changes, and typed
285
+ links from the domains it affects. Written as index chrome it gets none of those. If the
286
+ orientation pass produced a finding a newcomer needs in the first five minutes, give it a node and
287
+ let that node's `summary` carry the headline.
288
+
289
+ **Then verify, before reporting success.** Three checks, all cheap:
290
+ - `synapse query stale` must print nothing. (40s for a 125k-file namespace.)
291
+ - Every `[[wikilink]]` in the namespace must resolve to a file that exists — extract them all and
292
+ test `-f "$link.md"`. Nothing else catches a broken link, since Obsidian treats it as a link to
293
+ a note not yet created.
294
+ - Every node file must appear in `Index.md`. An unlisted node exists but is invisible to a reader.
295
+
296
+ ```yaml
297
+ ---
298
+ title: "{repo}@{branch} — Synapse index"
299
+ node_type: synapse-index
300
+ project: {repo}
301
+ branch: {branch}
302
+ remote: "{resolved remote or path}"
303
+ built_at: "<now>"
304
+ ---
305
+
306
+ # {repo}@{branch} — Synapse index
307
+
308
+ - [[World — entity/component/resource core]] — {one-line summary} (built {built_at})
309
+ - ...
310
+ ```
311
+
312
+ ## Re-running on an initialized project
313
+
314
+ This is the manual fallback for the `_unassigned` sweep that normally rides along on any lazy
315
+ regeneration (see the design note's Node Granularity & Grouping) — for a project that's gone fully
316
+ dormant and has no other regeneration event to piggyback on. It does **not** re-cluster or rebuild
317
+ existing nodes.
318
+
319
+ 1. Run `synapse index unassigned`. Empty → report "Nothing
320
+ unassigned, nothing to do" and stop.
321
+ 2. Read `synapse/{repo}@{branch}/Index.md` for the current node list (titles + summaries).
322
+ 3. Tag them **in one call, not one per file**: write the unassigned paths to a list and run
323
+ `synapse tags --paths {list}`. Output is attributable — an unindented line is a
324
+ path, the tab-indented lines under it are that path's tags — so one invocation classifies the
325
+ whole sweep. A per-file loop here costs ~33× more for the same answer, and `_unassigned` on a
326
+ large repo is not a short list. Fall back to a full read only for genuinely ambiguous cases.
327
+ Classify against the existing node list.
328
+ - **Fits an existing node** → append it (path + fresh `git hash-object`) to that node's
329
+ `sources` in frontmatter, and set that node's `stale: true` (it now covers a file it hasn't
330
+ summarized yet — its own next read regenerates it, this step does not regenerate it
331
+ immediately). Remove the path from `_unassigned` and add it under that node's key in
332
+ the index.
333
+ - **Fits nothing** → leave it in `_unassigned`.
334
+ - Announce each outcome as it happens (which file, which node or "still unassigned").
335
+ 4. Do not touch `built_at` on `Index.md` itself for this pass — the sweep doesn't rebuild the
336
+ index projection, only the affected nodes' own frontmatter and `_index.bin`.
337
+
338
+ ## Confirm
339
+
340
+ - **First-time build:** report the namespace path, node count, and a reminder that this project
341
+ will now be picked up automatically at the start of future sessions.
342
+ - **Re-run:** report how many unassigned files were resolved, how many remain, and to which nodes
343
+ anything was attached.
344
+ - **Namespace collision:** the refusal message from "Already initialized" above — nothing is
345
+ written.
346
+
347
+ ## Integration
348
+
349
+ - Nodes and projections written here are read by Codex directly at Synapse read time (Tier 2
350
+ staleness check + regeneration — a procedure, not a hook, documented alongside this skill) and
351
+ flagged stale by the file-edit staleness hook on every subsequent edit to a source file.
352
+ - The session-start context injection's pointer depends on this skill having run at least once —
353
+ it does a plain existence check on `synapse/{repo}@{branch}/Index.md` and does nothing if this was
354
+ never run.
@@ -0,0 +1,212 @@
1
+ ---
2
+ name: synapse-note
3
+ description: Create a plain note in Synapse Vault (bare mode — title + frontmatter, category resolved from the vault's index note), a tracked task note (task mode, scaffolds the checklist skeleton the synapse-task skill expects), list every tracked task, or search existing notes before creating a new one. Use for a note with no design framing, or any task note not compiled from a design discussion. Not for starting/continuing a design conversation (that's the synapse-design-note skill) or compiling a Ready one into a task (that's the synapse-task-note skill) — both of those delegate to this skill themselves.
4
+ ---
5
+
6
+ # Synapse Note: Plain Vault Notes, Task Notes, Listing, and Search
7
+
8
+ Create a note in Synapse Vault with a title and mode, list existing tracked tasks, or search
9
+ existing notes before creating a new one.
10
+
11
+ ## Determining what the user wants
12
+
13
+ There is no flag syntax here — read intent from how the user asks, then follow the matching mode
14
+ below:
15
+
16
+ - **Listing every tracked task** ("list my tasks", "what tasks are open") → **list mode**: see
17
+ "List mode" below, skip note creation entirely.
18
+ - **Searching for an existing note** ("is there already a note about X", "search the vault for X")
19
+ → **search mode**: see "Search mode" below, skip note creation entirely. This is also the mode to
20
+ reach for programmatically (not just when the user explicitly asks to search) — per the Synapse
21
+ Vault CLAUDE.md instructions, linking to an existing note is the highest-priority step before
22
+ creating a new one, so run a search here before every bare-mode note creation, not only when a
23
+ search is requested outright.
24
+ - **Asking to create a tracked, checklist-based task** ("make a task for X", "track this as a
25
+ task") → **task mode**: scaffold the note as a tracked task, following the `synapse-task` skill's
26
+ conventions. Task notes always live under `tasks/`.
27
+ - **Asking to create any other note** ("make a note about X", "save this as a note") → **bare
28
+ mode**: create an empty node (title + frontmatter only). Which category folder it lands in is
29
+ resolved from the vault's index note, per "Choosing a category (bare mode only)" below — not a
30
+ fixed set.
31
+
32
+ The title is whatever the user is naming or describing, minus any mode-signaling phrasing. Example:
33
+
34
+ - "make a note about my idea" → bare note titled "My idea"
35
+ - "track this as a task: implement Foo" → task note titled "Implement Foo" (before task-ID
36
+ resolution prepends the resolved ID, per "Resolving a missing task ID" below)
37
+
38
+ In task mode, also attempt to extract a task ID from the title by matching a `{prefix}-\d+` pattern
39
+ (letters, a hyphen, then digits) — the user sometimes already names one directly (e.g. "track
40
+ proj-035 — implement Foo"). Use it as `task_id` in frontmatter.
41
+
42
+ If no match is found, **don't just leave it blank** — see "Resolving a missing task ID" below before
43
+ proceeding.
44
+
45
+ ## List mode
46
+
47
+ Use `mcp__obsidian__search_query` with the JsonLogic query `{"var": "frontmatter.task_id"}` — this returns every file that has a `task_id` set, along with that file's `task_id` value as `result`. For each match, also read the file's `status` and `title` (either via a second query `{"var": "frontmatter.status"}` / `{"var": "frontmatter.title"}`, or via `mcp__obsidian__vault_read` on the handful of matched files — whichever is fewer round-trips for the count involved).
48
+
49
+ Categorize:
50
+ - **Prefixed notes**: `task_id` matches `{prefix}-\d+`. Group by the distinct prefix found (whatever
51
+ prefixes actually appear — don't assume a fixed set). Within each prefix, split further into:
52
+ - **Open**: `status` is `TODO`, `IN-PROGRESS`, or `REVIEW` (or missing — treat as open)
53
+ - **Closed**: `status` is `DONE`, `CANCELED`, or `CANCELLED`
54
+ - **Other notes**: no `task_id`, or one that doesn't match `{prefix}-\d+`
55
+
56
+ Sort each prefix group's notes numerically by task id (`{prefix}-9` before `{prefix}-10`); sort other notes alphabetically by title. Report one section per prefix found — "{prefix} notes — open", "{prefix} notes — closed" — plus "Other notes", each line as `{task-id or filename} — {title} [{status}]`. Omit a section header if it has zero entries. End with a total count.
57
+
58
+ Do not modify any files in list mode.
59
+
60
+ ## Search mode
61
+
62
+ The user's search text is the query — whatever they named after asking to search, quotes stripped.
63
+
64
+ 1. Run `mcp__obsidian__search_simple` with the query — this gives full-text relevance-ranked matches with context, the closest equivalent to a title/body search.
65
+ 2. If the query looks like it's targeting metadata specifically (a tag, a task ID, a status value) rather than free text, also run `mcp__obsidian__search_query` with an appropriate JsonLogic filter (e.g. `{"==": [{"var": "frontmatter.task_id"}, "proj-032"]}`).
66
+ 3. Report matches as `{title} — {file path relative to vault root}`, deduped across both. If nothing matches, say so plainly — the caller (agent or user) needs a clear "no existing note" signal to proceed with task-mode-less creation.
67
+
68
+ Do not modify any files in search mode.
69
+
70
+ ## Resolving a missing task ID (task mode only)
71
+
72
+ Triggered when task mode is requested but the title doesn't match `{prefix}-\d+`.
73
+
74
+ The known project/prefix pairs live in a plain local file named `synapse-projects.conf` (one
75
+ `project-name=prefix` line each) — read/appended as a plain text file, not the `obsidian`
76
+ MCP server, since it's outside the vault. It is deliberately **not** part of the portable
77
+ Synapse package and never copied between machines, so contexts that shouldn't mix (e.g.
78
+ personal vs. work projects) never end up in the same file. It's self-managed — this skill appends
79
+ newly resolved pairs to it — but also plain text, so the user can add, fix, or remove a line by hand
80
+ at any time.
81
+
82
+ Resolve *which* file that is with the same tiered lookup every `synapse-*.conf` file uses for
83
+ reading: first `$XDG_CONFIG_HOME/synapse/synapse-projects.conf` if `$XDG_CONFIG_HOME` is set, else
84
+ `~/.config/synapse/synapse-projects.conf`; then `~/.claude/synapse-projects.conf`. Read whichever of
85
+ those exists first. Append a newly resolved pair (step 5 below) to that same file.
86
+
87
+ If neither exists yet, decide where to create it fresh: `$XDG_CONFIG_HOME/synapse/synapse-projects.conf`
88
+ if `$XDG_CONFIG_HOME` is set; else `~/.config/synapse/synapse-projects.conf` if `~/.config` already
89
+ exists as a directory on this machine (it's adopted XDG conventions for other tools even without ever
90
+ setting the env var); else `~/.claude/synapse-projects.conf` as the final fallback — today's default,
91
+ unchanged for anyone who has never touched an XDG config directory.
92
+
93
+ 1. Identify the current project from context: the repo's `CLAUDE.md` (title/"About" section) or
94
+ `git remote`.
95
+ 2. Check the resolved `synapse-projects.conf` for a line whose project name matches (loosely —
96
+ case/whitespace-insensitive). If found, use that prefix directly — no need to ask.
97
+ 3. If the file doesn't have it yet, fall back to deducing from the vault itself (useful the first
98
+ time this runs, or for a project whose notes predate this file): `search_simple` for the
99
+ project/repo name across existing notes, and/or `search_query` on
100
+ `{"var": "frontmatter.task_id"}` to see which prefixes exist, then check whether any of those
101
+ prefixed notes reference this project. If exactly one prefix confidently matches, use it.
102
+ 4. If nothing confidently matches (new project, or an ambiguous/multiple match), ask the user
103
+ directly: "What's the project prefix for this task?" — plain free-text, not a multiple-choice
104
+ list. Don't offer or hint at any other project's prefix as an option.
105
+ 5. Whenever step 3 or step 4 resolves a pair not already in the conf file (including a fresh
106
+ `project-name=prefix` line matching what was just deduced or asked), append it — so the next task
107
+ for this project resolves from step 2 without a search or a question.
108
+ 6. Once the prefix is known, find the next number: run
109
+ `mcp__obsidian__search_query` with `{"var": "frontmatter.task_id"}`,
110
+ filter the returned `result` values client-side for ones matching
111
+ `{prefix}-\d+`, take the highest number found, add 1. If none exist yet
112
+ for that prefix, start at 1.
113
+ 7. Format the new task ID **zero-padded to 3 digits** (`{prefix}-001`, `{prefix}-030`,
114
+ `{prefix}-037`, ...), matching the org-roam-era convention — widening
115
+ naturally past 3 digits if a prefix ever needs it.
116
+ 8. **Prepend the resolved task ID to the title itself** — the final title
117
+ becomes `{task-id} — {original title}` (em dash). Use this same final
118
+ title for both the `title` frontmatter field and the `# ` heading, and
119
+ use the resolved task ID for `task_id`. Don't let the frontmatter task
120
+ ID and the visible title disagree.
121
+
122
+ The synapse-design-note and synapse-task-note skills read the same conf file directly for the same
123
+ reason — they don't duplicate this resolution logic, just this file.
124
+
125
+ ## Choosing a category (bare mode only)
126
+
127
+ Task mode always uses `tasks/` — skip this step entirely in task mode.
128
+
129
+ In bare mode, ask the user which category the note belongs to. Read the vault's index note's
130
+ folder list first — every top-level folder listed there except `designs`/`tasks`/`synapse`
131
+ (structurally fixed, not a bare-mode destination — see the vault-conventions skill's Folders
132
+ bullet) is a candidate category, offered with that folder's own index-note description as the
133
+ option's description. Don't hardcode a fixed option set: a fresh vault's index note lists
134
+ `research`/`scratchpad`/`inbox` (see the vault's own bootstrap template), but a vault owner's own
135
+ index note may have renamed or restructured these, and whatever it currently says is what gets
136
+ offered.
137
+
138
+ Resolve this to a `category` matching the folder name exactly as
139
+ the index note currently spells it, before moving on to the creation steps
140
+ below. No project-slug question is needed here — Obsidian filenames are
141
+ the title itself, not a slug-prefixed timestamp, so there's no separate
142
+ namespacing concern to resolve. The note always lands flat at
143
+ `{category}/{filename}.md` — never inferred into a subfolder such as a
144
+ triage/priority one a vault owner might maintain by hand (e.g.
145
+ `inbox/{high,medium,low}/`, per that folder's own index-note description);
146
+ sorting a note into one of those, if a category has one, is never an
147
+ agent's call to make.
148
+
149
+ ## Resolving the project folder (task mode only)
150
+
151
+ Task notes are grouped one level deeper by project, `tasks/{project}/{filename}.md` — see the
152
+ vault's index note's `tasks/` section. The prefix (`proj`, `sb`, ...) is not itself the folder name
153
+ — it names the *task*, not the *project* — so resolve what project it belongs to, regardless of how
154
+ the prefix became known (matched from the title, resolved in "Resolving a missing task ID" above,
155
+ or supplied directly by a caller like the synapse-task-note skill):
156
+
157
+ 1. Reverse-lookup the prefix in the resolved `synapse-projects.conf` (same tiered lookup as
158
+ "Resolving a missing task ID" above) — find the line whose value after `=` equals the prefix;
159
+ its key is the project name.
160
+ 2. If no line matches, check the index note's `tasks/` section, which documents the prefix-to-project
161
+ mapping directly (e.g. `proj-NNN` → `widget`).
162
+ 3. If still unresolved (a genuinely new prefix with no mapping anywhere), ask the user for the
163
+ project name and append `{project-name}={prefix}` to the conf file — so the next task note
164
+ under this prefix resolves without asking.
165
+
166
+ ## Creating the note
167
+
168
+ 1. Sanitize the title into a filename: replace filesystem-illegal
169
+ characters (`/ : * ? " < > |`) with `-`, collapse repeated whitespace.
170
+ No timestamp prefix, no project-slug prefix — the filename is just the
171
+ (sanitized) title.
172
+ 2. Fetch machine local time: `date '+%Y-%m-%d %H:%M'` — never use inferred
173
+ time. Use this for the `created` frontmatter field.
174
+ 3. Build the file content:
175
+
176
+ **Bare mode:**
177
+ ```
178
+ ---
179
+ title: "{title}"
180
+ created: "{now}"
181
+ ---
182
+
183
+ ```
184
+
185
+ **Task mode:**
186
+ ```
187
+ ---
188
+ title: "{title}"
189
+ created: "{now}"
190
+ task_id: {task-id}
191
+ status: TODO
192
+ last_updated: "{now}"
193
+ ---
194
+
195
+ # {title}
196
+
197
+ ## Notes
198
+
199
+ ```
200
+ 4. Write it with `mcp__obsidian__vault_write`. Task mode: path
201
+ `tasks/{project}/{filename}.md` (project resolved in "Resolving the
202
+ project folder" above). Bare mode: path `{category}/{filename}.md`
203
+ (category resolved above).
204
+
205
+ ## Confirm
206
+
207
+ Report the file path back to the user.
208
+
209
+ - Bare mode: note that the note is intentionally near-empty.
210
+ - Task mode: note the task ID extracted or resolved, and remind the user
211
+ to populate the `## Notes` section and checklist before starting work,
212
+ per the `synapse-task` skill.