@sporhq/spor 0.29.2 → 0.29.3
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 +31 -5
- package/GRAPH.md +4 -1
- package/bin/spor-hook.js +3 -1
- package/bin/spor.js +316 -4666
- package/lib/analytics.js +9 -8
- package/lib/changes.js +10 -2
- package/lib/config.js +220 -15
- package/lib/graph.js +20 -26
- package/lib/kernel/coupling.js +16 -10
- package/lib/kernel/gates.js +176 -1
- package/lib/kernel/graph.js +412 -422
- package/lib/kernel/queue.js +75 -0
- package/lib/kernel/ranker.js +184 -0
- package/lib/kernel/registry.js +32 -0
- package/lib/kernel/resolution.js +137 -101
- package/lib/kernel/satisfiability.js +19 -5
- package/lib/kernel/tokenizer.js +39 -3
- package/lib/queue.js +51 -31
- package/lib/remote.js +4 -0
- package/lib/schema.js +1 -1
- package/lib/seed/candidates/schema-factory.md +9 -1
- package/lib/seed/candidates/schema-gate.md +18 -1
- package/lib/seed/schema-issue.md +16 -2
- package/lib/seed/schema-task.md +16 -2
- package/lib/shell/agent-dispatch-runner.js +52 -73
- package/lib/shell/candidate-publish.js +17 -6
- package/lib/shell/ci-gate.js +229 -0
- package/lib/shell/completion.js +1 -1
- package/lib/shell/dispatch.js +1463 -0
- package/lib/shell/gate-deps.js +2585 -0
- package/lib/shell/gate-runner.js +54 -15
- package/lib/shell/git-exec.js +80 -5
- package/lib/shell/integration-runner.js +25 -0
- package/lib/shell/local-execution-lock.js +7 -7
- package/lib/shell/preflight.js +1 -1
- package/lib/shell/process-identity.js +89 -0
- package/lib/shell/seed.js +40 -0
- package/lib/shell/spool.js +6 -7
- package/lib/shell/work-outcome.js +228 -0
- package/lib/shell/work.js +936 -0
- package/package.json +1 -1
- package/scripts/engines/distill.js +42 -23
- package/scripts/engines/drain-outbox.js +22 -16
- package/scripts/engines/post-tool.js +4 -4
- package/scripts/engines/prompt-context.js +23 -10
- package/scripts/engines/session-start.js +36 -7
- package/scripts/engines/util.js +182 -14
- package/skills/factory/references/emitting.md +12 -1
- package/skills/spor/references/authoring-schemas.md +5 -2
|
@@ -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.29.
|
|
5
|
+
"version": "0.29.3",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "losthammer"
|
|
8
8
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spor",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.3",
|
|
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
|
@@ -638,7 +638,7 @@ agent subject sees no graph content; a coarse read-only/CI flag is not a bypass.
|
|
|
638
638
|
| Endpoint | Typical caller | Semantics |
|
|
639
639
|
|---|---|---|
|
|
640
640
|
| `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 |
|
|
641
|
-
| `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, resolution, 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. `resolution` is the per-type ATTESTATION path (issue-spor-offline-check-get-hook-resolution-proxy): `edge` — this type's completion is attested only by a live inbound resolving edge — or `status` — its own terminal status retires it — or `null` when the schema declares neither, which is a resident schema authored before the key existed (a reader falls back to the legacy proxy, `hooks` containing `get`) rather than a graph asserting either answer. A client verifying a dispatched run's outcome reads this, never the `hooks` array. The REST/MCP twin of the `spor schema` CLI: all three render one `graph.registry.snapshot()` so they never drift |
|
|
641
|
+
| `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, resolver_types, resolution, 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 (`resolver_types`: which node types count as that resolver, `[]` when undeclared). 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. `resolution` is the per-type ATTESTATION path (issue-spor-offline-check-get-hook-resolution-proxy): `edge` — this type's completion is attested only by a live inbound resolving edge — or `status` — its own terminal status retires it — or `null` when the schema declares neither, which is a resident schema authored before the key existed (a reader falls back to the legacy proxy, `hooks` containing `get`) rather than a graph asserting either answer. A client verifying a dispatched run's outcome reads this, never the `hooks` array. The REST/MCP twin of the `spor schema` CLI: all three render one `graph.registry.snapshot()` so they never drift |
|
|
642
642
|
| `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 |
|
|
643
643
|
| `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) |
|
|
644
644
|
| `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) |
|
|
@@ -672,7 +672,7 @@ agent subject sees no graph content; a coarse read-only/CI flag is not a bypass.
|
|
|
672
672
|
| `POST /v1/capture` | distill, /spor:defer | `capture` semantics: `{text, context: {project, project_explicit?, 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). `spor add --dedupe-key <key>` promotes a CALLER-chosen key into that slot instead of the per-invocation UUID (task-spor-add-dedupe-key-first-class), so a caller that re-files the same logical capture across separate invocations — a cron monitor re-alerting on one onset — dedupes too: within the window the repeat replays and the response carries `idempotent_replay: true`. The key is caller-supplied only, never derived from the text, and must match the server's key grammar (`^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$`) — the client rejects anything else rather than let the server silently run the capture unguarded. `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. `context.project_explicit` (additive boolean, task-spor-thread-explicit-project-flag) distinguishes a user-declared `context.project` from an ambient cwd default: only a literal `false` silences the fold-mismatch warning on a cross-project capture; **absent means explicit** (old-client back-compat, so a pre-flag client keeps today's warn-on-mismatch behavior). An additive `auto_write_id` rides the response when a flag-gated near-miss fold (`SPOR_GARDENER_AUTO_WRITE`, task-spor-capture-near-miss-ledger-and-undo) auto-wrote a merged capture-pending close or a body-append elaboration in place of filing to triage — the ledger id `POST /v1/gardener/auto-writes/undo` takes to reverse it; absent when no near-miss fold fired |
|
|
673
673
|
| `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 |
|
|
674
674
|
| `POST /v1/corrections` | /spor:correct | `propose_correction` semantics → 201 `{status, id, revision, warnings}` |
|
|
675
|
-
| `GET /v1/queue?project=&assignee=&type=&exclude_type=&readiness=&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, counts_by_readiness?, muted?, dormant?, questions, asked, findings, pending, reviews, policy?, project_warning?, 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). A `project` token that matches no repo, grouping, or project stamp on any resident node is still `200` with empty `items` — it rides back as the additive `project_warning` string (the same field and wording shape as `/v1/analytics`; absent when the scope is known, even if legitimately empty), the remote twin of local `spor next`'s projectKnown() check: remote `spor next`, `spor work`, and `spor dispatch --from-queue` print it verbatim on stderr and strip it from the envelope so `--json` matches local (norm-spor-cli-mode-parity, task-spor-remote-next-print-project-warning). `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). `readiness=` (comma-separated, repeatable; `agent`\|`human`\|`untriaged`) is the agent-readiness twin — a hard scope filter on the derived classification (dec-spor-agent-readiness-derived-classification, QUEUE.md "Agent-readiness"); an unknown value is `422 invalid_node`. `counts_by_readiness` (`{agent, human, untriaged}`) rides the envelope whenever the graph carries readiness signal or a `readiness=` facet was asked for — the remote twin of local `spor next`'s readiness lead line, forwarded by remote `spor next --readiness` (task-spor-queue-remote-readiness-ignored) |
|
|
675
|
+
| `GET /v1/queue?project=&assignee=&type=&exclude_type=&readiness=&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, counts_by_readiness?, muted?, dormant?, questions, asked, findings, pending, reviews, policy?, project_warning?, 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). A `project` token that matches no repo, grouping, or project stamp on any resident node is still `200` with empty `items` — it rides back as the additive `project_warning` string (the same field and wording shape as `/v1/analytics`; absent when the scope is known, even if legitimately empty), the remote twin of local `spor next`'s projectKnown() check: remote `spor next`, `spor work`, and `spor dispatch --from-queue` print it verbatim on stderr and strip it from the envelope so `--json` matches local (norm-spor-cli-mode-parity, task-spor-remote-next-print-project-warning). `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). `readiness=` (comma-separated, repeatable; `agent`\|`human`\|`untriaged`) is the agent-readiness twin — a hard scope filter on the derived classification (dec-spor-agent-readiness-derived-classification, QUEUE.md "Agent-readiness"); an unknown value is `422 invalid_node`. `counts_by_readiness` (`{agent, human, untriaged}`) rides the envelope whenever the graph carries readiness signal or a `readiness=` facet was asked for — the remote twin of local `spor next`'s readiness lead line, forwarded by remote `spor next --readiness` (task-spor-queue-remote-readiness-ignored). **This body is the canonical queue envelope** (task-spor-local-remote-single-renderer-conformance): every field except the per-viewer routing lists and the stamp (`awaiting_you`/`questions`/`asked`/`findings`/`pending`/`reviews`/`generated_at`, `SERVER_ONLY_QUEUE_FIELDS`) is shaped by the client kernel's `shapeQueueEnvelope`/`queueAggregates` (lib/kernel/queue.js) and the warning by `unknownProjectWarning` (lib/kernel/graph.js); local `spor next --json` emits the same envelope over a local graph, both modes render it through ONE `renderReport` (lib/queue.js), and test/mode-parity.test.js diffs the two against a stub of this route |
|
|
676
676
|
| `GET /v1/analytics?project=&type=&weeks=&top=&aging=&format=` | remote `spor analytics`, the `analytics` MCP tool | work-flow analytics — the SERVER twin of the local-only `spor analytics` consumer, for a remote/Cowork teammate with no local graph repo to fold (task-spor-server-analytics-surface): created-vs-completed weekly cohorts, throughput, cycle-time median/p90, current WIP by node type, and the oldest-open bottlenecks, computed by the pure analytics kernel over the resident graph + a HEAD-keyed status-transition fold. **Completion is a node's status-TRANSITION time** (when it entered its final terminal run, from git content history), never `updated_at`, so a later edge append can't corrupt the "completed last week" signal (dec-spor-git-derived-timestamps). Default returns the machine (JSON) report `{window, weekly, totals, throughput, cycleTimeDays, wip, bottlenecks, coverage}`; `?format=text` renders the human report. `project` resolves through the shared up-resolution like `/v1/queue` (bare repo slug → grouping union; `repo-<slug>`/`proj-<slug>` id pins) — a zero-match scope rides back as the additive `project_warning` field (text mode prefixes a `# ` line). `type=` (comma-separated, repeatable) restricts node types; `weeks`/`top`/`aging` shape the window (clamped 1–52 / 1–100 / 1–365). A bad slug/type is `422`. The remote arm of `spor analytics` fetches the JSON and renders it with the SAME `renderReport` the local consumer uses, so remote and local output match (norm-spor-cli-mode-parity, task-spor-analytics-remote-cli-dispatch) |
|
|
677
677
|
| `GET /v1/metrics/capture?since=` | the cross-author capture-discipline eval harvest (task-spor-tenant-capture-metrics-export) | capture-discipline aggregates for an **opted-in** deployment — the same kernel the dogfood CLI runs (`lib-engine/kernel/capture-metrics.js`), computed server-side over the resident graph plus the FULL request journal (every rotated `server.log` segment). Three gates stack (dec-spor-tenant-metrics-aggregates-only): the per-machine opt-in env `SPOR_METRICS_EXPORT` (unset → the route 404s, so a never-opted tenant shows no surface), admin auth (stewards→root, 403), and **unconditional redaction** — the body carries counts/rates only: by-identity keys are stable per-tenant pseudonyms (`author-<hash12>`, salted at `cache/metrics-salt` so per-author trends survive across windows), closure entries keep `{edge, latency_days}` but drop node ids, and id lists reduce to `open_count`/`slug_smell_count`. No journal lines, node bodies, or capture prose ever exit. `?since=YYYY-MM-DD` bounds the window (malformed → `422`) |
|
|
678
678
|
| `POST /v1/questions` `{text, title?, mentions?, project?, to?}` | ask_question's REST twin | file a question node; `to` (a person node id) routes it there directly, ahead of every inferred signal — an unknown or non-person id is `400`, and naming the asker themselves falls through to ordinary routing with a warning instead of a dead-end. Absent `to`, routing is deterministic over the mentioned nodes' live claim holder, `assigned` edge, then author, then an explicit `stewards` edge, then a one-hop neighbor of those, then a text-derived steward walk, then (skipping a person-direct asker throughout) the tenant-owner fallback — unrouted if nothing matches → 201 `{status, id, project, routed_to, via, routed_by, asker, revision, warnings}`. `routed_by` names which signal won (`explicit`\|`steward`\|`claim`\|`assigned`\|`author`\|`owner`, `null` if unrouted); `warnings` carries a line when the owner fallback fired, the question is genuinely unrouted, or an explicit `to` named the asker themselves. `GET /v1/status`'s `capabilities.ask_question_explicit_to` advertises this field — `spor ask --to <person-id>` sends it there when present, else falls back to the pre-capability leading-mention nudge. `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 |
|
|
@@ -927,11 +927,15 @@ as if the graph is empty" (§6).
|
|
|
927
927
|
A `429 rate_limited` response SHOULD carry a `Retry-After` header (delay
|
|
928
928
|
seconds or an HTTP-date); clients honor it, otherwise backing off
|
|
929
929
|
exponentially, capped, before retrying. Mechanical writers
|
|
930
|
-
(drain-outbox, distill) classify `401`, `400`, `413`, and `422` as
|
|
931
|
-
**permanent**
|
|
930
|
+
(drain-outbox, distill) classify `401`/`403`, `400`, `413`, and `422` as
|
|
931
|
+
**permanent** (one shared classifier, `classifyHttpFailure` in
|
|
932
|
+
`scripts/engines/util.js`, which session-start also reads its banner from) — a revoked token will not un-revoke, so these are
|
|
932
933
|
dead-lettered to `outbox/dead/` with a loud `journal/remote.log` line
|
|
933
934
|
rather than re-POSTed forever; `429` and `5xx` stay transient and are
|
|
934
|
-
retried with backoff.
|
|
935
|
+
retried with backoff. Before a `401`/`403` is read as permanent, a writer
|
|
936
|
+
whose tenant carries a `refresh_token` refreshes it once and retries the
|
|
937
|
+
POST (`curlWithRefresh`, the same refresh the CLI does, §6.2) — an expired
|
|
938
|
+
short-lived token is not a revoked one.
|
|
935
939
|
|
|
936
940
|
`GET /v1/export` response headers: `x-substrate-head` carries the graph
|
|
937
941
|
commit, `x-substrate-node-count` the entry count (plus `x-substrate-auth-files`
|
|
@@ -1111,8 +1115,30 @@ machine-local — never committed, always in the shared-graph `.gitignore`):
|
|
|
1111
1115
|
`journal/remote.log`. Under an explicit `mode: local`/`off` no tenant is
|
|
1112
1116
|
consulted, so a stray ambient org is moot there. `spor config explain` shows
|
|
1113
1117
|
which selector chose (or refused) the tenant.
|
|
1118
|
+
- **A repo `server` gets only a credential recorded for it.** A committed repo
|
|
1119
|
+
`.spor.json` may set `server` (how a repo points contributors at a team
|
|
1120
|
+
server), but the flat `token` is never repo-sourced — it was recorded beside
|
|
1121
|
+
the server the user/global config names. When the repo layer wins `server`
|
|
1122
|
+
(legacy flat path, or an ambient org satisfied by it), that token is sent only
|
|
1123
|
+
if its recorded server equals the repo's; otherwise a store credential
|
|
1124
|
+
recorded for exactly that server is used, else the cascade refuses
|
|
1125
|
+
(`server-mismatch`, naming both servers) the same way an unknown ambient org
|
|
1126
|
+
does (dec-spor-repo-server-key-requires-matching-credential). `spor auth
|
|
1127
|
+
login` is exempt and defaults to the repo's server — signing in there is the
|
|
1128
|
+
cure, and that credential is stored for that server only, not as the store
|
|
1129
|
+
default (which would move every other repo onto it). A repo `server` with no credential on hand at all resolves tokenless.
|
|
1114
1130
|
- **Refresh.** A 401/403 on a tenant carrying a `refresh_token` transparently
|
|
1115
1131
|
refreshes against its issuer (`grant_type=refresh_token`) and retries once.
|
|
1132
|
+
Only the store tenant's OWN bearer carries one: an explicit `SPOR_TOKEN` /
|
|
1133
|
+
`--token` that is not the stored `access_token` for that server — a
|
|
1134
|
+
dispatched agent's scoped token under the person's HOME is exactly this
|
|
1135
|
+
shape — resolves with no `refresh_token`, no store `key`, and its own JWT
|
|
1136
|
+
`org` claim, so an agent's 401 is never retried as the person
|
|
1137
|
+
(issue-spor-agent-token-scope-escalation-via-refresh-and-store-default).
|
|
1138
|
+
For the same reason `spor dispatch` exports `SPOR_SERVER` beside the
|
|
1139
|
+
agent-scoped `SPOR_TOKEN` to every remote-mode harness child: without the
|
|
1140
|
+
server the child's cascade never reads the env token and selects the store
|
|
1141
|
+
default — the person — instead.
|
|
1116
1142
|
- **Byte-identical.** With no credential store and only a flat
|
|
1117
1143
|
`server`+`token` or `SPOR_*` env set, every resolved value equals the prior
|
|
1118
1144
|
single-tenant behavior (norm-cc-byte-identical-refactor).
|
package/GRAPH.md
CHANGED
|
@@ -352,7 +352,10 @@ keys say, as registry data, what the hooks below enforce: `status.vocabulary`
|
|
|
352
352
|
the full set *including* the give-up outcomes `abandoned`/`superseded`/
|
|
353
353
|
`rejected`), and `status.resolver_required` (whether reaching that value also
|
|
354
354
|
demands a live resolving `decision`/`artifact`, the completion-resolver
|
|
355
|
-
invariant)
|
|
355
|
+
invariant), with `status.resolver_types` naming WHICH node types count as that
|
|
356
|
+
resolver (task/issue: `["decision", "artifact"]`; only legal beside
|
|
357
|
+
`resolver_required: true`; read as `registry.resolverTypes(type)`,
|
|
358
|
+
task-spor-registry-sole-terminal-status-source). **Declaring them gates nothing** — the hooks are still the only
|
|
356
359
|
write door, and this is exactly why they are not a field list or an enforced
|
|
357
360
|
enum. They exist so a READER can name the right terminal status without
|
|
358
361
|
parsing hook source: the gardener's finding remedies used to keep hand-written
|
package/bin/spor-hook.js
CHANGED
|
@@ -97,7 +97,9 @@ function tenantRefused(cfg) {
|
|
|
97
97
|
if (Date.now() - last < 3600000) return true;
|
|
98
98
|
fs.writeFileSync(stamp, "");
|
|
99
99
|
const log = u.makeLogger(path.join(journal, "remote.log"), "config: ");
|
|
100
|
-
|
|
100
|
+
if (te.kind === "server-mismatch") log(`${require("../lib/config.js").describeTenantRefusal(te)} — hook skipped; run 'spor auth login' for that server or override it`);
|
|
101
|
+
else if (te.kind === "agent-org" || te.kind === "agent-no-token") log(`${require("../lib/config.js").describeTenantRefusal(te)} — hook skipped; an agent run never selects the person's stored credential`);
|
|
102
|
+
else log(`org '${te.org}' (from ${te.origin}) has no stored credential — hook skipped; run 'spor auth login --org ${te.org}' or fix the selector`);
|
|
101
103
|
} catch {
|
|
102
104
|
/* logging must never break fail-open */
|
|
103
105
|
}
|