@sporhq/spor 0.27.0 → 0.28.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.27.0",
5
+ "version": "0.28.0",
6
6
  "author": {
7
7
  "name": "losthammer"
8
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spor",
3
- "version": "0.27.0",
3
+ "version": "0.28.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
@@ -619,6 +619,7 @@ agent subject sees no graph content; a coarse read-only/CI flag is not a bypass.
619
619
  | `POST /v1/queue/claim` `{ids, session?}` | `spor dispatch` claiming a working set; bulk-lease clients | **`claimAll`**: claim a whole working set in ONE round-trip instead of one `POST /v1/nodes/{id}/claim` per node (task-spor-bulk-claim-renew-apis). `ids` is REQUIRED (unlike `renewAll` below there is nothing to enumerate — a claim creates the lease it would have enumerated), bounded and deduped server-side. Each id runs through the same per-node `claim` logic, so one holder's node losing the race to another claimant lands in `failed` (`already_claimed`, naming the holder) while the rest of the batch still claims — a partial batch is reported, never rolled back → `{ok: true, status: "claimed"\|"partial"\|"refused", count, claimed: [ids], leases: [...], failed: [{node_id, code, message, holder?}], expires_in_ms?}`. `expires_in_ms` is the batch's own renewal horizon — the soonest deadline among the leases that landed, omitted (not nulled) on an empty or wholly-refused batch. `status` reads `"partial"` when some items landed and some didn't, `"refused"` when the whole batch was refused — **`ok: true` only means the call was well-formed; read `failed` for what didn't land, never `ok` alone**. The dispatch nonce and `force` (accepted on the singular `/claim`) are deliberately NOT accepted here — a dispatch tags a single agent launch at a single node, so that stays on the singular door |
620
620
  | `POST /v1/queue/renew` `{ids?, session?}` | post-tool heartbeat (batched), `spor dispatch`, bulk-lease clients | **`renewAll`**: the heartbeat for a whole working set in ONE round-trip (task-spor-bulk-claim-renew-apis) — what the client-side claim heartbeat uses instead of one `POST /v1/nodes/{id}/renew` per held node (task-spor-client-heartbeat-bulk-renew), through the **`ids`-omitted arm, with no `session` either** (dec-spor-heartbeat-adopts-blanket-renew-arm): a beat renews the person's whole live working set and never re-acquires what dropped out of it, and it omits `session` because in this arm that field is a FILTER, so sending it would skip the leases claimed outside a session (`spor claim`, `spor dispatch`'s pre-launch claim) and let them lapse mid-session. Two consequences ride with that choice: a beat renews the person's leases in EVERY project (this arm takes no project scope), so a lease nobody releases stops self-healing back into the pool at its TTL while its owner keeps writing anywhere; and since this arm SELECTS on `session` rather than stamping it, a lease's session binding is no longer re-pointed at the last editing session. Two modes, and they differ on auto-reclaim (dec-spor-lease-auto-reclaim-and-deadline-exposure — the ONE exemption, whose intended caller is the heartbeat): **`ids` omitted** enumerates every LIVE Tier-1 lease this caller holds (optionally narrowed to one `session`) and renews them all — the motivating case, one call per heartbeat instead of one per held node — but NEVER reclaims: its contract is "renew what you hold" from a snapshot, so a lease that lapsed (or was taken) lands in `failed` as `lease_lost` and simply drops out of the set, never silently re-acquired; **`ids` supplied** renews exactly that named set (bounded/deduped) with the SAME reclaim semantics as the singular `/renew` — a lapsed lease is auto-reclaimed (a real claim, durable `assigned` edge included), its id lands in `reclaimed`, and only a lease held by someone else still lands in `failed` as `lease_lost` (naming the holder). `session` is forwarded unchanged exactly as the singular `/renew` does — one contested node never costs the whole working set its heartbeat → `{ok: true, status: "renewed"\|"partial"\|"refused", count, renewed: [ids], leases: [...], failed: [...], expires_in_ms?, reclaimed?, skipped_other_session?, skipped_reserved?}`. `expires_in_ms` is the batch's own renewal horizon (soonest deadline among the leases that landed, omitted on an empty/wholly-refused batch); `reclaimed` (explicit-`ids` arm only) lists the ids whose lease had lapsed and was just re-established, so a batch reading "N/N renewed" doesn't hide that one of them was silently taken back off the pool. The two `skipped_*` counts ride ONLY on the enumerate arm: `skipped_other_session` names live leases excluded because they're bound to a different session (a zero renewed count there means "not under this session", not "you hold nothing"), `skipped_reserved` names Tier-2 resumption reservations a blanket heartbeat deliberately leaves parked at their grace-window expiry rather than demoting to a Tier-1 horizon |
621
621
  | `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 |
622
+ | `POST /v1/nodes/{id}/elaborations` `{text, note?, date?}` | scripts, mechanical writers filing into a running log | the deterministic elaboration append: the same fold the capture ELABORATE outcome lands through, minus the model — the server appends a dated `> elaboration (date): note` block to the node's body, following the `art-<stem>-<n>` continuation chain to its live tail and, when that tail is at the 8KB body cap, rolling the next continuation part itself and folding there (dec-spor-capture-fold-auto-spill-continuation) → `{status, id, revision, warnings, spilled_from?}`. `id` is where the text LANDED (a continuation when it spilled), `status` the underlying put's token (`updated` in place / `created` on a spill); `200` in place, `201` with `spilled_from` naming the full part when a spill rolled. `date` is `YYYY-MM-DD` (defaults to today); a blank `text` is `422`; a missing target `404`; an elaboration too large for even an empty part is `body_full`. No idempotency of its own — a re-sent elaboration appends again, so a caller that must land exactly once scans the family first (`scripts/harvest-file-datapoints.js` dedups by tenant + window) |
622
623
  | `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`) |
623
624
  | `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 |
624
625
  | `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) |
@@ -216,6 +216,14 @@ function normalizeGate(raw, { index = 0, source = "inline" } = {}) {
216
216
  if (!gate.profile) errors.push(`${at}: an agent-review gate needs a 'profile' to route the review to`);
217
217
  gate.instructions = typeof raw.instructions === "string" ? raw.instructions.trim() : "";
218
218
  gate.awaitMs = intOr(raw.await_ms, GATE_DEFAULTS.reviewAwaitMs, { min: 1000 });
219
+ // Risk classes ARM a review gate exactly as they arm a command or a human
220
+ // one (task-spor-review-gate-risk-arming). The precedent's argument is
221
+ // stronger here than anywhere else: an unarmed command gate saves a suite
222
+ // run on the worker's own box, while an unarmed review saves a whole agent
223
+ // dispatch — a model call, a lease, and an `await_ms` window up to the 1h
224
+ // default — for a review that had no diff in its subject area to reason
225
+ // about. The fail-closed boundaries it must NOT relax live in the runner.
226
+ gate.risk = arr(raw.risk !== undefined ? raw.risk : raw.risk_classes);
219
227
  } else if (kind === "human") {
220
228
  // Risk classes ARM the gate: an empty list means "always", a non-empty one
221
229
  // means the gate only applies when the change touched one of those classes.
@@ -459,11 +467,13 @@ function parseFactory(body, { gateNodes = new Map(), id = null, project = null }
459
467
  // outcome "fails closed" must never mean.
460
468
  errors.push("protected_paths are declared but 'test_lane_profile' is not — a protected-path hit has no separate lane to route to");
461
469
  }
462
- // Every human gate's risk class must be declared, or it can never arm and the
463
- // gate is decoration. Caught here rather than at runtime, where a silently
464
- // never-arming approval gate reads exactly like an approved one.
470
+ // Every gate's risk class must be declared, or it can never arm and the gate
471
+ // is decoration. Caught here rather than at runtime, where a silently
472
+ // never-arming approval gate reads exactly like an approved one. This runs
473
+ // over EVERY kind rather than a list of the ones that arm: a kind whose
474
+ // branch does not parse `risk` leaves it undefined, so the inner loop is a
475
+ // no-op there, and a kind that gains arming later is covered by construction.
465
476
  for (const g of gates) {
466
- if (g.kind !== "human" && g.kind !== "command") continue;
467
477
  for (const cls of g.risk || []) {
468
478
  if (!riskClasses[cls]) errors.push(`gate '${g.id}': risk class '${cls}' is not declared in 'risk_classes'`);
469
479
  }
@@ -2,7 +2,7 @@
2
2
  id: schema-factory
3
3
  type: schema
4
4
  kind: node-schema
5
- schema_version: 2026.09.03.1
5
+ schema_version: 2026.09.04.1
6
6
  title: Software-factory definition
7
7
  summary: A factory definition — the ordered gate list a worker enforces between claim and resolve, plus the trusted ref, the repos it may judge, protected test paths, test-change lane, risk classes those gates key on, an optional integration (merge-queue landing) stage, and an optional rescue lane (a strong-model step before any human escalation). Candidate pack; adopt it into a graph to use `spor work --factory`.
8
8
  date: 2026-08-26
@@ -64,9 +64,9 @@ convention schema nodes use:
64
64
  touches one fails its command gate CLOSED (unrun) and routes to
65
65
  `test_lane_profile`; declaring paths without a lane is an error, because
66
66
  "fails closed" must never mean "dropped on the floor".
67
- - `risk_classes` — named path predicates a `human` gate keys on. A human gate
68
- naming an undeclared class is an error: a gate that can never arm reads
69
- exactly like an approved one.
67
+ - `risk_classes` — named path predicates a gate of ANY kind keys on to ARM
68
+ itself (command, agent-review, human). A gate naming an undeclared class is
69
+ an error: a gate that can never arm reads exactly like an approved one.
70
70
  - `gates[].cycles` — how many implementer fix cycles a failing gate gets before
71
71
  the runner escalates by filing a human queue item. Default 0.
72
72
  - `gates[].reruns` (command gates) and `integration.reruns` — how many times a
@@ -2,7 +2,7 @@
2
2
  id: schema-gate
3
3
  type: schema
4
4
  kind: node-schema
5
- schema_version: 2026.08.26.1
5
+ schema_version: 2026.09.04.1
6
6
  title: Shareable factory gate
7
7
  summary: One reusable gate — command, agent-review or human — that any factory definition can reference by id, so an org vets a gate once (a `gate-security-review`) and reuses it product-wide instead of copying it into every factory.
8
8
  date: 2026-08-26
@@ -45,11 +45,16 @@ Keys by kind:
45
45
  cannot quietly relax another team's trusted boundary.
46
46
  - **agent-review** — `profile` (required: the review lane, cross-model by
47
47
  convention; the machine's declared binding decides what that actually
48
- executes), `instructions`, `await_ms`. The reviewer answers with a fenced JSON
49
- verdict which the runner parses in code — an unreadable verdict is a failure,
50
- never a pass.
51
- - **human** — `risk` (the factory-declared risk classes that ARM this gate; an
52
- empty list means always), `approval_timeout_ms`, `poll_ms`, `instructions`.
48
+ executes), `instructions`, `await_ms`, and `risk`. The reviewer answers with a
49
+ fenced JSON verdict which the runner parses in code — an unreadable verdict is
50
+ a failure, never a pass.
51
+ - **human** — `approval_timeout_ms`, `poll_ms`, `instructions`, and `risk`.
52
+
53
+ `risk` is common to all three kinds: the factory-declared risk classes that ARM
54
+ the gate, an empty list meaning always. Unarmed, the gate records `skipped` and
55
+ runs nothing — no suite, no reviewer dispatch, no approval item. Naming a class
56
+ the factory never declared is a load error, because a gate that can never arm
57
+ reads exactly like one that passed.
53
58
 
54
59
  `cycles` is common to all three: how many implementer fix cycles a failure gets
55
60
  before the runner escalates to a human queue item. Reruns come BEFORE fix
@@ -1427,6 +1427,22 @@ async function runOneGate({ gate, cycle, factory, item, changed, changedReason,
1427
1427
  }
1428
1428
 
1429
1429
  if (gate.kind === "agent-review") {
1430
+ // An UNREADABLE diff refuses a gate that declares risk classes, for the
1431
+ // reason the human branch gives below: a risk class is a path predicate,
1432
+ // and with no readable paths "assume it isn't armed" is the fail-open
1433
+ // direction. Scoped to a declaring gate on purpose (dec-spor-review-gate-
1434
+ // unreadable-diff-fails-closed-only-when-armed): a review gate declaring
1435
+ // NO risk consults no paths, and its reviewer reads the implementer's live
1436
+ // checkout for itself, so an unconditional refusal here would turn a
1437
+ // working review into a failure for every factory already in service.
1438
+ if (!changed && (gate.risk || []).length) {
1439
+ return {
1440
+ passed: false,
1441
+ verdict: "failed",
1442
+ noRetry: true,
1443
+ detail: changedReason || "the change under judgement could not be read, so this gate's risk classes could not be evaluated",
1444
+ };
1445
+ }
1430
1446
  // An EMPTY diff is not a clean one (issue-spor-review-gate-empty-diff-
1431
1447
  // vacuous-pass). The first live factory run's implementer landed its
1432
1448
  // commit on the trusted ref itself, so the gate diffed that commit against
@@ -1449,6 +1465,27 @@ async function runOneGate({ gate, cycle, factory, item, changed, changedReason,
1449
1465
  // is handed them (and the last fix) and must answer each before it may
1450
1466
  // raise anything new — the protocol lives in gates.parseReviewVerdict.
1451
1467
  const prior = gates.openPriorFindings(ledger);
1468
+ // ARMING (task-spor-review-gate-risk-arming): a review gate declaring risk
1469
+ // classes dispatches only when the change touched one — the same
1470
+ // predicate, the same `skipped` verdict and the same recorded fact the
1471
+ // command and human branches produce. Deliberately AFTER the empty-diff
1472
+ // refusal above: an empty diff arms nothing, so evaluating arming first
1473
+ // would convert that fail-closed failure into a silent pass and re-open
1474
+ // issue-spor-review-gate-empty-diff-vacuous-pass.
1475
+ const armed = gates.gateArmed(gate, changed, factory.riskClasses);
1476
+ // ...and never while the ledger holds an open blocking finding
1477
+ // (dec-spor-review-gate-arming-never-buries-open-findings). Arming is read
1478
+ // from a diff that changes across fix cycles, so a fix that reverts the
1479
+ // arming paths could otherwise disarm a gate mid-pipeline — and `skipped`
1480
+ // passes. A demonstrated defect is retired by a reviewer clearing it,
1481
+ // never by the paths that raised it going away.
1482
+ if (!armed.armed && !prior.length) {
1483
+ return {
1484
+ passed: true,
1485
+ verdict: "skipped",
1486
+ detail: `no declared risk class (${gate.risk.join(", ")}) was touched by this change — no review dispatched under ${gate.profile}`,
1487
+ };
1488
+ }
1452
1489
  // ...and the findings an earlier cycle rated blocking but could not
1453
1490
  // demonstrate: the reviewer may demonstrate one now, by id, without it
1454
1491
  // counting as a goalpost (gates.parseReviewVerdict, `raised`).
@@ -1506,10 +1543,14 @@ async function runOneGate({ gate, cycle, factory, item, changed, changedReason,
1506
1543
  }
1507
1544
  if (v.passed) {
1508
1545
  const advisory = (v.findings || []).filter((f) => !f.blocking).length;
1546
+ // Name the arming class on the way through, as the command gate does:
1547
+ // the `art-gate-*` fact is the only place a factory maintainer can later
1548
+ // read WHY an armed gate ran.
1549
+ const armedBy = armed.classes.length ? ` (armed by ${armed.classes.map((c) => c.class).join(", ")})` : "";
1509
1550
  return {
1510
1551
  passed: true,
1511
1552
  verdict: "passed",
1512
- detail: `the review under ${gate.profile} found nothing blocking (verdict: ${v.verdict}${advisory ? `, ${advisory} advisory note${advisory === 1 ? "" : "s"} recorded` : ""})${v.note ? ` — ${v.note}` : ""}`,
1553
+ detail: `the review under ${gate.profile} found nothing blocking (verdict: ${v.verdict}${advisory ? `, ${advisory} advisory note${advisory === 1 ? "" : "s"} recorded` : ""})${armedBy}${v.note ? ` — ${v.note}` : ""}`,
1513
1554
  ledger: next,
1514
1555
  };
1515
1556
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sporhq/spor",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Spor — a shared memory substrate for teams and agents. Decisions, their reasons, and the traces they leave. Knowledge-graph context compiler: session-start briefings, per-prompt digests, capture at discovery, end-of-session distillation, decision queue.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Anthony Allen",
@@ -167,7 +167,8 @@ right failure but a rude way to discover a typo:
167
167
  present, not that a matching profile node exists — a typo there is not
168
168
  caught until dispatch, so double-check it yourself, e.g. `spor get <profile>`);
169
169
  - `protected_paths` is declared **only** with a `test_lane_profile` to route to;
170
- - every risk class a human gate names is declared in `risk_classes`;
170
+ - every risk class a gate names is declared in `risk_classes` (any kind that
171
+ arms: command, agent-review, human);
171
172
  - gate ids are unique and kebab-case;
172
173
  - if you wrote a `rescue:` block: `profile` names a supervised, write-capable
173
174
  strong-model profile (the worker refuses a native-background one at load
@@ -236,8 +237,9 @@ Say no, briefly, and offer the nearest thing you can do:
236
237
  flagging what they do not care about, **raise** `cycles` so the implementer
237
238
  gets another pass before it escalates to them, declare a `rescue:` lane so
238
239
  a stronger model gets a go before they are paged, or retire the gate with a
239
- decision node saying why. Only a **human** gate has `risk` classes to narrow;
240
- an agent-review gate has no arming predicate and runs on every gated item.
240
+ decision node saying why. All three kinds take `risk` classes, so the
241
+ honest narrowing is to declare the paths the gate actually judges — a gate
242
+ that skips a docs-only change was never the gate for that change.
241
243
  And `cycles` is fix cycles *before* escalation — lowering it reaches a person
242
244
  sooner, not later.
243
245
  - **"Set protected_paths so agents can fix the tests when they fail"** —
@@ -108,7 +108,11 @@ Keys by kind (`lib/kernel/gates.js` is the authority):
108
108
  Deliberately *no* ref or protected-path key: those belong to the factory, so
109
109
  one shared gate can never relax another team's trusted boundary.
110
110
  - **agent-review** — `profile` (required), `instructions`, `await_ms` (default
111
- 3600000). Must be a **supervised** harness: the report is the verdict channel.
111
+ 3600000), `risk` (the factory-declared classes that arm it; empty means
112
+ always — the same predicate a command or human gate takes, and the biggest
113
+ saving of the three, since an unarmed review skips a whole agent dispatch
114
+ rather than a local suite run). Must be a **supervised** harness: the report
115
+ is the verdict channel.
112
116
  The review runs read-only and is stateful across fix cycles (WORKERS.md
113
117
  §10.4): only a DEMONSTRATED `blocking` finding blocks, review N is handed the
114
118
  prior findings by ledger id and must clear or confirm each before raising
@@ -181,8 +185,8 @@ Six things a factory must satisfy or it refuses to start the worker:
181
185
  2. every agent-review gate names a `profile`, and command gates a `command`;
182
186
  3. `protected_paths` without a `test_lane_profile` is an error — "fails closed"
183
187
  must never mean "dropped on the floor";
184
- 4. a human gate may only name risk classes declared in `risk_classes` — a gate
185
- that can never arm reads exactly like an approved one;
188
+ 4. a gate of ANY kind may only name risk classes declared in `risk_classes` —
189
+ a gate that can never arm reads exactly like an approved one;
186
190
  5. gate ids are unique and kebab-case;
187
191
  6. `repos`, if written, names at least one repo.
188
192
 
@@ -291,8 +295,8 @@ Then the gate itself carries the two fields such a suite needs:
291
295
  a serialized gate, or the integration stage, on the same repo before it
292
296
  starts. The same lease `integration.serialize` uses.
293
297
  - **`risk`** — the classes (from `risk_classes`) that ARM this gate, exactly
294
- as on a human gate. Unarmed, the gate records `skipped` and runs nothing;
295
- an unreadable diff still fails closed.
298
+ as on a human or an agent-review gate. Unarmed, the gate records `skipped`
299
+ and runs nothing; an unreadable diff still fails closed.
296
300
  - The suite's env carries **`SPOR_GATE_BASE`**, **`SPOR_GATE_HEAD`** (the shas
297
301
  under judgement), **`SPOR_TRUSTED_REF`**, **`SPOR_GATE_STAGE`** (`gate` |
298
302
  `integration`) and **`SPOR_GATE_NODE`**, beside `CI=1` and `SPOR_GATE=<id>`,