@sporhq/spor 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/API.md +71 -17
- package/GRAPH.md +92 -10
- package/QUEUE.md +7 -2
- package/README.md +250 -12
- package/bin/spor.js +5163 -377
- package/lib/candidates.js +179 -0
- package/lib/config.js +47 -0
- package/lib/graph.js +44 -0
- package/lib/kernel/gates.js +1458 -0
- package/lib/kernel/queue.js +11 -0
- package/lib/kernel/registry.js +118 -2
- package/lib/kernel/satisfiability.js +107 -5
- package/lib/schema.js +23 -1
- package/lib/seed/candidates/schema-edge-member-of-program.md +41 -0
- package/lib/seed/candidates/schema-factory.md +131 -0
- package/lib/seed/candidates/schema-gate.md +101 -0
- package/lib/seed/schema-agent.md +4 -3
- package/lib/seed/schema-artifact.md +21 -1
- package/lib/seed/schema-capture-pending.md +19 -2
- package/lib/seed/schema-correction.md +14 -1
- package/lib/seed/schema-decision.md +21 -1
- package/lib/seed/schema-issue.md +26 -2
- package/lib/seed/schema-profile.md +13 -1
- package/lib/seed/schema-question.md +21 -2
- package/lib/seed/schema-task.md +26 -2
- package/lib/shell/agent-dispatch-runner.js +868 -47
- package/lib/shell/dispatch-harnesses.js +1015 -23
- package/lib/shell/dispatch-terminal.js +645 -0
- package/lib/shell/gate-runner.js +1604 -0
- package/lib/shell/integration-runner.js +1033 -0
- package/lib/shell/work-loop.js +1365 -0
- package/lib/shell/worker-contract.js +138 -0
- package/package.json +5 -2
- package/prompts/client/distill-local.md +1 -1
- package/scripts/engines/doctor.js +34 -0
- package/scripts/engines/util.js +37 -4
- package/skills/brief/SKILL.md +1 -1
- package/skills/factory/SKILL.md +254 -0
- package/skills/factory/evals/evals.json +47 -0
- package/skills/factory/fixtures/README.md +48 -0
- package/skills/factory/fixtures/interview-acme-checkout.md +120 -0
- package/skills/factory/fixtures/nodes/factory-acme-checkout.md +78 -0
- package/skills/factory/fixtures/nodes/gate-adversarial-review.md +31 -0
- package/skills/factory/fixtures/nodes/profile-acme-rescue.md +28 -0
- package/skills/factory/fixtures/nodes/profile-acme-test-writer.md +15 -0
- package/skills/factory/fixtures/nodes/profile-codex-review.md +20 -0
- package/skills/factory/fixtures/nodes/task-acme-checkout-acceptance-suite.md +32 -0
- package/skills/factory/references/emitting.md +420 -0
- package/skills/factory/references/interview.md +183 -0
- package/skills/factory/references/maintenance.md +183 -0
- package/skills/onboard/SKILL.md +1 -1
- package/skills/spor/SKILL.md +7 -4
- package/skills/spor/references/authoring-schemas.md +25 -1
- package/skills/spor/references/concepts.md +5 -5
- package/skills/triage/SKILL.md +4 -3
|
@@ -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.27.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "losthammer"
|
|
8
8
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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
|
@@ -8,7 +8,10 @@ at `/mcp`) for model-driven clients — Cowork, claude.ai connectors,
|
|
|
8
8
|
in-session tool calls. Both doors require bearer auth (§4), are thin adapters
|
|
9
9
|
over the same core, and a tool call and its REST twin return byte-identical
|
|
10
10
|
payloads. Companion specs: [GRAPH.md](GRAPH.md) (node/edge format),
|
|
11
|
-
[QUEUE.md](QUEUE.md) (capture, decision queue, schema registry)
|
|
11
|
+
[QUEUE.md](QUEUE.md) (capture, decision queue, schema registry),
|
|
12
|
+
[WORKERS.md](WORKERS.md) (the worker protocol — claim/brief/work/report/
|
|
13
|
+
resolve, lease semantics, and the terminal-state contract dispatched agents
|
|
14
|
+
are built from).
|
|
12
15
|
|
|
13
16
|
## 1. Write semantics (both surfaces)
|
|
14
17
|
|
|
@@ -382,6 +385,17 @@ queue, not what the firehose hid. The type compared is the type the item
|
|
|
382
385
|
surfaces as, so `exclude_types: ["schema"]` also hides schema-approval items.
|
|
383
386
|
Omitting both (or passing empty arrays) filters nothing.
|
|
384
387
|
|
|
388
|
+
**Unknown project scope** (task-spor-remote-project-validation-warning). A
|
|
389
|
+
`project` that matches no repo, grouping, or project stamp on any resident node
|
|
390
|
+
is not an error: the result is a normal envelope with empty `items` plus the
|
|
391
|
+
additive `project_warning` string (the `/v1/analytics` shape), and the text
|
|
392
|
+
output carries the same line. It is absent when the scope is known — a known
|
|
393
|
+
project with nothing queued has no warning. Clients that print the queue for a
|
|
394
|
+
person should surface it (remote `spor next` prints it verbatim on stderr and
|
|
395
|
+
strips it from `--json`, matching local mode); a caller that ignores it sees
|
|
396
|
+
exactly what an empty queue looks like, which is the typo this field exists to
|
|
397
|
+
catch.
|
|
398
|
+
|
|
385
399
|
**Per-person scope** (task-cc-queue-assignee-filtering). `assignee` scopes the
|
|
386
400
|
ranked queue to the work one person carries — the union of nodes with an
|
|
387
401
|
`assigned` edge to them and the nodes they `steward`; `assignee: "me"` binds
|
|
@@ -578,7 +592,7 @@ agent subject sees no graph content; a coarse read-only/CI flag is not a bypass.
|
|
|
578
592
|
| Endpoint | Typical caller | Semantics |
|
|
579
593
|
|---|---|---|
|
|
580
594
|
| `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 |
|
|
581
|
-
| `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 |
|
|
595
|
+
| `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, 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. The REST/MCP twin of the `spor schema` CLI: all three render one `graph.registry.snapshot()` so they never drift |
|
|
582
596
|
| `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 |
|
|
583
597
|
| `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) |
|
|
584
598
|
| `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) |
|
|
@@ -610,7 +624,7 @@ agent subject sees no graph content; a coarse read-only/CI flag is not a bypass.
|
|
|
610
624
|
| `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) |
|
|
611
625
|
| `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 |
|
|
612
626
|
| `POST /v1/corrections` | /spor:correct | `propose_correction` semantics → 201 `{status, id, revision, warnings}` |
|
|
613
|
-
| `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) |
|
|
627
|
+
| `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?, 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) |
|
|
614
628
|
| `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) |
|
|
615
629
|
| `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`) |
|
|
616
630
|
| `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 |
|
|
@@ -717,20 +731,60 @@ anything with a token.
|
|
|
717
731
|
(Not to be confused with `spor dispatch --agent`, the unrelated `claude
|
|
718
732
|
--agent` harness passthrough.) The
|
|
719
733
|
token is minted **session-deferred** and bound to the real run session AFTER
|
|
720
|
-
launch (dec-spor-dispatch-bg-session-late-bind):
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
+
launch (dec-spor-dispatch-bg-session-late-bind): a harness self-allocates its
|
|
735
|
+
session, so dispatch reads the real one and binds it via `POST
|
|
736
|
+
/v1/agents/session` (§3) — the one place an agent token's session is set,
|
|
737
|
+
write-once. The session can't be forged a-priori (it isn't known until the run
|
|
738
|
+
exists) and can't ride the write payload (token-derived, §1), so the binding
|
|
739
|
+
is always the actual run. For the opt-in native launch (`spor dispatch --bg`,
|
|
740
|
+
`claude --bg` ignores `--session-id`) the launcher reads it from `claude agents
|
|
741
|
+
--json`. A **supervised**-harness dispatch (Claude Code by default, Codex,
|
|
742
|
+
OpenCode, GitHub Copilot CLI) follows
|
|
743
|
+
the same late-bind contract from its own supervisor process instead: it reads
|
|
744
|
+
the session id out of the run's supervised JSONL log rather than
|
|
745
|
+
`claude agents --json` — Claude Code off the `session_id` every stream-json
|
|
746
|
+
event carries (first on its `system`/`init` event), Codex off its
|
|
747
|
+
`thread.started` event, OpenCode off
|
|
748
|
+
the `sessionID` every event carries, Copilot off the `sessionId` on its
|
|
749
|
+
terminal `result` event (so a Copilot run binds only at exit, still before its
|
|
750
|
+
record goes terminal) — then binds it the same way. Token transport also
|
|
751
|
+
differs per harness: Claude Code gets a strict `--mcp-config` file, Codex gets
|
|
752
|
+
the token via an env var its own config references (`--config
|
|
753
|
+
mcp_servers.spor.bearer_token_env_var=SPOR_DISPATCH_MCP_TOKEN`), and OpenCode
|
|
754
|
+
and Copilot — neither of which can be handed an MCP server on the command line
|
|
755
|
+
without publishing the bearer to argv — get the agent-scoped token as
|
|
756
|
+
`SPOR_TOKEN` in the run's environment, so the `spor` CLI inside the run is
|
|
757
|
+
agent-attributed with no injected MCP. All of them land at the same self-serve
|
|
758
|
+
mint/bind pair above. A **declared** harness (below) is `env-token` too, and
|
|
759
|
+
binds off whatever JSON path its declaration names.
|
|
760
|
+
- **Declared custom harnesses — the graph names it, the machine binds it**
|
|
761
|
+
(task-spor-dispatch-declarative-custom-harness). A `profile` may select a
|
|
762
|
+
harness this client ships no adapter for; the profile then carries **only**
|
|
763
|
+
`harness: <id>`, and the id is bound to something executable by a
|
|
764
|
+
MACHINE-LOCAL declaration in the client config cascade,
|
|
765
|
+
`dispatch.harness.<id>` — machine-specific like `dispatch.bin.<harness>` and
|
|
766
|
+
`dispatch.repos`, and read ONLY from the user `$SPOR_HOME/config.json` or the
|
|
767
|
+
global one: both `dispatch.harness` and `dispatch.bin` are stripped from a
|
|
768
|
+
committable repo `.spor.json` with a warning (`REPO_FORBIDDEN_PATHS`,
|
|
769
|
+
lib/config.js), the way a repo-level `token` is, so a repo write is no more
|
|
770
|
+
able to choose what this box executes than a graph write is. The declaration carries `command`, an `args`
|
|
771
|
+
template (`{cwd}` / `{report}` / `{model}` tokens), a `label`, report recovery
|
|
772
|
+
(`report: "lastText"` with a `report.text` JSON path into the harness's event
|
|
773
|
+
stream, or `report: "file"` when the harness writes the report itself at the
|
|
774
|
+
`{report}` path), and an optional `session` JSON path for the late-bind above.
|
|
775
|
+
Everything else is FIXED by v1 scope and naming it is an error: launch mode is
|
|
776
|
+
supervised-jsonl, the prompt goes on stdin, identity is `env-token`.
|
|
777
|
+
**A graph write must never define what a machine executes**, and that is
|
|
778
|
+
enforced on both sides: a profile node carrying any launch-defining field
|
|
779
|
+
(`command`, `args`, `argv`, `bin`, `exec`, `entrypoint`, `env`, `report`,
|
|
780
|
+
`session`, `launch_mode`, `identity_mode`) is refused outright by `spor
|
|
781
|
+
dispatch` rather than honoured or silently ignored, and a machine with no
|
|
782
|
+
local binding for the id fails satisfiability — refusing loudly, leaving the
|
|
783
|
+
assignment and its lease untouched, and (in team mode) naming the fleet hosts
|
|
784
|
+
that CAN run it. The capability probe adds valid declared ids to
|
|
785
|
+
`machine.harnesses`, so `spor capabilities` and the fleet publish above
|
|
786
|
+
reflect them; a declaration whose `command` does not resolve is not reported
|
|
787
|
+
as available.
|
|
734
788
|
- **OAuth 2.1 for MCP connectors** (Cowork/claude.ai, which cannot carry a
|
|
735
789
|
static bearer token): protected-resource metadata discovery (RFC 9728,
|
|
736
790
|
advertised on the `/mcp` 401 via `WWW-Authenticate`), authorization-server
|
package/GRAPH.md
CHANGED
|
@@ -302,8 +302,8 @@ separately, but never shows a complete custom type in one piece.
|
|
|
302
302
|
|
|
303
303
|
**The constraint model is procedural, not declarative.** A schema's `json`
|
|
304
304
|
payload declares only *registry knobs* — `node_type`, `prefix`, `queueable`,
|
|
305
|
-
`traversable`, `always_on`, `capturable`, an edge `weight`,
|
|
306
|
-
partitions: `status.non_resolving` (resolver semantics — whether a node in this
|
|
305
|
+
`traversable`, `always_on`, `capturable`, an edge `weight`, the completion
|
|
306
|
+
policy (below), and the three status partitions: `status.non_resolving` (resolver semantics — whether a node in this
|
|
307
307
|
status retires the targets it points at), `status.terminal` (own-lifecycle
|
|
308
308
|
completion — the statuses in which a node of this type is *done*, unioned with the
|
|
309
309
|
kernel's legacy set and read by work-analytics so a schema-only terminal status
|
|
@@ -314,8 +314,33 @@ issue-spor-analytics-completion-ignores-schema-terminal-status), and
|
|
|
314
314
|
`terminal-status` register below; a schema that declares no `inert` set
|
|
315
315
|
INHERITS its `terminal` set, so only a schema whose two sets genuinely differ
|
|
316
316
|
declares it — the seed decision schema pins `settled` terminal but NOT inert,
|
|
317
|
-
dec-spor-status-inert-third-partition).
|
|
318
|
-
|
|
317
|
+
dec-spor-status-inert-third-partition).
|
|
318
|
+
|
|
319
|
+
**The completion policy — declared for readers, still enforced by code**
|
|
320
|
+
(task-spor-registry-declarative-terminal-status-policy). Three further `status`
|
|
321
|
+
keys say, as registry data, what the hooks below enforce: `status.vocabulary`
|
|
322
|
+
(the closed status enum the type's own `validate()` gates membership on),
|
|
323
|
+
`status.completion` (the single SUCCESS terminal value — task `done`, issue
|
|
324
|
+
`resolved`, question `answered` — as distinct from `status.terminal`, which is
|
|
325
|
+
the full set *including* the give-up outcomes `abandoned`/`superseded`/
|
|
326
|
+
`rejected`), and `status.resolver_required` (whether reaching that value also
|
|
327
|
+
demands a live resolving `decision`/`artifact`, the completion-resolver
|
|
328
|
+
invariant). **Declaring them gates nothing** — the hooks are still the only
|
|
329
|
+
write door, and this is exactly why they are not a field list or an enforced
|
|
330
|
+
enum. They exist so a READER can name the right terminal status without
|
|
331
|
+
parsing hook source: the gardener's finding remedies used to keep hand-written
|
|
332
|
+
tables of "task → done, everything else → resolved" and shipped remedies whose
|
|
333
|
+
`set_status` the door then refused
|
|
334
|
+
(issue-spor-gardener-terminal-status-fallback-off-vocab). A type whose terminal
|
|
335
|
+
values are several distinct OUTCOMES rather than one success (decision
|
|
336
|
+
settled/superseded/rejected, artifact merged/released/done, capture-pending
|
|
337
|
+
merged/rejected) declares a `vocabulary` and NO `completion` — that absence is
|
|
338
|
+
the machine-readable form of "there is no mechanical close here". Declaration
|
|
339
|
+
and hook are pinned together by `test/seed-declarative-status-policy.test.js`,
|
|
340
|
+
which drives every seed schema's hooks through the sandbox and fails if the two
|
|
341
|
+
disagree; read the live values with `spor schema <type>`.
|
|
342
|
+
|
|
343
|
+
Otherwise there is **no declarative field list and no status enum.** Custom fields are free-form: any flat frontmatter key the
|
|
319
344
|
regex parser accepts (simple `key: value` scalars, YAML-folded multi-line
|
|
320
345
|
values, `pin:`/`exclude:` inline lists, `- {type: X, to: Y}` edges — and nothing
|
|
321
346
|
fancier) is carried verbatim on the node. What a field MUST contain, and which
|
|
@@ -400,6 +425,46 @@ warns). A schema node goes through the same propose→activate flow it governs
|
|
|
400
425
|
bump the CalVer and add an `upgrades` chain only when the change is not
|
|
401
426
|
backward-readable.
|
|
402
427
|
|
|
428
|
+
Rollout-stage schemas the *product* ships ride the **candidate pack**
|
|
429
|
+
(`lib/seed/candidates/`): full schema-node markdown that travels with the npm
|
|
430
|
+
package but never enters the registry until a graph adopts it as a
|
|
431
|
+
graph-resident node — `spor schema candidates` lists each candidate's adoption
|
|
432
|
+
state, `spor schema adopt <id>` writes it through the validated node surface
|
|
433
|
+
(`status: proposed`; `--activate` is the trusted-admin form for solo/local
|
|
434
|
+
graphs), stamping `adopted_from`/`adopted_sha` provenance. Re-running adopt
|
|
435
|
+
after a package upgrade is CalVer-aware and idempotent: a pristine older copy
|
|
436
|
+
(canonical hash — `schema_version` + body — still matching its stamp) upgrades
|
|
437
|
+
in place with its status preserved; a locally modified or unstamped resident
|
|
438
|
+
refuses without `--force`. When a candidate stabilizes it is promoted into the
|
|
439
|
+
seed pack at a release; the resident copy then shadows the seed (the
|
|
440
|
+
stale-override warning above) and should be retired (`status: retired`).
|
|
441
|
+
|
|
442
|
+
Three candidates ship today: `schema-edge-member-of-program` (program
|
|
443
|
+
membership as its own edge type) and the pair the **software-factory gate
|
|
444
|
+
pipeline** reads — `schema-factory` and `schema-gate`
|
|
445
|
+
(task-spor-work-gate-pipeline). A `type: factory` node declares, in a fenced
|
|
446
|
+
JSON payload, the ordered gate list a worker enforces between claim and resolve
|
|
447
|
+
(`spor work --factory <id>`), plus the trusted ref its acceptance suite is taken
|
|
448
|
+
from, the protected test paths that fail a gate CLOSED, the separate
|
|
449
|
+
test-change lane those route to, and the named risk classes a human gate keys
|
|
450
|
+
on. A `type: gate` node is ONE such gate standing alone, so an org vets a
|
|
451
|
+
`gate-security-review` once and every factory references it by id — inline and
|
|
452
|
+
referenced gates are the same object to the runner. The same payload may also
|
|
453
|
+
declare an optional `integration` block — the merge-queue landing stage that
|
|
454
|
+
runs once every gate has passed (dec-spor-factory-integration-step): it is
|
|
455
|
+
deliberately NOT a fourth gate kind (a gate judges the branch; integration
|
|
456
|
+
mutates the target ref and serializes across workers), so it is its own key
|
|
457
|
+
beside `gates`, parsed the same fail-closed way — and an optional `rescue`
|
|
458
|
+
block (task-spor-factory-rescue-lane): a strong-model profile the runner
|
|
459
|
+
dispatches at a gate's exhaustion BEFORE any human escalation, to diagnose,
|
|
460
|
+
fix and file factory-improvement tasks, re-running the gates on what it
|
|
461
|
+
commits (WORKERS.md §10.10). Both `gate` and `factory` are
|
|
462
|
+
`capturable: false` (the distiller never drafts one: a factory changes what a
|
|
463
|
+
worker will accept, so it is written deliberately) and both arrive by adoption
|
|
464
|
+
rather than in the seed, for the same reason. WORKERS.md §10 documents the
|
|
465
|
+
runtime contract; the payload keys are documented on the candidate nodes
|
|
466
|
+
themselves.
|
|
467
|
+
|
|
403
468
|
A complete worked example — a `escalation` type with a required `severity`
|
|
404
469
|
field (enforced in `validate`) and an `open → mitigated → closed` status machine
|
|
405
470
|
whose terminal `closed` demands a resolver (enforced in `transitions`):
|
|
@@ -827,7 +892,8 @@ server-side (issue-cc-onboarding-email-mismatch-silent-degradation).
|
|
|
827
892
|
### Agents (person-owned principals)
|
|
828
893
|
|
|
829
894
|
An `agent` node (prefix `agent-`) is a person's automation principal — the
|
|
830
|
-
durable identity of a dispatched `claude
|
|
895
|
+
durable identity of a dispatched Claude Code run (a supervised `claude -p`, or
|
|
896
|
+
an opt-in `claude --bg` session)
|
|
831
897
|
(dec-spor-agent-identity-nodes). It generalizes the workflow-run principal: a
|
|
832
898
|
dispatched session is just another principal kind owned by a person, so work it
|
|
833
899
|
creates reads "agent **on behalf of** person" rather than person-direct.
|
|
@@ -907,11 +973,24 @@ date: 2026-06-18
|
|
|
907
973
|
```
|
|
908
974
|
|
|
909
975
|
- `harness:` (`claude-code` | `codex` | `opencode` | …) selects the launcher
|
|
910
|
-
(dec-cc-portable-core-adapters: claude-code → `claude
|
|
911
|
-
CLIs); `model:` → the harness `--model`; `skills`/`plugins` are preloaded;
|
|
976
|
+
(dec-cc-portable-core-adapters: claude-code → `claude -p` under the shared
|
|
977
|
+
supervisor, others → their CLIs); `model:` → the harness `--model`; `skills`/`plugins` are preloaded;
|
|
912
978
|
`mcp` is merged into the strict `--mcp-config` dispatch writes, so the agent's
|
|
913
979
|
toolset is exactly the profile plus the agent-spor server, nothing ambient
|
|
914
980
|
(dec-spor-session-identity-active-record).
|
|
981
|
+
- **The graph names the harness; the MACHINE binds what that name runs**
|
|
982
|
+
(task-spor-dispatch-declarative-custom-harness). `harness:` may name a
|
|
983
|
+
launcher the client ships no in-code adapter for — a team's modified build,
|
|
984
|
+
an internal wrapper — in which case the profile still carries nothing but the
|
|
985
|
+
id, and each machine that should run it declares `dispatch.harness.<id>`
|
|
986
|
+
({`command`, `args`, `label`, `report`, `session`}) in its own
|
|
987
|
+
`$SPOR_HOME/config.json`. **A graph write must never define what a machine
|
|
988
|
+
executes:** a profile carrying `command`, `args`, `argv`, `bin`, `exec`,
|
|
989
|
+
`entrypoint`, `env`, `report`, `session`, `launch_mode` or `identity_mode` is
|
|
990
|
+
REFUSED by `spor dispatch`, not honoured and not silently ignored. So an org
|
|
991
|
+
can publish a profile naming an unadapted harness and only the boxes whose
|
|
992
|
+
OWNER bound that id will take the work (a machine with no binding fails
|
|
993
|
+
satisfiability below).
|
|
915
994
|
- **The runtime fields ARE the satisfiability spec** — there is no separate
|
|
916
995
|
requirements block (dec-spor-machine-profile-satisfiability). A machine
|
|
917
996
|
declares ATOMIC capabilities in a machine-local `dispatch.capabilities` map
|
|
@@ -1103,9 +1182,12 @@ program keeps rendering. The preference is all-or-nothing at a node — declare
|
|
|
1103
1182
|
every member of an umbrella in one write, or the undeclared rest read as
|
|
1104
1183
|
"blocking but outside the program" rather than silently dropping. It ships as
|
|
1105
1184
|
a graph-resident schema node (`schema-edge-member-of-program`), not the seed
|
|
1106
|
-
pack
|
|
1107
|
-
|
|
1108
|
-
|
|
1185
|
+
pack — delivered as a packaged candidate (`spor schema adopt
|
|
1186
|
+
schema-edge-member-of-program` writes it into a graph that doesn't have it
|
|
1187
|
+
yet; see "Resolution and rollout" above) — so it needs activation (a
|
|
1188
|
+
*different* identity in team graphs; `--activate` in solo/local ones) before
|
|
1189
|
+
writes of this edge type validate; check `spor schema member-of-program` for
|
|
1190
|
+
its live status rather than assuming.
|
|
1109
1191
|
`capturable: false` — the distiller and capture nudge never emit it; only a
|
|
1110
1192
|
person or an agent working the program explicitly wires membership.
|
|
1111
1193
|
|
package/QUEUE.md
CHANGED
|
@@ -617,8 +617,13 @@ schemas already self-surface in the queue, so no duplicate findings are
|
|
|
617
617
|
filed for them. Trigger: `POST /v1/gardener` on demand (the `spor admin
|
|
618
618
|
gardener` CLI verb is the shell front-door), or
|
|
619
619
|
`SPOR_GARDENER_MS` (a server-side env var) for
|
|
620
|
-
an in-process interval
|
|
621
|
-
|
|
620
|
+
an in-process interval. Self-host/local: off by default — the schedule is
|
|
621
|
+
ops' choice. Hosted tenants (`SPOR_HOSTED`): on by default at 6h, and the
|
|
622
|
+
control plane bakes the same `SPOR_GARDENER_MS` onto every tenant it
|
|
623
|
+
provisions (`SPOR_CP_GARDENER_MS`; explicit `0` disables) — a suspended
|
|
624
|
+
tenant's timer fires on resume, so the cadence is per hour of awake time
|
|
625
|
+
(issue-spor-hosting-tenant-gardener-silent). Deferred: "done but
|
|
626
|
+
contradicted" (needs git-history analysis of resolving artifacts).
|
|
622
627
|
|
|
623
628
|
Findings route to stewards the same way questions do
|
|
624
629
|
(task-cc-findings-steward-routing): at filing time the finding's edge
|