@sporhq/spor 0.9.0 → 0.10.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 +17 -8
- package/GRAPH.md +114 -2
- package/QUEUE.md +9 -0
- package/bin/spor.js +20 -2
- package/lib/kernel/graph.js +25 -7
- package/lib/kernel/queue.js +1 -1
- package/lib/kernel/registry.js +66 -2
- package/lib/kernel/resolution.js +9 -5
- 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 +1 -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.10.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
|
|
|
@@ -301,9 +310,9 @@ endpoint is the REST twin of a core call:
|
|
|
301
310
|
| `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
311
|
| `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
312
|
| `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;
|
|
313
|
+
| `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
314
|
| `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 |
|
|
315
|
+
| `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
316
|
| `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
317
|
| `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
318
|
| `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) |
|
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,112 @@ 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; the dispatch matcher, the
|
|
621
|
+
routine engine, and the remote fleet scheduler are deferred
|
|
622
|
+
(task-spor-dispatch-capabilities-satisfiability).
|
|
623
|
+
|
|
624
|
+
### profile — the reusable runtime+capability bundle
|
|
625
|
+
|
|
626
|
+
A `profile` node (prefix `profile-`) is "the HOW" an agent dispatches under,
|
|
627
|
+
factored out of the agent node so a toolset is reusable across agents and people.
|
|
628
|
+
|
|
629
|
+
```markdown
|
|
630
|
+
---
|
|
631
|
+
id: profile-docs-writer
|
|
632
|
+
type: profile
|
|
633
|
+
title: Docs-writer profile
|
|
634
|
+
summary: Claude-code on Opus with the writing + spor skills and the spor MCP server.
|
|
635
|
+
harness: claude-code
|
|
636
|
+
model: opus
|
|
637
|
+
skills: [writing, brief]
|
|
638
|
+
plugins: [spor]
|
|
639
|
+
mcp: [spor]
|
|
640
|
+
status: active
|
|
641
|
+
date: 2026-06-18
|
|
642
|
+
---
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
- `harness:` (`claude-code` | `codex` | `opencode` | …) selects the launcher
|
|
646
|
+
(dec-cc-portable-core-adapters: claude-code → `claude --bg`, others → their
|
|
647
|
+
CLIs); `model:` → the harness `--model`; `skills`/`plugins` are preloaded;
|
|
648
|
+
`mcp` is merged into the strict `--mcp-config` dispatch writes, so the agent's
|
|
649
|
+
toolset is exactly the profile plus the agent-spor server, nothing ambient
|
|
650
|
+
(dec-spor-session-identity-active-record).
|
|
651
|
+
- **The runtime fields ARE the satisfiability spec** — there is no separate
|
|
652
|
+
requirements block (dec-spor-machine-profile-satisfiability). A machine
|
|
653
|
+
declares ATOMIC capabilities in a machine-local `dispatch.capabilities` map
|
|
654
|
+
(built like `dispatch.repos`, never committed), and `satisfies(machine,
|
|
655
|
+
profile)` checks `profile.harness ∈ machine.harnesses ∧ profile.mcp ⊆
|
|
656
|
+
machine.reachable_mcp ∧ profile.skills ⊆ machine.skills ∧ profile.plugins ⊆
|
|
657
|
+
machine.plugins ∧ profile ∉ machine.deny`. No satisfying machine → dispatch
|
|
658
|
+
fails soft and LOUD, leaves the assignment intact, NEVER substitutes a
|
|
659
|
+
different profile. Forward-compatible with the deferred remote fleet scheduler
|
|
660
|
+
(each agent publishes its capabilities; same vocabulary).
|
|
661
|
+
- **Reusable + both-scoped, with override.** Profiles are PERSONAL and
|
|
662
|
+
ORG-PUBLISHED (a curated, vetted toolset), with personal override. Org-published
|
|
663
|
+
profiles are where this meets policy: a policy can require that work of a risk
|
|
664
|
+
class go to an agent whose profile is org-approved (curated-toolset-as-governance).
|
|
665
|
+
- `status:` is declarative (`active` default); `capturable: false` (created
|
|
666
|
+
deliberately, never distilled).
|
|
667
|
+
|
|
668
|
+
### routine — owner-scoped trigger→action automation
|
|
669
|
+
|
|
670
|
+
A `routine` node (prefix `routine-`), `owned-by` a person, holds declarative
|
|
671
|
+
`when → do` rules. Triggers are graph events (a status change — e.g. to a
|
|
672
|
+
resolving state, or to `done` — or an edge like `reviewed-by`/
|
|
673
|
+
`changes-requested-by`) with `where:` filters; actions are the bounded verb set
|
|
674
|
+
`create-node` / `assign` (to a SPECIFIC agent, optionally with a `profile:`
|
|
675
|
+
override) / `dispatch` / `set-status` / `reassign` / `escalate`. Because the
|
|
676
|
+
regex frontmatter parser is flat, an instance carries its `rules` as a fenced
|
|
677
|
+
` ```json ` block in its BODY, parsed by the deferred routine engine.
|
|
678
|
+
|
|
679
|
+
Two invariants (dec-spor-agent-orchestration-layer): (1) **only the owner's
|
|
680
|
+
agents are ever dispatched** — the `person → routine → agent` RFC 8693 act-chain
|
|
681
|
+
is the audit trail; (2) **personal routines accelerate, org policy gates — they
|
|
682
|
+
AND, never bypass** — agent-on-behalf-of-X counts as X for the self-approval ban
|
|
683
|
+
and the definition-of-done quorum, so an owner's reviewer-agent cannot
|
|
684
|
+
auto-approve their implementer-agent's work past the org bar. Per
|
|
685
|
+
dec-spor-orchestration-routine-requires-threads (thread 1) the bounded
|
|
686
|
+
declarative register ships FIRST; attached sandboxed code is a later
|
|
687
|
+
schema-gated escape hatch. The routine ENGINE and its vetting (dry-run, mandatory
|
|
688
|
+
retry budget, self-approval-floor activation review, fire-time org-policy gate —
|
|
689
|
+
thread 2) are deferred, so this seed schema is declarative and UNGATED;
|
|
690
|
+
`capturable: false`.
|
|
691
|
+
|
|
692
|
+
### Routing is explicit assignment; the profile cascade
|
|
693
|
+
|
|
694
|
+
Routing is EXPLICIT `assigned` to a SPECIFIC target, not an eligibility match:
|
|
695
|
+
`assigned → person` is human work (the default human pool); `assigned → agent-X`
|
|
696
|
+
makes the task a dispatch candidate (X's profile says how and what it may touch).
|
|
697
|
+
An agent-targeted `assigned` edge may carry an optional `profile:` attribute —
|
|
698
|
+
the durable per-assignment override (`{type: assigned, to: agent-X, profile:
|
|
699
|
+
profile-Y}`, thread 3). Profile precedence, explicit wins: `--profile` dispatch
|
|
700
|
+
flag > assignment-edge `profile:` attribute > the agent's default `uses-profile`
|
|
701
|
+
edge.
|
|
702
|
+
|
|
703
|
+
### The `requires:` risk-class register
|
|
704
|
+
|
|
705
|
+
`requires:` is a flat list on a WORK node naming the risk/permission classes the
|
|
706
|
+
work may touch (`requires: [shell, prod-creds]`). It is a **registry-declared
|
|
707
|
+
extensible enum** — a `type: schema` node with `kind: register` and `register:
|
|
708
|
+
requires` (seed: `schema-requires`), so an org grows the vocabulary by editing a
|
|
709
|
+
schema node, never a code change. The kernel exposes it as a partition
|
|
710
|
+
(`graph.registry.requiresClasses()`); it stays policy-free. The register is
|
|
711
|
+
**DISTINCT from machine-satisfiability**: satisfiability asks "can this box LAUNCH
|
|
712
|
+
the profile"; `requires:` asks "what may this work touch", validated against the
|
|
713
|
+
assigned profile (a task's `requires:` must be ⊆ the profile's granted classes,
|
|
714
|
+
else warn/refuse) and gated by org policy via the same governs-traversal the
|
|
715
|
+
definition-of-done quorum uses (dec-spor-orchestration-routine-requires-threads
|
|
716
|
+
thread 4). The seed set is small — `shell`, `prod-creds`, `browser`, `network`,
|
|
717
|
+
`human`, `filesystem-write`, `paid-api`. `human` is unsatisfiable by any agent:
|
|
718
|
+
assign that work to a person.
|
|
719
|
+
|
|
609
720
|
## Edge types and traversal weights
|
|
610
721
|
|
|
611
722
|
| edge | weight | meaning |
|
|
@@ -618,7 +729,7 @@ edges:
|
|
|
618
729
|
| `resolves` | 0.9 | this node fixes/closes the target |
|
|
619
730
|
| `blocks` | 0.7 | target cannot proceed until this node does |
|
|
620
731
|
| `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
|
|
732
|
+
| `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
733
|
| `reviewed-by` | 0.5 | this person reviewed and approved the node — counts toward a policy quorum |
|
|
623
734
|
| `changes-requested-by` | 0.5 | this person reviewed the node and requested changes — not an approval |
|
|
624
735
|
| `relates-to` | 0.5 | weak association |
|
|
@@ -626,6 +737,7 @@ edges:
|
|
|
626
737
|
| `stewards` | 0.4 | this person stewards an area/spec/norm — the Tier-2 question-routing key |
|
|
627
738
|
| `grouped-under` | 0.3 | this repo's home project grouping (inverse `groups`); structural membership, not work dependency |
|
|
628
739
|
| `owned-by` | 0.3 | this agent is owned by that person (inverse `owns`); structural identity binding, not work dependency |
|
|
740
|
+
| `uses-profile` | 0.3 | this agent's default profile (the runtime+capability bundle it dispatches under); structural config binding, overridable per assignment/dispatch |
|
|
629
741
|
| `routed-to` | 0.3 | a question routed to this person for answering |
|
|
630
742
|
| `review-requested` | 0.3 | a review of this node is requested of this person (pending) — surfaces in their queue |
|
|
631
743
|
| `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*
|
package/bin/spor.js
CHANGED
|
@@ -2088,21 +2088,39 @@ async function compileBriefing(cfg, { nodeId, query, full, project }) {
|
|
|
2088
2088
|
async function topQueueItem(cfg, slug) {
|
|
2089
2089
|
const LIMIT = 25;
|
|
2090
2090
|
let items = [];
|
|
2091
|
+
// --from-queue dispatches an AGENT to do work, and questions are human
|
|
2092
|
+
// decisions — not agent-dispatchable (the standing model: agent-actionable
|
|
2093
|
+
// work is a task, not a question; dec-spor-questions-human-not-agent-dispatch).
|
|
2094
|
+
// Exclude them AT THE RANKER (the issue's preferred fix,
|
|
2095
|
+
// issue-spor-dispatch-from-queue-dispatches-questions): excludeTypes/
|
|
2096
|
+
// exclude_type is a hard scope filter applied BEFORE the limit, so the page is
|
|
2097
|
+
// a full LIMIT of actionable candidates rather than LIMIT-minus-questions —
|
|
2098
|
+
// the in-flight skip below then has the whole page to advance through (a page
|
|
2099
|
+
// crowded by top-ranked questions could otherwise starve it). Questions stay
|
|
2100
|
+
// queueable for the HUMAN queue (`spor next`). Sibling of
|
|
2101
|
+
// issue-spor-routed-questions-ignore-wake.
|
|
2091
2102
|
if (cfg.mode() === "remote") {
|
|
2092
|
-
const
|
|
2103
|
+
const base = `limit=${LIMIT}&exclude_type=question`;
|
|
2104
|
+
const q = slug ? `?project=${encodeURIComponent(slug)}&${base}` : `?${base}`;
|
|
2093
2105
|
const r = await remote.get(cfg, `/v1/queue${q}`, { timeoutMs: 6000 });
|
|
2094
2106
|
items = r.ok && r.json ? r.json.items || [] : [];
|
|
2095
2107
|
} else {
|
|
2096
2108
|
try {
|
|
2097
2109
|
const g = require(path.join(ROOT, "lib", "graph.js")).loadGraph(cfg.nodesDir());
|
|
2098
2110
|
const { rankQueue } = require(path.join(ROOT, "lib", "queue.js"));
|
|
2099
|
-
const
|
|
2111
|
+
const opts = { limit: LIMIT, excludeTypes: ["question"] };
|
|
2112
|
+
const r = rankQueue(g, slug ? { project: slug, ...opts } : opts);
|
|
2100
2113
|
items = r.items || [];
|
|
2101
2114
|
} catch {
|
|
2102
2115
|
items = [];
|
|
2103
2116
|
}
|
|
2104
2117
|
}
|
|
2105
2118
|
if (!items.length) return null;
|
|
2119
|
+
// Defense-in-depth: drop any question the ranker left in (an older server that
|
|
2120
|
+
// predates / ignores exclude_type), so a question is never dispatched even
|
|
2121
|
+
// against a stale backend. Primary exclusion is at the ranker above.
|
|
2122
|
+
items = items.filter((it) => it.type !== "question");
|
|
2123
|
+
if (!items.length) return null;
|
|
2106
2124
|
// Skip items already in flight on this machine; advance to the first free one.
|
|
2107
2125
|
const { items: free, hidden } = annotateInFlight(items, dispatchedAgents(), true);
|
|
2108
2126
|
if (hidden && free.length) {
|
package/lib/kernel/graph.js
CHANGED
|
@@ -90,15 +90,20 @@ const DEFAULT_MIN_SIM = 0.08;
|
|
|
90
90
|
// Supports simple
|
|
91
91
|
// key: value, YAML folded multi-line values (indented continuations),
|
|
92
92
|
// pin:/exclude:/queue_mute:/commits:/slugs:/fingerprints:/roles:/tags:/
|
|
93
|
-
// applies_to_tags:/applies_to_repos:/applies_to_projects
|
|
94
|
-
// and "- {type: X, to: Y}" edges
|
|
93
|
+
// applies_to_tags:/applies_to_repos:/applies_to_projects:/skills:/plugins:/
|
|
94
|
+
// mcp:/requires: inline lists, and "- {type: X, to: Y}" edges (which may carry
|
|
95
|
+
// extra flat attributes — "- {type: assigned, to: agent-X, profile: profile-Y}"
|
|
96
|
+
// — preserved on the edge object). commits entries are repo-qualified shas
|
|
95
97
|
// ("repo@sha", task-cc-commit-linking); slugs/fingerprints are the project
|
|
96
98
|
// node's alias and repo-evidence registers (task-cc-project-identity-nodes);
|
|
97
99
|
// roles is the person node's role-list register the org-defined policy layer's
|
|
98
100
|
// quorum gate counts qualified approvals against (task-cc-policy-layer); tags is
|
|
99
101
|
// the repo node's free-form label register, and applies_to_* are a norm's
|
|
100
102
|
// repo/tag-scope selectors matched against it (task-cc-norm-ride-along-repo-tag-
|
|
101
|
-
// scope)
|
|
103
|
+
// scope); skills/plugins/mcp are the orchestration-layer profile node's
|
|
104
|
+
// runtime+capability bundle and requires is a work node's risk-class list (the
|
|
105
|
+
// registry-declared `requires` register, dec-spor-orchestration-routine-
|
|
106
|
+
// requires-threads) — all flat strings, not objects, by parser design.
|
|
102
107
|
function parseFrontmatter(raw, file) {
|
|
103
108
|
const m = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
104
109
|
if (!m) throw new Error(`no frontmatter in ${file}`);
|
|
@@ -106,7 +111,7 @@ function parseFrontmatter(raw, file) {
|
|
|
106
111
|
const node = { edges: [], pin: [], exclude: [], body: body.trim(), file };
|
|
107
112
|
let lastKey = null; // for YAML folded scalars (indented continuation lines)
|
|
108
113
|
for (const line of fm.split("\n")) {
|
|
109
|
-
const list = line.match(/^(pin|exclude|queue_mute|commits|slugs|fingerprints|roles|tags|applies_to_tags|applies_to_repos|applies_to_projects):\s*\[([^\]]*)\]/);
|
|
114
|
+
const list = line.match(/^(pin|exclude|queue_mute|commits|slugs|fingerprints|roles|tags|applies_to_tags|applies_to_repos|applies_to_projects|skills|plugins|mcp|requires):\s*\[([^\]]*)\]/);
|
|
110
115
|
if (list) { node[list[1]] = list[2].split(",").map((s) => s.trim()).filter(Boolean); lastKey = null; continue; }
|
|
111
116
|
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
112
117
|
if (kv) {
|
|
@@ -114,8 +119,18 @@ function parseFrontmatter(raw, file) {
|
|
|
114
119
|
else lastKey = null;
|
|
115
120
|
continue;
|
|
116
121
|
}
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
// Edges are "- {type: X, to: Y}" and may carry trailing flat attributes
|
|
123
|
+
// ("- {type: assigned, to: agent-X, profile: profile-Y}", the per-assignment
|
|
124
|
+
// profile override, dec-spor-orchestration-routine-requires-threads thread 3).
|
|
125
|
+
// The first two captures stay type/to (byte-identical for the no-attribute
|
|
126
|
+
// form); any "key: value" pairs after `to:` are folded onto the edge object.
|
|
127
|
+
const edge = line.match(/-\s*\{type:\s*([\w-]+),\s*to:\s*([\w-]+)((?:\s*,\s*[\w-]+:\s*[\w-]+)*)\}/);
|
|
128
|
+
if (edge) {
|
|
129
|
+
const e = { type: edge[1], to: edge[2] };
|
|
130
|
+
if (edge[3]) for (const a of edge[3].matchAll(/,\s*([\w-]+):\s*([\w-]+)/g)) e[a[1]] = a[2];
|
|
131
|
+
node.edges.push(e);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
119
134
|
const cont = line.match(/^\s+(\S.*)$/);
|
|
120
135
|
if (cont && lastKey) node[lastKey] += ` ${cont[1].trim()}`;
|
|
121
136
|
}
|
|
@@ -1166,7 +1181,10 @@ function validateGraphFiles(files, seedSchemas = []) {
|
|
|
1166
1181
|
for (const line of m[1].split("\n")) {
|
|
1167
1182
|
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
1168
1183
|
if (kv && kv[1] !== "edges") node[kv[1]] = kv[2].replace(/^["']|["']$/g, "");
|
|
1169
|
-
|
|
1184
|
+
// Match the build parser: tolerate trailing flat edge attributes
|
|
1185
|
+
// (e.g. profile: on a profile-bearing assigned edge); validate only needs
|
|
1186
|
+
// type/to, so the extra attributes are matched and discarded here.
|
|
1187
|
+
const edge = line.match(/-\s*\{type:\s*([\w-]+),\s*to:\s*([\w-]+)(?:\s*,\s*[\w-]+:\s*[\w-]+)*\}/);
|
|
1170
1188
|
if (edge) node.edges.push({ type: edge[1], to: edge[2] });
|
|
1171
1189
|
}
|
|
1172
1190
|
for (const field of ["id", "type", "title", "summary"]) {
|
package/lib/kernel/queue.js
CHANGED
package/lib/kernel/registry.js
CHANGED
|
@@ -113,7 +113,7 @@ function applyUpgrades(node, schema) {
|
|
|
113
113
|
|
|
114
114
|
const FENCE_RE = /^```(\w+)?[ \t]*\n([\s\S]*?)^```[ \t]*$/gm;
|
|
115
115
|
const EXPORT_RE = /export\s+(?:async\s+)?(?:function|const|let)\s+([A-Za-z_$][\w$]*)/g;
|
|
116
|
-
const KINDS = new Set(["node-schema", "edge-schema", "queue-policy", "policy"]);
|
|
116
|
+
const KINDS = new Set(["node-schema", "edge-schema", "queue-policy", "policy", "register"]);
|
|
117
117
|
|
|
118
118
|
// Takes an already-parsed graph node (frontmatter fields + body) and extracts
|
|
119
119
|
// the registry entry. Never throws on bad data; returns { ok, errors, schema }.
|
|
@@ -127,7 +127,7 @@ function parseSchemaNode(node) {
|
|
|
127
127
|
const id = node.id ?? "?";
|
|
128
128
|
|
|
129
129
|
if (!KINDS.has(node.kind)) {
|
|
130
|
-
errors.push(`kind '${node.kind}' must be node-schema, edge-schema,
|
|
130
|
+
errors.push(`kind '${node.kind}' must be one of: node-schema, edge-schema, queue-policy, policy, register`);
|
|
131
131
|
}
|
|
132
132
|
if (!parseCalVer(node.schema_version)) {
|
|
133
133
|
errors.push(`schema_version '${node.schema_version}' is not CalVer YYYY.MM.DD.MICRO`);
|
|
@@ -221,6 +221,26 @@ function parseSchemaNode(node) {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
+
} else if (node.kind === "register") {
|
|
225
|
+
// A registry-declared extensible enum — a named vocabulary the kernel
|
|
226
|
+
// exposes as a partition (graph.registry.register(name)) instead of
|
|
227
|
+
// owning a hardcoded table (dec-spor-orchestration-routine-requires-
|
|
228
|
+
// threads thread 4: `requires:` is the first such register, the
|
|
229
|
+
// risk/permission axis). Keyed by the register NAME (payload.register),
|
|
230
|
+
// so a graph-resident register schema overrides/extends the seed one
|
|
231
|
+
// wholesale — an org grows the enum by editing a schema node, no code
|
|
232
|
+
// change. `classes` is the enum: an array of { id, description } objects;
|
|
233
|
+
// readers (the dispatch matcher, the policy layer) read the ids off the
|
|
234
|
+
// partition. The kernel only DECLARES the vocabulary here — validating a
|
|
235
|
+
// node's `requires:` against it, and policy gating, are downstream.
|
|
236
|
+
key = payload.register;
|
|
237
|
+
if (typeof key !== "string" || !key) errors.push(`register payload missing register name`);
|
|
238
|
+
if (payload.classes != null) {
|
|
239
|
+
if (!Array.isArray(payload.classes) ||
|
|
240
|
+
payload.classes.some((c) => !c || typeof c !== "object" || typeof c.id !== "string" || !c.id)) {
|
|
241
|
+
errors.push(`register classes must be an array of { id, description } objects`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
224
244
|
} else if (node.kind === "edge-schema") {
|
|
225
245
|
key = payload.edge_type;
|
|
226
246
|
if (typeof key !== "string" || !key) errors.push(`edge-schema payload missing edge_type`);
|
|
@@ -288,6 +308,12 @@ class Registry {
|
|
|
288
308
|
this.policySchemas = new Map(); // id -> policy schema (+ .source); the
|
|
289
309
|
// org-defined policy layer (kind: policy),
|
|
290
310
|
// NOT a singleton — selected by scope.
|
|
311
|
+
this.registers = new Map(); // name -> register schema (+ .source); the
|
|
312
|
+
// registry-declared extensible enums (kind:
|
|
313
|
+
// register), e.g. `requires` (the risk axis,
|
|
314
|
+
// dec-spor-orchestration-routine-requires-
|
|
315
|
+
// threads). Keyed by register name; graph
|
|
316
|
+
// beats seed, higher CalVer wins.
|
|
291
317
|
// key -> { graphId, graphVersion, seedVersion } when a graph-resident
|
|
292
318
|
// schema wins over a STRICTLY NEWER seed entry for the same type — the
|
|
293
319
|
// silent-shadow case (issue-cc-schema-override-seed-shadow). Surfaced as
|
|
@@ -340,6 +366,16 @@ class Registry {
|
|
|
340
366
|
this.policySchemas.set(schema.key, entry);
|
|
341
367
|
return true;
|
|
342
368
|
}
|
|
369
|
+
if (schema.kind === "register") {
|
|
370
|
+
// Keyed by register name. Same precedence as the rest (graph beats seed,
|
|
371
|
+
// higher version wins within a source); distinct register names coexist.
|
|
372
|
+
const existing = this.registers.get(schema.key);
|
|
373
|
+
const winner = Registry._prefer(existing, entry);
|
|
374
|
+
this._noteShadow(schema.key, winner, winner === entry ? existing : entry);
|
|
375
|
+
if (winner !== entry) return false;
|
|
376
|
+
this.registers.set(schema.key, entry);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
343
379
|
const map = schema.kind === "edge-schema" ? this.edgeSchemas : this.nodeSchemas;
|
|
344
380
|
const existing = map.get(schema.key);
|
|
345
381
|
const winner = Registry._prefer(existing, entry);
|
|
@@ -420,6 +456,34 @@ class Registry {
|
|
|
420
456
|
return !this.nonResolvingStatuses().has((status || "").toLowerCase());
|
|
421
457
|
}
|
|
422
458
|
|
|
459
|
+
// ---- registry-declared enums (kind: register) ----
|
|
460
|
+
|
|
461
|
+
// The full payload of a named register (its `classes` array and any other
|
|
462
|
+
// declared fields), or null when no schema declares it. The orchestration
|
|
463
|
+
// layer's `requires:` risk-class register is the first
|
|
464
|
+
// (dec-spor-orchestration-routine-requires-threads thread 4): a node's
|
|
465
|
+
// `requires:` lists risk/permission classes the work touches; the dispatch
|
|
466
|
+
// matcher and the org policy layer read the legal vocabulary off this
|
|
467
|
+
// partition rather than a hardcoded table (the registry is the contract).
|
|
468
|
+
register(name) {
|
|
469
|
+
const s = this.registers.get(name);
|
|
470
|
+
return s ? s.payload : null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// The class ids of a named register, in declaration order ([] if undeclared).
|
|
474
|
+
registerClasses(name) {
|
|
475
|
+
const s = this.registers.get(name);
|
|
476
|
+
const classes = s && Array.isArray(s.payload.classes) ? s.payload.classes : [];
|
|
477
|
+
return classes.map((c) => c.id);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// The `requires:` risk-class vocabulary as a Set, the convenience the matcher
|
|
481
|
+
// and policy gate read. Empty when no `requires` register is declared (a
|
|
482
|
+
// graph with neither the register nor any `requires:` fields is unaffected).
|
|
483
|
+
requiresClasses() {
|
|
484
|
+
return new Set(this.registerClasses("requires"));
|
|
485
|
+
}
|
|
486
|
+
|
|
423
487
|
// ---- the org-defined policy layer (task-cc-policy-layer) ----
|
|
424
488
|
|
|
425
489
|
// governs-traversal selection (dec-spor-policy-layer-activate): which policy
|
package/lib/kernel/resolution.js
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
// twice (issue-cc-status-lags-resolution-edges), so read surfaces and the
|
|
8
8
|
// queue derive actionability from edges, not status:
|
|
9
9
|
//
|
|
10
|
-
// resolutionMap(graph) -> { targetId: { by, edge, date } }
|
|
11
|
-
// node retired by a live resolver.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// resolutionMap(graph) -> { targetId: { by, edge, date, summary, title } }
|
|
11
|
+
// for every live node retired by a live resolver.
|
|
12
|
+
// `summary`/`title` are the RESOLVER's, so a read
|
|
13
|
+
// surface can show WHAT resolved/answered the target,
|
|
14
|
+
// not just that something did
|
|
15
|
+
// (task-spor-getnode-surface-resolution-on-terminal).
|
|
16
|
+
// `answers` only retires question nodes; `resolves`
|
|
17
|
+
// retires any target. Superseded or rejected/abandoned
|
|
14
18
|
// resolvers don't count — a withdrawn fix resolves
|
|
15
19
|
// nothing.
|
|
16
20
|
// resolutionOf(graph, id) -> that entry, or null.
|
|
@@ -51,7 +55,7 @@ function resolutionMap(graph) {
|
|
|
51
55
|
const target = graph.nodes[e.to];
|
|
52
56
|
if (!target || out[e.to]) continue;
|
|
53
57
|
if (e.type === "answers" && target.type !== "question") continue;
|
|
54
|
-
out[e.to] = { by: r.id, edge: e.type, date: r.date ?? null };
|
|
58
|
+
out[e.to] = { by: r.id, edge: e.type, date: r.date ?? null, summary: r.summary ?? null, title: r.title ?? null };
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
return out;
|
|
@@ -2,23 +2,45 @@
|
|
|
2
2
|
id: schema-edge-assigned
|
|
3
3
|
type: schema
|
|
4
4
|
kind: edge-schema
|
|
5
|
-
schema_version: 2026.06.
|
|
5
|
+
schema_version: 2026.06.18.1
|
|
6
6
|
title: Seed schema for assigned edges
|
|
7
|
-
summary: Edge schema for the assigned type — work is assigned to a person; the traversal key for per-person views and queues. Seed-pack default; a graph-resident schema node for this edge type overrides it.
|
|
8
|
-
date: 2026-06-
|
|
7
|
+
summary: Edge schema for the assigned type — work is assigned to a person OR an agent; the traversal key for per-person views and queues, and the orchestration layer's explicit-routing edge. An agent-targeted edge may carry an optional profile: attribute (the per-assignment profile override). Seed-pack default; a graph-resident schema node for this edge type overrides it.
|
|
8
|
+
date: 2026-06-18
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
Seed schema for the `assigned` edge type (work node → person), shipped
|
|
11
|
+
Seed schema for the `assigned` edge type (work node → person OR agent), shipped
|
|
12
12
|
with the plugin as a registry default (QUEUE.md §2). Per-person views
|
|
13
13
|
("what am I blocking") traverse it from the `$viewer` binding; per-person
|
|
14
14
|
queues filter on it — the queue's `assignee` parameter
|
|
15
15
|
(`GET /v1/queue?assignee=<person>`), unioned with the person's `stewards`
|
|
16
16
|
edges (task-cc-queue-assignee-filtering).
|
|
17
17
|
|
|
18
|
+
**Routing is explicit assignment, not eligibility**
|
|
19
|
+
(dec-spor-agent-orchestration-layer). The target distinguishes who does the work:
|
|
20
|
+
|
|
21
|
+
- `assigned → person` — human work. Unassigned + person-assigned = today's human
|
|
22
|
+
pool, unchanged.
|
|
23
|
+
- `assigned → agent-X` — an agent does it (X's profile says how and what it can
|
|
24
|
+
touch). Only agent-assigned tasks are dispatch candidates. Use this for work an
|
|
25
|
+
agent must NOT do — e.g. an irreversible real-world action — by assigning it to
|
|
26
|
+
the person instead.
|
|
27
|
+
|
|
28
|
+
An agent-targeted edge may carry an optional **`profile:` attribute** — the
|
|
29
|
+
durable, graph-recorded per-assignment profile override
|
|
30
|
+
(dec-spor-orchestration-routine-requires-threads thread 3): `{type: assigned, to:
|
|
31
|
+
agent-X, profile: profile-Y}` reads "when this task goes to this agent, run it
|
|
32
|
+
under this profile." It is the natural, auditable home for the override; cascade
|
|
33
|
+
precedence is `--profile` dispatch flag > this assignment-edge attribute > the
|
|
34
|
+
agent's default `uses-profile` (schema-edge-uses-profile). The attribute is flat
|
|
35
|
+
edge data the regex frontmatter parser preserves; the dispatch matcher reads it
|
|
36
|
+
(task-spor-dispatch-capabilities-satisfiability). An assignment with no `profile:`
|
|
37
|
+
falls through to the agent's default, so existing `assigned` edges are unchanged
|
|
38
|
+
(backward-readable, no upgrade chain). Weight is unchanged at 0.5.
|
|
39
|
+
|
|
18
40
|
```json
|
|
19
41
|
{
|
|
20
42
|
"edge_type": "assigned",
|
|
21
|
-
"description": "work assigned to this person",
|
|
43
|
+
"description": "work assigned to this person or agent — the explicit-routing edge; an agent target may carry a profile: per-assignment override",
|
|
22
44
|
"weight": 0.5
|
|
23
45
|
}
|
|
24
46
|
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: schema-edge-uses-profile
|
|
3
|
+
type: schema
|
|
4
|
+
kind: edge-schema
|
|
5
|
+
schema_version: 2026.06.18.1
|
|
6
|
+
title: Seed schema for uses-profile edges
|
|
7
|
+
summary: Edge schema for the uses-profile type — an agent → its DEFAULT profile (the runtime+capability bundle it dispatches under), overridable per assignment or per dispatch. A low-weight structural config binding like owned-by, not a work dependency. Seed-pack default; a graph-resident schema node for this edge type overrides it.
|
|
8
|
+
date: 2026-06-18
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Seed schema for the `uses-profile` edge type (agent → profile), shipped with the
|
|
12
|
+
plugin as a registry default (QUEUE.md §2). Written from a `type: agent` node
|
|
13
|
+
(schema-agent) to the `profile-` node (schema-profile) it dispatches under by
|
|
14
|
+
default — "the WHO points at the HOW" (dec-spor-agent-orchestration-layer).
|
|
15
|
+
|
|
16
|
+
The default is overridable without rewriting this edge: per assignment via the
|
|
17
|
+
`assigned → agent` edge's `profile:` attribute (schema-edge-assigned), or per
|
|
18
|
+
dispatch via `--profile`. Cascade precedence, explicit wins: `--profile` flag >
|
|
19
|
+
assignment-edge attribute > this default `uses-profile`
|
|
20
|
+
(dec-spor-orchestration-routine-requires-threads thread 3) — the same shape as
|
|
21
|
+
`dispatch.agent` vs `--as`.
|
|
22
|
+
|
|
23
|
+
A low structural weight (0.3), mirroring `owned-by` and `grouped-under`: an
|
|
24
|
+
agent's default profile is durable configuration, not work flow — the profile
|
|
25
|
+
rarely needs to be pulled into a work neighborhood, and a low weight keeps
|
|
26
|
+
runtime config from polluting task briefings. `capturable: false`: the binding
|
|
27
|
+
is created deliberately alongside the agent/profile (by `spor agent` config),
|
|
28
|
+
never drafted from a capture — mirroring the identity/config-node family it joins
|
|
29
|
+
(`owned-by`).
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"edge_type": "uses-profile",
|
|
34
|
+
"description": "this agent's default profile — the runtime+capability bundle it dispatches under (overridable per assignment or dispatch)",
|
|
35
|
+
"weight": 0.3,
|
|
36
|
+
"capturable": false
|
|
37
|
+
}
|
|
38
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: schema-profile
|
|
3
|
+
type: schema
|
|
4
|
+
kind: node-schema
|
|
5
|
+
schema_version: 2026.06.18.1
|
|
6
|
+
title: Seed schema for profile nodes
|
|
7
|
+
summary: Node schema for the profile type — a reusable runtime+capability bundle (harness, model, skills, plugins, mcp) an agent runs under. The runtime fields ARE the dispatch satisfiability spec the machine-capabilities matcher reads; there is no separate requirements block. Personal AND org-published with personal override. Seed-pack default; a graph-resident schema node for this type overrides it.
|
|
8
|
+
date: 2026-06-18
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Seed schema for the `profile` node type (ontology in GRAPH.md), shipped with the
|
|
12
|
+
plugin as a registry default (QUEUE.md §2). A `type: schema` node in the graph
|
|
13
|
+
with `kind: node-schema` and the same `node_type` overrides this entry.
|
|
14
|
+
|
|
15
|
+
A profile is the orchestration layer's runtime+capability bundle — "the HOW" —
|
|
16
|
+
that an `agent` (schema-agent, "the WHO") dispatches under
|
|
17
|
+
(dec-spor-agent-orchestration-layer). Factoring it out of the agent node makes
|
|
18
|
+
the toolset reusable across agents and people, and gives dispatch one structured
|
|
19
|
+
declaration to parameterize a launch from.
|
|
20
|
+
|
|
21
|
+
Instances carry, beyond the standard fields (`id`, `type`, `title`, `summary`,
|
|
22
|
+
`date`), the runtime register — all flat values the regex frontmatter parser
|
|
23
|
+
already supports:
|
|
24
|
+
|
|
25
|
+
- `harness:` — the launcher/adapter the work runs under (`claude-code` |
|
|
26
|
+
`codex` | `opencode` | …), operationalizing dec-cc-portable-core-adapters
|
|
27
|
+
(claude-code → `claude --bg`, codex/opencode → their CLIs). A flat scalar.
|
|
28
|
+
- `model:` — the model id passed to the harness (`--model`). A flat scalar.
|
|
29
|
+
- `skills:` / `plugins:` / `mcp:` — inline lists of the skills, plugins, and MCP
|
|
30
|
+
servers the harness preloads; `mcp` is merged into the strict `--mcp-config`
|
|
31
|
+
dispatch writes so the agent's toolset is exactly the profile plus the
|
|
32
|
+
agent-spor server, nothing ambient (dec-spor-session-identity-active-record).
|
|
33
|
+
|
|
34
|
+
**These runtime fields ARE the satisfiability spec** — there is no separate
|
|
35
|
+
requirements block (dec-spor-machine-profile-satisfiability, FORK A). A machine
|
|
36
|
+
declares ATOMIC capabilities in a machine-local `dispatch.capabilities` map, and
|
|
37
|
+
`satisfies(machine, profile)` is `profile.harness ∈ machine.harnesses ∧
|
|
38
|
+
profile.mcp ⊆ machine.reachable_mcp ∧ profile.skills ⊆ machine.skills ∧
|
|
39
|
+
profile.plugins ⊆ machine.plugins ∧ profile ∉ machine.deny`. The matcher and the
|
|
40
|
+
machine-local map are the deferred build this schema unblocks
|
|
41
|
+
(task-spor-dispatch-capabilities-satisfiability); the same atomic vocabulary is
|
|
42
|
+
forward-compatible with the deferred remote fleet scheduler (each agent publishes
|
|
43
|
+
its capabilities to the server). This schema only fixes the profile's field
|
|
44
|
+
contract that match reads.
|
|
45
|
+
|
|
46
|
+
**Reusable + both-scoped, with override.** Profiles are PERSONAL (you author
|
|
47
|
+
your own) AND ORG-PUBLISHED (the org curates a vetted toolset, e.g.
|
|
48
|
+
`profile-docs-writer`), with personal override. Org-published profiles are where
|
|
49
|
+
this meets policy — the org policy layer (dec-spor-policy-layer-activate) can
|
|
50
|
+
require that work of a given risk class be assigned to an agent whose profile is
|
|
51
|
+
org-approved (curated-toolset-as-governance). That gating lives in policy nodes,
|
|
52
|
+
not in this schema. An agent references its DEFAULT profile with a `uses-profile`
|
|
53
|
+
edge (schema-edge-uses-profile), overridable per assignment (the `assigned →
|
|
54
|
+
agent` edge's `profile:` attribute, schema-edge-assigned) or per dispatch
|
|
55
|
+
(`--profile`); cascade precedence is `--profile` flag > assignment-edge attribute
|
|
56
|
+
> agent's default.
|
|
57
|
+
|
|
58
|
+
`status:` is declarative (`active` by default; any other status, or none, reads
|
|
59
|
+
as before) — not gated by a `transitions()` enum, so the type stays
|
|
60
|
+
backward-readable. `capturable: false`: a profile is created deliberately (by a
|
|
61
|
+
person curating a toolset, or an org publishing one), never drafted from a
|
|
62
|
+
capture or distilled from a transcript — mirroring `agent`, `person`, `repo`,
|
|
63
|
+
and `workflow-run`. Graphs without profile nodes behave exactly as before.
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"node_type": "profile",
|
|
68
|
+
"description": "a reusable runtime+capability bundle an agent runs under — harness, model, skills, plugins, mcp; its runtime fields are the dispatch satisfiability spec",
|
|
69
|
+
"prefix": [
|
|
70
|
+
"profile-"
|
|
71
|
+
],
|
|
72
|
+
"capturable": false
|
|
73
|
+
}
|
|
74
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: schema-requires
|
|
3
|
+
type: schema
|
|
4
|
+
kind: register
|
|
5
|
+
schema_version: 2026.06.18.1
|
|
6
|
+
title: Seed schema for the requires risk-class register
|
|
7
|
+
summary: Registry-declared extensible enum (kind register) for the `requires:` risk/permission axis — the classes of capability a piece of work may touch (shell, prod-creds, browser, network, human, filesystem-write, paid-api). DISTINCT from machine-satisfiability: this is what the WORK needs, validated against the assigned profile and gated by org policy. The kernel exposes the vocabulary as a partition (graph.registry.requiresClasses()); reading/gating is downstream. Seed default; a graph-resident register schema for this name overrides/extends it.
|
|
8
|
+
date: 2026-06-18
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Seed schema for the `requires` register (ontology in GRAPH.md), shipped with the
|
|
12
|
+
plugin as a registry default (QUEUE.md §2). It declares a named, extensible
|
|
13
|
+
vocabulary — not a node or edge type. A `type: schema` node in the graph with
|
|
14
|
+
`kind: register` and `register: requires` overrides/extends this entry, so an org
|
|
15
|
+
grows the enum by editing a schema node, never by a code change (the registry is
|
|
16
|
+
the contract).
|
|
17
|
+
|
|
18
|
+
`requires:` is the risk/permission axis (dec-spor-orchestration-routine-requires-
|
|
19
|
+
threads thread 4): a flat list on a work node (`requires: [shell, prod-creds]`)
|
|
20
|
+
naming the classes of capability the work may touch. It is **DISTINCT from
|
|
21
|
+
machine-satisfiability** — that asks "can this box LAUNCH the assigned profile"
|
|
22
|
+
(harness/mcp/skills present, derived from the profile's runtime fields, decides
|
|
23
|
+
dispatch; dec-spor-machine-profile-satisfiability). `requires:` asks "what is this
|
|
24
|
+
work allowed to touch", validated against the assigned agent's profile (a task's
|
|
25
|
+
`requires:` must be ⊆ the profile's granted classes, else warn/refuse —
|
|
26
|
+
under-equipped or over-privileged) and gated by org policy.
|
|
27
|
+
|
|
28
|
+
The kernel stays policy-free: it only DECLARES the vocabulary and exposes it as a
|
|
29
|
+
partition (`graph.registry.requiresClasses()` / `register("requires")`), the same
|
|
30
|
+
way the resolving-status partition is read off the registry rather than a
|
|
31
|
+
hardcoded table. The org policy layer reads it via the same governs-traversal the
|
|
32
|
+
definition-of-done quorum gate uses (dec-spor-definition-of-done-org-policy):
|
|
33
|
+
the nearest governing policy node decides which classes need which approvals and
|
|
34
|
+
which agents may hold them. Validating a node's `requires:` against the register,
|
|
35
|
+
the profile match, and the policy gate are the deferred build this unblocks
|
|
36
|
+
(task-spor-dispatch-capabilities-satisfiability); this schema only fixes the
|
|
37
|
+
vocabulary.
|
|
38
|
+
|
|
39
|
+
`human` is special: it is unsatisfiable by ANY agent — work that needs a human
|
|
40
|
+
(an irreversible real-world action, e.g. delegating Namecheap NS to Cloudflare)
|
|
41
|
+
is assigned to a person, not an agent. The seed set is deliberately small;
|
|
42
|
+
org-extend it with a resident register schema.
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"register": "requires",
|
|
47
|
+
"description": "the risk/permission classes a piece of work may touch — validated against the assigned profile and gated by org policy; DISTINCT from machine-satisfiability",
|
|
48
|
+
"classes": [
|
|
49
|
+
{ "id": "shell", "description": "runs arbitrary shell commands on the host" },
|
|
50
|
+
{ "id": "prod-creds", "description": "uses production credentials or secrets" },
|
|
51
|
+
{ "id": "browser", "description": "drives a real web browser / does web automation" },
|
|
52
|
+
{ "id": "network", "description": "makes outbound network calls beyond the Spor server" },
|
|
53
|
+
{ "id": "human", "description": "needs a human — unsatisfiable by any agent; assign to a person" },
|
|
54
|
+
{ "id": "filesystem-write", "description": "writes to the filesystem outside a scratch/worktree sandbox" },
|
|
55
|
+
{ "id": "paid-api", "description": "calls a metered/paid external API" }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: schema-routine
|
|
3
|
+
type: schema
|
|
4
|
+
kind: node-schema
|
|
5
|
+
schema_version: 2026.06.18.1
|
|
6
|
+
title: Seed schema for routine nodes
|
|
7
|
+
summary: Node schema for the routine type — owner-scoped (owned-by a person) trigger→action automation: declarative when→do rules over graph events that dispatch ONLY the owner's agents, AND-ed with org policy. Declarative register first; attached sandboxed code is a later schema-gated escape hatch. Seed-pack default; a graph-resident schema node for this type overrides it.
|
|
8
|
+
date: 2026-06-18
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Seed schema for the `routine` node type (ontology in GRAPH.md), shipped with the
|
|
12
|
+
plugin as a registry default (QUEUE.md §2). A `type: schema` node in the graph
|
|
13
|
+
with `kind: node-schema` and the same `node_type` overrides this entry.
|
|
14
|
+
|
|
15
|
+
A routine is the orchestration layer's per-person automation
|
|
16
|
+
(dec-spor-agent-orchestration-layer): a node `owned-by` a person
|
|
17
|
+
(schema-edge-owned-by) holding `when → do` rules that fire on graph events. The
|
|
18
|
+
sonnet-implements → opus-reviews → approve/send-back/escalate loop and the "a
|
|
19
|
+
task touching docs reaches done → dispatch the docs-profile agent" handoff are
|
|
20
|
+
routines.
|
|
21
|
+
|
|
22
|
+
**Two invariants** (from the parent decision):
|
|
23
|
+
|
|
24
|
+
1. **Only the owner's agents are ever dispatched.** Enforced by the trigger
|
|
25
|
+
locus — locally your machine runs only your routines for your agents;
|
|
26
|
+
remotely the server fires a person's routine only for that person's work and
|
|
27
|
+
can only mint tokens for agents they own. The `person → routine → agent` RFC
|
|
28
|
+
8693 act-chain is the audit trail (dec-cc-attribution-from-token,
|
|
29
|
+
dec-cc-workflow-credential-issuance).
|
|
30
|
+
2. **Personal routines accelerate; org policy gates — they AND, never bypass.**
|
|
31
|
+
Agent-on-behalf-of-X counts as X for the self-approval ban and the
|
|
32
|
+
definition-of-done quorum (dec-spor-definition-of-done-org-policy), so an
|
|
33
|
+
owner's reviewer-agent CANNOT auto-approve their implementer-agent's work past
|
|
34
|
+
the org review bar. A routine gets work pre-vetted and clean; independent
|
|
35
|
+
approvals still require other people.
|
|
36
|
+
|
|
37
|
+
**Declarative register first** (dec-spor-orchestration-routine-requires-threads
|
|
38
|
+
thread 1). A routine instance carries its rules as a fenced ```json `rules` block
|
|
39
|
+
in its BODY (the regex frontmatter parser is flat, so structured rules live in
|
|
40
|
+
the body, exactly as a schema's payload does; the deferred routine ENGINE parses
|
|
41
|
+
and runs them). The bounded vocabulary:
|
|
42
|
+
|
|
43
|
+
- **Triggers (`when`)** are graph events — a status change (e.g. to a resolving
|
|
44
|
+
state, or to `done`), or an edge appearing (e.g. `reviewed-by` /
|
|
45
|
+
`changes-requested-by`) — with `where:` filters (type, project, requires,
|
|
46
|
+
assignee, …).
|
|
47
|
+
- **Actions (`do`)** are the bounded verb set: `create-node`, `assign` (to a
|
|
48
|
+
SPECIFIC agent, optionally with a `profile:` override), `dispatch`,
|
|
49
|
+
`set-status`, `reassign`, `escalate` (to the owner). Arbitrary attached
|
|
50
|
+
sandboxed code is the LATER schema-gated escape hatch, gated like lens/schema
|
|
51
|
+
code (dec-lens-custom-wasm-timing) — NOT this schema.
|
|
52
|
+
|
|
53
|
+
Shape of an instance body (documented for the engine; not parsed by the kernel —
|
|
54
|
+
shown as `jsonc` so the seed parser does not mistake it for this schema's own
|
|
55
|
+
payload, which is the first ```json block below):
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{
|
|
59
|
+
"rules": [
|
|
60
|
+
{
|
|
61
|
+
"when": { "event": "status-change", "to": "resolving", "where": { "type": "task", "project": "spor" } },
|
|
62
|
+
"do": [ { "action": "dispatch", "to": "agent-x", "profile": "profile-y" } ]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This schema fixes the routine's IDENTITY and its declarative vocabulary only. The
|
|
69
|
+
routine ENGINE and its vetting machinery — dry-run, the mandatory retry budget,
|
|
70
|
+
self-approval-floor activation review (a routine that auto-dispatches against
|
|
71
|
+
shared state is `proposed` until a different identity activates it), and the
|
|
72
|
+
fire-time org-policy gate (thread 2) — ride on the engine, deferred. So this
|
|
73
|
+
seed schema is declarative and UNGATED: `status:` is plain data
|
|
74
|
+
(backward-readable), no `transitions()` enum ships here; the activation gate is
|
|
75
|
+
added (with a `schema_version` bump) when the engine lands.
|
|
76
|
+
|
|
77
|
+
`capturable: false`: a routine is authored deliberately by its owner, never
|
|
78
|
+
drafted from a capture or distilled from a transcript — mirroring `agent`,
|
|
79
|
+
`profile`, `person`, and `workflow-run`. Graphs without routine nodes behave
|
|
80
|
+
exactly as before.
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"node_type": "routine",
|
|
85
|
+
"description": "owner-scoped trigger→action automation owned by a person — declarative when→do rules dispatching only the owner's agents, AND-ed with org policy",
|
|
86
|
+
"prefix": [
|
|
87
|
+
"routine-"
|
|
88
|
+
],
|
|
89
|
+
"capturable": false
|
|
90
|
+
}
|
|
91
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sporhq/spor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -19,6 +19,15 @@ Frontmatter: `id: schema-<type>` (or `schema-edge-<type>`), `type: schema`,
|
|
|
19
19
|
body holds prose, then a fenced **JSON payload**, then optional fenced **JS
|
|
20
20
|
functions**.
|
|
21
21
|
|
|
22
|
+
(Two more kinds exist for non-type schemas. `kind: policy` is the org-defined
|
|
23
|
+
policy layer — see GRAPH.md. `kind: register` declares a named, extensible
|
|
24
|
+
**enum** the kernel exposes as a partition rather than a hardcoded table: a
|
|
25
|
+
payload `{ "register": "<name>", "classes": [{ "id": …, "description": … }] }`,
|
|
26
|
+
keyed by register name (graph beats seed, higher CalVer wins). The seed
|
|
27
|
+
`requires` register — the work-node risk/permission axis — is the first; read it
|
|
28
|
+
with `graph.registry.requiresClasses()`. Grow it by writing a resident `kind:
|
|
29
|
+
register` schema with the same `register:` name.)
|
|
30
|
+
|
|
22
31
|
### Node schema — JSON payload keys
|
|
23
32
|
|
|
24
33
|
```json
|
|
@@ -24,6 +24,8 @@ the decision queue (QUEUE.md §4).
|
|
|
24
24
|
| question | `question-` | a routed ask the graph couldn't answer | queueable; status `open`/`answered`; joins the queue until answered |
|
|
25
25
|
| person | `person-` | an org member | anchor for `$viewer` binding and question routing |
|
|
26
26
|
| agent | `agent-` | a person-owned automation principal | a dispatched session's durable identity; owned by a person via `owned-by`; `capturable: false`; carries forward-compat `spiffe:`/`pubkey:` |
|
|
27
|
+
| profile | `profile-` | a reusable runtime+capability bundle an agent runs under | `harness`/`model`/`skills`/`plugins`/`mcp`; these runtime fields ARE the dispatch satisfiability spec; `capturable: false` |
|
|
28
|
+
| routine | `routine-` | owner-scoped trigger→action automation | `owned-by` a person; declarative `when → do` rules over graph events; dispatches only the owner's agents, AND-ed with org policy; `capturable: false` |
|
|
27
29
|
| capture-pending | `cap-` | raw captured text that fit no schema | born status-less; closes only as `merged` or `rejected` |
|
|
28
30
|
| finding | `find-` | a gardener observation (stale anchor, cold work) | filed as a queue item |
|
|
29
31
|
| repo | `repo-` | a durable git-repo identity | carries `slugs:` aliases + `fingerprints:`; heals renames at read time; optional `tags:` are the match key for a norm's `applies_to_tags` |
|
|
@@ -51,12 +53,13 @@ are same-direction synonyms renamed at write time.
|
|
|
51
53
|
| performs | 0.8 | this run is an execution of the target workflow | — |
|
|
52
54
|
| blocks | 0.7 | the target can't proceed until this does | inverse `blocked-by` |
|
|
53
55
|
| answers | 0.7 | this answers the target question | inverse `answered-by` |
|
|
54
|
-
| assigned | 0.5 | work assigned to this person | — |
|
|
56
|
+
| assigned | 0.5 | work assigned to this person OR agent (explicit routing; an agent target may carry a `profile:` per-assignment override) | — |
|
|
55
57
|
| relates-to | 0.5 | weak association | alias `related-to` |
|
|
56
58
|
| mentions | 0.5 | weakest association | — |
|
|
57
59
|
| stewards | 0.4 | this person stewards the target area/spec/norm | question-routing key |
|
|
58
60
|
| grouped-under | 0.3 | this repo's home project grouping (structural) | inverse `groups` |
|
|
59
61
|
| owned-by | 0.3 | this agent is owned by that person (structural identity) | inverse `owns` |
|
|
62
|
+
| uses-profile | 0.3 | this agent's default profile (runtime+capability bundle); structural config, overridable per assignment/dispatch | — |
|
|
60
63
|
| routed-to | 0.3 | this question is routed to that person | — |
|
|
61
64
|
| compiled-for | — | briefing → the task/query it was compiled for | provenance only |
|
|
62
65
|
| shaped-by | — | briefing → the corrections that shaped it | provenance only |
|