@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.
- package/Index.md.template +23 -0
- package/bin/synapse-hook.cjs +19 -0
- package/bin/synapse-setup.cjs +420 -0
- package/bin/synapse.cjs +20 -0
- package/commands/synapse-design-note.md +229 -0
- package/commands/synapse-init.md +354 -0
- package/commands/synapse-note.md +196 -0
- package/commands/synapse-rebuild-diff.md +314 -0
- package/commands/synapse-rebuild-full.md +152 -0
- package/commands/synapse-status.md +144 -0
- package/commands/synapse-task-note.md +133 -0
- package/commands/synapse-vault-tidy.md +187 -0
- package/harness/claude/hooks.json +54 -0
- package/harness/codex/hooks.json +54 -0
- package/harness/codex/skills/synapse-design-note/SKILL.md +236 -0
- package/harness/codex/skills/synapse-init/SKILL.md +354 -0
- package/harness/codex/skills/synapse-note/SKILL.md +212 -0
- package/harness/codex/skills/synapse-rebuild-diff/SKILL.md +315 -0
- package/harness/codex/skills/synapse-rebuild-full/SKILL.md +149 -0
- package/harness/codex/skills/synapse-status/SKILL.md +146 -0
- package/harness/codex/skills/synapse-task-note/SKILL.md +133 -0
- package/harness/codex/skills/synapse-vault-tidy/SKILL.md +187 -0
- package/harness/opencode/plugin/synapse.js +164 -0
- package/lib/obsidian-mcp-refresh.cjs +303 -0
- package/lib/resolve-binaries.cjs +54 -0
- package/package.json +26 -0
- package/skills/synapse-node/SKILL.md +211 -0
- package/skills/synapse-node-authoring/SKILL.md +188 -0
- package/skills/synapse-node-format/SKILL.md +205 -0
- package/skills/synapse-orientation/SKILL.md +468 -0
- package/skills/synapse-query/SKILL.md +99 -0
- package/skills/synapse-task/SKILL.md +261 -0
- package/skills/synapse-vault/SKILL.md +107 -0
- package/synapse-claude.md +220 -0
- package/synapse-fence-languages.conf.template +24 -0
- package/synapse-ignore-files.conf.template +45 -0
- package/synapse-module-boilerplate.conf.template +24 -0
- package/synapse-projects.conf.template +14 -0
- package/synapse-prompt-stopwords.conf.template +594 -0
- 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.
|