@sporhq/spor 0.2.2 → 0.2.5

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.
@@ -2,6 +2,6 @@
2
2
  "name": "spor",
3
3
  "displayName": "Spor Context Compiler",
4
4
  "description": "Maintains a typed, versioned knowledge graph and compiles compact briefings from it: session-start injection, per-prompt relevance digests, capture at discovery, end-of-session distillation, decision queue.",
5
- "version": "0.2.2",
5
+ "version": "0.2.5",
6
6
  "author": { "name": "losthammer" }
7
7
  }
package/README.md CHANGED
@@ -64,10 +64,12 @@ For the per-host event mapping, fidelity notes, distiller backend, and the
64
64
  `AGENTS.md` fallback for hosts with no hook support, see
65
65
  [adapters/](adapters/).
66
66
 
67
- To start with a populated graph, point the bundled `spor-backfill` agent at
68
- your existing sources: it mines git history, design docs, and issue trackers
69
- into a first graph. Or skip that and just work distillation grows the graph one session
70
- at a time.
67
+ To start with a populated graph, run `/spor:backfill` the onboarding door. It
68
+ dispatches the heavy mining (git history, design docs, issue trackers, edges
69
+ first) to the bundled `spor-backfill` **subagent**, which runs in its own
70
+ context, and then proposes how to group your repos into projects (re-run it as
71
+ you add repos). Or skip it and just work — distillation grows the graph one
72
+ session at a time.
71
73
 
72
74
  ## What your agent gets, and gives back
73
75
 
@@ -88,7 +90,10 @@ The loop runs without you having to drive it:
88
90
  You can also ask for any of this directly: an on-demand briefing for a task,
89
91
  a correction when a briefing was wrong, a capture of work you're deferring,
90
92
  and a ranked queue of what to do next. In Claude Code these surface as
91
- `/spor:brief`, `/spor:correct`, `/spor:defer`, and `/spor:next`.
93
+ `/spor:brief`, `/spor:correct`, `/spor:defer`, and `/spor:next`, plus
94
+ `/spor:backfill` to bootstrap/extend the graph and organize repos into projects.
95
+ (`/spor:backfill` is the discoverable door; the heavy git-history mining still
96
+ runs in the `spor-backfill` subagent it dispatches.)
92
97
 
93
98
  Corrections are durable. When a briefing includes something stale or misses
94
99
  something it should have known, you record the correction once, and every
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sporhq/spor",
3
- "version": "0.2.2",
3
+ "version": "0.2.5",
4
4
  "description": "Spor — a shared memory substrate for teams and agents. Decisions, their reasons, and the traces they leave. Knowledge-graph context compiler: session-start briefings, per-prompt digests, capture at discovery, end-of-session distillation, decision queue.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Anthony Allen",
@@ -61,6 +61,25 @@ function nodeBody(raw) {
61
61
  return u.stripTrailingNewlines(u.byteHead(awkOut, 7000));
62
62
  }
63
63
 
64
+ // Durable repo-identity node text (issue-spor-onboard-no-repo-identity-node),
65
+ // mirroring the server's learnFingerprints registration so local and remote
66
+ // produce the same shape. Ungrouped by default — no grouped-under edge; the
67
+ // standing grouping suggester proposes the single home project.
68
+ function repoNodeMarkdown(slug, fp, today) {
69
+ return `---
70
+ id: repo-${slug}
71
+ type: repo
72
+ title: ${slug}
73
+ summary: Git-repo identity for '${slug}', auto-registered from its fingerprints when the repo first appeared in the local graph. Ungrouped — no home project yet; the grouping suggester proposes a grouped-under home.
74
+ slugs: [${slug}]
75
+ fingerprints: [${fp.join(", ")}]
76
+ date: ${today}
77
+ ---
78
+
79
+ Auto-registered repo identity for \`${slug}\` (issue-spor-onboard-no-repo-identity-node). Created on first sight so the repo is a first-class node: the anchor for rename-healing fingerprint matching and for a \`grouped-under\` home. It starts UNGROUPED by default — repo-scoped reads work on the slug, and the standing grouping suggester proposes its single home project for confirmation. Slug aliases and fingerprints accumulate here across renames and re-clones.
80
+ `;
81
+ }
82
+
64
83
  async function sessionStart(input) {
65
84
  const graph = u.graphHome();
66
85
  const nodes = path.join(graph, "nodes");
@@ -141,7 +160,11 @@ async function sessionStart(input) {
141
160
  const item = (q.items || [])[0];
142
161
  if (item && item.id) {
143
162
  const why = item.why || "";
144
- oline = `\nopen front: ${item.id} ${item.title || ""}${why ? ` (${why})` : ""}`;
163
+ // Emitted phrasing is deliberately plain ("next up:", not the internal
164
+ // "open front" coinage) so it doesn't prime the agent to parrot jargon
165
+ // at the human (issue-cc-skill-queue-jargon-mode-theater). The concept
166
+ // is still the open front; only the user-facing words changed.
167
+ oline = `\nnext up: ${item.id} — ${item.title || ""}${why ? ` (${why})` : ""}`;
145
168
  if ((item.suggest || "do") === "close") oline += " — the queue suggests CLOSING it, not doing it";
146
169
  oline += ". Full queue: /spor:next.";
147
170
  }
@@ -309,7 +332,8 @@ ${body}`;
309
332
  const r = rankQueue(g, { project: slug, limit: 1 });
310
333
  const item = (r.items || [])[0];
311
334
  if (item && item.id) {
312
- oline = `\nopen front: ${item.id} ${item.title || ""}${item.why ? ` (${item.why})` : ""}`;
335
+ // Plain user-facing phrasingmirror remote mode's "next up:" line.
336
+ oline = `\nnext up: ${item.id} — ${item.title || ""}${item.why ? ` (${item.why})` : ""}`;
313
337
  if (item.suggest === "close") oline += " — the queue suggests CLOSING it, not doing it";
314
338
  oline += ". Full queue: /spor:next.";
315
339
  }
@@ -412,6 +436,26 @@ ${pbody}`;
412
436
  /* fail open */
413
437
  }
414
438
 
439
+ // Ensure this repo has a durable identity node once it actually has content
440
+ // in the graph (issue-spor-onboard-no-repo-identity-node). Mirrors the
441
+ // server's learnFingerprints in remote mode: a backfilled/distilled repo with
442
+ // no `type: repo` node yet gets one registered from its git fingerprints, so
443
+ // it is a first-class node — the anchor for a grouped-under home and for
444
+ // rename-healing. Gated on projCount>0 (real graph content), no owning repo
445
+ // node yet, and fingerprints present, so a session in some unrelated checkout
446
+ // never spawns an identity node. Ungrouped by default; the grouping suggester
447
+ // proposes the home. Pure side effect, written AFTER the briefing is built so
448
+ // it never alters this run's output; fail-open — never blocks session-start.
449
+ try {
450
+ const fp = cwd && fs.existsSync(cwd) ? u.repoFingerprints(cwd) : [];
451
+ const repoFile = path.join(nodes, `repo-${slug}.md`);
452
+ if (!owner && projCount > 0 && fp.length && /^[a-z0-9][a-z0-9-]*$/.test(slug) && !fs.existsSync(repoFile)) {
453
+ fs.writeFileSync(repoFile, repoNodeMarkdown(slug, fp, new Date().toISOString().slice(0, 10)));
454
+ }
455
+ } catch {
456
+ /* best effort — never block session-start */
457
+ }
458
+
415
459
  return envelope(ctx);
416
460
  }
417
461
 
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: backfill
3
+ description: Bootstrap or extend a project's Spor graph and organize its repos into projects. Use to backfill a repo's history into the graph, to onboard a newly cloned repo, or to group repos ("organize my repos", "set up / pick a project for this repo", "what project does this repo belong to", "group these repos under a project"). Proposes project groupings from the repos already in the graph and writes nothing without confirmation.
4
+ ---
5
+
6
+ # Backfill & organize
7
+
8
+ Two onboarding jobs behind one door: get a repo's history INTO the graph, and
9
+ organize the repos that are in it into `type: project` groupings. Both are
10
+ re-runnable — run this again whenever you add a repo.
11
+
12
+ ## 1. Populate the graph (backfill), if the repo is thin
13
+
14
+ If the current repo has little or no graph content yet, offer to backfill it:
15
+ spawn the **spor-backfill subagent** (Task tool, `subagent_type:
16
+ spor:spor-backfill`) pointed at this repo — it mines git history, design docs,
17
+ and issue trackers into typed nodes, edges first. Skip this when the repo is
18
+ already well represented (the enumeration in step 2 tells you what is present).
19
+ The heavy mining always runs in the subagent, never inline in this session.
20
+
21
+ ## 2. Suggest project groupings
22
+
23
+ A repo's home is a `type: project` grouping it sits under via a `grouped-under`
24
+ edge (dec-cc-repo-project-membership-edge); project-scoped reads union every
25
+ repo grouped-under one project. A freshly onboarded repo starts **ungrouped**,
26
+ which is valid — repo-scoped reads work on its slug. This step proposes the
27
+ single home for confirmation; it is the whole point of running this again as
28
+ repos accumulate.
29
+
30
+ ### a. Enumerate what exists
31
+
32
+ Gather every `type: repo` and `type: project` node, plus each repo's `slugs`,
33
+ `fingerprints`, and current `grouped-under` edge (if any):
34
+
35
+ - **With the Spor MCP tools** (Cowork, or Claude Code with the connector):
36
+ call `render_lens` with no `lens_id` to list the saved lenses, render the
37
+ project-breakdown one to see existing projects and their members, and
38
+ `query_graph` for repo nodes; `get_node` each repo to read its `fingerprints`.
39
+ - **Local mode** (`SPOR_SERVER` unset): scan the graph home directly —
40
+ ```bash
41
+ SPOR_HOME="${SPOR_HOME:-$HOME/.spor}"; [ -d "$SPOR_HOME/nodes" ] || SPOR_HOME="$HOME/.substrate"
42
+ grep -lE '^type: (repo|project)$' "$SPOR_HOME"/nodes/*.md
43
+ ```
44
+ Read each match: repo nodes carry `slugs:` and `fingerprints:` and (if homed)
45
+ a `grouped-under` edge; project nodes are the groupings themselves.
46
+
47
+ The repos with no `grouped-under` edge are the ones to home.
48
+
49
+ **Bridge older backfills.** A repo backfilled before identity-node
50
+ auto-registration (client ≤ 0.2.2) has `repo:`/`project:` stamps on its work
51
+ nodes but no `type: repo` node — and a home edge has nothing to attach to
52
+ without one. So before grouping, find the distinct stamp slugs that have no
53
+ matching `type: repo` node (no `repo-<slug>` id and not in any `slugs:`
54
+ register) and register an ungrouped identity node for each:
55
+
56
+ - **Local:** write `$SPOR_HOME/nodes/repo-<slug>.md` — `type: repo`,
57
+ `title: <slug>`, `slugs: [<slug>]`, today's `date`, no edges — then validate
58
+ (plugin root resolved as in step d).
59
+ - **Spor MCP tools:** `put_node` the same `type: repo` node.
60
+
61
+ Register them with **slugs only** — fingerprints accrue automatically when a
62
+ session next runs in each repo (session-start records them). The repo you are
63
+ currently in usually already has its node, since session-start registers it on
64
+ first sight; this step exists so a single `/spor:backfill` run can surface and
65
+ home *all* of a user's already-backfilled repos, not just the current one.
66
+ Announce which identity nodes you registered. (This is mechanical — the slugs
67
+ demonstrably exist in the graph — so it does not need the per-grouping
68
+ confirmation that step c does.)
69
+
70
+ ### b. Group the ungrouped repos by signal
71
+
72
+ For each ungrouped repo, find its best home. Signals, strongest first:
73
+
74
+ 1. **Git remote org** — `fingerprints: [remote:github.com/<org>/<repo>]`. Repos
75
+ sharing an org almost always belong to one product (`sporhq/spor` +
76
+ `sporhq/spor-server` → one project). This is the primary signal.
77
+ 2. **Shared name stem** — `acme-web`, `acme-api`, `acme-mobile` → `acme`.
78
+ 3. **Cross-repo edges** — a repo whose nodes carry `derived-from`/`blocks`/
79
+ `relates-to` edges into another repo's nodes belongs with that repo.
80
+ 4. Shared people (contributors/stewards), then — weakest, last resort — topic
81
+ similarity across the repos' node text.
82
+
83
+ ### c. Propose — never auto-write
84
+
85
+ Present each suggestion with its evidence and let the user confirm before any
86
+ write:
87
+
88
+ > - Group **repo-acme-api** under **proj-acme** (existing, ⊇ {repo-acme-web}) —
89
+ > shared org `github.com/acme`, shared stem `acme`. [extend]
90
+ > - Create **proj-acme-platform** ⊇ {repo-acme-api, repo-acme-jobs} — shared
91
+ > org + 3 cross-repo edges. [new]
92
+
93
+ Rules, from the two-layer identity model (dec-cc-repo-project-two-layer-identity):
94
+
95
+ - **One home per repo.** Never propose a repo into two projects — suggestions
96
+ are mutually exclusive per repo.
97
+ - **Co-ownership is banned.** A repo genuinely shared by two products gets its
98
+ OWN grouping, not membership in both; cross-cutting work stays at the work
99
+ layer as edges, not as duplicated groupings.
100
+ - **Ungrouped is a fine outcome.** If there is no real signal, leave the repo
101
+ ungrouped and say so — don't invent a singleton project to fill the slot
102
+ unless the user asks for one.
103
+ - Prefer **extending** an existing project over creating a near-duplicate.
104
+
105
+ ### d. Write the confirmed homes
106
+
107
+ A grouping node's id is `proj-<stem>` (the `proj-` grouping prefix); it owns no
108
+ slugs or fingerprints — those live on the repo nodes. The home is a
109
+ `grouped-under` edge written ON the repo node, pointing TO the project.
110
+
111
+ - **Spor MCP tools:** `put_node` the new `type: project` node (skip if it
112
+ exists), then for each member repo `add_edge {id: "repo-<slug>", type:
113
+ "grouped-under", to: "proj-<stem>"}`. To RE-HOME a repo later, remove the old
114
+ `grouped-under` edge and add the new one — still exactly one home.
115
+ - **Local mode:** create `$SPOR_HOME/nodes/proj-<stem>.md` (`type: project`,
116
+ `title`, `summary`, today's `date`), and add a
117
+ `- {type: grouped-under, to: proj-<stem>}` line under each member repo node's
118
+ `edges:`. Then validate — resolve the plugin root the session-start hook
119
+ cached (issue-cc-skill-plugin-root-unsubstituted):
120
+ ```bash
121
+ SPOR_ROOT="$(cat "${SPOR_HOME:-$HOME/.spor}/cache/plugin-root" 2>/dev/null \
122
+ || cat "$HOME/.substrate/cache/plugin-root" 2>/dev/null)"
123
+ SPOR_ROOT="${SPOR_ROOT:-$CLAUDE_PLUGIN_ROOT}"
124
+ node "$SPOR_ROOT/lib/validate.js"
125
+ ```
126
+ Fix anything it flags, and commit the graph repo if it is one.
127
+
128
+ Report what you grouped, and — just as important — what you left ungrouped and
129
+ why.
@@ -9,6 +9,13 @@ You are the distiller stage of the Spor context compiler. The traversal
9
9
  stage is mechanical; your job is to turn its neighborhood document into a
10
10
  briefing an agent (or human) can act on without reading anything else.
11
11
 
12
+ **Resolve mode silently.** The Spor status line injected at session start tells
13
+ you which mode you're in (`team graph: …` = remote, `A Spor knowledge graph is
14
+ active: …` = local); use it, or test `[ -n "$SPOR_SERVER" ]` once if it isn't in
15
+ context. Don't echo `SPOR_SERVER`/`SPOR_TOKEN`/`SPOR_HOME` or announce the mode
16
+ to the user unless they ask, and run the local-mode resolution below without
17
+ echoing `$SPOR_ROOT`.
18
+
12
19
  Steps:
13
20
 
14
21
  1. Run the traversal. `$ARGUMENTS` is either a node id (e.g. `issue-86`) or a
@@ -8,6 +8,13 @@ description: Record a standing correction to a Spor briefing (pin/exclude nodes,
8
8
  Corrections are nodes. They persist in the graph and are applied at every
9
9
  future compile of their target — a context fix made once applies forever.
10
10
 
11
+ **Resolve mode silently.** The Spor status line injected at session start tells
12
+ you which mode you're in (`team graph: …` = remote, `A Spor knowledge graph is
13
+ active: …` = local); use it, or test `[ -n "$SPOR_SERVER" ]` once if it isn't in
14
+ context. Don't echo `SPOR_SERVER`/`SPOR_TOKEN`/`SPOR_HOME` or announce the mode
15
+ to the user unless they ask, and run the local-mode resolution below without
16
+ echoing `$SPOR_ROOT`.
17
+
11
18
  ## Remote mode (team graph) — when `SPOR_SERVER` is set
12
19
 
13
20
  (Env vars here are the `SPOR_*` family; the legacy `SUBSTRATE_*` names are
@@ -14,6 +14,13 @@ Write 2-3 standalone sentences: WHAT the work is and WHY it was deferred
14
14
  concrete names (files, endpoints, node ids). Do not pick node types or ids —
15
15
  the server's ingestion model does that against the live schema registry.
16
16
 
17
+ **Resolve mode silently.** The Spor status line injected at session start tells
18
+ you which mode you're in (`team graph: …` = remote, `A Spor knowledge graph is
19
+ active: …` = local); use it, or test `[ -n "$SPOR_SERVER" ]` once if it isn't in
20
+ context. Don't echo `SPOR_SERVER`/`SPOR_TOKEN`/`SPOR_HOME` or announce the mode
21
+ to the user unless they ask, and run the local-mode resolution below without
22
+ echoing `$SPOR_ROOT`.
23
+
17
24
  ## Remote mode (team graph) — when `SPOR_SERVER` is set
18
25
 
19
26
  (Env vars here are the `SPOR_*` family; the legacy `SUBSTRATE_*` names are
@@ -11,6 +11,16 @@ unprocessed captures, org-defined types) ranked by an advisory blend — what th
11
11
  their neighborhood, age — plus any human-set `priority:`. High staleness
12
12
  (anchors superseded or gone) flips the suggestion to **close**, not do.
13
13
 
14
+ ## Mode — resolve it silently, never announce it
15
+
16
+ You already know your mode: the Spor status line injected at session start says
17
+ either `team graph: …` (remote — a `SPOR_SERVER` is set) or `A Spor knowledge
18
+ graph is active: …` (local). Use that; if it isn't in context, test
19
+ `[ -n "$SPOR_SERVER" ]` once. Either way, do **not** echo
20
+ `SPOR_SERVER`/`SPOR_TOKEN`/`SPOR_HOME`, and do **not** tell the user which mode
21
+ is running unless they ask — it's plumbing they don't need. Run the commands
22
+ below quietly (no `echo` of `$SPOR_ROOT` or the slug).
23
+
14
24
  ## Remote mode (team graph) — when `SPOR_SERVER` is set
15
25
 
16
26
  (Env vars here are the `SPOR_*` family; the legacy `SUBSTRATE_*` names are
@@ -42,18 +52,30 @@ plugin root from the path the session-start hook cached
42
52
  SPOR_ROOT="$(cat "${SPOR_HOME:-$HOME/.spor}/cache/plugin-root" 2>/dev/null \
43
53
  || cat "$HOME/.substrate/cache/plugin-root" 2>/dev/null)"
44
54
  SPOR_ROOT="${SPOR_ROOT:-$CLAUDE_PLUGIN_ROOT}"
45
- node "$SPOR_ROOT/lib/queue.js" # or --project <slug>, --json
55
+ node "$SPOR_ROOT/lib/queue.js" --json # or --project <slug>, --limit <n>
46
56
  ```
47
57
 
58
+ Use `--json` and compose the human view from it — don't show the bare CLI
59
+ listing, whose leading `[<score>]` is an internal ranking number, not something
60
+ to surface (see Presenting).
61
+
48
62
  (No server means no activity feed, so heat is 0 locally; the other signals
49
63
  are identical.)
50
64
 
51
65
  ## Presenting and acting
52
66
 
53
- Show the ranked items with their `why` lines and any `suggest: close` flags
54
- the signals are advisory; the human picks. If the result carries `muted` or
55
- `dormant` counts, mention them (hidden by the viewer's `queue_mute` /
56
- parked by a `wake:` date never silently dropped). Then:
67
+ Present in plain language you are talking to a human who may be new to Spor.
68
+ Lead with the top one or two items to pick up and a short, plain reason each is
69
+ there ("the oldest still-open piece of work", "it blocks three other tasks",
70
+ "nothing's touched it in months"). **Translate** the signals behind each `why`
71
+ line into ordinary words; do **not** surface the raw `score` or internal
72
+ coinages — *open front*, *heat*, *staleness*, *front* are ranking internals, not
73
+ terms a newcomer knows. Honor `suggest: close` by framing the item as likely
74
+ done/abandonable rather than work to start. The ranking is advisory; the human
75
+ picks. If the result carries `muted` or `dormant` counts, mention them in
76
+ passing (hidden by the viewer's `queue_mute` / parked by a `wake:` date — never
77
+ silently dropped). Don't open with a glossary or re-explain Spor on every run —
78
+ one plain sentence of "why this is first" is enough. Then:
57
79
 
58
80
  1. **Item picked to DO** → start pre-briefed: run a full root compile for it
59
81
  (/spor:brief `<item-id>`, or locally with `$SPOR_ROOT` resolved as above: