@sporhq/spor 0.11.0 → 0.12.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/.claude-plugin/plugin.json +1 -1
- package/API.md +78 -3
- package/GRAPH.md +25 -5
- package/QUEUE.md +35 -1
- package/README.md +13 -3
- package/bin/spor.js +936 -36
- package/lib/auth.js +389 -0
- package/lib/config.js +178 -6
- package/lib/graph.js +68 -3
- package/lib/kernel/queue.js +102 -8
- package/lib/kernel/timestamps.js +128 -0
- package/lib/queue.js +5 -0
- package/lib/remote.js +36 -13
- package/lib/seed/schema-incident.md +64 -1
- package/lib/seed/schema-issue.md +64 -1
- package/lib/seed/schema-question.md +65 -1
- package/lib/seed/schema-task.md +123 -1
- package/lib/shell/gittime.js +79 -0
- package/package.json +1 -1
- package/scripts/engines/util.js +6 -2
- package/skills/backfill/SKILL.md +18 -22
- package/skills/brief/SKILL.md +35 -56
- package/skills/correct/SKILL.md +46 -90
- package/skills/defer/SKILL.md +34 -79
- package/skills/next/SKILL.md +45 -96
- package/skills/spor/SKILL.md +16 -12
- package/skills/spor/references/authoring-schemas.md +16 -5
|
@@ -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.
|
|
5
|
+
"version": "0.12.0",
|
|
6
6
|
"author": { "name": "losthammer" }
|
|
7
7
|
}
|
package/API.md
CHANGED
|
@@ -90,6 +90,10 @@ Output: `{ "found": bool, "text": "<digest or full neighborhood>",
|
|
|
90
90
|
|
|
91
91
|
Input `{ "id": "dec-..." }` → full raw markdown, parsed frontmatter, and
|
|
92
92
|
`revision` (git blob SHA) for use in updates. Unknown id → not-found error.
|
|
93
|
+
**Read-time enrichment** rides along on the same response: the node's active
|
|
94
|
+
schema may carry a `get(node, ctx)` hook (GRAPH.md) that attaches derived
|
|
95
|
+
context as extra top-level keys — e.g. `resolution` (what answered/resolved
|
|
96
|
+
this node). These are additive; a client ignores keys it does not know.
|
|
93
97
|
|
|
94
98
|
### `put_node`
|
|
95
99
|
|
|
@@ -314,10 +318,11 @@ endpoint is the REST twin of a core call:
|
|
|
314
318
|
| Endpoint | Typical caller | Semantics |
|
|
315
319
|
|---|---|---|
|
|
316
320
|
| `GET /v1/status` | session-start, monitoring | `{node_count, projects: {...}, head, uptime, metrics}`; doubles as the health check. `?titles=1` adds `titles: [{id, type, project, title}]` — the one-round-trip graph index the distiller dedups against |
|
|
317
|
-
| `GET /v1/me` | `spor whoami`/`status`, onboarding | identity echo for the bearer token → `{person, name, email, bound, is_admin}`. `bound:false` means the token authenticates but maps to **no person node** (legacy/OAuth, or minted before the node existed), so routed questions and the personal queue will be empty — the client warns on it (the silent identity-degradation signal). `is_admin` reflects the `stewards→root` edge that gates the token-admin surface |
|
|
321
|
+
| `GET /v1/me` | `spor whoami`/`status`, onboarding | identity echo for the bearer token → `{person, name, email, bound, is_admin, org}`. `bound:false` means the token authenticates but maps to **no person node** (legacy/OAuth, or minted before the node existed), so routed questions and the personal queue will be empty — the client warns on it (the silent identity-degradation signal). `is_admin` reflects the `stewards→root` edge that gates the token-admin surface. `org` is the slug this tenant routes to (`SPOR_ORG`/legacy `SUBSTRATE_ORG`, else `"local"`); it lets a client key its `(issuer, org)` credential store for an **opaque** `spor_oat_`/`spor_pat_` token that carries no readable `org` claim — the client falls back to it after `--org` and the JWT `org` claim (task-spor-frontdoor-me-org-echo). A connector JWT's `org` claim is enforced equal to this echo |
|
|
322
|
+
| `GET /v1/me/org-choices` | `spor auth list` (live membership refresh) | re-queries the IdP's *current* org membership for the held credential's subject and returns `{org_choices: [{slug, label, default?}], source: "idp"\|"bound"}` — `source:"idp"` is a true live enumeration (orgs added/removed since the last login surface without re-authenticating); `source:"bound"` means a single org-scoped token the server couldn't expand (no enumeration). The client treats only `source:"idp"` as live and **fails open** to its cached tenant listing on anything else — `source:"bound"`, a `502 {error.code:"membership_requery_failed"}` (IdP unreachable), a `404` (older server without the endpoint), or any transport/parse error (task-spor-cli-auth-list-live-membership-requery; server half task-spor-frontdoor-held-credential-membership-requery) |
|
|
318
323
|
| `GET /v1/briefing/{project}` | session-start | read the `brief-<project>` node → `{found, version, body, project_brief?, graph_status}`. The slug resolves through project-node aliases (GRAPH.md "Project identity nodes") before lookup. A BARE repo slug also rides up to its home-project grouping: the grouping's `brief-<grouping>` node returns alongside as `project_brief` (the product context spanning sibling repos), matching the shared up-resolution (dec-spor-queue-slug-resolves-to-grouping); passing the repo NODE id (`repo-<slug>`) is the escape hatch that returns only the repo brief, no `project_brief`. Optional `?fp=root:<sha>,remote:<host/path>,...` carries the repo's fingerprints: the server learns them onto the owning project node, and an unknown slug with a known fingerprint files an alias proposal in the queue |
|
|
319
324
|
| `POST /v1/digest` `{query, root?, project?, min_sim?}` | prompt-context, /spor:brief | digest-mode compile → `{found, text}`; `found: false` is a successful empty result. `root` is the structural-walk twin of `query` (the two are mutually exclusive; `root` wins, an unknown id is `422`). Optional `project` is the session slug: the server scopes the compile to it — the same-project relevance boost, the grouping union, and the `always_on` norm `applies_to_*` ride-along — resolving the slug through project-node aliases/groupings inside compile (dec-spor-queue-slug-resolves-to-grouping), exactly as `/v1/queue` does. A bad slug is `422`; **omitting `project` runs the digest project-blind (byte-identical to before)**, so older clients that send only `{query}` are unaffected |
|
|
320
|
-
| `GET /v1/nodes/{id}` | /spor:brief | `get_node` semantics; a live inbound resolves/answers edge
|
|
325
|
+
| `GET /v1/nodes/{id}` | /spor:brief | `get_node` semantics; the node's active schema may attach read-time enrichment via a `get(node, ctx)` hook (GRAPH.md) — the seed `question`/`issue`/`task`/`incident` schemas attach `resolution`: a live inbound resolves/answers edge carrying the resolver's `summary`/`title` and a `lagging` flag (set when it contradicts a still-open status, clear when the node is already terminal, e.g. an answered question pointing at its answer). Open gardener findings about the node ride along as `open_findings`, and a node marked stale by an inbound supersedes edge as `superseded_by`. All enrichment is additive top-level keys; ignore unknown ones |
|
|
321
326
|
| `POST /v1/nodes` | drain-outbox, mechanical writers | `put_node` semantics, batch: `{nodes: [...], if_exists: "skip"}` (entries may be raw strings or `{node, if_exists, revision}`) → `{results: [...]}`, 207 when any entry failed |
|
|
322
327
|
| `POST /v1/nodes/{id}/edges` `{type, to, attrs?}` | scripts, mechanical writers | `add_edge` semantics (§1): normalize/flip, dedupe, append — no revision echo. Optional `attrs` adds trailing flat edge attributes (e.g. a per-assignment `profile:` override); re-adding the same edge with different attrs upserts the set |
|
|
323
328
|
| `POST /v1/nodes/{id}/status` `{status}` | scripts, mechanical writers | `set_status` semantics (§1): one-scalar update through the `transitions()` gate. Setting a work node to an in-progress status also CLAIMS it (same lease as `/claim` below) |
|
|
@@ -428,6 +433,33 @@ anything with a token.
|
|
|
428
433
|
`{name, email}` attribution record. Access tokens are `spor_oat_…` (30d;
|
|
429
434
|
legacy `sub_oat_…` accepted); refresh tokens are `spor_ort_…` (90d,
|
|
430
435
|
rotating, single-use). Authorization codes are single-use, 10-minute.
|
|
436
|
+
- **CLI interactive sign-in — the device authorization grant.** `spor auth
|
|
437
|
+
login` (flat alias `spor login`) defaults to the OAuth 2.0 device
|
|
438
|
+
authorization grant (RFC 8628), brokered at the Spor front door so it works
|
|
439
|
+
headless / over SSH (dec-spor-cli-auth-device-grant-front-door). Flow:
|
|
440
|
+
`POST /oauth/device_authorization {client_id?, scope?}` →
|
|
441
|
+
`{device_code, user_code, verification_uri, verification_uri_complete,
|
|
442
|
+
expires_in, interval}`; the CLI prints the URL + code (auto-opening a local
|
|
443
|
+
browser when one is present), the human approves in any browser (the
|
|
444
|
+
verification leg runs the same AuthKit login + org resolution as the
|
|
445
|
+
connector flow), and the CLI polls `POST /oauth/token
|
|
446
|
+
{grant_type: urn:ietf:params:oauth:grant-type:device_code, device_code}` —
|
|
447
|
+
answering `authorization_pending`/`slow_down` until approval, then minting the
|
|
448
|
+
same person-bound, **org-scoped**, refreshable `spor_oat_…`/`spor_ort_…` pair
|
|
449
|
+
`addGrant` mints. `--web` is the localhost-loopback optimization for when a
|
|
450
|
+
browser is on the same machine (OAuth 2.1 authorization-code + PKCE, RFC 8252):
|
|
451
|
+
the CLI binds a one-shot `http://127.0.0.1:<port>/callback` listener,
|
|
452
|
+
anonymously registers a public client for it (`POST /oauth/register`, RFC 7591),
|
|
453
|
+
opens the browser to `GET /oauth/authorize {response_type=code, client_id,
|
|
454
|
+
redirect_uri, code_challenge, code_challenge_method=S256, state, scope?}`,
|
|
455
|
+
captures the redirected `?code` (CSRF-checked against `state`), and exchanges it
|
|
456
|
+
at `POST /oauth/token {grant_type: authorization_code, code, code_verifier,
|
|
457
|
+
client_id, redirect_uri}` for the same token pair; it then best-effort
|
|
458
|
+
unregisters the throwaway client (RFC 7592 `DELETE`). It falls back to the
|
|
459
|
+
device grant when the front door exposes no loopback/DCR endpoints. `spor auth
|
|
460
|
+
login <url> <token>` / `spor join <url>
|
|
461
|
+
<token>` is the non-interactive paste path; CI stays `SPOR_TOKEN`. The minted
|
|
462
|
+
credential is stored per-tenant (§6.2).
|
|
431
463
|
- **Render tickets (shared lens links).** `POST /v1/lens/{id}/ticket` (§3)
|
|
432
464
|
mints a signed, expiring, **read-only** ticket carrying `{lens_id,
|
|
433
465
|
sharer_person_id, exp}` — the credential a *shared* view link carries instead
|
|
@@ -476,10 +508,15 @@ spellings are still read — dual-read back-compat window); unset means local
|
|
|
476
508
|
mode, reading `$SPOR_HOME` directly:
|
|
477
509
|
|
|
478
510
|
```
|
|
479
|
-
SPOR_SERVER=https://
|
|
511
|
+
SPOR_SERVER=https://api.sporhq.io # hosted Spor REST base (the onboarding default)
|
|
480
512
|
SPOR_TOKEN=spor_pat_... # per-user token (§4)
|
|
513
|
+
SPOR_ORG=acme # select a stored tenant by org (§6.2)
|
|
481
514
|
```
|
|
482
515
|
|
|
516
|
+
`spor join <token>` writes both for you, defaulting `SPOR_SERVER` to the hosted
|
|
517
|
+
base `https://api.sporhq.io`; pass a URL (`spor join <url> <token>`) to point at
|
|
518
|
+
a self-hosted server instead.
|
|
519
|
+
|
|
483
520
|
**Opt-in activation.** Spor is opt-in per repo: with the plugin installed, the
|
|
484
521
|
hooks are a full no-op (no context injected, nothing distilled) in any repo that
|
|
485
522
|
has not opted in. A repo is active when its mode is not `off` AND either an
|
|
@@ -548,3 +585,41 @@ Contract:
|
|
|
548
585
|
`nodes/` and brief `history/` are committed. The SessionEnd distiller leaves
|
|
549
586
|
distilled nodes **uncommitted** (for the human PR flow) instead of
|
|
550
587
|
auto-committing when the graph home is the same git repo as the code repo.
|
|
588
|
+
|
|
589
|
+
### 6.2 Multi-tenant credentials (the credential store + tenant selector)
|
|
590
|
+
|
|
591
|
+
Server tokens are **org-scoped** (the `org` claim is the routing + isolation
|
|
592
|
+
key), so a person in N orgs holds N credentials. The client is multi-tenant
|
|
593
|
+
(dec-spor-client-cli-mode-tenant-resolution): tokens live in a credential store
|
|
594
|
+
**keyed by `(issuer, org)`** at `$SPOR_HOME/auth/credentials.json` (mode `0600`,
|
|
595
|
+
machine-local — never committed, always in the shared-graph `.gitignore`):
|
|
596
|
+
|
|
597
|
+
```
|
|
598
|
+
{ "version": 1,
|
|
599
|
+
"tenants": {
|
|
600
|
+
"<server>/<org>": { "server": "...", "org": "...", "person": "...",
|
|
601
|
+
"email": "...", "access_token": "...",
|
|
602
|
+
"refresh_token": "...", "exp": 1234567890 } },
|
|
603
|
+
"default": "<server>/<org>" }
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
- **Acquire.** `spor auth login` (device grant, §4) and `spor join <url>
|
|
607
|
+
<token>` (paste) both **ADD** a tenant — they never overwrite a sibling. The
|
|
608
|
+
org is read from the token (JWT `org` claim) or `--org`. The first tenant
|
|
609
|
+
becomes the active default.
|
|
610
|
+
- **Manage.** `spor auth list` (tenants + active + token health, with a **live
|
|
611
|
+
org-membership refresh** — one `GET /v1/me/org-choices` against the active
|
|
612
|
+
tenant's issuer surfaces orgs added/removed since the last login, fail-open to
|
|
613
|
+
the cached listing when the server can't enumerate; task-spor-cli-auth-list-
|
|
614
|
+
live-membership-requery), `spor auth switch <org>`, `spor auth whoami
|
|
615
|
+
[--all]`, `spor auth logout [<org>|--all]`.
|
|
616
|
+
- **Tenant selector** (which credential is active), highest wins:
|
|
617
|
+
`--org`/`--server` flag > `SPOR_SERVER`(+`SPOR_TOKEN`)/`SPOR_ORG` env > repo
|
|
618
|
+
`.spor` `org:` marker (committable, nearest-ancestor — the remote-mode sibling
|
|
619
|
+
of the `graph:` binding in §6.1) > store `default` > legacy flat config.json
|
|
620
|
+
`server`+`token` (migrated on read) > local.
|
|
621
|
+
- **Refresh.** A 401/403 on a tenant carrying a `refresh_token` transparently
|
|
622
|
+
refreshes against its issuer (`grant_type=refresh_token`) and retries once.
|
|
623
|
+
- **Byte-identical.** With no credential store and only a flat
|
|
624
|
+
`server`+`token` or `SPOR_*` env set, every resolved value equals the prior
|
|
625
|
+
single-tenant behavior (norm-cc-byte-identical-refactor).
|
package/GRAPH.md
CHANGED
|
@@ -49,6 +49,15 @@ Rules:
|
|
|
49
49
|
- `summary` is mandatory and must stand alone — most consumers only ever see
|
|
50
50
|
the summary.
|
|
51
51
|
- `date` is YYYY-MM-DD (date of the underlying event, not of node creation).
|
|
52
|
+
System `created_at`/`updated_at` are NOT stored in node bytes — they are
|
|
53
|
+
derived from the graph repo's git history into an in-memory `graph.timestamps`
|
|
54
|
+
index at `loadGraph` (the FIRST commit touching `nodes/<id>.md` is `created_at`,
|
|
55
|
+
the LAST is `updated_at`), so node files stay byte-identical and git stays the
|
|
56
|
+
single source of truth (dec-spor-git-derived-timestamps). The index is lazy
|
|
57
|
+
(off the no-LLM prompt path), HEAD-keyed and cached under `cache/`. An optional
|
|
58
|
+
explicit frontmatter `created_at`/`updated_at` OVERRIDES the git-derived value
|
|
59
|
+
(the escape hatch for squash/rebase graphs); `date` is the last-resort fallback
|
|
60
|
+
when git has nothing — it stays distinct from the system `created_at`.
|
|
52
61
|
- `author` and `authored_via` are optional. The remote server (see API.md
|
|
53
62
|
§1) stamps `author: Name <email>` and `authored_via:
|
|
54
63
|
mcp|rest|capture|gardener` from the authenticated identity on every node
|
|
@@ -238,12 +247,23 @@ return values cross back, so `require`, `process`, `eval`, the `Function`
|
|
|
238
247
|
constructor, host prototypes, ambient time/IO, and unbounded loops are all
|
|
239
248
|
unavailable (a runaway hits a fuel/memory interrupt). A hook that throws does not
|
|
240
249
|
wave the write through — it **rejects** it. Write the functions accordingly: no
|
|
241
|
-
external state, no side effects, decide only from the arguments.
|
|
250
|
+
external state, no side effects, decide only from the arguments. Three more
|
|
242
251
|
attached exports are recognized: `queueSignals(node, ctx)` (a `{ name: number }`
|
|
243
|
-
map blended into the decision-queue ranking)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
252
|
+
map blended into the decision-queue ranking); `get(node, ctx)` — the **read-time
|
|
253
|
+
enrichment** hook, run on `get_node`, returning an object whose keys ride along on
|
|
254
|
+
the read. Where `transitions()` is the write-time gate, `get()` is its read-time
|
|
255
|
+
peer: the server hands it a *bounded one-hop neighborhood* (`ctx.neighbors` — this
|
|
256
|
+
node's inbound + outbound edges each with the neighbor's `{id, edge, dir, type,
|
|
257
|
+
status, title, summary, date, superseded}`, capped — plus `ctx.non_resolving_statuses`
|
|
258
|
+
and `ctx.terminal`), and the hook attaches derived context — e.g. an answered
|
|
259
|
+
`question` surfacing WHAT answered it. It is read-only and **fail-soft** (a throw
|
|
260
|
+
drops the enrichment, never breaks the read; the only hook that fails open rather
|
|
261
|
+
than closed); reserved core keys (`id`/`raw`/`frontmatter`/`revision`) can't be
|
|
262
|
+
clobbered. The seed `question`/`issue`/`task`/`incident` schemas carry one, the
|
|
263
|
+
single mechanism that expresses the resolution ride-along. Finally, named upgrade
|
|
264
|
+
functions referenced from `payload.upgrades` (lazy, forward-only field migrations
|
|
265
|
+
on a `schema_version` bump). The **client** half (hooks, the `spor` CLI, `lib/`)
|
|
266
|
+
only *parses and indexes* this code for the registry knobs — it never executes it;
|
|
247
267
|
the server is the sole executor.
|
|
248
268
|
|
|
249
269
|
Resolution and rollout: a graph-resident schema always beats the seed pack, and
|
package/QUEUE.md
CHANGED
|
@@ -118,6 +118,15 @@ export function transitions(node, proposed, graph) { ... }
|
|
|
118
118
|
export function queueSignals(node, graph, activity) {
|
|
119
119
|
return { blocking: ..., heat: ..., staleness: ..., age_days: ... };
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
// get(node, ctx) — read-time enrichment, run on get_node; the read-time peer of
|
|
123
|
+
// transitions(). The host hands in a bounded one-hop neighborhood (ctx.neighbors,
|
|
124
|
+
// not a live graph), and the returned object's keys ride along on the read.
|
|
125
|
+
// Read-only, fail-soft. Expresses the resolution ride-along on the seed
|
|
126
|
+
// question/issue/task/incident schemas (task-spor-schema-get-hook-readtime-enrichment).
|
|
127
|
+
export function get(node, ctx) {
|
|
128
|
+
return { resolution: ... };
|
|
129
|
+
}
|
|
121
130
|
```
|
|
122
131
|
```
|
|
123
132
|
|
|
@@ -402,8 +411,30 @@ signals via its schema's `queueSignals()`:
|
|
|
402
411
|
so provenance hubs can't ride it; capped below the p1 bump so human
|
|
403
412
|
priority stays supreme; and the why-line states the actual window
|
|
404
413
|
("N writes in the last D days") (dec-cc-queue-front-from-attribution).
|
|
414
|
+
The continuity loop ("work X → X stays on top") terminates by finishing X
|
|
415
|
+
(a resolves edge retires it) — EXCEPT a task **held open on an external
|
|
416
|
+
gate**, which has nothing to resolve, so each held pass re-raises front and
|
|
417
|
+
re-surfaces it. The **held-task self-limit** breaks that loop
|
|
418
|
+
(task-spor-queue-front-loop-self-limit-on-held-tasks): an OPEN task carrying
|
|
419
|
+
an inbound non-resolving outcome (an artifact/decision linked by any edge but
|
|
420
|
+
resolves/answers) with no live resolving edge and no live blocker has its
|
|
421
|
+
front damped to 0 in the score and its suggestion flipped `do → triage`
|
|
422
|
+
("resolve, gate with blocked-by, set wake, or abandon") — the same posture
|
|
423
|
+
staleness takes (flip the suggestion, don't boost the score). `signals.front`
|
|
424
|
+
keeps the raw count so a `queue-policy` rank() can re-weight. The read-time
|
|
425
|
+
twin is the `schema-task` `get()` hook, which rides a `held` note along on
|
|
426
|
+
`get_node`.
|
|
405
427
|
- **staleness** — anchors superseded or gone; high staleness suggests
|
|
406
428
|
closing, not doing.
|
|
429
|
+
- **cold_neighbors** — the count of the node's traversable neighbors whose
|
|
430
|
+
git-derived `updated_at` is newer than its own: a node that went cold while its
|
|
431
|
+
neighborhood kept moving ("context moved around it"). Fed by the
|
|
432
|
+
`graph.timestamps` index the caller injects as `timestamps`
|
|
433
|
+
(task-spor-git-derived-timestamp-index); like `needed_by_days` it rides along
|
|
434
|
+
ONLY when the index is present and the count is >0, so the queue is
|
|
435
|
+
byte-identical without it (conformance, the prompt path, a non-git home). It is
|
|
436
|
+
surfaced (signal + why-line), not yet a score weight — a ranking contribution is
|
|
437
|
+
a deliberate tuning change deferred to its own task.
|
|
407
438
|
- **age**, and any org-specific signal the schema's code adds (SLA clocks,
|
|
408
439
|
sprint membership, whatever the org's process actually is).
|
|
409
440
|
|
|
@@ -606,7 +637,10 @@ the structural truth:
|
|
|
606
637
|
§2.4 sandbox in step 4 alongside `transitions()`. The blend shipped
|
|
607
638
|
opinionated (§8): `priority bump + 3·blocking − 3·blocked_by +
|
|
608
639
|
min(log₂(1+front), 5) + log₂(1+heat) + age/30 (capped) + neededBy urgency (0-5)`, with staleness ≥ 0.5 flipping the item's suggestion to
|
|
609
|
-
"close"
|
|
640
|
+
"close" and the held-task self-limit flipping it to "triage" (front damped
|
|
641
|
+
to 0) for an open task with a recorded non-resolving outcome but no resolver
|
|
642
|
+
and no blocker (task-spor-queue-front-loop-self-limit-on-held-tasks). The
|
|
643
|
+
`needed_by: YYYY-MM-DD` deadline term is the inverse of
|
|
610
644
|
`wake`: where `wake` hides a node until its date, `needed_by` keeps it
|
|
611
645
|
visible from creation and ramps its score linearly over a 30-day window to
|
|
612
646
|
3 at the date, then on to a hard cap of 5 once overdue — surfacing
|
package/README.md
CHANGED
|
@@ -198,11 +198,21 @@ answered from what's already there, it routes to the person most likely to
|
|
|
198
198
|
know, and their answer flows back into the graph as a node — so the next
|
|
199
199
|
person who asks gets it from the graph instead.
|
|
200
200
|
|
|
201
|
-
A client joins a team graph
|
|
201
|
+
A client joins a team graph with a token from your team's invite:
|
|
202
202
|
|
|
203
203
|
```bash
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
spor join spor_pat_... # hosted Spor (https://api.sporhq.io)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`spor join` writes the server URL and token to your user config and confirms
|
|
208
|
+
the connection. The URL defaults to the hosted Spor service
|
|
209
|
+
(`https://api.sporhq.io`); pass your own to point elsewhere
|
|
210
|
+
(`spor join https://spor.example.com spor_pat_...`). The equivalent two
|
|
211
|
+
environment variables work too:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
export SPOR_SERVER=https://api.sporhq.io
|
|
215
|
+
export SPOR_TOKEN=spor_pat_...
|
|
206
216
|
```
|
|
207
217
|
|
|
208
218
|
Set those and the client talks to the team graph over REST and MCP; leave
|