@sporhq/spor 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/API.md +34 -14
- package/GRAPH.md +120 -2
- package/QUEUE.md +9 -0
- package/bin/spor.js +790 -19
- package/lib/config.js +5 -0
- package/lib/kernel/graph.js +25 -7
- package/lib/kernel/queue.js +23 -2
- package/lib/kernel/registry.js +66 -2
- package/lib/kernel/resolution.js +9 -5
- package/lib/kernel/satisfiability.js +156 -0
- package/lib/queue.js +3 -0
- package/lib/seed/schema-edge-assigned.md +27 -5
- package/lib/seed/schema-edge-uses-profile.md +38 -0
- package/lib/seed/schema-profile.md +74 -0
- package/lib/seed/schema-requires.md +58 -0
- package/lib/seed/schema-routine.md +91 -0
- package/package.json +3 -3
- package/scripts/engines/post-tool.js +75 -1
- package/scripts/engines/session-start.js +80 -0
- package/scripts/engines/util.js +163 -0
- package/skills/spor/SKILL.md +6 -1
- package/skills/spor/references/authoring-schemas.md +9 -0
- package/skills/spor/references/concepts.md +4 -1
|
@@ -2,6 +2,6 @@
|
|
|
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.11.0",
|
|
6
6
|
"author": { "name": "losthammer" }
|
|
7
7
|
}
|
package/API.md
CHANGED
|
@@ -117,12 +117,21 @@ changes the description without a deploy.
|
|
|
117
117
|
### `add_edge`
|
|
118
118
|
|
|
119
119
|
Micro-mutation. Input `{ "id": "<node>", "type": "<edge type>",
|
|
120
|
-
"to": "<target>" }` — accepts canonical, alias, and
|
|
121
|
-
forms are flipped onto the target before writing. No
|
|
122
|
-
Output `{ "status": "updated|skipped",
|
|
123
|
-
"revision", "warnings" }` (`skipped` = edge
|
|
124
|
-
idempotent). Both nodes must exist. The tool
|
|
125
|
-
registry-generated vocabulary as `put_node`.
|
|
120
|
+
"to": "<target>", "attrs"?: { "<k>": "<v>" } }` — accepts canonical, alias, and
|
|
121
|
+
inverse forms; inverse forms are flipped onto the target before writing. No
|
|
122
|
+
revision echo is needed. Output `{ "status": "updated|skipped",
|
|
123
|
+
"id": <node actually modified>, "revision", "warnings" }` (`skipped` = edge
|
|
124
|
+
already present — the call is idempotent). Both nodes must exist. The tool
|
|
125
|
+
description carries the same registry-generated vocabulary as `put_node`.
|
|
126
|
+
|
|
127
|
+
The optional `attrs` carries trailing flat edge attributes — the per-assignment
|
|
128
|
+
`profile:` override on an `assigned → agent` edge is the motivating case. Values
|
|
129
|
+
round-trip only simple `[A-Za-z0-9_-]` tokens (the frontmatter edge grammar);
|
|
130
|
+
richer values need `put_node`, and `type`/`to` are reserved. With `attrs`, a
|
|
131
|
+
duplicate `(type, to)` becomes an **upsert**: same attributes → still `skipped`;
|
|
132
|
+
different attributes → the edge's attribute set is replaced (not merged) in
|
|
133
|
+
place. Omitting `attrs` never touches an existing edge's attributes, so a bare
|
|
134
|
+
`add_edge` is unchanged.
|
|
126
135
|
|
|
127
136
|
### `set_status`
|
|
128
137
|
|
|
@@ -227,11 +236,18 @@ Omitting both (or passing empty arrays) filters nothing.
|
|
|
227
236
|
### `ask_question`
|
|
228
237
|
|
|
229
238
|
File a question the graph could not answer. Input `{ "text": "<the
|
|
230
|
-
question>", "title"?: "<short title>", "mentions"?: ["<node id>", ...]
|
|
231
|
-
(routing considers `mentions` first). The question
|
|
232
|
-
deterministically routed to the steward of the closest
|
|
233
|
-
(unrouted if none matches), and joins the decision queue until
|
|
234
|
-
Answer by writing a node with an `answers` edge to the question.
|
|
239
|
+
question>", "title"?: "<short title>", "mentions"?: ["<node id>", ...],
|
|
240
|
+
"project"?: "<slug>" }` (routing considers `mentions` first). The question
|
|
241
|
+
becomes a durable node, deterministically routed to the steward of the closest
|
|
242
|
+
relevant node (unrouted if none matches), and joins the decision queue until
|
|
243
|
+
answered. Answer by writing a node with an `answers` edge to the question.
|
|
244
|
+
|
|
245
|
+
By default the question's project is derived from its relevance neighborhood
|
|
246
|
+
(its `mentions`, then the compiler's picks), falling back to the asker's home
|
|
247
|
+
project. An explicit `project` overrides that derivation — pass it for a
|
|
248
|
+
mention-less question whose neighborhood would otherwise yield nothing (a
|
|
249
|
+
dispatched background agent injects its session project here, since the
|
|
250
|
+
launcher environment never reaches it).
|
|
235
251
|
|
|
236
252
|
### `run_workflow`
|
|
237
253
|
|
|
@@ -301,9 +317,9 @@ endpoint is the REST twin of a core call:
|
|
|
301
317
|
| `GET /v1/me` | `spor whoami`/`status`, onboarding | identity echo for the bearer token → `{person, name, email, bound, is_admin}`. `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 |
|
|
302
318
|
| `GET /v1/briefing/{project}` | session-start | read the `brief-<project>` node → `{found, version, body, project_brief?, graph_status}`. The slug resolves through project-node aliases (GRAPH.md "Project identity nodes") before lookup. A BARE repo slug also rides up to its home-project grouping: the grouping's `brief-<grouping>` node returns alongside as `project_brief` (the product context spanning sibling repos), matching the shared up-resolution (dec-spor-queue-slug-resolves-to-grouping); passing the repo NODE id (`repo-<slug>`) is the escape hatch that returns only the repo brief, no `project_brief`. Optional `?fp=root:<sha>,remote:<host/path>,...` carries the repo's fingerprints: the server learns them onto the owning project node, and an unknown slug with a known fingerprint files an alias proposal in the queue |
|
|
303
319
|
| `POST /v1/digest` `{query, root?, project?, min_sim?}` | prompt-context, /spor:brief | digest-mode compile → `{found, text}`; `found: false` is a successful empty result. `root` is the structural-walk twin of `query` (the two are mutually exclusive; `root` wins, an unknown id is `422`). Optional `project` is the session slug: the server scopes the compile to it — the same-project relevance boost, the grouping union, and the `always_on` norm `applies_to_*` ride-along — resolving the slug through project-node aliases/groupings inside compile (dec-spor-queue-slug-resolves-to-grouping), exactly as `/v1/queue` does. A bad slug is `422`; **omitting `project` runs the digest project-blind (byte-identical to before)**, so older clients that send only `{query}` are unaffected |
|
|
304
|
-
| `GET /v1/nodes/{id}` | /spor:brief | `get_node` semantics;
|
|
320
|
+
| `GET /v1/nodes/{id}` | /spor:brief | `get_node` semantics; a live inbound resolves/answers edge rides along as `resolution` (carrying the resolver's `summary`/`title` and a `lagging` flag — set when it contradicts a still-open status, clear when the node is already terminal, e.g. an answered question pointing at its answer), and open gardener findings about the node ride along as `open_findings` |
|
|
305
321
|
| `POST /v1/nodes` | drain-outbox, mechanical writers | `put_node` semantics, batch: `{nodes: [...], if_exists: "skip"}` (entries may be raw strings or `{node, if_exists, revision}`) → `{results: [...]}`, 207 when any entry failed |
|
|
306
|
-
| `POST /v1/nodes/{id}/edges` `{type, to}` | scripts, mechanical writers | `add_edge` semantics (§1): normalize/flip, dedupe, append — no revision echo |
|
|
322
|
+
| `POST /v1/nodes/{id}/edges` `{type, to, attrs?}` | scripts, mechanical writers | `add_edge` semantics (§1): normalize/flip, dedupe, append — no revision echo. Optional `attrs` adds trailing flat edge attributes (e.g. a per-assignment `profile:` override); re-adding the same edge with different attrs upserts the set |
|
|
307
323
|
| `POST /v1/nodes/{id}/status` `{status}` | scripts, mechanical writers | `set_status` semantics (§1): one-scalar update through the `transitions()` gate. Setting a work node to an in-progress status also CLAIMS it (same lease as `/claim` below) |
|
|
308
324
|
| `POST /v1/nodes/{id}/claim` `{session?}` | `claim`/`set_status` MCP tools, `spor dispatch` | take the heartbeat-renewed lease (dec-cc-task-claim-lease): writes the durable `assigned` edge once, attributes to `$viewer` from the token (never an argument), and creates the ephemeral lease → `{ok, status, lease: {node_id, by, expires, expires_at, session, claimed_at}, edge}`. A live lease held by ANOTHER person is `409 conflict` naming the holder + expiry (re-claiming your OWN live claim just renews it). `session` scopes the heartbeat (omit to leave it person-scoped, so any of the claimer's sessions may renew — what `spor dispatch` does at the PRE-launch claim, since `claude --bg` self-allocates the run session only at launch; dispatch then renews with the real session once it has read it from `claude agents --json`, dec-spor-dispatch-bg-session-late-bind) |
|
|
309
325
|
| `POST /v1/nodes/{id}/renew` `{session?}` | post-tool heartbeat, `renew` MCP tool, `spor dispatch` | bump the live lease's expiry only — no commit; the heartbeat that keeps a claim from lapsing. A lapsed/stolen lease is `409` (names the current holder). Person-scoped: any of the claimer's sessions may renew; a `session` binds the lease to that run (`spor dispatch` uses this to bind the captured `claude --bg` session post-launch) |
|
|
@@ -315,7 +331,7 @@ endpoint is the REST twin of a core call:
|
|
|
315
331
|
| `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 |
|
|
316
332
|
| `POST /v1/corrections` | /spor:correct | `propose_correction` semantics → 201 `{status, id, revision, warnings}` |
|
|
317
333
|
| `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) |
|
|
318
|
-
| `POST /v1/questions` `{text, title?, mentions?}` | 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, routed_to, via, asker, revision, warnings}` |
|
|
334
|
+
| `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 |
|
|
319
335
|
| `POST /v1/gardener` | ops cron / on demand | run a gardener sweep now; findings filed as queue items → `{filed, resolved, ..., generated_at}` |
|
|
320
336
|
| `GET /v1/lens/{id}/render?format=html\|text\|json` | browsers, teammates without a checkout | run a lens OR workspace node and render its view tree (html default, plain text, or the raw tree as json). Read-only — no action forms; writes stay with `/v1/nodes` and the MCP tools. Auth is the caller's bearer header OR a signed read-only **render ticket** for shared links (browser links can't carry an Authorization header): `?ticket=<blob>` is accepted once and exchanged via a 302 for an HttpOnly `spor_render_ticket` cookie (kept out of URLs, logs, and view-to-view hrefs). The ticket binds `$viewer` to the recorded sharer and the render shows a "Viewing as <sharer>" banner. The former `?token=<PAT>` sharing path is **removed** — a shared link can never carry a write-capable credential |
|
|
321
337
|
| `POST /v1/lens/{id}/ticket` `{expires?}` | sharing a view | mint a signed, expiring, read-only render ticket for the lens/workspace, recording the authenticated caller as the sharer → `{ticket, url, lens_id, sharer_person_id, exp}`. `expires` is `<N>d` or an ISO date (default `7d`, max `30d`); the caller must be bound to a person node (else `422 no_person`). The ticket carries no write scope and is honored only on the render route |
|
|
@@ -327,6 +343,10 @@ endpoint is the REST twin of a core call:
|
|
|
327
343
|
| `POST /v1/admin/agents` `{label, owner?, id?, pubkey?}` | `spor agent create`, onboarding | create a person-owned `agent` node + its `owned-by` edge (`owner` defaults to the caller's person; `id` derives from `label`) → 201 `{id, owner, spiffe, pubkey, status, revision}`. 409 dup id / 422 invalid / 403 non-admin. Admin-only, same `stewards→root` gate as `/v1/admin/tokens` |
|
|
328
344
|
| `POST /v1/agents/{id}/token` `{session?, audience?, expires?}` | `spor dispatch` | **self-serve** (NOT admin): mint a short-TTL, per-session token scoped to agent `{id}` → 201 `{token, expires_at, agent, session}` (`session: null` when deferred). Authorized iff the caller's person **owns** the agent (its `owned-by` edge) — else `403`; `404` unknown agent; `422` a SUPPLIED `session` that is malformed. `session` is now **OPTIONAL** (deferred binding, dec-spor-dispatch-bg-session-late-bind): `spor dispatch` mints it deferred because `claude --bg` self-allocates the run session only at launch, then binds the real one via `POST /v1/agents/session` below. The token carries `{agent, session?}` (the person is derived from the `owned-by` edge at verify time); a write under it is stamped agent-on-behalf-of-person (§1). A caller `expires` may only shorten the default TTL, never extend it |
|
|
329
345
|
| `POST /v1/agents/session` `{session}` | `spor dispatch` (post-launch) | **late session binding** for a session-deferred agent token (dec-spor-dispatch-bg-session-late-bind). Authenticated by the **agent token itself** (the bearer hash identifies its own record — no agent id in the path, no ownership re-check), so only an agent-scoped token may call it (`403` otherwise). Sets that token's `session` → `200 {ok, agent, session}`. **Write-once**: idempotent on the same value (`{unchanged: true}`), `409 conflict` on a different one (a token's session is provenance, not a mutable field); `422` missing/malformed `session`. Every subsequent write under the token then stamps the bound session |
|
|
346
|
+
| `POST /v1/agents/{id}/capabilities` `{harnesses?, reachable_mcp?, skills?, plugins?, deny?}` | `spor capabilities publish`, session-start auto-publish | **publish** this box's machine capabilities to the fleet scheduler (task-spor-remote-fleet-scheduler, dec-spor-machine-profile-satisfiability). The remote twin of the machine-local `dispatch.capabilities` map: the server collapses the body with the SAME `effectiveCapabilities()` the client runs (so a raw `{probed,declared,deny}` map or the already-flat axes both work; also accepts a `{capabilities: {...}}` envelope) and stores it BESIDE the agent node (operational store, not the durable git-tracked node — capabilities are machine-local, probe-refreshed, never committed) → `200 {agent, capabilities, published_at, last_seen, published_by, session?, changed}`. Authorized iff the caller **owns** the agent (its `owned-by` edge) OR is the agent itself (a self-publish under an agent token) — else `403`; `404` unknown agent; `422` a malformed map. A publish stamps both `published_at` (when the CAPS last changed) and `last_seen` (last contact); `last_seen` ALSO advances on the cheap `POST .../heartbeat` below, and the host-match keys staleness off `last_seen` not `published_at`. Beyond the manual verb, `session-start` AUTO-publishes here in remote mode whenever a `dispatch.agent` is configured (task-spor-fleet-capabilities-autopublish-session-start) — bounded + fail-open, so every session refreshes this box's caps and last-contact without a manual call; disable with `SPOR_CAPABILITIES_PUBLISH=0` |
|
|
347
|
+
| `GET /v1/agents/{id}/capabilities` | steward fleet view, debugging | read back an agent's published capabilities → `200 {agent, capabilities, published_at, last_seen, published_by, session?}`; `404` if none published. Readable by the **owner**, the **agent itself**, or an **admin** (a stewards→root fleet-capacity view) — else `403` |
|
|
348
|
+
| `POST /v1/agents/{id}/heartbeat` | post-tool mid-session liveness tick | **liveness ping** (task-spor-fleet-scheduler-hardening): refresh this box's `last_seen` WITHOUT re-uploading capabilities — the cheap "still here" signal, decoupled from a caps re-publish, so a box that published once and runs for hours stays a live fleet host. The host-match keys staleness off `last_seen`, so a box that keeps heartbeating is never demoted while a genuinely dead one ages out under `max_age` → `200 {agent, capabilities, published_at, last_seen, …}` (the refreshed record). Same owner/self gate as publish — else `403`; `404` unknown agent OR nothing published yet (publish before heartbeat — liveness without caps is meaningless to the scheduler); `422` a malformed agent id. The CLIENT caller (task-spor-fleet-scheduler-client-heartbeat-tick): the `post-tool` hook ticks this in REMOTE mode whenever a `dispatch.agent` is configured (the SAME opt-in as the session-start auto-publish), piggybacking on write-activity but THROTTLED to one ping per `dispatch.heartbeatIntervalMs` (default 5min) — so a long session keeps `last_seen` fresh between session-starts (which today refresh it the expensive way, via a full re-publish) without re-probing. Bounded + fail-open; disable with `SPOR_HEARTBEAT=0` |
|
|
349
|
+
| `GET /v1/profiles/{id}/hosts` `?owner=me\|person-X&max_age=<dur>` | `spor capabilities hosts <profile>`; `spor dispatch` (auto on a FORK B refusal) | **host-match** a `type: profile` against every agent's published capabilities using the SAME pure `satisfies()` matcher the client runs locally → `200 {profile, satisfiable: [{agent, owner, published_at, last_seen, age_seconds}], unsatisfiable: [{agent, owner, published_at, last_seen, age_seconds, reasons}], counts}`. Satisfiable hosts are freshest-first (by `last_seen`); the unsatisfiable carry the matcher's own reasons (the failing atoms), enabling **substitution-free re-routing** — pick a box that satisfies the profile, NEVER substitute a different one (dec-spor-machine-profile-satisfiability FORK B). The CLIENT consumer (task-spor-fleet-scheduler-autoroute-dispatch): `spor capabilities hosts` lists the re-route targets directly, and when `spor dispatch` refuses because THIS box can't satisfy the resolved profile it calls this endpoint and names the satisfiable hosts to re-route to — or, when none satisfy it, escalates to the owner (fail-soft: an unreachable scheduler degrades to a generic hint). **Visibility is steward-scoped** (task-spor-fleet-scheduler-hardening): the whole-fleet view (every member's boxes + caps) is a multi-tenant cross-member disclosure, so an **admin** (stewards→root) sees the whole fleet and may scope to any `owner=person-X`, while an ordinary **member** is scoped to THEIR OWN boxes (default `owner` = the caller's person; an agent token resolves to its owner; `owner=me` is the explicit form) and a member asking for a colleague's `owner=person-X` is `403`. `max_age` (`30m`/`12h`/`7d`/ms) demotes hosts whose `last_seen` is older than it to unsatisfiable (the liveness filter). `404` unknown/non-profile id; `422` bad `max_age`/`owner` |
|
|
330
350
|
|
|
331
351
|
Path parameters (node ids, project slugs) must match
|
|
332
352
|
`^[a-z0-9][a-z0-9-]*$`. Request bodies are capped at 1MB
|
package/GRAPH.md
CHANGED
|
@@ -58,7 +58,10 @@ Rules:
|
|
|
58
58
|
omit them.
|
|
59
59
|
- Edges may point at ids that don't exist yet; the compiler skips them. Don't
|
|
60
60
|
delete an edge just because the target is missing — it marks a node worth
|
|
61
|
-
creating.
|
|
61
|
+
creating. An edge may also carry extra flat attributes after `to:` —
|
|
62
|
+
`- {type: assigned, to: agent-X, profile: profile-Y}` — preserved on the edge
|
|
63
|
+
object (the per-assignment profile override; see "The agent orchestration
|
|
64
|
+
layer"). Plain `{type, to}` edges are unchanged.
|
|
62
65
|
- `commits` is an optional inline list of repo-qualified git shas
|
|
63
66
|
(`commits: [wf@1a2b3c4d, ...]`, kebab-case repo slug + 7–40 hex) linking
|
|
64
67
|
the node to the code commits that implement it (task-cc-commit-linking).
|
|
@@ -95,6 +98,8 @@ Rules:
|
|
|
95
98
|
| question | `question-` | a routed ask the graph could not answer (queueable; status `open`/`answered`, gated) |
|
|
96
99
|
| person | `person-` | a member of the org — the identity anchor for `$viewer` binding and Tier-2 question routing (team mode; see "People, routing, and onboarding") |
|
|
97
100
|
| agent | `agent-` | a person-owned automation principal — a dispatched session's durable identity, owned by a person via an `owned-by` edge; its writes attribute "agent on behalf of person" (see "Agents") |
|
|
101
|
+
| profile | `profile-`| a reusable runtime+capability bundle an agent runs under: `harness`, `model`, `skills`/`plugins`/`mcp`. Its runtime fields ARE the dispatch satisfiability spec; `capturable: false` (see "The agent orchestration layer") |
|
|
102
|
+
| routine | `routine-`| owner-scoped trigger→action automation (`owned-by` a person): declarative `when → do` rules over graph events that dispatch only the owner's agents, AND-ed with org policy; `capturable: false` (see "The agent orchestration layer") |
|
|
98
103
|
| capture-pending | `cap-` | raw captured text that fit no schema; filed by the server for later triage (QUEUE.md §2.3); born status-less, closed only as `merged` (content now in proper node(s)) or `rejected` (no durable fact) — a `transitions()` gate rejects other statuses at write time |
|
|
99
104
|
| finding | `find-` | a gardener observation about another node, filed as a queue item (QUEUE.md §6) |
|
|
100
105
|
| repo | `repo-` | durable git-repo identity: slug aliases + repo fingerprints; heals renames at read time (below) |
|
|
@@ -606,6 +611,118 @@ edges:
|
|
|
606
611
|
mirroring `person`, `repo`, and `workflow-run`. (So the distiller's emit
|
|
607
612
|
vocabulary deliberately omits `agent`/`owned-by`.)
|
|
608
613
|
|
|
614
|
+
## The agent orchestration layer
|
|
615
|
+
|
|
616
|
+
The layer ABOVE agent identity (dec-spor-agent-orchestration-layer): how work is
|
|
617
|
+
routed to agents, and how per-person automation fires on graph events. The node
|
|
618
|
+
model is `person ──owns──▶ agent ──uses-profile──▶ profile`, with owner-scoped
|
|
619
|
+
`routine` nodes driving automation. It composes with — never bypasses — the org
|
|
620
|
+
policy layer. The schemas ship in the seed pack, and so do the machine-local
|
|
621
|
+
`dispatch.capabilities` map + `satisfies()` matcher + fail-soft dispatch
|
|
622
|
+
(task-spor-dispatch-capabilities-satisfiability); the routine engine and the
|
|
623
|
+
remote fleet scheduler are still deferred.
|
|
624
|
+
|
|
625
|
+
### profile — the reusable runtime+capability bundle
|
|
626
|
+
|
|
627
|
+
A `profile` node (prefix `profile-`) is "the HOW" an agent dispatches under,
|
|
628
|
+
factored out of the agent node so a toolset is reusable across agents and people.
|
|
629
|
+
|
|
630
|
+
```markdown
|
|
631
|
+
---
|
|
632
|
+
id: profile-docs-writer
|
|
633
|
+
type: profile
|
|
634
|
+
title: Docs-writer profile
|
|
635
|
+
summary: Claude-code on Opus with the writing + spor skills and the spor MCP server.
|
|
636
|
+
harness: claude-code
|
|
637
|
+
model: opus
|
|
638
|
+
skills: [writing, brief]
|
|
639
|
+
plugins: [spor]
|
|
640
|
+
mcp: [spor]
|
|
641
|
+
status: active
|
|
642
|
+
date: 2026-06-18
|
|
643
|
+
---
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
- `harness:` (`claude-code` | `codex` | `opencode` | …) selects the launcher
|
|
647
|
+
(dec-cc-portable-core-adapters: claude-code → `claude --bg`, others → their
|
|
648
|
+
CLIs); `model:` → the harness `--model`; `skills`/`plugins` are preloaded;
|
|
649
|
+
`mcp` is merged into the strict `--mcp-config` dispatch writes, so the agent's
|
|
650
|
+
toolset is exactly the profile plus the agent-spor server, nothing ambient
|
|
651
|
+
(dec-spor-session-identity-active-record).
|
|
652
|
+
- **The runtime fields ARE the satisfiability spec** — there is no separate
|
|
653
|
+
requirements block (dec-spor-machine-profile-satisfiability). A machine
|
|
654
|
+
declares ATOMIC capabilities in a machine-local `dispatch.capabilities` map
|
|
655
|
+
(built like `dispatch.repos`, never committed), and `satisfies(machine,
|
|
656
|
+
profile)` checks `profile.harness ∈ machine.harnesses ∧ profile.mcp ⊆
|
|
657
|
+
machine.reachable_mcp ∧ profile.skills ⊆ machine.skills ∧ profile.plugins ⊆
|
|
658
|
+
machine.plugins ∧ profile ∉ machine.deny`. No satisfying machine → dispatch
|
|
659
|
+
fails soft and LOUD, leaves the assignment intact, NEVER substitutes a
|
|
660
|
+
different profile. The probe seeds `reachable_mcp: [spor]` from CONFIGURED-ness
|
|
661
|
+
(a bound Spor server/connector, remote mode) rather than a network ping — the
|
|
662
|
+
agent-spor server is part of every dispatched session's toolset by construction
|
|
663
|
+
(above), so an `mcp: [spor]` profile satisfies on a fresh box with no manual
|
|
664
|
+
`allow-mcp` (task-spor-mcp-reachability-deterministic-seed). Forward-compatible
|
|
665
|
+
with the deferred remote fleet scheduler (each agent publishes its capabilities;
|
|
666
|
+
same vocabulary).
|
|
667
|
+
- **Reusable + both-scoped, with override.** Profiles are PERSONAL and
|
|
668
|
+
ORG-PUBLISHED (a curated, vetted toolset), with personal override. Org-published
|
|
669
|
+
profiles are where this meets policy: a policy can require that work of a risk
|
|
670
|
+
class go to an agent whose profile is org-approved (curated-toolset-as-governance).
|
|
671
|
+
- `status:` is declarative (`active` default); `capturable: false` (created
|
|
672
|
+
deliberately, never distilled).
|
|
673
|
+
|
|
674
|
+
### routine — owner-scoped trigger→action automation
|
|
675
|
+
|
|
676
|
+
A `routine` node (prefix `routine-`), `owned-by` a person, holds declarative
|
|
677
|
+
`when → do` rules. Triggers are graph events (a status change — e.g. to a
|
|
678
|
+
resolving state, or to `done` — or an edge like `reviewed-by`/
|
|
679
|
+
`changes-requested-by`) with `where:` filters; actions are the bounded verb set
|
|
680
|
+
`create-node` / `assign` (to a SPECIFIC agent, optionally with a `profile:`
|
|
681
|
+
override) / `dispatch` / `set-status` / `reassign` / `escalate`. Because the
|
|
682
|
+
regex frontmatter parser is flat, an instance carries its `rules` as a fenced
|
|
683
|
+
` ```json ` block in its BODY, parsed by the deferred routine engine.
|
|
684
|
+
|
|
685
|
+
Two invariants (dec-spor-agent-orchestration-layer): (1) **only the owner's
|
|
686
|
+
agents are ever dispatched** — the `person → routine → agent` RFC 8693 act-chain
|
|
687
|
+
is the audit trail; (2) **personal routines accelerate, org policy gates — they
|
|
688
|
+
AND, never bypass** — agent-on-behalf-of-X counts as X for the self-approval ban
|
|
689
|
+
and the definition-of-done quorum, so an owner's reviewer-agent cannot
|
|
690
|
+
auto-approve their implementer-agent's work past the org bar. Per
|
|
691
|
+
dec-spor-orchestration-routine-requires-threads (thread 1) the bounded
|
|
692
|
+
declarative register ships FIRST; attached sandboxed code is a later
|
|
693
|
+
schema-gated escape hatch. The routine ENGINE and its vetting (dry-run, mandatory
|
|
694
|
+
retry budget, self-approval-floor activation review, fire-time org-policy gate —
|
|
695
|
+
thread 2) are deferred, so this seed schema is declarative and UNGATED;
|
|
696
|
+
`capturable: false`.
|
|
697
|
+
|
|
698
|
+
### Routing is explicit assignment; the profile cascade
|
|
699
|
+
|
|
700
|
+
Routing is EXPLICIT `assigned` to a SPECIFIC target, not an eligibility match:
|
|
701
|
+
`assigned → person` is human work (the default human pool); `assigned → agent-X`
|
|
702
|
+
makes the task a dispatch candidate (X's profile says how and what it may touch).
|
|
703
|
+
An agent-targeted `assigned` edge may carry an optional `profile:` attribute —
|
|
704
|
+
the durable per-assignment override (`{type: assigned, to: agent-X, profile:
|
|
705
|
+
profile-Y}`, thread 3). Profile precedence, explicit wins: `--profile` dispatch
|
|
706
|
+
flag > assignment-edge `profile:` attribute > the agent's default `uses-profile`
|
|
707
|
+
edge.
|
|
708
|
+
|
|
709
|
+
### The `requires:` risk-class register
|
|
710
|
+
|
|
711
|
+
`requires:` is a flat list on a WORK node naming the risk/permission classes the
|
|
712
|
+
work may touch (`requires: [shell, prod-creds]`). It is a **registry-declared
|
|
713
|
+
extensible enum** — a `type: schema` node with `kind: register` and `register:
|
|
714
|
+
requires` (seed: `schema-requires`), so an org grows the vocabulary by editing a
|
|
715
|
+
schema node, never a code change. The kernel exposes it as a partition
|
|
716
|
+
(`graph.registry.requiresClasses()`); it stays policy-free. The register is
|
|
717
|
+
**DISTINCT from machine-satisfiability**: satisfiability asks "can this box LAUNCH
|
|
718
|
+
the profile"; `requires:` asks "what may this work touch", validated against the
|
|
719
|
+
assigned profile (a task's `requires:` must be ⊆ the profile's granted classes,
|
|
720
|
+
else warn/refuse) and gated by org policy via the same governs-traversal the
|
|
721
|
+
definition-of-done quorum uses (dec-spor-orchestration-routine-requires-threads
|
|
722
|
+
thread 4). The seed set is small — `shell`, `prod-creds`, `browser`, `network`,
|
|
723
|
+
`human`, `filesystem-write`, `paid-api`. `human` is unsatisfiable by any agent:
|
|
724
|
+
assign that work to a person.
|
|
725
|
+
|
|
609
726
|
## Edge types and traversal weights
|
|
610
727
|
|
|
611
728
|
| edge | weight | meaning |
|
|
@@ -618,7 +735,7 @@ edges:
|
|
|
618
735
|
| `resolves` | 0.9 | this node fixes/closes the target |
|
|
619
736
|
| `blocks` | 0.7 | target cannot proceed until this node does |
|
|
620
737
|
| `answers` | 0.7 | this node answers that question (inverse `answered-by`); pulls the answer through the asker's next compile |
|
|
621
|
-
| `assigned` | 0.5 | work is assigned to this person
|
|
738
|
+
| `assigned` | 0.5 | work is assigned to this person OR agent (the explicit-routing edge; an agent target may carry a `profile:` per-assignment override) |
|
|
622
739
|
| `reviewed-by` | 0.5 | this person reviewed and approved the node — counts toward a policy quorum |
|
|
623
740
|
| `changes-requested-by` | 0.5 | this person reviewed the node and requested changes — not an approval |
|
|
624
741
|
| `relates-to` | 0.5 | weak association |
|
|
@@ -626,6 +743,7 @@ edges:
|
|
|
626
743
|
| `stewards` | 0.4 | this person stewards an area/spec/norm — the Tier-2 question-routing key |
|
|
627
744
|
| `grouped-under` | 0.3 | this repo's home project grouping (inverse `groups`); structural membership, not work dependency |
|
|
628
745
|
| `owned-by` | 0.3 | this agent is owned by that person (inverse `owns`); structural identity binding, not work dependency |
|
|
746
|
+
| `uses-profile` | 0.3 | this agent's default profile (the runtime+capability bundle it dispatches under); structural config binding, overridable per assignment/dispatch |
|
|
629
747
|
| `routed-to` | 0.3 | a question routed to this person for answering |
|
|
630
748
|
| `review-requested` | 0.3 | a review of this node is requested of this person (pending) — surfaces in their queue |
|
|
631
749
|
| `compiled-for` | — | briefing → its task/query (provenance only) |
|
package/QUEUE.md
CHANGED
|
@@ -142,6 +142,15 @@ is the default and graph-resident schema nodes override/extend it. The
|
|
|
142
142
|
`schema` node type itself is recognized natively by the core — no
|
|
143
143
|
schema-for-schemas regress.
|
|
144
144
|
|
|
145
|
+
Beyond `node-schema`/`edge-schema`, a schema node may carry a non-type `kind`:
|
|
146
|
+
`policy` (the org-defined policy layer, §below), `queue-policy` (the ranking
|
|
147
|
+
override singleton), and `register` — a registry-declared extensible **enum**
|
|
148
|
+
the kernel exposes as a partition (`graph.registry.register(name)` /
|
|
149
|
+
`requiresClasses()`) instead of a hardcoded table. The seed `requires` register
|
|
150
|
+
(`schema-requires`, the work-node risk/permission axis) is the first; an org
|
|
151
|
+
grows the vocabulary by writing a resident `kind: register` schema with the same
|
|
152
|
+
`register:` name (graph beats seed, higher CalVer wins — like every other kind).
|
|
153
|
+
|
|
145
154
|
Resolution is **graph beats seed wholesale, regardless of version**: a
|
|
146
155
|
resident override replaces the seed entry for that type entirely, so a seed
|
|
147
156
|
behavior change (a new `transitions()` gate, a default, a prefix) does *not*
|