@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,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Create a plain note in Synapse Vault (bare mode — title + frontmatter, category resolved from Index.md), a tracked task note (--task, scaffolds the checklist skeleton synapse-task expects), list every tracked task (--list), or search existing notes (--search) 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 synapse-design-note) or compiling a Ready one into a task (that's synapse-task-note) — both of those delegate to this command themselves.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Create a note in Synapse Vault with the title and options, list existing notes, or search existing notes: $ARGUMENTS
|
|
6
|
+
|
|
7
|
+
## Argument parsing
|
|
8
|
+
|
|
9
|
+
If `$ARGUMENTS` is `--list` (or starts with `--list`) → **list mode**: see "List mode" below, skip note creation entirely.
|
|
10
|
+
|
|
11
|
+
If `$ARGUMENTS` starts with `--search` → **search mode**: see "Search mode" below, skip note creation entirely. This is also the mode to reach for programmatically (not just when the user explicitly asks to search) — per the Synapse Vault CLAUDE.md instructions, linking to an existing note is the highest-priority step before creating a new one, so run a search here before every bare-mode note creation, not only when a search is requested outright.
|
|
12
|
+
|
|
13
|
+
Otherwise, split `$ARGUMENTS` on `--task`:
|
|
14
|
+
|
|
15
|
+
- If `--task` is present → **task mode**: scaffold the note as a tracked task, following the `synapse-task` skill's conventions. Task notes always live under `tasks/`.
|
|
16
|
+
- Otherwise → **bare mode**: create an empty node (title + frontmatter only). Which category folder it lands in is resolved from `Index.md`, per "Choosing a category (bare mode only)" below — not a fixed set.
|
|
17
|
+
|
|
18
|
+
The title is everything before `--task` (trimmed). Example:
|
|
19
|
+
|
|
20
|
+
- `/synapse-note "My idea"` → bare note titled "My idea"
|
|
21
|
+
- `/synapse-note "proj-035 — Implement Foo" --task` → task note titled "proj-035 — Implement Foo"
|
|
22
|
+
|
|
23
|
+
In task mode, also attempt to extract a task ID from the title by matching a `{prefix}-\d+` pattern (letters, a hyphen, then digits). Use it as `task_id` in frontmatter.
|
|
24
|
+
|
|
25
|
+
If no match is found, **don't just leave it blank** — see "Resolving a missing task ID" below before proceeding.
|
|
26
|
+
|
|
27
|
+
## List mode
|
|
28
|
+
|
|
29
|
+
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).
|
|
30
|
+
|
|
31
|
+
Categorize:
|
|
32
|
+
- **Prefixed notes**: `task_id` matches `{prefix}-\d+`. Group by the distinct prefix found (whatever
|
|
33
|
+
prefixes actually appear — don't assume a fixed set). Within each prefix, split further into:
|
|
34
|
+
- **Open**: `status` is `TODO`, `IN-PROGRESS`, or `REVIEW` (or missing — treat as open)
|
|
35
|
+
- **Closed**: `status` is `DONE`, `CANCELED`, or `CANCELLED`
|
|
36
|
+
- **Other notes**: no `task_id`, or one that doesn't match `{prefix}-\d+`
|
|
37
|
+
|
|
38
|
+
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.
|
|
39
|
+
|
|
40
|
+
Do not modify any files in list mode.
|
|
41
|
+
|
|
42
|
+
## Search mode
|
|
43
|
+
|
|
44
|
+
Everything after `--search` (trimmed, quotes stripped) is the query.
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
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"]}`).
|
|
48
|
+
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`-less creation.
|
|
49
|
+
|
|
50
|
+
Do not modify any files in search mode.
|
|
51
|
+
|
|
52
|
+
## Resolving a missing task ID (task mode only)
|
|
53
|
+
|
|
54
|
+
Triggered when `--task` is given but the title doesn't match `{prefix}-\d+`.
|
|
55
|
+
|
|
56
|
+
The known project/prefix pairs live in a plain local file named `synapse-projects.conf` (one
|
|
57
|
+
`project-name=prefix` line each) — read/appended with the Read/Edit tools, not the `obsidian`
|
|
58
|
+
MCP server, since it's outside the vault. It is deliberately **not** part of the portable
|
|
59
|
+
Synapse package and never copied between machines, so contexts that shouldn't mix (e.g.
|
|
60
|
+
personal vs. work projects) never end up in the same file. It's self-managed — this command appends
|
|
61
|
+
newly resolved pairs to it — but also plain text, so the user can add, fix, or remove a line by hand
|
|
62
|
+
at any time.
|
|
63
|
+
|
|
64
|
+
Resolve *which* file that is with the same tiered lookup every `synapse-*.conf` file uses for
|
|
65
|
+
reading: first `$XDG_CONFIG_HOME/synapse/synapse-projects.conf` if `$XDG_CONFIG_HOME` is set, else
|
|
66
|
+
`~/.config/synapse/synapse-projects.conf`; then `~/.claude/synapse-projects.conf`. Read whichever of
|
|
67
|
+
those exists first. Append a newly resolved pair (step 5 below) to that same file.
|
|
68
|
+
|
|
69
|
+
If neither exists yet, decide where to create it fresh: `$XDG_CONFIG_HOME/synapse/synapse-projects.conf`
|
|
70
|
+
if `$XDG_CONFIG_HOME` is set; else `~/.config/synapse/synapse-projects.conf` if `~/.config` already
|
|
71
|
+
exists as a directory on this machine (it's adopted XDG conventions for other tools even without ever
|
|
72
|
+
setting the env var); else `~/.claude/synapse-projects.conf` as the final fallback — today's default,
|
|
73
|
+
unchanged for anyone who has never touched an XDG config directory.
|
|
74
|
+
|
|
75
|
+
1. Identify the current project from context: the repo's `CLAUDE.md` (title/"About" section) or
|
|
76
|
+
`git remote`.
|
|
77
|
+
2. Check the resolved `synapse-projects.conf` for a line whose project name matches (loosely —
|
|
78
|
+
case/whitespace-insensitive). If found, use that prefix directly — no need to ask.
|
|
79
|
+
3. If the file doesn't have it yet, fall back to deducing from the vault itself (useful the first
|
|
80
|
+
time this runs, or for a project whose notes predate this file): `search_simple` for the
|
|
81
|
+
project/repo name across existing notes, and/or `search_query` on
|
|
82
|
+
`{"var": "frontmatter.task_id"}` to see which prefixes exist, then check whether any of those
|
|
83
|
+
prefixed notes reference this project. If exactly one prefix confidently matches, use it.
|
|
84
|
+
4. If nothing confidently matches (new project, or an ambiguous/multiple match), ask the user
|
|
85
|
+
directly: "What's the project prefix for this task?" — plain free-text, not a multiple-choice
|
|
86
|
+
list. Don't offer or hint at any other project's prefix as an option.
|
|
87
|
+
5. Whenever step 3 or step 4 resolves a pair not already in the conf file (including a fresh
|
|
88
|
+
`project-name=prefix` line matching what was just deduced or asked), append it — so the next task
|
|
89
|
+
for this project resolves from step 2 without a search or a question.
|
|
90
|
+
6. Once the prefix is known, find the next number: run
|
|
91
|
+
`mcp__obsidian__search_query` with `{"var": "frontmatter.task_id"}`,
|
|
92
|
+
filter the returned `result` values client-side for ones matching
|
|
93
|
+
`{prefix}-\d+`, take the highest number found, add 1. If none exist yet
|
|
94
|
+
for that prefix, start at 1.
|
|
95
|
+
7. Format the new task ID **zero-padded to 3 digits** (`{prefix}-001`, `{prefix}-030`,
|
|
96
|
+
`{prefix}-037`, ...), matching the org-roam-era convention — widening
|
|
97
|
+
naturally past 3 digits if a prefix ever needs it.
|
|
98
|
+
8. **Prepend the resolved task ID to the title itself** — the final title
|
|
99
|
+
becomes `{task-id} — {original title}` (em dash). Use this same final
|
|
100
|
+
title for both the `title` frontmatter field and the `# ` heading, and
|
|
101
|
+
use the resolved task ID for `task_id`. Don't let the frontmatter task
|
|
102
|
+
ID and the visible title disagree.
|
|
103
|
+
|
|
104
|
+
`/synapse-design-note`/`/synapse-task-note` read the same conf file directly for the same reason —
|
|
105
|
+
they don't duplicate this resolution logic, just this file.
|
|
106
|
+
|
|
107
|
+
## Choosing a category (bare mode only)
|
|
108
|
+
|
|
109
|
+
Task mode always uses `tasks/` — skip this step entirely in task mode.
|
|
110
|
+
|
|
111
|
+
In bare mode, ask the user which category the note belongs to. Read
|
|
112
|
+
`Index.md`'s folder list first — every top-level folder listed there
|
|
113
|
+
except `designs`/`tasks`/`synapse` (structurally fixed, not a bare-mode
|
|
114
|
+
destination — see `synapse-claude.md`'s Folders bullet) is a candidate
|
|
115
|
+
category, offered with that folder's own `Index.md` description as the
|
|
116
|
+
option's description. Don't hardcode a fixed option set: a fresh vault's
|
|
117
|
+
`Index.md` lists `research`/`scratchpad`/`inbox` (see
|
|
118
|
+
`${CLAUDE_PLUGIN_ROOT}/Index.md.template`), but a vault owner's own `Index.md` may have
|
|
119
|
+
renamed or restructured these, and whatever it currently says is what gets
|
|
120
|
+
offered.
|
|
121
|
+
|
|
122
|
+
Resolve this to a `category` matching the folder name exactly as
|
|
123
|
+
`Index.md` currently spells it, before moving on to the creation steps
|
|
124
|
+
below. No project-slug question is needed here — Obsidian filenames are
|
|
125
|
+
the title itself, not a slug-prefixed timestamp, so there's no separate
|
|
126
|
+
namespacing concern to resolve. The note always lands flat at
|
|
127
|
+
`{category}/{filename}.md` — never inferred into a subfolder such as a
|
|
128
|
+
triage/priority one a vault owner might maintain by hand (e.g.
|
|
129
|
+
`inbox/{high,medium,low}/`, per that folder's own `Index.md` description);
|
|
130
|
+
sorting a note into one of those, if a category has one, is never an
|
|
131
|
+
agent's call to make.
|
|
132
|
+
|
|
133
|
+
## Resolving the project folder (task mode only)
|
|
134
|
+
|
|
135
|
+
Task notes are grouped one level deeper by project, `tasks/{project}/{filename}.md` — see
|
|
136
|
+
`Index.md`'s `tasks/` section. The prefix (`proj`, `sb`, ...) is not itself the folder name — it
|
|
137
|
+
names the *task*, not the *project* — so resolve what project it belongs to, regardless of how
|
|
138
|
+
the prefix became known (matched from the title, resolved in "Resolving a missing task ID" above,
|
|
139
|
+
or supplied directly by a caller like `/synapse-task-note`):
|
|
140
|
+
|
|
141
|
+
1. Reverse-lookup the prefix in the resolved `synapse-projects.conf` (same tiered lookup as
|
|
142
|
+
"Resolving a missing task ID" above) — find the line whose value after `=` equals the prefix;
|
|
143
|
+
its key is the project name.
|
|
144
|
+
2. If no line matches, check `Index.md`'s `tasks/` section, which documents the prefix-to-project
|
|
145
|
+
mapping directly (e.g. `proj-NNN` → `widget`).
|
|
146
|
+
3. If still unresolved (a genuinely new prefix with no mapping anywhere), ask the user for the
|
|
147
|
+
project name and append `{project-name}={prefix}` to the conf file — so the next task note
|
|
148
|
+
under this prefix resolves without asking.
|
|
149
|
+
|
|
150
|
+
## Creating the note
|
|
151
|
+
|
|
152
|
+
1. Sanitize the title into a filename: replace filesystem-illegal
|
|
153
|
+
characters (`/ : * ? " < > |`) with `-`, collapse repeated whitespace.
|
|
154
|
+
No timestamp prefix, no project-slug prefix — the filename is just the
|
|
155
|
+
(sanitized) title.
|
|
156
|
+
2. Fetch machine local time: `date '+%Y-%m-%d %H:%M'` — never use inferred
|
|
157
|
+
time. Use this for the `created` frontmatter field.
|
|
158
|
+
3. Build the file content:
|
|
159
|
+
|
|
160
|
+
**Bare mode:**
|
|
161
|
+
```
|
|
162
|
+
---
|
|
163
|
+
title: "{title}"
|
|
164
|
+
created: "{now}"
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Task mode:**
|
|
170
|
+
```
|
|
171
|
+
---
|
|
172
|
+
title: "{title}"
|
|
173
|
+
created: "{now}"
|
|
174
|
+
task_id: {task-id}
|
|
175
|
+
status: TODO
|
|
176
|
+
last_updated: "{now}"
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
# {title}
|
|
180
|
+
|
|
181
|
+
## Notes
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
4. Write it with `mcp__obsidian__vault_write`. Task mode: path
|
|
185
|
+
`tasks/{project}/{filename}.md` (project resolved in "Resolving the
|
|
186
|
+
project folder" above). Bare mode: path `{category}/{filename}.md`
|
|
187
|
+
(category resolved above).
|
|
188
|
+
|
|
189
|
+
## Confirm
|
|
190
|
+
|
|
191
|
+
Report the file path back to the user.
|
|
192
|
+
|
|
193
|
+
- Bare mode: note that the note is intentionally near-empty.
|
|
194
|
+
- Task mode: note the task ID extracted or resolved, and remind the user
|
|
195
|
+
to populate the `## Notes` section and checklist before starting work,
|
|
196
|
+
per the `synapse-task` skill.
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: synapse-rebuild-diff
|
|
3
|
+
description: Manually bring a repo's Synapse namespace back in line after major same-branch drift — a pull, a rebase, or a long absence. Triages each drifted node into reseat / patch-from-diff / re-orient rather than rebuilding everything. Refuses outright on a cross-branch mismatch; for a full rebuild from scratch, use /synapse-rebuild-full instead.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Synapse Rebuild Diff: Reconcile a Namespace After Same-Branch Drift
|
|
7
|
+
|
|
8
|
+
`/synapse-init` builds a namespace. The two staleness tiers and `synapse query drift` *detect*
|
|
9
|
+
that it has moved. This command is the deliberate, human-invoked repair for the case where enough has
|
|
10
|
+
moved that lazy per-read regeneration is the wrong instrument.
|
|
11
|
+
|
|
12
|
+
**Same-branch only.** Every scenario below happens on the branch the namespace already describes —
|
|
13
|
+
a pull, a rebase, time passing, your own hand-written commits. None of them involve the current
|
|
14
|
+
checkout being on a *different* branch than the namespace's own recorded `branch:` field. If it is,
|
|
15
|
+
this command refuses outright rather than attempting a diff — see the branch-identity check under
|
|
16
|
+
Prerequisites. Comparing one branch's tree against another isn't drift, it's just two unrelated
|
|
17
|
+
states, and none of the triage classes below (reseat / patch / re-orient) were built for that.
|
|
18
|
+
|
|
19
|
+
## When to run it
|
|
20
|
+
|
|
21
|
+
Manually, when you already expect major drift:
|
|
22
|
+
|
|
23
|
+
- **A plain `pull`** that landed a meaningful number of commits — fast-forward, same branch throughout.
|
|
24
|
+
- **A `pull --rebase`** — your local commits get new SHAs, so `synapse query drift` will likely
|
|
25
|
+
report the baseline as "not an ancestor of HEAD". That's expected here, not a sign of anything
|
|
26
|
+
wrong: you were on the same branch the whole time, only its history got rewritten.
|
|
27
|
+
- **A large merge landing in your current branch** — another branch's tip merged in via `git merge`.
|
|
28
|
+
Still same-branch throughout: `HEAD` gains a merge commit, it doesn't move to a different branch.
|
|
29
|
+
- **A long absence** — weeks or months of other people's commits landed while you were elsewhere, on
|
|
30
|
+
the same branch.
|
|
31
|
+
- **You wrote a lot of code by hand.** The plainest case and probably the most common: days of ordinary
|
|
32
|
+
work in your own branch, in your own editor, with no model involved. Tier 1 only fires on
|
|
33
|
+
`Write`/`Edit`/`MultiEdit` *in this session*, so none of it was flagged as it happened. `stale` will
|
|
34
|
+
still catch content changes to files a node already claims — but it reports a rename as "gone", and a
|
|
35
|
+
**newly added file it cannot see at all**, because a path in no node's `sources` has nothing to
|
|
36
|
+
compare against. Real feature work adds files, so this is exactly where the graph goes quietly out of
|
|
37
|
+
date. Same applies to anything else that bypasses the session: an IDE refactor (which produces both
|
|
38
|
+
bad cases at once — renames *and* new paths), a `sed -i`, generated code that the build rewrote from a
|
|
39
|
+
schema, a dependency bump, or a moved submodule pointer.
|
|
40
|
+
|
|
41
|
+
Do **not** run it after an ordinary pull. Tier 1 flags what this session edited, the `synapse-node`
|
|
42
|
+
skill regenerates a node lazily when its body is actually needed, and `synapse query drift` is the
|
|
43
|
+
cheap check that tells you whether anything more is warranted. This command exists for when the answer
|
|
44
|
+
is clearly yes.
|
|
45
|
+
|
|
46
|
+
**A large job is the expected outcome, not a warning sign.** On a monorepo with a hundred thousand
|
|
47
|
+
files and heavy traffic, most of the graph moving at once is simply what the situation looks like, and
|
|
48
|
+
forty nodes in *re-orient* is a normal shape for this command rather than a reason to hesitate. Nothing
|
|
49
|
+
invokes this automatically — a human typed it, knowing their own repo and why they are here. So report
|
|
50
|
+
the size, then **do the work**. Do not recommend against a rebuild on the grounds that it is expensive,
|
|
51
|
+
do not offer a reduced version of it unasked, and do not describe replacing the graph as destructive:
|
|
52
|
+
replacing it is the entire point. Volunteer a smaller option only where a *correctness* reason argues
|
|
53
|
+
for one, and even then do the full job if the human says so.
|
|
54
|
+
|
|
55
|
+
**One mechanical fact about branches, because it is not guessable.** A namespace is keyed by repo
|
|
56
|
+
*and branch* (`synapse/{repo}@{branch}/`), so each branch has its own or has none. A branch switch
|
|
57
|
+
therefore no longer invalidates anything: the graph you built on the mainline stays intact and keeps
|
|
58
|
+
describing the mainline, and the branch you switched to simply has no namespace until someone runs
|
|
59
|
+
`/synapse-init` there. That is an ordinary state, not a problem to fix.
|
|
60
|
+
|
|
61
|
+
So the massive-drift case this command exists for is now the *unusual* one rather than the norm. It
|
|
62
|
+
still happens — a branch can be checked out inside any worktree, and a long-lived branch gets rebased
|
|
63
|
+
onto a moved trunk, which leaves the recorded baseline off the current line exactly as a branch switch
|
|
64
|
+
used to. Read a "not an ancestor of HEAD" warning as "history moved under this graph", and reach for
|
|
65
|
+
this command when it does. What no longer happens is arriving here merely because you changed branch.
|
|
66
|
+
|
|
67
|
+
## Prerequisites
|
|
68
|
+
|
|
69
|
+
- The namespace must exist. If `synapse/{repo}@{branch}/Index.md` is absent, this is a first build — use
|
|
70
|
+
`/synapse-init`.
|
|
71
|
+
- **Branch-identity check — hard stop, run this before anything else, including drift/grounding.**
|
|
72
|
+
Compare the current checkout's branch against the namespace's own recorded `branch:` frontmatter
|
|
73
|
+
field:
|
|
74
|
+
```sh
|
|
75
|
+
current_branch="$(git symbolic-ref --short HEAD)"
|
|
76
|
+
ns_branch="$(grep -m1 '^branch:' "synapse/{repo}@{branch}/Index.md" \
|
|
77
|
+
| sed -e 's/^branch: *//' -e 's/^"//' -e 's/"$//')"
|
|
78
|
+
```
|
|
79
|
+
If they don't match, **refuse immediately** — do not run `synapse query drift`, do not read
|
|
80
|
+
anything else. Say plainly that this namespace describes a different branch than the current
|
|
81
|
+
checkout, and point at `/synapse-init` (if the current branch has no namespace of its own) or at
|
|
82
|
+
checking out the branch/worktree the namespace actually describes. This is a distinct, harder check
|
|
83
|
+
than the "baseline is not an ancestor of HEAD" ancestry signal below — that one is a *soft*,
|
|
84
|
+
informational finding (expected after a same-branch `pull --rebase`); this one is a hard refusal,
|
|
85
|
+
because it is not this checkout's namespace to diff at all. Never conflate the two: a non-ancestor
|
|
86
|
+
baseline on a branch-identity match still proceeds normally, per the "One mechanical fact about
|
|
87
|
+
branches" section above.
|
|
88
|
+
- The work directory (`$SYNAPSE_WORK_DIR`, default `~/.claude/synapse-work/{repo}@{branch}/`) ideally
|
|
89
|
+
holds the `manifest.tsv` from the original build. Without it, new paths cannot be classified as
|
|
90
|
+
auto-claimable, and clustering decisions have to be re-derived — say so rather than proceeding as if
|
|
91
|
+
nothing were missing. `synapse/{repo}@{branch}/_manifest.tsv` is the fallback copy.
|
|
92
|
+
- Read `synapse/{repo}@{branch}/_profile.txt` if it exists, before triaging anything. It records the
|
|
93
|
+
aggregations that carried signal for this repo and the searches that came back empty.
|
|
94
|
+
|
|
95
|
+
## Procedure
|
|
96
|
+
|
|
97
|
+
### 1. Size the job before doing any of it
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
~/.synapse query drift
|
|
101
|
+
~/.synapse query grounding
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Report what it says, in the human's terms, **before** touching anything: how far the baseline is from
|
|
105
|
+
HEAD (and whether it is an ancestor at all), how many nodes are flagged in each class, and how many
|
|
106
|
+
added paths need a decision. Silence means nothing to rebuild — say so and stop, including when the
|
|
107
|
+
repo is behind its upstream: drift prints that only alongside a finding, because an accurate graph
|
|
108
|
+
plus unpulled commits is nothing to repair yet.
|
|
109
|
+
|
|
110
|
+
Two answers change the plan:
|
|
111
|
+
|
|
112
|
+
- **"baseline … is not an ancestor of HEAD"** — a branch switch or a reset. The file-level diff is
|
|
113
|
+
still exactly right (it compares trees, not history), but expect deletions to dominate: files that
|
|
114
|
+
exist on the built line and simply are not here.
|
|
115
|
+
- **"no commit recorded"** or **"baseline … not in local history"** — those nodes cannot be diffed at
|
|
116
|
+
all. They go straight to the *re-orient* class in step 3; there is no cheaper option for them.
|
|
117
|
+
|
|
118
|
+
### 2. Mechanical phase — always, and cheap
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
~/.synapse build-lists --reenumerate
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`--reenumerate` matters here: without it an existing `all.txt` is reused, so a branch switch would be
|
|
125
|
+
invisible to enumeration. **Read the coverage report.** On a branch switch, per-node list sizes will
|
|
126
|
+
move a lot and some may reach zero.
|
|
127
|
+
|
|
128
|
+
- **A node whose list is now empty** means that subsystem does not exist on this branch. **Do not
|
|
129
|
+
write it** — `synapse write-node` refuses an empty path list, and that refusal is correct. Report
|
|
130
|
+
the node and leave it in place, untouched. **Never delete a node to tidy up a branch switch:**
|
|
131
|
+
`## Notes` is human-authored, lives outside the generated fence, and is unrecoverable.
|
|
132
|
+
- **Unclaimed added paths** are a judgment call: widen an existing manifest line where a path belongs
|
|
133
|
+
to a cluster that already exists, and leave a genuinely new subsystem for a new manifest line and
|
|
134
|
+
its own node. Re-run `synapse build-lists` after editing the manifest, and check coverage again.
|
|
135
|
+
|
|
136
|
+
Then rebuild the reverse index so the hook and the read path agree with the new enumeration:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
~/.synapse build-index
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 3. Triage each flagged node — reseat, patch, or re-orient
|
|
143
|
+
|
|
144
|
+
**Hard requirement for the *re-orient* class, not a recommendation: invoke the `synapse-orientation`
|
|
145
|
+
skill before writing one sentence of new prose for that node.** Reading the node's own source
|
|
146
|
+
directly and reasoning from what you already know about the codebase is not a substitute, no matter
|
|
147
|
+
how accurate or hard-won that knowledge is — a node's premises being re-derived is the same problem
|
|
148
|
+
a first build faces, and it gets the same required technique, every time, not just when nothing
|
|
149
|
+
faster is available. The reason this can't be skipped even by a confident, correct reading of the
|
|
150
|
+
source: the skill's own verification step is to *run* something real (`synapse tags`/`synapse query`
|
|
151
|
+
against a real sample) and judge the actual output, not what the source implies the output should
|
|
152
|
+
be — and only an executed check can surface a gap in the tooling itself, one the source reading alone
|
|
153
|
+
has no way to reveal, however carefully it's done. A node whose own subject is the tagging or
|
|
154
|
+
extraction pipeline is exactly the case most tempting to shortcut this way, and exactly the case
|
|
155
|
+
where doing so costs the most.
|
|
156
|
+
|
|
157
|
+
`synapse-node-format` is the other skill to load before triaging, for every class, not just
|
|
158
|
+
*re-orient*: the node contract — frontmatter, the crux pointer, `## Links`, `grounded_in` — the same
|
|
159
|
+
one `/synapse-init` and the `synapse-node` skill write against.
|
|
160
|
+
|
|
161
|
+
**The principle: compute new prose from the diff, not by re-reading the node's sources.** A node
|
|
162
|
+
covering 15,000 files where 12 changed already has prose encoding the other 14,988. Re-reading it all
|
|
163
|
+
is the expensive mistake this command exists to avoid, and it also throws away hard-won findings the
|
|
164
|
+
diff has nothing to say about.
|
|
165
|
+
|
|
166
|
+
**Take the nodes that lost files first, and keep the full list of deleted paths in front of you for
|
|
167
|
+
every node after that.** A deletion in one node is routinely the other half of an addition in
|
|
168
|
+
another — a type moved from the binary into a library, a module promoted out of a `util`. Triaged in
|
|
169
|
+
drift's arbitrary order, the node that *gained* the file is patched first, with no way to know the
|
|
170
|
+
file came from anywhere, and the patch fills the gap with a guess. One cheap list, read once:
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
git diff --diff-filter=D --name-only -M <commit>..HEAD
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Size each node by changed lines, not by changed files.** Drift reports file counts because that is
|
|
177
|
+
what it can compute without a diff, but a file count saturates immediately in a repo of small
|
|
178
|
+
modules: a node of 9 files with 5 touched reads as 56% when the actual change is 199 lines out of
|
|
179
|
+
2,077, and every one of them a formatting or import edit. Get the real ratio before choosing:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
git diff --numstat <commit>..HEAD -- $(tr '\n' ' ' < "$W/lists/NN.txt") \
|
|
183
|
+
| awk '{a += $1; d += $2} END {print a + d}'
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
against the node's own line count, and use `synapse query sources "{Node}" --count` for the file
|
|
187
|
+
count drift's numbers are relative to. Then pick one of three strategies and **say which one you
|
|
188
|
+
picked and why**:
|
|
189
|
+
|
|
190
|
+
**Restore the crux directive before writing any node back.** `synapse query body` returns the
|
|
191
|
+
*expanded* crux — the fenced code the writer sliced — not the directive that produced it. Writing that
|
|
192
|
+
straight back stores a quote of a file as it looked at the old baseline, presented as if it were
|
|
193
|
+
current. So rebuild the directive from the pointer the writer recorded:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
~/.synapse query field "{Node}" crux_path
|
|
197
|
+
~/.synapse query field "{Node}" crux_lines
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
and replace the fenced block with `<!-- crux: <crux_path> <crux_lines> -->` so it is cut from the
|
|
201
|
+
current file. One judgment goes with it: re-slicing the same range is only safe if that file did not
|
|
202
|
+
change. If it did, the line numbers may now point at something else entirely — treat the crux as
|
|
203
|
+
needing a fresh pointer, exactly as the prose needs a fresh sentence. A node with no `crux_path` had
|
|
204
|
+
`none`, and stays that way.
|
|
205
|
+
|
|
206
|
+
**The same applies to groundings, and forgetting them loses more.** `grounded_in` is frontmatter and
|
|
207
|
+
its directives are stripped from the body, so a recovered body contains none — write it back as-is and
|
|
208
|
+
the node's provenance is gone with no error. Recover the pointers per node:
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
~/.synapse query grounding "{Node}" --list # path<TAB>lines
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
and re-emit a `<!-- grounded_in: <path> <lines> -->` for each. Run `synapse query grounding` before
|
|
215
|
+
triaging: it is cheaper than the diff and sharper than a churn ratio. A **`moved`** line hands you the
|
|
216
|
+
corrected range outright, no reading. A **`changed`** line points at evidence that no longer says what
|
|
217
|
+
the summary claims — which is a better reason to re-read a node than any percentage, because it names
|
|
218
|
+
the sentence at risk rather than the volume of change around it.
|
|
219
|
+
|
|
220
|
+
**Reseat** — renames only, no content change. No reading at all. Recover the existing prose with
|
|
221
|
+
`synapse query body "{Node}"`, drop its trailing `## Sources` block (the writer regenerates that),
|
|
222
|
+
re-enumerate so the list holds the new paths, and write it back. Repeating this is safe: the writer
|
|
223
|
+
trims the body's leading and trailing blank lines, so a reseat is idempotent rather than accreting
|
|
224
|
+
padding each time. The concept did not change; only paths moved. This also
|
|
225
|
+
works on a machine that never built the namespace, because the body came from the node itself rather
|
|
226
|
+
than from a work-dir file.
|
|
227
|
+
|
|
228
|
+
**Patch from the diff** — a small fraction of the node's *lines* changed (rule of thumb: under ~15%),
|
|
229
|
+
and the file its `crux` quotes still exists. Read three things and nothing else:
|
|
230
|
+
|
|
231
|
+
1. the current prose — `synapse query body "{Node}"`;
|
|
232
|
+
2. `git diff --name-status -M <commit>..HEAD` restricted to that node's paths, for *which* files moved;
|
|
233
|
+
3. hunks for a **bounded** selection of those files — always including any file the `crux` quotes.
|
|
234
|
+
|
|
235
|
+
Then amend only the sentences the diff contradicts, and keep everything else verbatim.
|
|
236
|
+
|
|
237
|
+
**A patch may say what the node now contains. It must not say where something came from** unless the
|
|
238
|
+
rename is in the diff it read. Provenance is the one claim a node-restricted diff structurally cannot
|
|
239
|
+
support: the other end of the move is in a different node's paths, so the diff shows an unexplained
|
|
240
|
+
new file, and the plausible local origin is an invention. `git`'s rename detection is not a
|
|
241
|
+
safety net here — a file that moved between modules and was rewritten on the way lands below the
|
|
242
|
+
similarity threshold and shows up as a delete in one node and an add in another even under
|
|
243
|
+
`-M --find-copies-harder`. Which is why deletions get read first, below.
|
|
244
|
+
|
|
245
|
+
**Re-orient** — a large fraction changed, the baseline is unusable, or there is a structural signal:
|
|
246
|
+
the `crux` file is gone or renamed, whole modules entered or left the node, or a package root changed
|
|
247
|
+
name. Here the prose's premises are suspect, so patching would preserve a claim that is no longer
|
|
248
|
+
true. Re-run this node's aggregations from `_profile.txt` (path-level, so cheap even on a hub node),
|
|
249
|
+
read the few load-bearing files the aggregations point at, and re-author as `/synapse-init` would.
|
|
250
|
+
|
|
251
|
+
**The diff must be projected as carefully as `sources` is.** `git diff` with hunks across a hub node's
|
|
252
|
+
paths over hundreds of commits runs to megabytes — the same constraint that makes `sources` unreadable
|
|
253
|
+
applies to the diff. So: `--name-status` for names, `--stat` to size it, and hunks only for a bounded
|
|
254
|
+
selection. Never pipe an unbounded `git diff <commit>..HEAD` into a context window.
|
|
255
|
+
|
|
256
|
+
### 4. Write each rebuilt node
|
|
257
|
+
|
|
258
|
+
```sh
|
|
259
|
+
~/.synapse write-node --title "{Node}" --summary "{one line}" \
|
|
260
|
+
--paths "$W/lists/NN.txt" --body "$W/body.md"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
It re-records `commit`, so this checkout becomes the node's new baseline — which is what makes the next
|
|
264
|
+
`drift` meaningful. **Re-check the one-line `summary`**: after a branch switch it can be wrong in kind,
|
|
265
|
+
not merely stale, if the subsystem's shape differs on this line.
|
|
266
|
+
|
|
267
|
+
### 5. Rebuild the projections and verify
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
~/.synapse build-index
|
|
271
|
+
~/.synapse build-project-index
|
|
272
|
+
~/.synapse query drift # expect silence
|
|
273
|
+
~/.synapse query stale # expect silence
|
|
274
|
+
~/.synapse query grounding # expect silence: re-pointed, not dropped
|
|
275
|
+
~/.synapse query links --check # expect silence: no dangling targets
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`links --check` covers what used to be a manual instruction here: a broken `[[wikilink]]` is a valid
|
|
279
|
+
link to a not-yet-existing note, so Obsidian renders it without complaint and nothing else in the
|
|
280
|
+
system notices. It now reports `Node<TAB>relation -> Target (no such node)` per dangling edge.
|
|
281
|
+
|
|
282
|
+
One check is still yours, because no command performs it: **every node file appears in `Index.md`**. An
|
|
283
|
+
unlisted node exists and is reachable by search, but is invisible to anyone reading the map.
|
|
284
|
+
|
|
285
|
+
### 6. Report what happened
|
|
286
|
+
|
|
287
|
+
Per node: the strategy chosen and why. Plus what was deliberately left alone — nodes drift did not
|
|
288
|
+
flag, and nodes whose sources vanished on this branch. A rebuild that silently re-authored forty nodes
|
|
289
|
+
is indistinguishable, from the outside, from one that did nothing.
|
|
290
|
+
|
|
291
|
+
## Guardrails
|
|
292
|
+
|
|
293
|
+
- **Never `pull`, `fetch --prune`, `rebase`, `reset` or `checkout`.** The human chose this checkout;
|
|
294
|
+
this command describes and records it. Report how far behind the upstream ref is and stop there.
|
|
295
|
+
- **Never re-read a node's full sources to patch a small diff.** That is the specific waste this
|
|
296
|
+
command is built to avoid.
|
|
297
|
+
- **Know what patching cannot fix.** It keeps every sentence the diff does not contradict, so a claim
|
|
298
|
+
that was wrong when the node was *built* survives every future patch untouched — the diff has
|
|
299
|
+
nothing to say about a statement that was never true. Patching is therefore only as good as the
|
|
300
|
+
baseline prose, and a node's most likely error is not drift but an explanation invented at build
|
|
301
|
+
time. The `crux` is no longer the exposure it was — `synapse write-node` slices it out of the
|
|
302
|
+
file from a `<!-- crux: path start-end -->` directive, so it is verbatim by construction rather than
|
|
303
|
+
by instruction. What remains unguarded is the prose. A sentence asserting a *mechanism* ("X is
|
|
304
|
+
behind a mutex, which is why Y")
|
|
305
|
+
deserves more suspicion than one asserting structure — if the diff touches its file at all, verify
|
|
306
|
+
it rather than carrying it over.
|
|
307
|
+
- **Never write a node with an empty path list**, and never delete a node whose sources vanished — its
|
|
308
|
+
`## Notes` is human-authored and outside the generated fence.
|
|
309
|
+
- **Never hand-write frontmatter or the `## Sources` mirror.** `synapse write-node` owns them; doing
|
|
310
|
+
it by hand cannot scale to a hub node and silently drops `summary` and `commit`.
|
|
311
|
+
- **Never rebuild a node drift did not flag.** Regeneration has real cost and it is not free of risk —
|
|
312
|
+
each rewrite is a chance to lose a good sentence.
|
|
313
|
+
- **Say when the graph now describes a different branch than it did before**, in the final report. That
|
|
314
|
+
fact outlives the session, and the next reader has no other way to know.
|