@sporhq/spor 0.25.0 → 0.26.1

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,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.25.0",
5
+ "version": "0.26.1",
6
6
  "author": {
7
7
  "name": "losthammer"
8
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spor",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
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
5
  "author": {
6
6
  "name": "Spor",
package/API.md CHANGED
@@ -578,7 +578,7 @@ agent subject sees no graph content; a coarse read-only/CI flag is not a bypass.
578
578
  | Endpoint | Typical caller | Semantics |
579
579
  |---|---|---|
580
580
  | `GET /v1/status` | session-start, monitoring | `{node_count, projects: {...}, head, uptime, metrics}`; doubles as the health check. Graph counts/projects are the viewer-visible projection. `?titles=1` adds viewer-visible `titles: [{id, type, project, title}]` — the one-round-trip graph index the distiller dedups against |
581
- | `GET /v1/schema` | `spor schema`, agents introspecting the contract | the live schema registry as data (task-spor-schema-introspection-surface; server half task-spor-server-schema-endpoint): `{default_edge_weight, node_types: [{type, description, prefix, always_on, traversable, capturable, queueable, non_resolving, hooks, schema_id, schema_version, source}], edge_types: [{type, description, weight, weight_default, inverse_label, aliases, capturable, hooks, ...}], queue_policy, policies, registers, stale_overrides, alias_collisions}` — the seed pack MERGED with graph-resident `type: schema` overrides, each entry tagged by `source` (`seed`/`graph`/`native`) and the active schema node's id+version. `?code=1` embeds each hook's source under `code: {name: src}` (omitted by default to keep the response lean). The registry IS the contract (norm-cc-registry-is-contract); this read surface closes the failure mode of agents reverse-engineering it from `lib/seed/` files (which miss resident overrides). The REST/MCP twin of the `spor schema` CLI: all three render one `graph.registry.snapshot()` so they never drift |
581
+ | `GET /v1/schema` | `spor schema`, agents introspecting the contract | the live schema registry as data (task-spor-schema-introspection-surface; server half task-spor-server-schema-endpoint): `{default_edge_weight, node_types: [{type, description, prefix, always_on, traversable, capturable, queueable, non_resolving, terminal, inert, inert_inherited, vocabulary, completion, resolver_required, hooks, schema_id, schema_version, source}], edge_types: [{type, description, weight, weight_default, inverse_label, aliases, capturable, hooks, ...}], queue_policy, policies, registers, stale_overrides, alias_collisions}` — the seed pack MERGED with graph-resident `type: schema` overrides, each entry tagged by `source` (`seed`/`graph`/`native`) and the active schema node's id+version. `?code=1` embeds each hook's source under `code: {name: src}` (omitted by default to keep the response lean). The registry IS the contract (norm-cc-registry-is-contract); this read surface closes the failure mode of agents reverse-engineering it from `lib/seed/` files (which miss resident overrides). `vocabulary`/`completion`/`resolver_required` are the declarative completion policy (task-spor-registry-declarative-terminal-status-policy): the closed status enum a type's `validate()` gates on, its single SUCCESS terminal value (`null` when the type's terminal values are several distinct outcomes rather than one success, i.e. no mechanical close exists), and whether that value also needs a live resolving decision/artifact. They DECLARE what the hooks enforce — a reader (the gardener's finding remedies) names the right terminal status from here instead of hand-mirroring hook source. The REST/MCP twin of the `spor schema` CLI: all three render one `graph.registry.snapshot()` so they never drift |
582
582
  | `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 |
583
583
  | `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) |
584
584
  | `GET /v1/me/tokens` | `spor token list` | list the caller's OWN personal access tokens → `{tokens: [{hash_prefix, person, label, name, email, created, expires, expired, last_used}], count}` — caller-scoped (only their person-bound PATs; agent session tokens excluded), never plaintext, never full hashes. `403 forbidden` if the bearer maps to **no person node** (you need a bound identity to own a PAT). The self-serve, no-admin twin of `GET /v1/admin/tokens` below (task-spor-app-me-tokens-self-serve) |
@@ -723,14 +723,21 @@ anything with a token.
723
723
  one place an agent token's session is set, write-once. The session can't be
724
724
  forged a-priori (it isn't known until the run exists) and can't ride the write
725
725
  payload (token-derived, §1), so the binding is always the actual run. A
726
- **Codex**-harness dispatch follows the same late-bind contract from its own
727
- supervisor process instead: it reads the session id off the `thread.started`
728
- event in the run's supervised JSONL log rather than `claude agents --json`,
729
- then binds it the same way. Token transport also differs per harness —
730
- Claude Code gets a strict `--mcp-config` file, Codex gets the token via an
731
- env var its own config references (`--config
732
- mcp_servers.spor.bearer_token_env_var=SPOR_DISPATCH_MCP_TOKEN`) — but both
733
- land at the same self-serve mint/bind pair above.
726
+ **supervised**-harness dispatch (Codex, OpenCode, GitHub Copilot CLI) follows
727
+ the same late-bind contract from its own supervisor process instead: it reads
728
+ the session id out of the run's supervised JSONL log rather than
729
+ `claude agents --json` — Codex off its `thread.started` event, OpenCode off
730
+ the `sessionID` every event carries, Copilot off the `sessionId` on its
731
+ terminal `result` event (so a Copilot run binds only at exit, still before its
732
+ record goes terminal) — then binds it the same way. Token transport also
733
+ differs per harness: Claude Code gets a strict `--mcp-config` file, Codex gets
734
+ the token via an env var its own config references (`--config
735
+ mcp_servers.spor.bearer_token_env_var=SPOR_DISPATCH_MCP_TOKEN`), and OpenCode
736
+ and Copilot — neither of which can be handed an MCP server on the command line
737
+ without publishing the bearer to argv — get the agent-scoped token as
738
+ `SPOR_TOKEN` in the run's environment, so the `spor` CLI inside the run is
739
+ agent-attributed with no injected MCP. All of them land at the same self-serve
740
+ mint/bind pair above.
734
741
  - **OAuth 2.1 for MCP connectors** (Cowork/claude.ai, which cannot carry a
735
742
  static bearer token): protected-resource metadata discovery (RFC 9728,
736
743
  advertised on the `/mcp` 401 via `WWW-Authenticate`), authorization-server
package/GRAPH.md CHANGED
@@ -302,8 +302,8 @@ separately, but never shows a complete custom type in one piece.
302
302
 
303
303
  **The constraint model is procedural, not declarative.** A schema's `json`
304
304
  payload declares only *registry knobs* — `node_type`, `prefix`, `queueable`,
305
- `traversable`, `always_on`, `capturable`, an edge `weight`, and the three status
306
- partitions: `status.non_resolving` (resolver semantics — whether a node in this
305
+ `traversable`, `always_on`, `capturable`, an edge `weight`, the completion
306
+ policy (below), and the three status partitions: `status.non_resolving` (resolver semantics — whether a node in this
307
307
  status retires the targets it points at), `status.terminal` (own-lifecycle
308
308
  completion — the statuses in which a node of this type is *done*, unioned with the
309
309
  kernel's legacy set and read by work-analytics so a schema-only terminal status
@@ -314,8 +314,33 @@ issue-spor-analytics-completion-ignores-schema-terminal-status), and
314
314
  `terminal-status` register below; a schema that declares no `inert` set
315
315
  INHERITS its `terminal` set, so only a schema whose two sets genuinely differ
316
316
  declares it — the seed decision schema pins `settled` terminal but NOT inert,
317
- dec-spor-status-inert-third-partition). There is **no
318
- declarative field list and no status enum.** Custom fields are free-form: any flat frontmatter key the
317
+ dec-spor-status-inert-third-partition).
318
+
319
+ **The completion policy — declared for readers, still enforced by code**
320
+ (task-spor-registry-declarative-terminal-status-policy). Three further `status`
321
+ keys say, as registry data, what the hooks below enforce: `status.vocabulary`
322
+ (the closed status enum the type's own `validate()` gates membership on),
323
+ `status.completion` (the single SUCCESS terminal value — task `done`, issue
324
+ `resolved`, question `answered` — as distinct from `status.terminal`, which is
325
+ the full set *including* the give-up outcomes `abandoned`/`superseded`/
326
+ `rejected`), and `status.resolver_required` (whether reaching that value also
327
+ demands a live resolving `decision`/`artifact`, the completion-resolver
328
+ invariant). **Declaring them gates nothing** — the hooks are still the only
329
+ write door, and this is exactly why they are not a field list or an enforced
330
+ enum. They exist so a READER can name the right terminal status without
331
+ parsing hook source: the gardener's finding remedies used to keep hand-written
332
+ tables of "task → done, everything else → resolved" and shipped remedies whose
333
+ `set_status` the door then refused
334
+ (issue-spor-gardener-terminal-status-fallback-off-vocab). A type whose terminal
335
+ values are several distinct OUTCOMES rather than one success (decision
336
+ settled/superseded/rejected, artifact merged/released/done, capture-pending
337
+ merged/rejected) declares a `vocabulary` and NO `completion` — that absence is
338
+ the machine-readable form of "there is no mechanical close here". Declaration
339
+ and hook are pinned together by `test/seed-declarative-status-policy.test.js`,
340
+ which drives every seed schema's hooks through the sandbox and fails if the two
341
+ disagree; read the live values with `spor schema <type>`.
342
+
343
+ Otherwise there is **no declarative field list and no status enum.** Custom fields are free-form: any flat frontmatter key the
319
344
  regex parser accepts (simple `key: value` scalars, YAML-folded multi-line
320
345
  values, `pin:`/`exclude:` inline lists, `- {type: X, to: Y}` edges — and nothing
321
346
  fancier) is carried verbatim on the node. What a field MUST contain, and which
@@ -400,6 +425,20 @@ warns). A schema node goes through the same propose→activate flow it governs
400
425
  bump the CalVer and add an `upgrades` chain only when the change is not
401
426
  backward-readable.
402
427
 
428
+ Rollout-stage schemas the *product* ships ride the **candidate pack**
429
+ (`lib/seed/candidates/`): full schema-node markdown that travels with the npm
430
+ package but never enters the registry until a graph adopts it as a
431
+ graph-resident node — `spor schema candidates` lists each candidate's adoption
432
+ state, `spor schema adopt <id>` writes it through the validated node surface
433
+ (`status: proposed`; `--activate` is the trusted-admin form for solo/local
434
+ graphs), stamping `adopted_from`/`adopted_sha` provenance. Re-running adopt
435
+ after a package upgrade is CalVer-aware and idempotent: a pristine older copy
436
+ (canonical hash — `schema_version` + body — still matching its stamp) upgrades
437
+ in place with its status preserved; a locally modified or unstamped resident
438
+ refuses without `--force`. When a candidate stabilizes it is promoted into the
439
+ seed pack at a release; the resident copy then shadows the seed (the
440
+ stale-override warning above) and should be retired (`status: retired`).
441
+
403
442
  A complete worked example — a `escalation` type with a required `severity`
404
443
  field (enforced in `validate`) and an `open → mitigated → closed` status machine
405
444
  whose terminal `closed` demands a resolver (enforced in `transitions`):
@@ -1103,9 +1142,12 @@ program keeps rendering. The preference is all-or-nothing at a node — declare
1103
1142
  every member of an umbrella in one write, or the undeclared rest read as
1104
1143
  "blocking but outside the program" rather than silently dropping. It ships as
1105
1144
  a graph-resident schema node (`schema-edge-member-of-program`), not the seed
1106
- pack, so it needs a *different* identity to activate it (the standing
1107
- propose→activate flow above) before writes of this edge type validate; check
1108
- `spor schema member-of-program` for its live status rather than assuming.
1145
+ pack — delivered as a packaged candidate (`spor schema adopt
1146
+ schema-edge-member-of-program` writes it into a graph that doesn't have it
1147
+ yet; see "Resolution and rollout" above) — so it needs activation (a
1148
+ *different* identity in team graphs; `--activate` in solo/local ones) before
1149
+ writes of this edge type validate; check `spor schema member-of-program` for
1150
+ its live status rather than assuming.
1109
1151
  `capturable: false` — the distiller and capture nudge never emit it; only a
1110
1152
  person or an agent working the program explicitly wires membership.
1111
1153
 
package/README.md CHANGED
@@ -283,8 +283,9 @@ Terminal records age out after `dispatch.runRetentionMs` (default 14 days).
283
283
  ### Choosing a harness
284
284
 
285
285
  By default, `spor dispatch` launches a Claude Code agent (`claude --bg`). To
286
- dispatch under a different coding-agent CLI — Codex is also supported — resolve
287
- a **profile**: a node that bundles a harness, model, and toolset.
286
+ dispatch under a different coding-agent CLI — Codex, OpenCode, and GitHub
287
+ Copilot CLI are also supported — resolve a **profile**: a node that bundles a
288
+ harness, model, and toolset.
288
289
 
289
290
  ```bash
290
291
  spor dispatch issue-86 --profile profile-codex-sol
@@ -312,23 +313,53 @@ its dispatches pick a harness without a flag every time; `--profile` on the
312
313
  command line always wins over that default.
313
314
 
314
315
  Before launching anything, dispatch checks whether **this machine** can
315
- actually run the resolved profile — is the `codex` CLI on PATH, are the right
316
- MCP servers reachable, and so on (see `spor capabilities`). If it can't,
316
+ actually run the resolved profile — is the harness's CLI reachable, are the
317
+ right MCP servers reachable, and so on (see `spor capabilities`). If it can't,
317
318
  dispatch refuses outright rather than silently falling back to Claude Code; in
318
319
  team mode it also names any other machine in the fleet that can run it.
319
320
 
320
321
  Codex-specific flags (`--sandbox`, `--approval-policy`) and Claude-specific
321
322
  ones (`--permission-mode`, `--agent`) are mutually exclusive — passing the
322
323
  wrong one for the resolved harness is a hard error, so a dispatch can't launch
323
- half-configured for the wrong CLI.
324
+ half-configured for the wrong CLI. The one exception: `--permission-mode
325
+ bypassPermissions` against a Codex profile has a real Codex equivalent
326
+ ("run fully unattended"), so instead of erroring it translates to `--sandbox
327
+ danger-full-access --approval-policy never` (an explicit `--sandbox`/
328
+ `--approval-policy` you also pass wins over that default) and prints a loud
329
+ warning naming the translation — so an orchestrator or script that passes the
330
+ same bypass flag to every dispatch regardless of harness keeps working.
331
+ Every other permission-mode value still hard-errors against Codex.
332
+
333
+ The harnesses do not all confine a run the same way. Codex dispatch defaults to
334
+ `--sandbox workspace-write`, so its filesystem reach is bounded. OpenCode
335
+ (`--auto`) and GitHub Copilot CLI (`--allow-all --no-ask-user`) have no
336
+ equivalent — a dispatch under either runs with unrestricted tool access,
337
+ because an unattended run has no human to answer a permission prompt. Dispatch
338
+ those into a worktree or a checkout you are willing to have an agent change.
339
+
340
+ **Naming a launcher explicitly.** A dispatched run does not inherit your
341
+ interactive shell, so a CLI installed under a prefix that only an interactive
342
+ shell sees (a common Homebrew setup) resolves when you check it by hand and
343
+ resolves to nothing when Spor launches it. Point Spor at the binary directly
344
+ rather than relying on `PATH`, in `~/.spor/config.json` — machine-specific, like
345
+ `dispatch.repos`, so it never belongs in a committable `.spor.json`:
346
+
347
+ ```json
348
+ { "dispatch": { "bin": { "opencode": "/home/linuxbrew/.linuxbrew/bin/opencode" } } }
349
+ ```
350
+
351
+ `SPOR_CLAUDE_CMD` / `SPOR_CODEX_CMD` / `SPOR_OPENCODE_CMD` / `SPOR_COPILOT_CMD`
352
+ override the same thing per harness and win over the config file. An explicit
353
+ launcher is used verbatim and is never quietly swapped for something on `PATH`;
354
+ with none set, the bare name resolves on `PATH` as before.
324
355
 
325
356
  Claude Code dispatch detaches into Claude Code's own background-agent daemon —
326
357
  the launcher exits immediately, and `spor dispatch` can only reconcile what
327
- happened to it afterwards from the harness's own session transcript. Codex
328
- dispatch instead runs under a small supervisor Spor itself owns: it launches
329
- `codex exec` in the background, streams its progress into a private log, and
330
- captures the run's final message to a report file. At launch it prints where
331
- everything lives:
358
+ happened to it afterwards from the harness's own session transcript. Every
359
+ other harness instead runs under a small supervisor Spor itself owns: it
360
+ launches the CLI's headless mode in the background, streams its progress into a
361
+ private log, and captures the run's final message to a report file. At launch
362
+ it prints where everything lives:
332
363
 
333
364
  ```text
334
365
  run: 3f9a2c1e-... (Codex supervisor running)
@@ -337,7 +368,7 @@ report: ~/.spor/journal/dispatch/3f9a2c1e-....report.md
337
368
  session: 019f7a51-...
338
369
  ```
339
370
 
340
- `log` is the full JSONL progress stream; `report` is Codex's final message —
371
+ `log` is the full JSONL progress stream; `report` is the run's final message —
341
372
  the thing to read for "what did it conclude". Both paths, plus the run's
342
373
  outcome, are also recorded durably and can be looked up later, same as any
343
374
  other dispatch: