@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,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.
|