@sporhq/spor 0.20.1 → 0.21.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/.codex-plugin/plugin.json +1 -1
- package/API.md +20 -0
- package/GRAPH.md +5 -3
- package/QUEUE.md +37 -0
- package/bin/spor.js +490 -3
- package/lib/changes.js +14 -1
- package/lib/kernel/coupling.js +14 -2
- package/lib/kernel/graph.js +58 -23
- package/lib/kernel/queue.js +205 -5
- package/lib/queue.js +11 -2
- package/lib/seed/schema-issue.md +17 -0
- package/lib/seed/schema-task.md +17 -0
- package/package.json +1 -1
- package/prompts/client/digest-intent.md +8 -6
- package/scripts/engines/agents-md.js +13 -2
- package/scripts/engines/post-tool.js +20 -5
- package/scripts/engines/session-start.js +6 -5
- package/scripts/engines/util.js +114 -19
- package/skills/next/SKILL.md +15 -3
- package/skills/spor/SKILL.md +34 -0
- package/skills/spor/references/cowork.md +7 -0
- package/skills/triage/SKILL.md +110 -16
|
@@ -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.21.0",
|
|
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.21.0",
|
|
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
|
@@ -206,6 +206,24 @@ so an agent-set priority is distinguishable from human triage
|
|
|
206
206
|
(issue-cc-priority-attribution-gap). The CLI wrapper is `spor priority <id>
|
|
207
207
|
<p1|p2|p3|clear>`.
|
|
208
208
|
|
|
209
|
+
### `set_readiness`
|
|
210
|
+
|
|
211
|
+
Micro-mutation, the agent-readiness manual override
|
|
212
|
+
(dec-spor-agent-readiness-derived-classification), a verbatim sibling of
|
|
213
|
+
`set_priority` above. Input `{ "id": "<node>", "readiness": "<value>" }`,
|
|
214
|
+
where `<value>` is `agent` (the ONE hand-settable value of the otherwise
|
|
215
|
+
structurally-derived `agent|human|untriaged` classification `rankQueue`
|
|
216
|
+
computes) or a clearing form (`none`/`clear`/`""`) to demote the item back off
|
|
217
|
+
agent-ready. There is no hand-settable `readiness: human` — human is always
|
|
218
|
+
derived structurally (`requires: human`, `assigned → person`, held-task state,
|
|
219
|
+
an open neighborhood question) and always wins over the stamp, so a later
|
|
220
|
+
human-signal edit still flips a stamped item back. Output `{ "status":
|
|
221
|
+
"updated", "id", "revision", "warnings" }`; an unknown value is `invalid_node`
|
|
222
|
+
with the allowed value in `details`. Like `set_priority` it is a server-side
|
|
223
|
+
read-modify-write — no client revision round-trip — and it stamps
|
|
224
|
+
`readiness_by` (acting identity), `readiness_at`, and `readiness_via` (the
|
|
225
|
+
door). The CLI wrapper is `spor ready <id> [--needs-input]`.
|
|
226
|
+
|
|
209
227
|
### `reserve`
|
|
210
228
|
|
|
211
229
|
The fifth task-lease action (dec-cc-task-resumption-reservation), alongside
|
|
@@ -512,6 +530,7 @@ endpoint is the REST twin of a core call:
|
|
|
512
530
|
| `DELETE /v1/nodes/{id}/edges` `{type, to}` | scripts, mechanical writers | `remove_edge` semantics (§1): the withdrawal twin of the POST above — drop one typed edge by `{type, to}`, normalize/flip exactly as `add_edge` (an inverse form removes the canonical edge on the *other* node and echoes its id), no revision echo. A missing edge is an idempotent `skipped`. For *withdrawing* a relationship the review flip can't express — a pulled review request, a dismissed review |
|
|
513
531
|
| `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) |
|
|
514
532
|
| `POST /v1/nodes/{id}/priority` `{priority}` | `spor priority`, queue triage | `set_priority` semantics (§1): one-scalar human-override update — `p1`/`p2`/`p3` or a clearing form (`none`/`clear`/`""`/`p0`). Server-side read-modify-write (no revision), stamping `priority_by`/`priority_at`/`priority_via` for the audit trail (issue-cc-priority-attribution-gap). Unknown value → `invalid_node` with the allowed list |
|
|
533
|
+
| `POST /v1/nodes/{id}/readiness` `{readiness}` | `spor ready`, triage make-ready pass | `set_readiness` semantics (§1): one-scalar agent-readiness override — `agent` or a clearing form (`none`/`clear`/`""`) to demote back to derived. No hand-settable `human` value (always structurally derived, always wins). Server-side read-modify-write (no revision), stamping `readiness_by`/`readiness_at`/`readiness_via`. Unknown value → `invalid_node` with the allowed value |
|
|
515
534
|
| `POST /v1/nodes/{id}/claim` `{session?}` | `claim`/`set_status` MCP tools, `spor claim` CLI, `spor dispatch` | take the heartbeat-renewed lease (dec-cc-task-claim-lease): writes the durable `assigned` edge once, attributes to `$viewer` from the token (never an argument), and creates the ephemeral lease → `{ok, status, lease: {node_id, by, expires, expires_at, session, claimed_at}, edge}`. A live lease held by ANOTHER person is `409 conflict` naming the holder + expiry (re-claiming your OWN live claim just renews it). `session` scopes the heartbeat (omit to leave it person-scoped, so any of the claimer's sessions may renew — what `spor claim` and `spor dispatch` do, since `claude --bg` self-allocates the run session only at launch; dispatch then renews with the real session once it has read it from `claude agents --json`, dec-spor-dispatch-bg-session-late-bind) |
|
|
516
535
|
| `POST /v1/nodes/{id}/renew` `{session?}` | post-tool heartbeat, `renew` MCP tool, `spor renew` CLI, `spor dispatch` | bump the live lease's expiry only — no commit; the heartbeat that keeps a claim from lapsing. A lapsed/stolen lease is `409` (names the current holder). Person-scoped: any of the claimer's sessions may renew; a `session` binds the lease to that run (`spor dispatch` uses this to bind the captured `claude --bg` session post-launch) |
|
|
517
536
|
| `POST /v1/nodes/{id}/extend` `{ms, session?}` | `extend` MCP tool, `spor extend` CLI | manually stretch your live lease by `ms` milliseconds for a known long idle gap → `{ok, status, lease, capped_to_max?, claim_ttl_max_ms?}`. Bounded by the tenant's `claim_ttl_max` policy (a request past the ceiling caps to it, flagged `capped_to_max`); never shortens a lease. `ms` must be a positive number (`spor extend <id> <2h|45m|…>` parses the human duration client-side). A lapsed/stolen lease is `409 lease_lost` naming the holder |
|
|
@@ -531,6 +550,7 @@ endpoint is the REST twin of a core call:
|
|
|
531
550
|
| `GET /v1/program/{id}?format=json\|text&depth=&max_nodes=` | program oversight, /spor:brief follow-ups | the program/progress view (`render_program`'s REST twin, one kernel behind both doors): the gating tree of everything that `blocks` `{id}` transitively, with resolution-derived progress (`{progress: {total, done, active, blocked, open, pct, statuses}}` on the view root; done = terminal status / supersession / live resolves-answers edge, exactly the queue's truth). JSON view tree by default, `?format=text` for the terminal rendering; `depth`/`max_nodes` bound expansion and count skipped branches into `truncated`, never silently. 404 for an unknown id |
|
|
532
551
|
| `GET /v1/lens/{id}/render?format=html\|text\|json` | browsers, teammates without a checkout | run a lens OR workspace node and render its view tree (html default, plain text, or the raw tree as json). Read-only — no action forms; writes stay with `/v1/nodes` and the MCP tools. Auth is the caller's bearer header OR a signed read-only **render ticket** for shared links (browser links can't carry an Authorization header): `?ticket=<blob>` is accepted once and exchanged via a 302 for an HttpOnly `spor_render_ticket` cookie (kept out of URLs, logs, and view-to-view hrefs). The ticket binds `$viewer` to the recorded sharer and the render shows a "Viewing as <sharer>" banner. The former `?token=<PAT>` sharing path is **removed** — a shared link can never carry a write-capable credential |
|
|
533
552
|
| `POST /v1/lens/{id}/ticket` `{expires?}` | sharing a view; `spor share` | mint a signed, expiring, read-only render ticket for the lens/workspace, recording the authenticated caller as the sharer → `{ticket, url, lens_id, sharer_person_id, exp}`. `expires` is `<N>d` or an ISO date (default `7d`, max `30d`); the caller must be bound to a person node (else `422 no_person`). The ticket carries no write scope and is honored only on the render route. The `spor share <lens-id> [--expires <Nd>]` CLI verb is the shell front-door (remote-only — tickets are minted and signed server-side); it prints the shareable link ready to paste, `--json` for the raw envelope |
|
|
553
|
+
| `POST /v1/merge` `{nodes: [...], mode?: "plan"\|"apply", id_map?: {...}, trust_attached_code?: bool, force?: bool}` | admin promoting one graph into another — pilot-to-org, or a local dogfood graph into a hosted tenant | bring another graph's exported node files (`nodes`: an array of raw node markdown strings) into this one without the failure mode of the naive `GET /v1/export \| POST /v1/nodes --if-exists skip` — silently DROPPING every colliding node while imported edges that pointed at it re-bind to this graph's unrelated same-named node (ordinal id schemes like `cap-<date>-<n>` collide across any two independently started graphs). **Admin-gated** (stewards→root, else `403 forbidden`): `mode:"apply"` writes through the same trusted bulk-import door the server uses internally, which preserves each incoming node's original attribution (a merge moves history, it does not re-author it) and skips the `transitions()`/policy gates (content validation still runs per node; create-only, one deferred commit). Every incoming node classifies as **imported** (id unknown here), **deduped** (id collides, content identical — attribution-blind, this graph's copy wins), **remapped** (id collides, content differs, and the id's final dash-segment is all-digits/*ordinal* — rewritten to `<id>-<sha256(content)[:7]>`, with every reference to the old id across the incoming batch rewritten to match), or **conflict** (id collides with different content and a *semantic* id; a `person` node's email already bound to a different id; or a `schema`/`workflow`/`workflow-run` node or a `stewards`-to-this-graph's-root edge — none of these ever merge silently). Conflicts are reported for manual triage and never written; the schema/workflow class is the one skippable via `trust_attached_code: true` (only for a whole graph you own). `mode:"plan"` (the default) runs the same classification and validation and returns the report without writing anything; `mode:"apply"` **refuses with `409 conflict`** (nothing written) whenever the plan still carries conflicts or validation errors, unless `force: true` — which imports the clean subset and knowingly leaves any reference to a skipped id unresolved. `id_map` (`{"old-id": "new-id"}`) seeds cross-id rewrites; feed a plan's own `id_map` back into the next request when a graph is too large for one batch (plan every batch first to build the complete map, then apply each). Response: `{mode, counts: {incoming, imported, deduped, remapped, conflicts, errors}, imported, deduped, remapped, conflicts, errors, id_map, results?, generated_at}` — `imported`/`deduped`/`remapped`/`conflicts` are arrays of `{id, new_id?, title?, reason?}`; `errors` is `{id, index?, errors: [...]}` (unparseable/invalid entries); `results` (apply mode only) carries the import door's per-entry write verdicts. Deterministic and idempotent — re-running an identical merge dedups everything the first run imported, safe after a partial failure. Called directly today (bearer admin token + `curl`); a CLI wrapper defaulting to plan mode is tracked separately (task-spor-cli-merge-verb) |
|
|
534
554
|
| `GET /v1/export` | bootstrap/offline; `spor export` | ustar tarball of `nodes/` for seeding a local read replica (`?gzip=1` compresses); see §5 for the response headers. `curl … \| tar x` reproduces `nodes/` byte-for-byte. `?history=1` instead streams a `git bundle --all` of the repo (`application/x-git-bundle`, full commit provenance, the customer data-exit path — `git clone <bundle> graph`); `?auth=1` ALSO bundles `auth/*.json` so a disaster restore reproduces the credential set (admin-gated: stewards-root → `403` otherwise). The `spor export [--gzip] [--history\|--auth] [--out <file>]` CLI verb is the shell front-door (remote downloads this; `--gzip`/`--out` also build the same `nodes/` tarball locally, while `--history`/`--auth` are remote-only) |
|
|
535
555
|
| `GET /v1/admin/tokens` | offboarding / audit; `spor admin token list` (= `spor token list --all`) | list PATs → `{tokens: [{hash_prefix, person, name, email, created, expires, expired}], count}` — never plaintext, never full hashes. Admin-only (§4). The team-wide view; the caller's own PATs are the self-serve `GET /v1/me/tokens` above |
|
|
536
556
|
| `POST /v1/admin/tokens` `{person, expires?}` | onboarding; `spor invite` | mint a PAT bound to an existing person node (`expires` is `<N>d` or an ISO date) → 201 `{token, hash_prefix, person, name, email, expires}`; the plaintext `token` is returned **once**. Admin-only. Binds someone *else* (onboarding); the self-serve mint is `POST /v1/me/tokens` above |
|
package/GRAPH.md
CHANGED
|
@@ -75,9 +75,11 @@ Rules:
|
|
|
75
75
|
`- {type: assigned, to: agent-X, profile: profile-Y}` — preserved on the edge
|
|
76
76
|
object (the per-assignment profile override; see "The agent orchestration
|
|
77
77
|
layer"). Plain `{type, to}` edges are unchanged.
|
|
78
|
-
- `commits` is an optional
|
|
79
|
-
(`commits: [wf@1a2b3c4d, ...]
|
|
80
|
-
|
|
78
|
+
- `commits` is an optional list of repo-qualified git shas (kebab-case repo
|
|
79
|
+
slug + 7–40 hex), inline (`commits: [wf@1a2b3c4d, ...]`) or as a YAML block
|
|
80
|
+
list (`commits:` followed by indented `- wf@1a2b3c4d` lines — both forms
|
|
81
|
+
parse to the same array), linking the node to the code commits that
|
|
82
|
+
implement it (task-cc-commit-linking).
|
|
81
83
|
Commits are not nodes — a node-per-commit would mirror `git log` and drown
|
|
82
84
|
the curated graph; the field plus the `Spor: <node-id>`
|
|
83
85
|
commit-message trailer give both directions (node→commit here, commit→node
|
package/QUEUE.md
CHANGED
|
@@ -528,6 +528,43 @@ The queue is a compile mode, not a new store:
|
|
|
528
528
|
Tier-2 routed questions are all queueable schemas. "Home is a decision
|
|
529
529
|
queue" falls out of one mechanism.
|
|
530
530
|
|
|
531
|
+
**Agent-readiness** (dec-spor-agent-readiness-derived-classification). Each
|
|
532
|
+
ranked item carries a **derived** classification `readiness:
|
|
533
|
+
agent|human|untriaged` (plus `readiness_reasons[]`), computed structurally in
|
|
534
|
+
the render pass — no LLM (the `queueSignals` hook can only add numbers to
|
|
535
|
+
score), no graph state (a status overloads the lifecycle vocab; an edge has no
|
|
536
|
+
natural target). It answers "can a coding agent complete this unattended, or
|
|
537
|
+
does a human have to act first?" Derivation, **human wins over agent** so a
|
|
538
|
+
later open question or `requires: human` edit flips a stamped item back:
|
|
539
|
+
- **human** — `requires:` includes `human` (the first consumer of the
|
|
540
|
+
risk-class register, §schema-requires: work unsatisfiable by any agent) · an
|
|
541
|
+
`assigned → person` edge (it is that person's work) · held-task state (the
|
|
542
|
+
front self-limit's `suggest: triage`) · the item is itself an open question
|
|
543
|
+
or unprocessed capture · an open (live, unanswered) question node in its
|
|
544
|
+
1-hop neighborhood (a soft spec gap to close first).
|
|
545
|
+
- **agent** — an explicit `readiness: agent` frontmatter stamp with
|
|
546
|
+
`readiness_by` provenance (mirroring `priority`/`priority_by`, the one
|
|
547
|
+
hand-set piece — a make-ready stamp verb + REST twin, slice 2) · or an
|
|
548
|
+
`assigned → agent` edge.
|
|
549
|
+
- **untriaged** otherwise — the deliberate third bucket ("nobody has checked
|
|
550
|
+
the spec"), which rides **no** readiness fields, so the item is
|
|
551
|
+
byte-identical when no readiness data exists.
|
|
552
|
+
|
|
553
|
+
Readiness leads the why-line when decisive (`agent-ready: …` / `needs human:
|
|
554
|
+
…`), and the envelope gains `counts_by_readiness` ({agent, human, untriaged},
|
|
555
|
+
present only when there is readiness signal or a readiness facet was asked
|
|
556
|
+
for) — the headline "how much of my queue can an agent take right now?" A
|
|
557
|
+
**readiness filter** (`rankQueue({readiness})`, a class or array; today on the
|
|
558
|
+
kernel opt and the local CLI's `--readiness`, comma-separated — the `GET
|
|
559
|
+
/v1/queue?readiness=` param, `show_queue {readiness}`, and remote `spor next
|
|
560
|
+
--readiness` forwarding land with the server render-surfaces slice,
|
|
561
|
+
task-spor-queue-readiness-render-surfaces) narrows the queue to a class as a
|
|
562
|
+
hard scope like `project`/`type` (schema-approval items, outside the
|
|
563
|
+
classification, are excluded). A graph with no readiness data is byte-identical to before — no
|
|
564
|
+
fields, no clause, no count. Hard triage gaps recorded during a make-ready pass
|
|
565
|
+
become explicit `blocks` edges (which correctly remove the item until
|
|
566
|
+
answered); readiness handles the soft/derived side.
|
|
567
|
+
|
|
531
568
|
**Per-person queues** (task-cc-queue-assignee-filtering). `rankQueue`'s
|
|
532
569
|
`assignee` parameter — and `GET /v1/queue?assignee=<person-id>` /
|
|
533
570
|
`show_queue {assignee}` (use `assignee=me` to bind to the caller) — scopes the
|