@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/API.md +16 -9
- package/GRAPH.md +49 -7
- package/README.md +42 -11
- package/bin/spor.js +371 -74
- package/lib/candidates.js +179 -0
- package/lib/kernel/queue.js +3 -0
- package/lib/kernel/registry.js +99 -2
- package/lib/kernel/satisfiability.js +36 -4
- package/lib/schema.js +23 -1
- package/lib/seed/candidates/schema-edge-member-of-program.md +41 -0
- package/lib/seed/schema-artifact.md +21 -1
- package/lib/seed/schema-capture-pending.md +19 -2
- package/lib/seed/schema-correction.md +14 -1
- package/lib/seed/schema-decision.md +21 -1
- package/lib/seed/schema-issue.md +26 -2
- package/lib/seed/schema-question.md +21 -2
- package/lib/seed/schema-task.md +26 -2
- package/lib/shell/agent-dispatch-runner.js +30 -4
- package/lib/shell/dispatch-harnesses.js +297 -7
- package/package.json +3 -1
- package/prompts/client/distill-local.md +1 -1
- package/scripts/engines/doctor.js +34 -0
- package/scripts/engines/util.js +24 -4
- package/skills/brief/SKILL.md +1 -1
- package/skills/spor/SKILL.md +6 -4
- package/skills/spor/references/authoring-schemas.md +25 -1
- package/skills/spor/references/concepts.md +4 -4
- package/skills/triage/SKILL.md +4 -3
|
@@ -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.
|
|
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.
|
|
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
|
-
**
|
|
727
|
-
supervisor process instead: it reads
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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`,
|
|
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).
|
|
318
|
-
|
|
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
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
|
287
|
-
a **profile**: a node that bundles a
|
|
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
|
|
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.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
captures the run's final message to a report file. At launch
|
|
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
|
|
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:
|