@sporhq/spor 0.14.0 → 0.15.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 +13 -7
- package/GRAPH.md +4 -3
- package/QUEUE.md +11 -2
- package/README.md +3 -2
- package/bin/spor.js +1058 -96
- package/lib/kernel/queue.js +16 -4
- package/lib/tar.js +36 -1
- package/package.json +1 -1
- package/scripts/engines/distill.js +17 -2
- package/scripts/engines/session-start.js +6 -3
- package/skills/ask/SKILL.md +68 -0
- package/skills/backfill/SKILL.md +3 -2
- package/skills/spor/SKILL.md +10 -5
- package/skills/spor/references/concepts.md +3 -2
- package/skills/team-graph/SKILL.md +20 -0
|
@@ -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.15.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "losthammer"
|
|
8
8
|
}
|
package/API.md
CHANGED
|
@@ -394,6 +394,9 @@ endpoint is the REST twin of a core call:
|
|
|
394
394
|
| `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 |
|
|
395
395
|
| `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 |
|
|
396
396
|
| `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) |
|
|
397
|
+
| `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) |
|
|
398
|
+
| `POST /v1/me/tokens` `{expires?, label?}` | `spor token create` | mint a human-identity `spor_pat_` PAT bound to the CALLER's own person → 201 `{token, hash_prefix, person, name, email, label, expires}`; the plaintext `token` is returned **once**. `expires` is `<N>d` or an ISO date, user-set, defaulting to and **capped at 1 year** (a past date or beyond-cap is `422`, rejected not silently clamped); `label` is an optional ≤200-char note surfaced in the listing. `403` if unbound. The self-serve mint twin of admin `POST /v1/admin/tokens` (which binds someone *else*) |
|
|
399
|
+
| `DELETE /v1/me/tokens/{hash-prefix}` | `spor token revoke` | revoke one of the caller's OWN PATs by hash prefix → `{revoked, hash_prefix, oauth_grants_revoked}`; a prefix that isn't one of the caller's is `404` (never another person's token). `403` if unbound. Shares the admin revoke's OAuth-grant cascade-completeness invariant (issue-cc-pat-revoke-cascades-all-oauth-grants) |
|
|
397
400
|
| `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 |
|
|
398
401
|
| `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 |
|
|
399
402
|
| `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 |
|
|
@@ -411,7 +414,7 @@ endpoint is the REST twin of a core call:
|
|
|
411
414
|
| `POST /v1/nodes/{id}/commits` `{repo, sha}` | post-tool / link-commits | `link_commit`: append `repo@sha` to the node's `commits:` list (kebab-case repo slug, 7–40 lowercase hex, ≤40 commits per node); idempotent, prefix-aware dedup |
|
|
412
415
|
| `GET /v1/commits/{sha}?repo=` | `spor blame`/`commits` CLI verb; sessions doing git archaeology | sha → nodes lookup over the `commits:` fields (≥7 hex, abbreviated or full); each match carries `{repo, sha, id, type, title, summary, status, project}` — blame a line, get the why. The `spor blame <sha> [--repo <slug>]` CLI verb (alias `spor commits <sha>`) wraps this remotely and runs the same lookup over the local graph in local mode (`lib/query.js` `lookupCommit`) |
|
|
413
416
|
| `GET /v1/changes?since=&project=&limit=` | `recent_changes`'s REST twin; audit review | the remote audit trail: a git-log projection over `nodes/` → `{changes: [{id, change, commit, date, committed_by, type, title, authored_via, author}], count, head, since, generated_at}`, newest change per node first. `since` is a 7–40 hex sha (`sha..HEAD`) or a date/relative phrase git understands (`--since`); an unresolvable sha is `422`. `project` scopes to one project's nodes (deletions are omitted when scoped, their project being gone). `limit` bounds nodes returned (default 100, **max 500**). Each entry's `authored_via` is the current machine-vs-human signal (`capture`/`distill`/`gardener` = machine). Lets a remote client review what agents wrote without the whole `/v1/export` tarball |
|
|
414
|
-
| `POST /v1/capture` | distill, /spor:defer | `capture` semantics: `{text, context: {project, during, blocks?, needed_by?}, source?}` → ingestion model + validate + commit → `{status, ids, nodes, summary, warnings}`. `source: "distill"` marks backstop captures in the journal. `context.blocks` (a node id, must exist) and `context.needed_by` (`YYYY-MM-DD`) declare a cross-project dependency (task-cc-xproject-dependency-loop): set `context.project` to the SERVING project and the server attaches a `blocks` edge to the requester + the deadline deterministically (not via the model) onto the primary node. A missing `blocks` target is `404`; a non-date `needed_by` is `422` — both rejected before any model call |
|
|
417
|
+
| `POST /v1/capture` | distill, /spor:defer | `capture` semantics: `{text, context: {project, during, blocks?, needed_by?}, source?, idempotency_key?}` → ingestion model + validate + commit → `{status, ids, nodes, summary, warnings}`. `source: "distill"` marks backstop captures in the journal. `idempotency_key` (client-generated; equivalently the `Idempotency-Key` header) guards the whole capture against the timeout-then-server-completes race (issue-cc-capture-transport-idempotency): a key the server has already seen returns the original result instead of re-ingesting, so a client that aborted at its read timeout but landed server-side does NOT double-write when the spooled body is replayed by `spor drain`. The client puts the key in the BODY so the verbatim outbox replay carries it for free (issue-spor-add-cli-duplicate-on-timeout-drain). `context.blocks` (a node id, must exist) and `context.needed_by` (`YYYY-MM-DD`) declare a cross-project dependency (task-cc-xproject-dependency-loop): set `context.project` to the SERVING project and the server attaches a `blocks` edge to the requester + the deadline deterministically (not via the model) onto the primary node. A missing `blocks` target is `404`; a non-date `needed_by` is `422` — both rejected before any model call |
|
|
415
418
|
| `POST /v1/distill/report` | distill | sweep telemetry, journal-only (no store mutation): `{facts, captured?, spooled?, rejected?, project?, session?}` → `{status: "reported"}`; zero-fact sweeps report too |
|
|
416
419
|
| `POST /v1/corrections` | /spor:correct | `propose_correction` semantics → 201 `{status, id, revision, warnings}` |
|
|
417
420
|
| `GET /v1/queue?project=&assignee=&type=&exclude_type=&limit=&offset=` | /spor:next, session-start | the ranked decision queue: `{items, count, offset, returned_count, total_count, truncated, next_offset, counts_by_type, counts_by_project, counts_by_suggest, muted?, dormant?, questions, asked, findings, pending, reviews, policy?, generated_at}` — items retired by a live resolves/answers edge are excluded; items hidden by the viewer's `queue_mute` or parked by a future `wake:` date (QUEUE.md §4) are counted, never silently dropped; `questions`/`findings`/`pending` are the routed-to-me-plus-unrouted views for the authenticated identity, `asked` is the questions you filed, and `reviews` is the nodes whose review is requested of you (an open `review-requested` edge to your person node — explicitly targeted, no unrouted fallback). `limit` is the page size (default 20, **max 100**, clamped not rejected) and `offset` skips that many items in the ranked order (default 0); the `counts_*`/`total_count` aggregates always cover the FULL ranked set regardless of the page, so one call answers "how many issues vs tasks" without paging, while `truncated`/`next_offset` let a client walk the rest by re-requesting with `offset=next_offset` until `next_offset` is null. Pagination is offset over a point-in-time ranked slice (the queue re-ranks every call), not a cursor — it resumes the same slice only across an unchanged ranking. `project` resolves through the shared up-resolution (dec-spor-queue-slug-resolves-to-grouping): a bare repo slug unions its home-project grouping's member queues, the repo NODE id (`repo-<slug>`) pins one repo, a grouping id (`proj-<slug>`) is used directly; **omitting `project` is the cross-project firehose** (every repo's queue at once). `assignee=<person-id>` scopes to the work that person carries (their `assigned`/`stewards` edges) — a manager's "who is carrying what"; `assignee=me` binds to the caller (empty if the token maps to no person node). `type=`/`exclude_type=` (comma-separated, repeatable) whitelist/blacklist node types from the ranking (exclude wins on overlap) — a hard scope filter applied before scoring, so the aggregates describe the filtered queue (task-cc-queue-filtering-enhancements) |
|
|
@@ -419,14 +422,17 @@ endpoint is the REST twin of a core call:
|
|
|
419
422
|
| `POST /v1/questions` `{text, title?, mentions?, project?}` | ask_question's REST twin | file a question node; deterministically routed to the steward of the closest relevance-neighborhood node, unrouted if none → 201 `{status, id, project, routed_to, via, asker, revision, warnings}`. `project` is derived from the relevance neighborhood (then the asker's home project) unless an explicit `project` slug overrides it — pass that for a mention-less question (a dispatched agent injects its session project); a malformed slug → 400 |
|
|
420
423
|
| `POST /v1/gardener` | ops cron / on demand; `spor admin gardener` | run a gardener sweep now; findings filed as queue items → `{checked, filed, resolved, skipped, generated_at}` (`filed`/`resolved`/`skipped` are id lists, `checked` a count). The `spor admin gardener [--json]` CLI verb is the shell front-door (remote-only — the server owns the gardener); authenticated but **not** admin-gated server-side today (unlike `/v1/backup`), so any valid team token can trigger it — the verb still surfaces a 403 as an admin-privilege (stewards→root) hint for a deployment that adds the gate |
|
|
421
424
|
| `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 |
|
|
422
|
-
| `POST /v1/lens/{id}/ticket` `{expires?}` | sharing a view | 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 |
|
|
425
|
+
| `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 |
|
|
423
426
|
| `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) |
|
|
424
|
-
| `GET /v1/admin/tokens` | offboarding / audit | list PATs → `{tokens: [{hash_prefix, person, name, email, created, expires, expired}], count}` — never plaintext, never full hashes. Admin-only (§4) |
|
|
425
|
-
| `POST /v1/admin/tokens` `{person, expires?}` | onboarding | 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 |
|
|
426
|
-
| `DELETE /v1/admin/tokens/{hash-prefix}` | offboarding / rotation | revoke the single PAT matching the hash prefix (≥8 hex chars; an ambiguous prefix is a 409) → `{revoked, hash_prefix}`. Admin-only |
|
|
427
|
+
| `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 |
|
|
428
|
+
| `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 |
|
|
429
|
+
| `DELETE /v1/admin/tokens/{hash-prefix}` | offboarding / rotation; `spor admin token revoke` (= `spor token revoke --all`) | revoke the single PAT matching the hash prefix (≥8 hex chars; an ambiguous prefix is a 409) → `{revoked, hash_prefix}`. Admin-only. Revokes ANY token; the self-serve revoke (the caller's own) is `DELETE /v1/me/tokens/{hash-prefix}` above |
|
|
427
430
|
| `GET /v1/agents` | `spor agent list` | list the agents the caller **owns** → `{agents: [{id, label, owner, spiffe, pubkey, status}], count}`; `?all=1` lists every agent (admin-only) |
|
|
428
|
-
| `POST /v1/
|
|
429
|
-
| `POST /v1/agents
|
|
431
|
+
| `POST /v1/agents` `{label, id?, pubkey?}` | `spor agent create` | **self-serve** (NOT admin): create an `agent` node owned by the CALLER's bound person + its `owned-by` edge (owner is never payload-asserted; `id` derives from `label`) → 201 `{id, owner, spiffe, pubkey, status, revision}` (the shared `createAgentNode` body the admin door also runs). 409 dup id / 422 invalid / `403` if the caller maps to **no person node** (you need a subject to own one). The default door for `spor agent create`; the admin `POST /v1/admin/agents` is reached only by `--owner <other>` (task-spor-app-agents-self-serve-create) |
|
|
432
|
+
| `POST /v1/admin/agents` `{label, owner?, id?, pubkey?}` | `spor agent create --owner <other>`, onboarding | create an `agent` node + `owned-by` edge on behalf of ANOTHER person (`owner` defaults to the caller's person; `id` derives from `label`) → 201 `{id, owner, spiffe, pubkey, status, revision}`. 409 dup id / 422 invalid / 403 non-admin. Admin-only, same `stewards→root` gate as `/v1/admin/tokens`. The self-serve `POST /v1/agents` above is the owner=caller path |
|
|
433
|
+
| `POST /v1/agents/{id}/token` `{session?, audience?, expires?, standing?, label?}` | `spor dispatch` (per-session), `spor agent token <id>` (standing) | **self-serve** (NOT admin), ownership-gated (the caller's person **owns** the agent — else `403`; `404` unknown agent). Two modes. **Per-session** (default): mint a short-TTL token scoped to agent `{id}` → 201 `{token, expires_at, agent, session}` (`session: null` when deferred); `session` is OPTIONAL (deferred binding, dec-spor-dispatch-bg-session-late-bind) — `spor dispatch` mints it deferred because `claude --bg` self-allocates the run session only at launch, then binds the real one via `POST /v1/agents/session` below; a caller `expires` may only SHORTEN the default TTL, never extend past the 7d cap; `422` on a malformed SUPPLIED `session`. **Standing** (`{standing:true}`, task-spor-app-standing-agent-pat): mint a long-lived agent-scoped `spor_pat_` — the durable `SPOR_TOKEN` a headless agent (Claude Code on the Web) runs under → 201 `{token, hash_prefix, agent, owner, label, expires, standing:true}`; user-set `expires` defaults to and is **capped at 1 year** (a past/beyond-cap date is `422`, rejected not clamped), `label` is an optional ≤200-char note, a supplied `session` is `422` (a standing credential carries none); listable/revocable via `/v1/agents/{id}/tokens` below. Both modes: a write under the token is stamped agent-on-behalf-of-person (§1) |
|
|
434
|
+
| `GET /v1/agents/{id}/tokens` | `spor agent token <id> list` | list the agent's STANDING PATs → `{tokens: [{hash_prefix, label, standing, created, expires, expired, last_used, …}], count}` (short per-session dispatch tokens are excluded — they age out on their own). Same OWNERSHIP gate as the mint (the agent's owner — else `403`; `404` unknown agent) |
|
|
435
|
+
| `DELETE /v1/agents/{id}/tokens/{hash-prefix}` | `spor agent token <id> revoke` | revoke one of the agent's standing PATs by hash prefix → `{revoked, hash_prefix, oauth_grants_revoked}`; a prefix that isn't one of THIS agent's standing PATs is `404` (never a session token or another agent's PAT). Same OWNERSHIP gate as the mint — revocable per-environment without touching the owner's other access |
|
|
430
436
|
| `POST /v1/agents/session` `{session}` | `spor dispatch` (post-launch) | **late session binding** for a session-deferred agent token (dec-spor-dispatch-bg-session-late-bind). Authenticated by the **agent token itself** (the bearer hash identifies its own record — no agent id in the path, no ownership re-check), so only an agent-scoped token may call it (`403` otherwise). Sets that token's `session` → `200 {ok, agent, session}`. **Write-once**: idempotent on the same value (`{unchanged: true}`), `409 conflict` on a different one (a token's session is provenance, not a mutable field); `422` missing/malformed `session`. Every subsequent write under the token then stamps the bound session |
|
|
431
437
|
| `POST /v1/agents/{id}/capabilities` `{harnesses?, reachable_mcp?, skills?, plugins?, deny?}` | `spor capabilities publish`, session-start auto-publish | **publish** this box's machine capabilities to the fleet scheduler (task-spor-remote-fleet-scheduler, dec-spor-machine-profile-satisfiability). The remote twin of the machine-local `dispatch.capabilities` map: the server collapses the body with the SAME `effectiveCapabilities()` the client runs (so a raw `{probed,declared,deny}` map or the already-flat axes both work; also accepts a `{capabilities: {...}}` envelope) and stores it BESIDE the agent node (operational store, not the durable git-tracked node — capabilities are machine-local, probe-refreshed, never committed) → `200 {agent, capabilities, published_at, last_seen, published_by, session?, changed}`. Authorized iff the caller **owns** the agent (its `owned-by` edge) OR is the agent itself (a self-publish under an agent token) — else `403`; `404` unknown agent; `422` a malformed map. A publish stamps both `published_at` (when the CAPS last changed) and `last_seen` (last contact); `last_seen` ALSO advances on the cheap `POST .../heartbeat` below, and the host-match keys staleness off `last_seen` not `published_at`. Beyond the manual verb, `session-start` AUTO-publishes here in remote mode whenever a `dispatch.agent` is configured (task-spor-fleet-capabilities-autopublish-session-start) — bounded + fail-open, so every session refreshes this box's caps and last-contact without a manual call; disable with `SPOR_CAPABILITIES_PUBLISH=0` |
|
|
432
438
|
| `GET /v1/agents/{id}/capabilities` | `spor capabilities show <agent>`, steward fleet view, debugging | read back an agent's published capabilities → `200 {agent, capabilities, published_at, last_seen, published_by, session?}`; `404` if none published. Readable by the **owner**, the **agent itself**, or an **admin** (a stewards→root fleet-capacity view) — else `403`. The CLIENT reader (task-spor-capabilities-read-agent-cli-verb): `spor capabilities show <agent-id>` (`me` = this box's `dispatch.agent`) renders the stored caps + timestamps without raw REST — the read twin of `spor capabilities publish` and the per-agent companion to `spor capabilities hosts`; remote-only, fail-soft |
|
package/GRAPH.md
CHANGED
|
@@ -438,9 +438,10 @@ against the session's OWN repo (task-cc-norm-ride-along-repo-tag-scope):
|
|
|
438
438
|
ANY within an axis — deliberately unlike the policy layer's `governs`
|
|
439
439
|
(AND-across-axes). A norm that declares any `applies_to_*` and matches none is
|
|
440
440
|
**excluded** (strict, including in a repo with no `tags` — repo tagging is the
|
|
441
|
-
opt-in that turns scoped norms on
|
|
442
|
-
|
|
443
|
-
|
|
441
|
+
opt-in that turns scoped norms on; set them with `spor repos tag <slug>
|
|
442
|
+
<tag...>` rather than hand-editing the `repo-<slug>` node); a norm with none
|
|
443
|
+
keeps the project-scoped behavior above, so a graph using no `applies_to_*` is
|
|
444
|
+
byte-identical (norm-cc-byte-identical-refactor).
|
|
444
445
|
|
|
445
446
|
Because a norm rides along with no relevance gate and the team trust model lets
|
|
446
447
|
every writer author one, the briefing renderer treats norm bodies as an
|
package/QUEUE.md
CHANGED
|
@@ -439,8 +439,17 @@ signals via its schema's `queueSignals()`:
|
|
|
439
439
|
(task-spor-git-derived-timestamp-index); like `needed_by_days` it rides along
|
|
440
440
|
ONLY when the index is present and the count is >0, so the queue is
|
|
441
441
|
byte-identical without it (conformance, the prompt path, a non-git home). It is
|
|
442
|
-
surfaced
|
|
443
|
-
|
|
442
|
+
**surfaced-not-scored by decision** (dec-spor-cold-neighbors-suggestion-only,
|
|
443
|
+
task-spor-cold-neighbors-weight-conformance): it contributes **0** to the
|
|
444
|
+
default blend (signal + why-line only, the staleness posture), because the
|
|
445
|
+
direction is ambiguous (cold-with-moving-neighbors argues for revisit *or* for
|
|
446
|
+
closing) and the count scales with neighbor degree like heat once did, so a
|
|
447
|
+
default weight needs a chosen sign, log-compression, and empirical validation
|
|
448
|
+
the client has no harness for — while the server gardener's cold-work finder
|
|
449
|
+
already escalates genuine cold work as a ride-along finding. The raw count stays
|
|
450
|
+
on `signals.cold_neighbors` so an org `queue-policy` rank() can weight it; the
|
|
451
|
+
`conformance/cold-neighbors` goldens lock the 0-weight contract (a future
|
|
452
|
+
default weight is the change that moves them).
|
|
444
453
|
- **age**, and any org-specific signal the schema's code adds (SLA clocks,
|
|
445
454
|
sprint membership, whatever the org's process actually is).
|
|
446
455
|
|
package/README.md
CHANGED
|
@@ -140,8 +140,9 @@ The loop runs without you having to drive it:
|
|
|
140
140
|
|
|
141
141
|
You can also ask for any of this directly: an on-demand briefing for a task,
|
|
142
142
|
a correction when a briefing was wrong, a capture of work you're deferring,
|
|
143
|
-
and a ranked queue of what to do
|
|
144
|
-
`/spor:brief`, `/spor:correct`,
|
|
143
|
+
a question filed when the graph can't answer, and a ranked queue of what to do
|
|
144
|
+
next. In Claude Code these surface as `/spor:brief`, `/spor:correct`,
|
|
145
|
+
`/spor:defer`, `/spor:ask`, and `/spor:next`, plus
|
|
145
146
|
`/spor:backfill` to bootstrap/extend the graph and organize repos into projects.
|
|
146
147
|
(`/spor:backfill` is the discoverable door; the heavy git-history mining still
|
|
147
148
|
runs in the `spor-backfill` subagent it dispatches.)
|