@sporhq/spor 0.4.0 → 0.4.2

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.4.0",
5
+ "version": "0.4.2",
6
6
  "author": { "name": "losthammer" }
7
7
  }
package/bin/spor.js CHANGED
@@ -925,27 +925,39 @@ function claudePluginInfo() {
925
925
  return p ? { version: p.version, scope: p.scope, enabled: p.enabled, installPath: p.installPath } : null;
926
926
  }
927
927
 
928
- // Best-effort: is a claude.ai Spor MCP connector bound on this box? A connector
929
- // added in claude.ai surfaces in Claude Code as the mcp__…_Spor__* tools
930
- // (art-cc-spor-connector-dual-host), i.e. a SECOND live write surface alongside
931
- // the local file graph. Claude Code records connected claude.ai connectors in
932
- // ~/.claude.json's `claudeAiMcpEverConnected` array (entries like
933
- // "claude.ai Spor"); we key the detection on a Spor-named entry there (matching
934
- // the pre-rename "Substrate" name too the connector predates the rename and
935
- // the array keeps historical entries). This is the only discoverable signal a
936
- // plain Claude Code box exposes: there is no per-session "currently active"
937
- // manifest. FAIL-OPEN by contract — any missing/unreadable/unparseable file
938
- // returns false so `spor status` never emits a false split-brain warning or
939
- // crashes. SPOR_FAKE_CLAUDE_JSON overrides the path for tests.
928
+ // Best-effort: is a claude.ai Spor MCP connector CURRENTLY bound on this box? A
929
+ // connector added in claude.ai surfaces in Claude Code as the mcp__…_Spor__*
930
+ // tools (art-cc-spor-connector-dual-host), i.e. a SECOND live write surface
931
+ // alongside the local file graph. We read the LIVE set from `claude mcp list`
932
+ // (mirroring claudePluginInfo's spawn) and look for a Spor-named connector —
933
+ // matching the pre-rename "Substrate" name too. We deliberately do NOT key on
934
+ // ~/.claude.json's `claudeAiMcpEverConnected`: that array is a sticky historical
935
+ // "ever connected" list that never clears when a connector is disabled or
936
+ // removed, so it warned forever after the user unbound the connector
937
+ // (issue-spor-status-split-brain-warning-false-positive). FAIL-OPEN by contract:
938
+ // claude absent / nonzero exit / timeout / empty output all return false, so
939
+ // `spor status` never emits a false split-brain warning or hangs. The health
940
+ // status (Connected / Needs authentication / Failed) is ignored — any current
941
+ // binding is a configured second write surface. SPOR_FAKE_MCP_LIST injects
942
+ // canned `claude mcp list` output for tests.
940
943
  function sporConnectorBound() {
941
944
  try {
942
- const p = process.env.SPOR_FAKE_CLAUDE_JSON || path.join(homeDir(), ".claude.json");
943
- const j = JSON.parse(fs.readFileSync(p, "utf8"));
944
- const ever = j && j.claudeAiMcpEverConnected;
945
- if (!Array.isArray(ever)) return false;
946
- return ever.some((name) => typeof name === "string" && /\bspor\b|\bsubstrate\b/i.test(name));
945
+ let text = process.env.SPOR_FAKE_MCP_LIST;
946
+ if (text == null) {
947
+ const cmd = claudeCmd();
948
+ if (cmd === "claude" && !hasCmd("claude")) return false;
949
+ const r = spawnSync(cmd, ["mcp", "list"], { encoding: "utf8", timeout: 8000 });
950
+ if (r.status !== 0 || !r.stdout) return false;
951
+ text = r.stdout;
952
+ }
953
+ // Each connector is a line like "claude.ai Spor: <url> - <status>". Match the
954
+ // NAME segment (before the first colon) only, so a "spor" in a URL or status
955
+ // can't trip it; \b keeps "Spotify"/"Supabase" from matching "spor".
956
+ return text
957
+ .split("\n")
958
+ .some((line) => /\bspor\b|\bsubstrate\b/i.test(line.split(":")[0] || ""));
947
959
  } catch {
948
- return false; // no file, unreadable, or malformed => assume no connector
960
+ return false; // claude missing, spawn error, or unparseable => assume none
949
961
  }
950
962
  }
951
963
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sporhq/spor",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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",
@@ -37,14 +37,26 @@ Steps:
37
37
  **Local mode (personal graph) — `SPOR_SERVER` unset:** first resolve the
38
38
  plugin root — `${CLAUDE_PLUGIN_ROOT}` is empty in the Bash tool, so read
39
39
  the path the session-start hook cached
40
- (issue-cc-skill-plugin-root-unsubstituted):
40
+ (issue-cc-skill-plugin-root-unsubstituted) — then resolve THIS repo's
41
+ project slug the same way a session does, so the compile is scoped to the
42
+ repo you're in instead of project-blind:
41
43
  ```bash
42
44
  SPOR_ROOT="$(cat "${SPOR_HOME:-$HOME/.spor}/cache/plugin-root" 2>/dev/null \
43
45
  || cat "$HOME/.substrate/cache/plugin-root" 2>/dev/null)"
44
46
  SPOR_ROOT="${SPOR_ROOT:-$CLAUDE_PLUGIN_ROOT}"
47
+ SLUG="$(node -e 'process.stdout.write(require("'"$SPOR_ROOT"'/scripts/engines/util.js").projectSlug(process.cwd()))' 2>/dev/null)"
45
48
  ```
46
- - node id: `node "$SPOR_ROOT/lib/compile.js" --root <id> --skeleton`
47
- - query: `node "$SPOR_ROOT/lib/compile.js" --query "<text>"`
49
+ - node id: `node "$SPOR_ROOT/lib/compile.js" --root <id> --skeleton ${SLUG:+--project "$SLUG"}`
50
+ - query: `node "$SPOR_ROOT/lib/compile.js" --query "<text>" ${SLUG:+--project "$SLUG"}`
51
+
52
+ **Always pass `--project "$SLUG"`.** Without it, `compile --root`/`--query`
53
+ run *project-blind* (`sessionProject == null`): the org-norm ride-along then
54
+ keeps every `always_on` norm and ignores `applies_to_repos`/`applies_to_tags`/
55
+ `applies_to_projects` scoping entirely — so an unscoped briefing shows norms a
56
+ real session in this repo would filter out (the `applies_to_*` selectors match
57
+ the SESSION repo, not the `--root` node's repo). `--project` makes the compile
58
+ match what session-start actually injects here.
59
+
48
60
  (The compiler defaults to the global graph at `$SPOR_HOME/nodes`, falling
49
61
  back to `~/.spor/nodes` — or a pre-existing `~/.substrate/nodes` when
50
62
  `~/.spor` is absent.)
@@ -100,6 +100,10 @@ mode dispatches to the server (mirroring `/spor:brief`). Much of this is what
100
100
  the session hooks already inject for you automatically; pulling one on demand
101
101
  with `spor brief`, `spor compile`, or `/spor:brief` is the same briefing.
102
102
  Passing `--nodes <dir>` always targets that local checkout, even under a server.
103
+ In local mode, add `--project <repo-slug>` to scope to a repo: without it
104
+ `compile --root`/`--query` run *project-blind* and the `always_on` norm
105
+ ride-along ignores `applies_to_*` scoping (every norm rides along) — pass it to
106
+ match what a real session in that repo sees. `/spor:brief` does this for you.
103
107
 
104
108
  **In Cowork (Anthropic's chat workspace) and Claude Code with the connector**
105
109
  there is no shell and no ambient injection — reach the graph through the