@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,229 @@
1
+ ---
2
+ description: Start or continue a free-form, cross-project design discussion, written to Synapse Vault rather than tied to one repo — thinking through a problem/approach/tradeoffs out loud before anything is built. Use whenever the user wants to open, resume, or reason through a design ("let's create/write a design note", "let's think through X", "let's design this", "let's talk this through"). Not for a note that's already ready to become tracked work (that's synapse-task-note), or a plain vault note with no design framing (that's synapse-note, whose project-resolution logic this command reuses).
3
+ ---
4
+
5
+ # Synapse Design Note: Cross-Project Personal Design Discussion
6
+
7
+ A free-form "think it through out loud" pipeline for a design conversation, written to Synapse
8
+ Vault rather than any one repo. Use it when the design conversation isn't (or shouldn't be) tied
9
+ to one repo's lifecycle — it's findable from any project immediately, with no separate
10
+ pointer-note step, because the vault itself is already the cross-project store.
11
+
12
+ Not every design discussion ends with something to build. See `Status: Reference` below for the
13
+ "no implementation attached" ending.
14
+
15
+ ## Usage
16
+
17
+ ```
18
+ /synapse-design-note "topic" # Start or resume a design discussion
19
+ /synapse-design-note --continue # Resume an incomplete design note
20
+ /synapse-design-note --list # List every Obsidian design note, regardless of status
21
+ ```
22
+
23
+ ## Prerequisites
24
+
25
+ - Requires the `obsidian` MCP server (`mcp__obsidian__*` tools). If unreachable, say so and stop —
26
+ there is no local-file fallback for this command.
27
+ - If `designs/` doesn't exist in the vault yet, `mcp__obsidian__vault_write` creates it implicitly on
28
+ first write — but add a `designs/` entry to the vault's `Index.md` folder layout in the same
29
+ action (per the Synapse Vault folder-layout rule: a new top-level folder must never fall behind the
30
+ index).
31
+
32
+ ## Determining the project
33
+
34
+ Every design note is tagged with the project it belongs to — both in the title
35
+ (`{PROJECT} — {Topic}`) and as `project: {prefix}` in frontmatter (the same short prefix
36
+ `/synapse-note --task` uses for task IDs) — so a flat `designs/` folder still reads clearly, and
37
+ both note kinds can be filtered together via `search_query`.
38
+
39
+ Same resolution `/synapse-note` uses for a missing task ID (its "Resolving a missing task ID"),
40
+ reading the same file:
41
+
42
+ 1. Infer the project from the current repo: check its project `CLAUDE.md` (title/"About" section) or
43
+ `git remote`.
44
+ 2. Check the resolved `synapse-projects.conf` (plain `project-name=prefix` lines, read/appended
45
+ via Read/Edit, not the vault — resolve which file that is with the same tiered lookup as
46
+ `/synapse-note`'s "Resolving a missing task ID": `$XDG_CONFIG_HOME/synapse/synapse-projects.conf`
47
+ or `~/.config/synapse/synapse-projects.conf`, then `~/.claude/synapse-projects.conf`, first one
48
+ that exists) for a loosely-matching project name. If found, use that prefix directly.
49
+ 3. If not in the file yet, fall back to searching the vault for a prefix already in use for this
50
+ project. If exactly one confidently matches, use it — and append the pair to the conf file.
51
+ 4. If nothing confidently matches, ask the user for a short project tag — plain free-text, not a
52
+ multiple-choice list, never hinting at any other project's tag as an example — then append the
53
+ resolved pair to the conf file.
54
+
55
+ Never hardcode a specific project/prefix pair in this command's own instructions — the conf file is
56
+ machine-local and deliberately outside the portable Synapse package, so projects from
57
+ unrelated contexts (e.g. personal vs. work) never end up in the same place.
58
+
59
+ ## Handling Arguments
60
+
61
+ **No arguments:**
62
+ 1. Check for incomplete design notes: search `designs/` (via `mcp__obsidian__vault_list` +
63
+ `vault_read`, or `search_query` scoped to the `designs/` path) for notes whose `## Status` line
64
+ reads `Discussing`.
65
+ 2. If found: show a short state summary — title and current section — and offer to resume.
66
+ 3. If none: ask "What are we designing?"
67
+
68
+ **With a topic:**
69
+ 1. Search first — `mcp__obsidian__search_simple` for the topic text across `designs/` (per the
70
+ Synapse Vault rule: link/reuse over duplicate). Also check for an obvious title match.
71
+ 2. If found with `Status: Discussing` → ask "Resume this design?" or "Start fresh?"
72
+ 3. If found with `Status: Ready` → ask "Already marked Ready. Reopen to revise, or start a new note?"
73
+ 4. If found with `Status: Reference` → ask "This concluded as Reference (no implementation intended).
74
+ Reopen to revise, or is that still accurate?"
75
+ 5. Otherwise: start a new design note (see "Determining the project" above for the title/frontmatter
76
+ tag).
77
+
78
+ **--continue:**
79
+ 1. Find notes with `Status: Discussing` (same lookup as "No arguments").
80
+ 2. Multiple → list them, ask which to continue.
81
+ 3. One → resume it.
82
+ 4. None → "No incomplete design note found. Start one with `/synapse-design-note \"topic\"`."
83
+
84
+ **--list:**
85
+ 1. `mcp__obsidian__vault_list` on `designs/`, then `vault_read` each (or a `search_query` scoped to
86
+ that path) to pull title and `## Status`.
87
+ 2. None found → "No design notes yet. Start one with `/synapse-design-note \"topic\"`."
88
+ 3. Group into **Active** (`Discussing`, `Ready`) and **Closed** (`Reference`) — active first, title
89
+ and status in backticks, not bold.
90
+
91
+ ---
92
+
93
+ ## Workflow
94
+
95
+ Free-form conversation, no fixed step order. Create the note on the first substantive answer and
96
+ update it after every meaningful exchange — don't wait until the end.
97
+
98
+ **Write it as settled understanding, not as a transcript of how it was reached.** Updating after
99
+ every exchange is about *when* to write, not license to narrate the conversation in the prose
100
+ itself. A section should read as if authored fresh today, stating the problem/approach/constraints
101
+ as they now stand — never as a log of what changed ("corrected during discussion," "reworked during
102
+ a live investigation," "the user pointed out," "originally X, now Y"). This applies strictly to
103
+ `## Problem`, `## Constraints`, and `## Open Questions` — always direct, current statements, no
104
+ exceptions.
105
+
106
+ `## Approach` is the one place a *trail* can be legitimate content — a rejected alternative and why
107
+ it failed is real, useful information for whoever reads this later, worth keeping even once the
108
+ working approach makes it moot. But phrase it as a fact about the solution space ("X fails because
109
+ Y, confirmed by Z"), never as commentary on the discussion that found it ("investigation showed,"
110
+ "the first attempt was"). Test: would the sentence still make sense to someone who wasn't in the
111
+ conversation and has no idea anything was ever revised? If not, rewrite it as a direct statement.
112
+
113
+ **Prune `## Open Questions` the moment a question resolves — every time the note is touched, not
114
+ just at conclusion.** This section must only ever contain what is currently, actually unresolved:
115
+ a reader (or the agent checking for a blocking question before marking `Ready`, below) has to be
116
+ able to tell in one pass whether anything is still open, not read through a history of past answers
117
+ to find the one real one. When a question is answered during discussion, delete its bullet outright
118
+ — if the answer matters going forward, fold it into `## Approach` (as a direct settled fact, same
119
+ rule as above) or `### Alternatives considered` (if something specific was tried and rejected along
120
+ the way). Never leave a "Resolved on {date}" or "Revised ({review}): ..." bullet parked under Open
121
+ Questions as a historical record — that is exactly the changelog-as-current-state failure the rule
122
+ above already prohibits, just easier to miss here because the section reads as a list either way.
123
+
124
+ Angles worth covering (skip whatever's not relevant):
125
+ - What problem are we solving, and why now?
126
+ - What's the chosen approach? If there were real alternatives, a one-line "why not" for each.
127
+ - What are the hard constraints?
128
+ - Anything risky, or that needs deciding now vs. can be deferred?
129
+
130
+ ### Concluding: Discussing, Ready, or Reference
131
+
132
+ A design note starts `Discussing` and stays there by default — reaching `Ready` is a deliberate
133
+ judgment call, not the automatic result of an `## Approach` section existing. Before marking
134
+ `Ready`, check both explicitly:
135
+ - Is `## Approach` complete and actionable as written, with no hedge language standing in for a
136
+ decision that hasn't actually been made ("a plausible shape," "TBD," "leaning toward," "something
137
+ like")? If the approach still contains that kind of placeholder, it isn't `Ready` yet regardless
138
+ of how much has been discussed.
139
+ - Does every `## Open Questions` bullet, if any survive the pruning above, sit off the critical
140
+ path — genuinely deferred, not something `/synapse-task-note` would need answered to turn this
141
+ into real work? A `Ready` note is not required to have zero open questions (a deferred, non-
142
+ blocking question is fine to carry forward), but a note with a *blocking* open question is not
143
+ `Ready`, no matter how developed the rest of it is.
144
+
145
+ - **Both hold** → `Status: Ready`. Confirm: "Design note ready: `designs/{title}.md`.
146
+ Whenever you're ready to implement, generate the task with
147
+ `/synapse-task-note \"{topic}\"` — no rush, nothing here expires."
148
+ - **Genuinely nothing to build** → `Status: Reference`. Confirm: "Design note concluded as
149
+ Reference: `designs/{title}.md`. No task note needed."
150
+ - **Anything else** (the approach is still hedged, or an open question blocks it) → stays
151
+ `Status: Discussing`. This is the default outcome, not a fallback to apologize for — most
152
+ conversations end here, and that's fine; nothing here expires either.
153
+
154
+ If genuinely unsure which, ask the user directly.
155
+
156
+ There is no closing/renaming step here — nothing reads these notes automatically at session start,
157
+ so the `## Status` line is the only lifecycle marker that matters. It simply stays
158
+ `Ready`/`Reference` indefinitely.
159
+
160
+ ---
161
+
162
+ ## Design Note Format
163
+
164
+ ```
165
+ ---
166
+ title: "{PROJECT} — {Topic}"
167
+ project: {prefix}
168
+ created: "{now}"
169
+ ---
170
+
171
+ # {PROJECT} — {Topic}
172
+
173
+ ## Status
174
+ Discussing | Ready | Reference
175
+
176
+ ## Problem
177
+ {What are we solving, why does it matter, why now}
178
+
179
+ ## Approach
180
+ {Chosen approach}
181
+
182
+ ### Alternatives considered (optional)
183
+ - {Option}: why not
184
+
185
+ ## Constraints
186
+ {Hard constraints, non-negotiables}
187
+
188
+ ## Open Questions (optional)
189
+ - {Anything deferred or unresolved}
190
+ ```
191
+
192
+ Fetch machine local time for `created` (`date '+%Y-%m-%d %H:%M'`) — never infer it.
193
+
194
+ No `Notes`/changelog section — it stays a small, single-conclusion note; there's no long-running
195
+ edit history here worth tracking separately.
196
+
197
+ ## Filename
198
+
199
+ `designs/{PROJECT} — {Topic}.md` — sanitize filesystem-illegal characters (`/ : * ? " < > |`). No
200
+ slug, no numbering — Obsidian filenames are the title itself.
201
+
202
+ ---
203
+
204
+ ## Integration
205
+
206
+ - `/synapse-task-note "{topic}"` reads the matching `Ready` note as its only input.
207
+
208
+ ---
209
+
210
+ ## Example
211
+
212
+ ```
213
+ User: /synapse-design-note "rollup direct storage"
214
+
215
+ Claude: [searches designs/ for existing matches — none found]
216
+
217
+ What are we designing?
218
+
219
+ User: Replacing the AggregatingMergeTree rollup resolution with direct-value storage.
220
+
221
+ Claude: [creates designs/{PROJECT} — Rollup direct storage.md, Status: Discussing]
222
+
223
+ What's driving this over just tuning the current argMaxMerge query?
224
+ ...
225
+ User: The resolution cost alone dominates the query, tuning won't fix that. Mark it ready.
226
+
227
+ Claude: Design note ready: designs/{PROJECT} — Rollup direct storage.md.
228
+ Generate the task with /synapse-task-note "rollup direct storage" whenever you're ready.
229
+ ```
@@ -0,0 +1,354 @@
1
+ ---
2
+ 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 synapse-rebuild-diff) or a full wipe-and-rebuild (that's synapse-rebuild-full).
3
+ ---
4
+
5
+ # Synapse Init: Build or Refresh a Repo's Code-Graph Namespace
6
+
7
+ Builds a repo's Synapse Graph namespace in Synapse Vault — a small set of LLM-authored node
8
+ notes (summary + crux + typed links per subsystem/concept) plus the two derived projections that
9
+ keep it cheap to consult and keep stale (`_index.bin`, `synapse/{repo}@{branch}/Index.md`).
10
+
11
+ This is the **only** way a project gets a Synapse namespace in the first place — nothing else in
12
+ this system creates one unprompted, matching the "zero cost for projects that never opt in"
13
+ constraint. Run it once per repo to bootstrap; running it again later is a lighter operation (see
14
+ "Already initialized" below), not a full rebuild.
15
+
16
+ ## Usage
17
+
18
+ ```
19
+ /synapse-init
20
+ ```
21
+
22
+ No arguments — always operates on the repo containing the current working 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
+ `/synapse-init` 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
59
+ `/synapse-note`/`/synapse-design-note` — unrelated conventions that happen to both involve the
60
+ word "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 by the `SessionStart` hook 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 `/synapse-init`
75
+ still does for an already-initialized project is the manual "process it now" sweep of
76
+ `_unassigned` — 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 `SessionStart` hook uses — contaminating one project's graph
83
+ with another's is worse than a blocked command.
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
156
+ `/synapse-rebuild`'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 `/synapse-rebuild`, 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 `PostToolUse` hook and the read-time procedure both use this
259
+ value directly as a vault path with no extension-handling of their own) that claim it, plus an
260
+ `_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 command and the `PostToolUse` 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 the
341
+ `SessionStart` hook will now pick this project up automatically.
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 Claude directly at Synapse read time (Tier 2
350
+ staleness check + regeneration — a procedure, not a hook, documented alongside this command) and
351
+ flagged stale by the `PostToolUse` hook on every subsequent edit to a source file.
352
+ - The `SessionStart` hook's pointer injection depends on this command 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.