@sporhq/spor 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/GRAPH.md +29 -11
  3. package/adapters/README.md +1 -1
  4. package/adapters/codex/README.md +19 -3
  5. package/adapters/codex/hooks.json +4 -4
  6. package/adapters/copilot/spor.json +3 -3
  7. package/adapters/cursor/hooks.json +3 -3
  8. package/adapters/gemini/README.md +3 -3
  9. package/adapters/gemini/hooks/hooks.json +4 -4
  10. package/adapters/opencode/README.md +2 -2
  11. package/adapters/opencode/spor.js +11 -9
  12. package/bin/spor-hook.cmd +1 -0
  13. package/bin/spor-hook.js +4 -2
  14. package/bin/spor.cmd +1 -0
  15. package/bin/spor.js +312 -52
  16. package/bin/substrate-hook.cmd +1 -0
  17. package/bin/substrate-hook.js +8 -3
  18. package/hooks/hooks.json +5 -5
  19. package/lib/config.js +22 -2
  20. package/lib/history.js +8 -5
  21. package/lib/kernel/graph.js +2 -1
  22. package/lib/kernel/queue.js +7 -1
  23. package/lib/kernel/resolution.js +5 -0
  24. package/lib/query.js +16 -14
  25. package/lib/remote.js +48 -22
  26. package/lib/seed/schema-capture-pending.md +8 -7
  27. package/lib/seed/schema-edge-member-of-org.md +28 -0
  28. package/lib/seed/schema-issue.md +1 -1
  29. package/lib/seed/schema-organization.md +44 -0
  30. package/lib/seed/schema-person.md +35 -4
  31. package/lib/seed/schema-task.md +1 -1
  32. package/package.json +1 -1
  33. package/prompts/client/distill-local.md +3 -0
  34. package/scripts/engines/distill.js +1 -1
  35. package/scripts/engines/post-tool.js +5 -3
  36. package/scripts/engines/prompt-context.js +109 -4
  37. package/scripts/engines/util.js +32 -7
  38. package/skills/next/SKILL.md +2 -2
  39. package/skills/spor/references/concepts.md +3 -1
  40. package/skills/triage/SKILL.md +2 -2
@@ -2,7 +2,7 @@
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.16.0",
5
+ "version": "0.18.0",
6
6
  "author": {
7
7
  "name": "losthammer"
8
8
  }
package/GRAPH.md CHANGED
@@ -109,7 +109,8 @@ Rules:
109
109
  | briefing | `brief-` | a compiled briefing (output of this system; never traversed) |
110
110
  | correction | `corr-` | standing fix to a briefing: pin/exclude/guidance (never traversed) |
111
111
  | question | `question-` | a routed ask the graph could not answer (queueable; status `open`/`answered`, gated) |
112
- | person | `person-` | a member of the org — the identity anchor for `$viewer` binding and Tier-2 question routing (team mode; see "People, routing, and onboarding") |
112
+ | person | `person-` | a member of the org — mutable display name plus the identity anchor for `$viewer` binding and Tier-2 question routing (team mode; see "People, routing, and onboarding") |
113
+ | organization | `org-` | a durable organization identity anchor; people connect with `member-of-org` for membership and `stewards` for org-admin authority (`org-root` remains the virtual graph-wide operator anchor) |
113
114
  | agent | `agent-` | a person-owned automation principal — a dispatched session's durable identity, owned by a person via an `owned-by` edge; its writes attribute "agent on behalf of person" (see "Agents") |
114
115
  | profile | `profile-`| a reusable runtime+capability bundle an agent runs under: `harness`, `model`, `skills`/`plugins`/`mcp`. Its runtime fields ARE the dispatch satisfiability spec; `capturable: false` (see "The agent orchestration layer") |
115
116
  | routine | `routine-`| owner-scoped trigger→action automation (`owned-by` a person): declarative `when → do` rules over graph events that dispatch only the owner's agents, AND-ed with org policy; `capturable: false` (see "The agent orchestration layer") |
@@ -571,14 +572,25 @@ date: 2026-06-13
571
572
  Team mode (API.md) adds people to the graph. A `person` node is the org
572
573
  member's identity anchor — every authenticated token's canonical subject is a
573
574
  `person-` node, and `{name, email}` attribution resolves *from that node at
574
- read time* (API.md §4). Tier-2 question routing and `$viewer`-scoped views
575
- (your queue, your mutes, "what am I blocking") all key off the person node the
576
- caller's token is bound to.
575
+ read time* (API.md §4). `name` is the mutable user-facing display label; clients
576
+ render `name || title || email || id`, leaving the opaque `person-…` id as the
577
+ stable machine reference for graph edges, URLs, filters, and token subjects.
578
+ Tier-2 question routing and `$viewer`-scoped views (your queue, your mutes,
579
+ "what am I blocking") all key off the person node the caller's token is bound
580
+ to.
581
+
582
+ On a shared identity front door, organization authority is also graph-native.
583
+ Each org slug has a durable `organization` node (`org-<slug>`, carrying
584
+ `slug: <slug>`). A person's `member-of-org -> org-<slug>` edge records membership;
585
+ an additional `stewards -> org-<slug>` edge records org-admin authority.
586
+ `stewards -> org-root` keeps its distinct graph-wide operator meaning. Provider
587
+ roles, token bits, and email-domain mappings do not confer either relation.
577
588
 
578
589
  ```markdown
579
590
  ---
580
591
  id: person-anthony
581
592
  type: person
593
+ name: Anthony Allen
582
594
  title: Anthony Allen
583
595
  summary: Maintainer; stewards the schema registry and the hook engines.
584
596
  email: losthammer@gmail.com
@@ -635,15 +647,19 @@ edges:
635
647
  Joining someone to a team graph is three deliberate steps — do all three or
636
648
  routing degrades quietly:
637
649
 
638
- 1. **Author the `person-<name>` node** (frontmatter above), with `email` set to
639
- the address the member commits and authenticates under.
650
+ 1. **Author the `person-…` node** (frontmatter above), using the stable canonical
651
+ subject chosen by the org. Prefer the CLI/admin minting paths, which default
652
+ new ids to opaque email-derived subjects instead of mutable display-name
653
+ slugs. Set `name` to the person's display label and `email` to the address
654
+ the member commits and authenticates under.
640
655
  2. **Add `stewards` edges** from that node to the areas/specs/norms they own.
641
656
  Without at least one steward in a topic's neighborhood, questions there route
642
657
  to no one and fall back to surfacing for everybody — answerable, but not
643
658
  *directed*.
644
- 3. **Mint a token bound to the node:** an admin runs `spor-mint-token --person
645
- person-<name>` on the server box, or `POST /v1/admin/tokens {person}` over
646
- REST (admin-only; API.md §3/§4). The plaintext token is returned once.
659
+ 3. **Mint a token bound to the node:** an admin runs `spor invite --person
660
+ person-…` / `spor invite --name <name> --email <email>`, or
661
+ `POST /v1/admin/tokens {person}` over REST (admin-only; API.md §3/§4). The
662
+ plaintext token is returned once.
647
663
 
648
664
  If a member is reading and writing but **never receives routed questions, sees
649
665
  an empty personal queue, or has no mutes take effect**, their token did not bind
@@ -833,6 +849,7 @@ assign that work to a person.
833
849
  | `relates-to` | 0.5 | weak association |
834
850
  | `mentions` | 0.5 | weakest association |
835
851
  | `stewards` | 0.4 | this person stewards an area/spec/norm — the Tier-2 question-routing key |
852
+ | `member-of-org` | 0.3 | this person is a member of the target organization (inverse `has-org-member`); structural identity binding, not admin authority |
836
853
  | `grouped-under` | 0.3 | this repo's home project grouping (inverse `groups`); structural membership, not work dependency |
837
854
  | `owned-by` | 0.3 | this agent is owned by that person (inverse `owns`); structural identity binding, not work dependency |
838
855
  | `uses-profile` | 0.3 | this agent's default profile (the runtime+capability bundle it dispatches under); structural config binding, overridable per assignment/dispatch |
@@ -841,11 +858,12 @@ assign that work to a person.
841
858
  | `compiled-for` | — | briefing → its task/query (provenance only) |
842
859
  | `shaped-by` | — | briefing → corrections applied (provenance only) |
843
860
 
844
- `answers`, `assigned`, `stewards`, and `routed-to` are the person-graph
861
+ `answers`, `assigned`, `stewards`, `member-of-org`, and `routed-to` are person-graph
845
862
  edges of Tier-2 question routing; they ship in the seed pack and are
846
863
  documented under "People, routing, and onboarding" above. `assigned` and
847
864
  `routed-to` point at a `person-` node; `stewards` points from a person to
848
- the area they own; `answers` points from any answer node back at the
865
+ the area or organization they own; `member-of-org` points from a person to an
866
+ `organization` identity anchor; `answers` points from any answer node back at the
849
867
  `question-` it resolves.
850
868
 
851
869
  `review-requested`, `reviewed-by`, and `changes-requested-by` are the
@@ -13,7 +13,7 @@ The Spor client is a portable core behind per-host adapters
13
13
 
14
14
  - **Core**: the four hook engines in `scripts/` (session-start briefing,
15
15
  prompt-context digest, post-tool journal, distill capture) and the
16
- `bin/spor-hook` dispatcher that normalizes host payload quirks and
16
+ `bin/spor-hook.js` dispatcher that normalizes host payload quirks and
17
17
  envelope event names. All engines are fail-open: any failure injects
18
18
  nothing and exits 0.
19
19
  - **Universal surfaces**: the server's MCP endpoint (`/mcp`) and REST
@@ -2,7 +2,7 @@
2
2
 
3
3
  Codex's hooks are a near-clone of Claude Code's (same JSON-over-stdin payload
4
4
  fields, same `hookSpecificOutput.additionalContext` envelope), so this adapter
5
- is just a manifest over `bin/spor-hook`.
5
+ is just a manifest over `bin/spor-hook.js`.
6
6
 
7
7
  ## Install
8
8
 
@@ -18,8 +18,24 @@ is just a manifest over `bin/spor-hook`.
18
18
  (Or merge into an existing `~/.codex/hooks.json` / `[hooks]` table in
19
19
  `~/.codex/config.toml`. Per-repo installs go in `<repo>/.codex/hooks.json`.)
20
20
 
21
- 3. Approve the hooks in Codex's `/hooks` trust prompt on first run.
22
- 4. Environment (the same variables on every host):
21
+ 3. Add the backfill custom agent:
22
+
23
+ ```sh
24
+ mkdir -p ~/.codex/agents
25
+ {
26
+ printf 'name = "spor-backfill"\n'
27
+ printf 'description = "Populate or extend a project'\''s Spor graph (its nodes/ dir) from existing sources -- git history, issue/plan docs, specs, or external trackers (GitHub/Jira/Confluence via gh CLI or MCP tools). Use when bootstrapping a graph from scratch or importing a new source into an existing graph."\n'
28
+ printf 'developer_instructions = """\n'
29
+ awk 'BEGIN { n = 0 } /^---$/ { n++; next } n >= 2 { print }' "$SPOR_ROOT/agents/backfill.md"
30
+ printf '"""\n'
31
+ } > ~/.codex/agents/spor-backfill.toml
32
+ ```
33
+
34
+ `spor install codex` performs both steps automatically and keeps the custom
35
+ agent sourced from `agents/backfill.md`.
36
+
37
+ 4. Approve the hooks in Codex's `/hooks` trust prompt on first run.
38
+ 5. Environment (the same variables on every host):
23
39
 
24
40
  ```sh
25
41
  export SPOR_SERVER=https://spor.example.com # remote mode
@@ -4,14 +4,14 @@
4
4
  {
5
5
  "matcher": "startup|resume",
6
6
  "hooks": [
7
- { "type": "command", "command": "__SPOR_ROOT__/bin/spor-hook session-start --host codex", "timeout": 20, "statusMessage": "Loading Spor briefing" }
7
+ { "type": "command", "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" session-start --host codex", "timeout": 20, "statusMessage": "Loading Spor briefing" }
8
8
  ]
9
9
  }
10
10
  ],
11
11
  "UserPromptSubmit": [
12
12
  {
13
13
  "hooks": [
14
- { "type": "command", "command": "__SPOR_ROOT__/bin/spor-hook prompt-context --host codex", "timeout": 15 }
14
+ { "type": "command", "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" prompt-context --host codex", "timeout": 15 }
15
15
  ]
16
16
  }
17
17
  ],
@@ -19,14 +19,14 @@
19
19
  {
20
20
  "matcher": "*",
21
21
  "hooks": [
22
- { "type": "command", "command": "__SPOR_ROOT__/bin/spor-hook post-tool --host codex", "timeout": 10 }
22
+ { "type": "command", "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" post-tool --host codex", "timeout": 10 }
23
23
  ]
24
24
  }
25
25
  ],
26
26
  "Stop": [
27
27
  {
28
28
  "hooks": [
29
- { "type": "command", "command": "__SPOR_ROOT__/bin/spor-hook distill --host codex --debounce 900", "timeout": 10 }
29
+ { "type": "command", "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" distill --host codex --debounce 900", "timeout": 10 }
30
30
  ]
31
31
  }
32
32
  ]
@@ -2,13 +2,13 @@
2
2
  "version": 1,
3
3
  "hooks": {
4
4
  "sessionStart": [
5
- { "type": "command", "command": "__SPOR_ROOT__/bin/spor-hook agents-md", "timeoutSec": 20 }
5
+ { "type": "command", "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" agents-md", "timeoutSec": 20 }
6
6
  ],
7
7
  "postToolUse": [
8
- { "type": "command", "command": "__SPOR_ROOT__/bin/spor-hook post-tool --host copilot", "timeoutSec": 10 }
8
+ { "type": "command", "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" post-tool --host copilot", "timeoutSec": 10 }
9
9
  ],
10
10
  "agentStop": [
11
- { "type": "command", "command": "__SPOR_ROOT__/bin/spor-hook distill --host copilot --debounce 900", "timeoutSec": 10 }
11
+ { "type": "command", "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" distill --host copilot --debounce 900", "timeoutSec": 10 }
12
12
  ]
13
13
  }
14
14
  }
@@ -2,13 +2,13 @@
2
2
  "version": 1,
3
3
  "hooks": {
4
4
  "sessionStart": [
5
- { "command": "__SPOR_ROOT__/bin/spor-hook session-start --host cursor", "timeout": 20 }
5
+ { "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" session-start --host cursor", "timeout": 20 }
6
6
  ],
7
7
  "afterFileEdit": [
8
- { "command": "__SPOR_ROOT__/bin/spor-hook post-tool --host cursor", "timeout": 10 }
8
+ { "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" post-tool --host cursor", "timeout": 10 }
9
9
  ],
10
10
  "sessionEnd": [
11
- { "command": "__SPOR_ROOT__/bin/spor-hook distill --host cursor", "timeout": 300 }
11
+ { "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" distill --host cursor", "timeout": 300 }
12
12
  ]
13
13
  }
14
14
  }
@@ -3,7 +3,7 @@
3
3
  Gemini CLI's hooks use the same JSON-over-stdin payloads and the same
4
4
  `hookSpecificOutput.additionalContext` envelope as Claude Code (it even ships
5
5
  a `CLAUDE_PROJECT_DIR` compatibility alias), so this adapter is a manifest
6
- over `bin/spor-hook`. The only real differences: the per-prompt event is
6
+ over `bin/spor-hook.js`. The only real differences: the per-prompt event is
7
7
  called `BeforeAgent` (the dispatcher echoes that name back in the envelope),
8
8
  the post-tool event is `AfterTool`, and timeouts are in milliseconds.
9
9
 
@@ -51,7 +51,7 @@ export SPOR_NUDGE_CMD='gemini --model gemini-2.5-flash'
51
51
 
52
52
  - Gemini transcripts are not Claude's JSONL shape; the distiller falls back
53
53
  to a generic extractor (every nested `.text` string).
54
- - Hook stdout must be pure JSON on Gemini; `bin/spor-hook` already
54
+ - Hook stdout must be pure JSON on Gemini; `bin/spor-hook.js` already
55
55
  discards engine stderr and emits either one JSON object or nothing.
56
56
  - For on-demand graph access, add the Spor MCP server to settings:
57
57
 
@@ -60,4 +60,4 @@ export SPOR_NUDGE_CMD='gemini --model gemini-2.5-flash'
60
60
  ```
61
61
 
62
62
  - `GEMINI.md` users: point `contextFileName` at `AGENTS.md` and run
63
- `bin/spor-hook agents-md` to get the standing briefing without hooks.
63
+ `spor-hook agents-md` to get the standing briefing without hooks.
@@ -3,14 +3,14 @@
3
3
  "SessionStart": [
4
4
  {
5
5
  "hooks": [
6
- { "name": "spor-session-start", "type": "command", "command": "__SPOR_ROOT__/bin/spor-hook session-start --host gemini", "timeout": 20000 }
6
+ { "name": "spor-session-start", "type": "command", "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" session-start --host gemini", "timeout": 20000 }
7
7
  ]
8
8
  }
9
9
  ],
10
10
  "BeforeAgent": [
11
11
  {
12
12
  "hooks": [
13
- { "name": "spor-prompt-context", "type": "command", "command": "__SPOR_ROOT__/bin/spor-hook prompt-context --host gemini", "timeout": 15000 }
13
+ { "name": "spor-prompt-context", "type": "command", "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" prompt-context --host gemini", "timeout": 15000 }
14
14
  ]
15
15
  }
16
16
  ],
@@ -18,14 +18,14 @@
18
18
  {
19
19
  "matcher": "write_file|replace",
20
20
  "hooks": [
21
- { "name": "spor-post-tool", "type": "command", "command": "__SPOR_ROOT__/bin/spor-hook post-tool --host gemini", "timeout": 10000 }
21
+ { "name": "spor-post-tool", "type": "command", "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" post-tool --host gemini", "timeout": 10000 }
22
22
  ]
23
23
  }
24
24
  ],
25
25
  "SessionEnd": [
26
26
  {
27
27
  "hooks": [
28
- { "name": "spor-distill", "type": "command", "command": "__SPOR_ROOT__/bin/spor-hook distill --host gemini", "timeout": 300000 }
28
+ { "name": "spor-distill", "type": "command", "command": "node \"__SPOR_ROOT__/bin/spor-hook.js\" distill --host gemini", "timeout": 300000 }
29
29
  ]
30
30
  }
31
31
  ]
@@ -1,8 +1,8 @@
1
1
  # OpenCode adapter
2
2
 
3
3
  OpenCode has no command-hook system — plugins are in-process JS — so this
4
- adapter is a single zero-dependency plugin file (`spor.js`) that shells
5
- out to `bin/spor-hook` with Claude-shaped payloads. Same core, same
4
+ adapter is a single zero-dependency plugin file (`spor.js`) that launches
5
+ `bin/spor-hook.js` with Claude-shaped payloads. Same core, same
6
6
  fail-open behavior as every other host.
7
7
 
8
8
  ## Install
@@ -2,8 +2,8 @@
2
2
  //
3
3
  // OpenCode has no command-hook system; plugins are in-process JS. This one
4
4
  // stays a thin shim over the same portable core every other host uses: each
5
- // hook synthesizes a Claude-shaped payload and shells out to
6
- // bin/spor-hook, so briefing/digest/journal/distill behavior is
5
+ // hook synthesizes a Claude-shaped payload and launches the Node dispatcher
6
+ // (bin/spor-hook.js), so briefing/digest/journal/distill behavior is
7
7
  // identical across hosts.
8
8
  //
9
9
  // chat.message -> session-start briefing (first message of a session)
@@ -18,7 +18,7 @@
18
18
  // nothing. Zero dependencies: node builtins only.
19
19
  //
20
20
  // Install: symlink this file into ~/.config/opencode/plugins/ (the symlink is
21
- // resolved to find bin/spor-hook), or copy it and set SPOR_ROOT (legacy
21
+ // resolved to find bin/spor-hook.js), or copy it and set SPOR_ROOT (legacy
22
22
  // SUBSTRATE_ROOT still read).
23
23
 
24
24
  import { spawn } from "node:child_process"
@@ -36,18 +36,20 @@ const GRAPH =
36
36
  ? join(homedir(), ".spor")
37
37
  : join(homedir(), ".substrate"))
38
38
  const DEBOUNCE = process.env.SPOR_DEBOUNCE || process.env.SUBSTRATE_DEBOUNCE || "900"
39
+ const EMBEDDED_ROOT = "__SPOR_ROOT__"
39
40
 
40
41
  function findDispatcher() {
41
42
  const candidates = []
42
- const root = process.env.SPOR_ROOT || process.env.SUBSTRATE_ROOT
43
+ const root =
44
+ process.env.SPOR_ROOT ||
45
+ process.env.SUBSTRATE_ROOT ||
46
+ (EMBEDDED_ROOT !== "__SPOR_ROOT__" ? EMBEDDED_ROOT : "")
43
47
  if (root) {
44
- candidates.push(join(root, "bin", "spor-hook"))
45
- candidates.push(join(root, "bin", "substrate-hook"))
48
+ candidates.push(join(root, "bin", "spor-hook.js"))
46
49
  }
47
50
  try {
48
51
  const here = dirname(realpathSync(fileURLToPath(import.meta.url)))
49
- candidates.push(join(here, "..", "..", "bin", "spor-hook"))
50
- candidates.push(join(here, "..", "..", "bin", "substrate-hook"))
52
+ candidates.push(join(here, "..", "..", "bin", "spor-hook.js"))
51
53
  } catch {}
52
54
  return candidates.find((c) => existsSync(c)) || null
53
55
  }
@@ -59,7 +61,7 @@ function run(bin, args, payload, timeoutMs) {
59
61
  let done = false
60
62
  const finish = (out) => { if (!done) { done = true; resolve(out) } }
61
63
  try {
62
- const child = spawn("bash", [bin, ...args], { stdio: ["pipe", "pipe", "ignore"] })
64
+ const child = spawn(process.execPath, [bin, ...args], { stdio: ["pipe", "pipe", "ignore"] })
63
65
  let out = ""
64
66
  const timer = setTimeout(() => { try { child.kill("SIGKILL") } catch {}; finish("") }, timeoutMs)
65
67
  child.stdout.on("data", (d) => { out += d })
package/bin/spor-hook.cmd CHANGED
@@ -1,3 +1,4 @@
1
1
  @echo off
2
+ where node >nul 2>nul || exit /b 0
2
3
  node "%~dp0spor-hook.js" %*
3
4
  exit /b 0
package/bin/spor-hook.js CHANGED
@@ -248,7 +248,9 @@ function logCrash(err) {
248
248
  if (require.main === module) {
249
249
  main()
250
250
  .catch((err) => logCrash(err))
251
- .finally(() => process.exit(0));
251
+ .finally(() => {
252
+ process.exitCode = 0;
253
+ });
252
254
  }
253
255
 
254
- module.exports = { logCrash };
256
+ module.exports = { logCrash, main };
package/bin/spor.cmd CHANGED
@@ -1,3 +1,4 @@
1
1
  @echo off
2
+ where node >nul 2>nul || exit /b 0
2
3
  node "%~dp0spor.js" %*
3
4
  exit /b %errorlevel%