@sporhq/spor 0.20.0 → 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.
@@ -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.20.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.20.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
@@ -95,6 +95,26 @@ schema may carry a `get(node, ctx)` hook (GRAPH.md) that attaches derived
95
95
  context as extra top-level keys — e.g. `resolution` (what answered/resolved
96
96
  this node). These are additive; a client ignores keys it does not know.
97
97
 
98
+ ### `explore_graph`
99
+
100
+ Browse/map the team graph's **structure** — a bounded neighborhood as plain
101
+ nodes + typed edges, each node carrying truth flags
102
+ (`superseded`/`resolved`/`blocked`) and a count of further unexpanded
103
+ neighbors (`more`). Input `{ "root_id"?, "query"?, "depth"?, "limit"? }` → the
104
+ view-tree slice (`view`, `node_ids`) plus a text rendering. Call with **no
105
+ arguments** for the birds-eye programs overview — every umbrella root (any
106
+ node other work `blocks`) with resolution-derived completion %, most complete
107
+ first. Pass `root_id` to walk outward from one node (depth 1-2, deterministic,
108
+ no LLM; default depth 1, limit 40 capped at 80); pass `query` instead to seed
109
+ the roots by relevance. The two are mutually exclusive; `root_id` wins when
110
+ both are given, and an unknown `root_id` is an error (`unknown_root`) rather
111
+ than an empty result — the same precedence `/v1/digest`'s `root`/`query` pair
112
+ uses (§3). Re-call with a neighbor's id as `root_id` to expand the frontier —
113
+ the browse/map twin of `query_graph`'s recursive deepen, for structure rather
114
+ than compiled digests. In MCP-Apps hosts this renders the
115
+ interactive graph navigator (lineage bands, expand/re-root, node inspector);
116
+ elsewhere it returns the same slice as text. **MCP-only — no REST twin.**
117
+
98
118
  ### `put_node`
99
119
 
100
120
  Input:
@@ -186,6 +206,49 @@ so an agent-set priority is distinguishable from human triage
186
206
  (issue-cc-priority-attribution-gap). The CLI wrapper is `spor priority <id>
187
207
  <p1|p2|p3|clear>`.
188
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
+
227
+ ### `reserve`
228
+
229
+ The fifth task-lease action (dec-cc-task-resumption-reservation), alongside
230
+ `claim`/`renew`/`extend`/`release` — all five share one ephemeral per-node
231
+ lease table and one REST route family (`POST /v1/nodes/{id}/<action>`, §3).
232
+ Converts your LIVE claim into an owner-exclusive **resumption reservation**
233
+ when a session ends cleanly with the task advanced but unfinished: the
234
+ heartbeat is dropped, `expires` is re-pointed at a grace-window expiry
235
+ (~2 days, tenant policy — a timestamp, not a graph edge), and the durable
236
+ `assigned` edge is kept (so a steward/capacity view still reads "reserved by
237
+ you"). Input `{ "id": "<task node id you hold a claim on>", "session"? }` →
238
+ `{ "ok": true, "status": "reserved", "lease", "grace_window_ms" }`.
239
+ `rankQueue` floats a reservation to the top of the owner's queue while
240
+ dropping it from teammates' actionable lists. Within the grace window the
241
+ reservation still counts as a live lease, so the owner claiming, renewing, or
242
+ extending it drops the `reserved` flag and re-establishes a normal Tier-1
243
+ heartbeat lease; once the grace window lapses the entry is gone (full pool,
244
+ everyone) and `renew`/`extend` return `409 lease_lost` same as any lapsed
245
+ lease — only a fresh `claim` picks the task back up. Reserving itself fails
246
+ `409 lease_lost` (naming the current holder) if you do not hold a live claim
247
+ on the node. The client SessionEnd hook
248
+ (task-cc-client-sessionend-reserve-hook) is the intended caller: it holds the
249
+ transcript, so it is the one thing that can tell "advanced but unfinished"
250
+ (→ reserve) from "finished" (→ release) apart.
251
+
189
252
  ### `propose_correction`
190
253
 
191
254
  Sugar over `put_node` for the correction loop. Input:
@@ -361,6 +424,18 @@ result whose prose says how to model the program (add `blocks` edges from the
361
424
  gating tasks). Unknown `id` errors with `{ "found": false, "error":
362
425
  "unknown_root" }`. The REST twin is `GET /v1/program/{id}` (§3).
363
426
 
427
+ ### `apply_lens_action`
428
+
429
+ App-only execution door for one declarative action on a saved lens's rendered
430
+ item — visible only to MCP-Apps hosts (`_meta.ui.visibility: ["app"]`), not a
431
+ tool a model calls directly. Input `{ "lens_id", "action_id", "target_id",
432
+ "params"? }`. The server re-runs the lens, verifies the target and action are
433
+ still eligible, resolves authenticated-viewer parameter bindings, and passes
434
+ the scalar update through the target node's schema `validate()`/`transitions()`
435
+ gate — the same write discipline as `set_status`, reached through a lens's
436
+ declarative action instead of a direct mutation call. **MCP-only — no REST
437
+ twin.**
438
+
364
439
  ### `recent_changes`
365
440
 
366
441
  The team's recent-activity feed — the temporal entry point the other read
@@ -425,6 +500,12 @@ additive: hosts without the apps surface ignore `_meta.ui` and show the text
425
500
  content. Write-path actions are not emitted; writes stay with the tools
426
501
  above.
427
502
 
503
+ `hello_mcp_app` is the minimal debug twin: a no-input, no-op tool that
504
+ renders a tiny hello-world widget, used only to check whether a host can
505
+ mount an MCP app resource for the Spor connector at all — it intentionally
506
+ bypasses the queue view-tree renderer and carries no graph semantics of its
507
+ own. **MCP-only — no REST twin.**
508
+
428
509
  ## 3. REST surface (`/v1/*`)
429
510
 
430
511
  Plain HTTPS + JSON, bearer auth on every route, versioned under `/v1/`. Each
@@ -449,10 +530,12 @@ endpoint is the REST twin of a core call:
449
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 |
450
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) |
451
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 |
452
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) |
453
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) |
454
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 |
455
537
  | `POST /v1/nodes/{id}/release` | `release` MCP tool, `spor release` CLI | drop the lease AND retire the durable `assigned` edge, returning the node to the pool. Idempotent (releasing a node you hold no lease on still succeeds, cleaning up any lingering `assigned` edge of yours); releasing a claim someone else holds is `409` naming the holder |
538
+ | `POST /v1/nodes/{id}/reserve` `{session?}` | `reserve` MCP tool, client SessionEnd hook (task-cc-client-sessionend-reserve-hook) | convert your live claim into an owner-exclusive resumption reservation (dec-cc-task-resumption-reservation) when a session ends cleanly with the task advanced but unfinished → `{ok, status: "reserved", lease, grace_window_ms}`. Drops the heartbeat, re-points `expires` at a grace-window expiry (~2 days, tenant policy — a timestamp, not a graph edge), and keeps the durable `assigned` edge so a steward view still reads "reserved by you"; `rankQueue` floats it to the top of the owner's queue while dropping it from teammates' actionable lists until the grace window lapses (full pool, everyone) or the owner claims/renews/extends it within that window (drops the `reserved` flag, back to a normal heartbeat lease). `409 lease_lost` (naming the holder) if you do not hold a live claim |
456
539
  | `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 |
457
540
  | `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`) |
458
541
  | `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 |
@@ -467,6 +550,7 @@ endpoint is the REST twin of a core call:
467
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 |
468
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 &lt;sharer&gt;" banner. The former `?token=<PAT>` sharing path is **removed** — a shared link can never carry a write-capable credential |
469
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) |
470
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) |
471
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 |
472
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 |
@@ -563,6 +647,23 @@ anything with a token.
563
647
  `{name, email}` attribution record. Access tokens are `spor_oat_…` (30d;
564
648
  legacy `sub_oat_…` accepted); refresh tokens are `spor_ort_…` (90d,
565
649
  rotating, single-use). Authorization codes are single-use, 10-minute.
650
+ - **Connector grant teardown — token-scoped revocation (RFC 7009).**
651
+ `POST /oauth/revoke` `{token, token_type_hint?}` ends exactly the grant
652
+ that `token` (access or refresh) belongs to and nothing else — the
653
+ caller's PATs and any other connector grants for the same identity are
654
+ untouched. This is the narrow, safe way to disconnect one MCP connector
655
+ (e.g. removing it from a host's settings), distinct from the identity-wide
656
+ cascades: `DELETE /v1/me/tokens/{hash-prefix}` (§3) revokes a PAT plus
657
+ every grant *it* minted, and the admin offboarding cascade revokes
658
+ *every* grant for a person — using either of those to "clean up one
659
+ connector" collaterally logs out the identity's other live sessions
660
+ (issue-spor-teardown-revoke-by-identity-logs-out-operator;
661
+ dec-spor-pat-revoke-cascade-token-scoped). Like the rest of the
662
+ `/oauth/*` surface it is unversioned and takes no bearer — public client,
663
+ the token being revoked is itself the credential. Per RFC 7009 §2.2 the
664
+ response is always `200` whether or not `token` was known (anything else
665
+ is an unauthenticated validity oracle), so success never confirms the
666
+ token existed.
566
667
  - **CLI interactive sign-in — the device authorization grant.** `spor auth
567
668
  login` (flat alias `spor login`) defaults to the OAuth 2.0 device
568
669
  authorization grant (RFC 8628), brokered at the Spor front door so it works
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 inline list of repo-qualified git shas
79
- (`commits: [wf@1a2b3c4d, ...]`, kebab-case repo slug + 7–40 hex) linking
80
- the node to the code commits that implement it (task-cc-commit-linking).
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
@@ -622,6 +624,10 @@ Each org slug has a durable `organization` node (`org-<slug>`, carrying
622
624
  an additional `stewards -> org-<slug>` edge records org-admin authority.
623
625
  `stewards -> org-root` keeps its distinct graph-wide operator meaning. Provider
624
626
  roles, token bits, and email-domain mappings do not confer either relation.
627
+ `org-root` is a **virtual** anchor — no node ever carries that id — so the
628
+ graph-wide lint (`spor validate` / `validateGraphFiles`) special-cases
629
+ `stewards -> <rootId>` (default `org-root`, override with `SPOR_ROOT_ID`) and
630
+ never flags it as a dangling edge.
625
631
 
626
632
  ```markdown
627
633
  ---
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