@sporhq/spor 0.18.4 → 0.18.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/API.md +53 -9
- package/QUEUE.md +9 -9
- package/adapters/README.md +4 -1
- package/adapters/codex/README.md +1 -1
- package/bin/spor.js +90 -5
- package/lib/kernel/graph.js +1 -1
- package/lib/seed/schema-edge-review-requested.md +1 -1
- package/package.json +1 -1
- package/scripts/engines/agents-md.js +89 -41
- package/skills/next/SKILL.md +13 -5
- package/skills/spor/SKILL.md +51 -2
- package/skills/spor/references/concepts.md +1 -1
- package/skills/triage/SKILL.md +9 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "spor",
|
|
3
3
|
"displayName": "Spor Context Compiler",
|
|
4
4
|
"description": "Maintains a typed, versioned knowledge graph and compiles compact briefings from it: session-start injection, per-prompt relevance digests, capture at discovery, end-of-session distillation, decision queue.",
|
|
5
|
-
"version": "0.18.
|
|
5
|
+
"version": "0.18.6",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "losthammer"
|
|
8
8
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.6",
|
|
4
4
|
"description": "Maintains a typed, versioned knowledge graph and compiles compact briefings from it: session-start injection, per-prompt relevance digests, capture at discovery, end-of-session distillation, decision queue.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Spor",
|
package/API.md
CHANGED
|
@@ -58,9 +58,9 @@ Streamable HTTP, implemented with the official SDK. All tool results are
|
|
|
58
58
|
returned as both human-readable text content and structured JSON.
|
|
59
59
|
|
|
60
60
|
The server advertises **`instructions`** (the SDK initialize result, surfaced
|
|
61
|
-
by clients as an "MCP Server Instructions" block). It frames the
|
|
62
|
-
as an **ORIENT → TRAVERSE → COMMIT** loop rather than
|
|
63
|
-
verbs, so an assistant can infer a recursive research chain — e.g. `
|
|
61
|
+
by clients as an "MCP Server Instructions" block). It frames the tools
|
|
62
|
+
as an **ORIENT → TRAVERSE → COMMIT** loop rather than a flat list of independent
|
|
63
|
+
verbs, so an assistant can infer a recursive research chain — e.g. `show_queue`
|
|
64
64
|
(or `recent_changes` for "what happened lately") → `query_graph` with `root_id`
|
|
65
65
|
(deepen) → `render_lens` on a lineage lens →
|
|
66
66
|
`put_node`/`capture` the outcome — instead of reconstructing it from per-tool
|
|
@@ -232,10 +232,12 @@ text fit no schema (or failed validation twice) and was preserved as a
|
|
|
232
232
|
`cap-…` capture-pending node — ingestion-quality failures never lose text.
|
|
233
233
|
Only an unreachable ingestion model is an error (`ingestion_unavailable`).
|
|
234
234
|
|
|
235
|
-
### `
|
|
235
|
+
### `show_queue`
|
|
236
236
|
|
|
237
|
-
The decision queue (QUEUE.md §4/§5)
|
|
238
|
-
"
|
|
237
|
+
The decision queue (QUEUE.md §4/§5) — the data answer to "show my queue" /
|
|
238
|
+
"what's next" / "the backlog". Input `{ "project"?: "slug",
|
|
239
|
+
"types"?: ["task"], "exclude_types"?: ["capture-pending"], "assignee"?:
|
|
240
|
+
"<person-id>|me", "limit"?: 20,
|
|
239
241
|
"offset"?: 0 }` → `{ "items": [{id, title, type, status,
|
|
240
242
|
priority, score, signals: {blocking, heat, staleness, age_days}, suggest:
|
|
241
243
|
"do|close", why}], "count": N, "offset": 0, "returned_count": N,
|
|
@@ -277,6 +279,23 @@ queue, not what the firehose hid. The type compared is the type the item
|
|
|
277
279
|
surfaces as, so `exclude_types: ["schema"]` also hides schema-approval items.
|
|
278
280
|
Omitting both (or passing empty arrays) filters nothing.
|
|
279
281
|
|
|
282
|
+
**Per-person scope** (task-cc-queue-assignee-filtering). `assignee` scopes the
|
|
283
|
+
ranked queue to the work one person carries — the union of nodes with an
|
|
284
|
+
`assigned` edge to them and the nodes they `steward`; `assignee: "me"` binds
|
|
285
|
+
to the caller (QUEUE.md §4). This is a **narrower carrying view**, not the
|
|
286
|
+
queue: for the ordinary "my queue" / "what's next" answer, omit `assignee`
|
|
287
|
+
entirely — pass it only when the caller explicitly asks for directly
|
|
288
|
+
assigned or stewarded work.
|
|
289
|
+
|
|
290
|
+
### `render_queue`
|
|
291
|
+
|
|
292
|
+
The widget twin of `show_queue`: same input, same queue, but this tool
|
|
293
|
+
declares the view-tree UI resource (below) so an MCP-Apps host reliably
|
|
294
|
+
attaches the interactive queue widget. Semantics, ranking, filters, and
|
|
295
|
+
pagination match `show_queue` exactly — it exists only to make widget
|
|
296
|
+
attachment an explicit choice; `show_queue` remains the data-oriented
|
|
297
|
+
queue tool for hosts (and turns) that just need the answer.
|
|
298
|
+
|
|
280
299
|
### `ask_question`
|
|
281
300
|
|
|
282
301
|
File a question the graph could not answer. Input `{ "text": "<the
|
|
@@ -319,10 +338,33 @@ render). Unknown `lens_id` still errors, carrying the same `catalog`/`lenses`
|
|
|
319
338
|
list; engine failures (missing param, broken blocks) error with the message
|
|
320
339
|
verbatim.
|
|
321
340
|
|
|
341
|
+
### `render_program`
|
|
342
|
+
|
|
343
|
+
The program/progress view over `blocks` topology — the birds-eye "where do we
|
|
344
|
+
stand" for a large workstream, auto-derived on demand with no lens authoring.
|
|
345
|
+
Input `{ "id": "<root-node-id>", "max_depth"?, "max_nodes"? }` → `{ "found":
|
|
346
|
+
true, "root_id", "progress": {"total", "done", "active", "blocked", "open",
|
|
347
|
+
"pct", "statuses"}, "count", "truncated"?, "view", "node_ids" }`. Given a root
|
|
348
|
+
node (an umbrella task, a milestone — anything other work `blocks`), the
|
|
349
|
+
server walks its gating tree — every node that blocks it, transitively over
|
|
350
|
+
inbound `blocks` edges — and derives each node's bucket from the same truth
|
|
351
|
+
the queue uses: terminal statuses, supersession, and live `resolves`/`answers`
|
|
352
|
+
edges count as **done** (even while the status field lags — the effective
|
|
353
|
+
status then reads `resolved` with a `resolved_by` ride-along); a node gated by
|
|
354
|
+
its own live unresolved blockers is **blocked**; live unblocked work splits
|
|
355
|
+
**active** vs **open**. `view` is the standard view tree (`as: "tree"` with an
|
|
356
|
+
additive `progress` block); the text content is a progress-bar header plus the
|
|
357
|
+
glyphed gating tree. Shared blockers render once and repeat as `repeat: true`
|
|
358
|
+
leaves (counted once); `max_depth`/`max_nodes` caps count skipped branches
|
|
359
|
+
into `truncated`, never silently. A root nothing blocks is a successful empty
|
|
360
|
+
result whose prose says how to model the program (add `blocks` edges from the
|
|
361
|
+
gating tasks). Unknown `id` errors with `{ "found": false, "error":
|
|
362
|
+
"unknown_root" }`. The REST twin is `GET /v1/program/{id}` (§3).
|
|
363
|
+
|
|
322
364
|
### `recent_changes`
|
|
323
365
|
|
|
324
366
|
The team's recent-activity feed — the temporal entry point the other read
|
|
325
|
-
tools lack (`query_graph` is semantic search, `
|
|
367
|
+
tools lack (`query_graph` is semantic search, `show_queue` is forward-looking
|
|
326
368
|
open work, `render_lens` renders current state). It answers "what changed /
|
|
327
369
|
what was done in the last N hours", "what did the agents write overnight", and
|
|
328
370
|
"what landed since `<commit>`". Input `{ "since"?, "project"?, "limit"? }` →
|
|
@@ -351,7 +393,7 @@ oldest-open bottlenecks. **Completion is a node's status-TRANSITION time** (when
|
|
|
351
393
|
it entered its final terminal run), never `updated_at`, so a later edge append
|
|
352
394
|
can't corrupt the "completed last week" signal (dec-spor-git-derived-timestamps).
|
|
353
395
|
`project` (a repo slug or grouping id) scopes it through the same up-resolution
|
|
354
|
-
as `
|
|
396
|
+
as `show_queue`; `types` restricts node types; `weeks`/`top`/`aging` shape the
|
|
355
397
|
window. The MCP twin of `GET /v1/analytics` (§3) over the same `store.analytics`
|
|
356
398
|
core — for the shell-less Cowork audience that can't run the CLI.
|
|
357
399
|
|
|
@@ -373,7 +415,7 @@ contract from `lib/seed/` files — those miss resident overrides
|
|
|
373
415
|
|
|
374
416
|
### The MCP-app widget (`ui://spor/view-tree.html`)
|
|
375
417
|
|
|
376
|
-
`
|
|
418
|
+
`render_queue`, `render_lens`, and `render_program` declare a UI resource via
|
|
377
419
|
`_meta.ui.resourceUri`: a single trusted interpreter of the view-tree
|
|
378
420
|
component catalog that MCP-apps hosts (Claude, Goose, VS Code) render as an
|
|
379
421
|
interactive iframe — status chips, progress bars, lineage trees, node detail
|
|
@@ -419,8 +461,10 @@ endpoint is the REST twin of a core call:
|
|
|
419
461
|
| `POST /v1/corrections` | /spor:correct | `propose_correction` semantics → 201 `{status, id, revision, warnings}` |
|
|
420
462
|
| `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) |
|
|
421
463
|
| `GET /v1/analytics?project=&type=&weeks=&top=&aging=&format=` | remote `spor analytics`, the `analytics` MCP tool | work-flow analytics — the SERVER twin of the local-only `spor analytics` consumer, for a remote/Cowork teammate with no local graph repo to fold (task-spor-server-analytics-surface): created-vs-completed weekly cohorts, throughput, cycle-time median/p90, current WIP by node type, and the oldest-open bottlenecks, computed by the pure analytics kernel over the resident graph + a HEAD-keyed status-transition fold. **Completion is a node's status-TRANSITION time** (when it entered its final terminal run, from git content history), never `updated_at`, so a later edge append can't corrupt the "completed last week" signal (dec-spor-git-derived-timestamps). Default returns the machine (JSON) report `{window, weekly, totals, throughput, cycleTimeDays, wip, bottlenecks, coverage}`; `?format=text` renders the human report. `project` resolves through the shared up-resolution like `/v1/queue` (bare repo slug → grouping union; `repo-<slug>`/`proj-<slug>` id pins) — a zero-match scope rides back as the additive `project_warning` field (text mode prefixes a `# ` line). `type=` (comma-separated, repeatable) restricts node types; `weeks`/`top`/`aging` shape the window (clamped 1–52 / 1–100 / 1–365). A bad slug/type is `422`. The remote arm of `spor analytics` fetches the JSON and renders it with the SAME `renderReport` the local consumer uses, so remote and local output match (norm-spor-cli-mode-parity, task-spor-analytics-remote-cli-dispatch) |
|
|
464
|
+
| `GET /v1/metrics/capture?since=` | the cross-author capture-discipline eval harvest (task-spor-tenant-capture-metrics-export) | capture-discipline aggregates for an **opted-in** deployment — the same kernel the dogfood CLI runs (`lib-engine/kernel/capture-metrics.js`), computed server-side over the resident graph plus the FULL request journal (every rotated `server.log` segment). Three gates stack (dec-spor-tenant-metrics-aggregates-only): the per-machine opt-in env `SPOR_METRICS_EXPORT` (unset → the route 404s, so a never-opted tenant shows no surface), admin auth (stewards→root, 403), and **unconditional redaction** — the body carries counts/rates only: by-identity keys are stable per-tenant pseudonyms (`author-<hash12>`, salted at `cache/metrics-salt` so per-author trends survive across windows), closure entries keep `{edge, latency_days}` but drop node ids, and id lists reduce to `open_count`/`slug_smell_count`. No journal lines, node bodies, or capture prose ever exit. `?since=YYYY-MM-DD` bounds the window (malformed → `422`) |
|
|
422
465
|
| `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 |
|
|
423
466
|
| `POST /v1/gardener` | ops cron / on demand; `spor admin gardener` | run a gardener sweep now; findings filed as queue items → `{checked, filed, resolved, skipped, generated_at}` (`filed`/`resolved`/`skipped` are id lists, `checked` a count). The `spor admin gardener [--json]` CLI verb is the shell front-door (remote-only — the server owns the gardener); authenticated but **not** admin-gated server-side today (unlike `/v1/backup`), so any valid team token can trigger it — the verb still surfaces a 403 as an admin-privilege (stewards→root) hint for a deployment that adds the gate |
|
|
467
|
+
| `GET /v1/program/{id}?format=json\|text&depth=&max_nodes=` | program oversight, /spor:brief follow-ups | the program/progress view (`render_program`'s REST twin, one kernel behind both doors): the gating tree of everything that `blocks` `{id}` transitively, with resolution-derived progress (`{progress: {total, done, active, blocked, open, pct, statuses}}` on the view root; done = terminal status / supersession / live resolves-answers edge, exactly the queue's truth). JSON view tree by default, `?format=text` for the terminal rendering; `depth`/`max_nodes` bound expansion and count skipped branches into `truncated`, never silently. 404 for an unknown id |
|
|
424
468
|
| `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 |
|
|
425
469
|
| `POST /v1/lens/{id}/ticket` `{expires?}` | sharing a view; `spor share` | 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. The `spor share <lens-id> [--expires <Nd>]` CLI verb is the shell front-door (remote-only — tickets are minted and signed server-side); it prints the shareable link ready to paste, `--json` for the raw envelope |
|
|
426
470
|
| `GET /v1/export` | bootstrap/offline; `spor export` | ustar tarball of `nodes/` for seeding a local read replica (`?gzip=1` compresses); see §5 for the response headers. `curl … \| tar x` reproduces `nodes/` byte-for-byte. `?history=1` instead streams a `git bundle --all` of the repo (`application/x-git-bundle`, full commit provenance, the customer data-exit path — `git clone <bundle> graph`); `?auth=1` ALSO bundles `auth/*.json` so a disaster restore reproduces the credential set (admin-gated: stewards-root → `403` otherwise). The `spor export [--gzip] [--history\|--auth] [--out <file>]` CLI verb is the shell front-door (remote downloads this; `--gzip`/`--out` also build the same `nodes/` tarball locally, while `--history`/`--auth` are remote-only) |
|
package/QUEUE.md
CHANGED
|
@@ -331,7 +331,7 @@ person), `reviewed-by` (they approved — what `view.approvals` carries and the
|
|
|
331
331
|
quorum counts), and `changes-requested-by` (they asked for changes — carried
|
|
332
332
|
separately as `view.changes_requested`, never an approval). A single edge
|
|
333
333
|
flips type in place across the lifecycle. An open `review-requested` edge
|
|
334
|
-
surfaces the node in that reviewer's `
|
|
334
|
+
surfaces the node in that reviewer's `show_queue` `reviews` set, through the same
|
|
335
335
|
per-person routing filter questions and findings use — reviewer *selection* at
|
|
336
336
|
request time and quorum *enforcement* at gate time are distinct points.
|
|
337
337
|
|
|
@@ -474,7 +474,7 @@ node id; an optional `@YYYY-MM-DD` makes the mute self-expiring, so
|
|
|
474
474
|
"sideline this project for now" can't silently rot into a permanent blind
|
|
475
475
|
spot. Muting is per-viewer presentation at queue-compile time, not graph
|
|
476
476
|
state — items stay live and visible to everyone else — and the hidden
|
|
477
|
-
count is reported (`muted: N` on the result, a trailing line on `
|
|
477
|
+
count is reported (`muted: N` on the result, a trailing line on `show_queue`)
|
|
478
478
|
so the queue never silently truncates.
|
|
479
479
|
|
|
480
480
|
Scheduled dormancy is the graph-state counterpart of the mute: a queueable
|
|
@@ -508,18 +508,18 @@ The queue is a compile mode, not a new store:
|
|
|
508
508
|
(`repo-<slug>`) is the escape hatch back to one repo; an exact grouping id
|
|
509
509
|
(`proj-<slug>`) is used directly; an ungrouped repo falls back to itself.
|
|
510
510
|
**Omitting `project` entirely is the cross-project firehose** (every repo's
|
|
511
|
-
queue at once); `/spor:next --all-projects` and `
|
|
511
|
+
queue at once); `/spor:next --all-projects` and `show_queue` / `GET /v1/queue`
|
|
512
512
|
with no `project` reach it.
|
|
513
513
|
- `includeTypes`/`excludeTypes` (task-cc-queue-filtering-enhancements) whitelist
|
|
514
514
|
or blacklist node types FROM THE RANKING — `GET /v1/queue?type=&exclude_type=`
|
|
515
|
-
(comma-separated), `
|
|
515
|
+
(comma-separated), `show_queue {types, exclude_types}`, and
|
|
516
516
|
`spor next --type/--exclude-type`. Given both, the include set is narrowed and
|
|
517
517
|
then the excludes are removed from it (exclude wins on overlap). Like `project`
|
|
518
518
|
it is a hard scope applied before scoring (the aggregates describe the filtered
|
|
519
519
|
queue), it composes with `project`/`assignee`, and the type compared is the one
|
|
520
520
|
the item surfaces as (so excluding `schema` hides schema-approval items too).
|
|
521
521
|
- Exposed as `GET /v1/queue` (hooks, session-start "open front" line),
|
|
522
|
-
`
|
|
522
|
+
`show_queue` (the registered MCP stub finally does work — and when Tier 2
|
|
523
523
|
routing lands, routed questions and stewarded items join the same queue),
|
|
524
524
|
and `/spor:next` (presents the queue; picking an item triggers a
|
|
525
525
|
full `--root <task-id>` compile so work starts pre-briefed).
|
|
@@ -530,7 +530,7 @@ The queue is a compile mode, not a new store:
|
|
|
530
530
|
|
|
531
531
|
**Per-person queues** (task-cc-queue-assignee-filtering). `rankQueue`'s
|
|
532
532
|
`assignee` parameter — and `GET /v1/queue?assignee=<person-id>` /
|
|
533
|
-
`
|
|
533
|
+
`show_queue {assignee}` (use `assignee=me` to bind to the caller) — scopes the
|
|
534
534
|
ranked queue to the work one person carries: the union of nodes with an
|
|
535
535
|
outbound `assigned` edge to them (work→person) and the nodes they `steward`
|
|
536
536
|
(person→node). Assignment is an edge like everything else (Tier-2's person
|
|
@@ -584,7 +584,7 @@ targets — subject first — form its relevance neighborhood, the first
|
|
|
584
584
|
stewarded node wins (the server's deterministic Tier-2 routing walk), and
|
|
585
585
|
the route materializes as a `routed-to` edge plus a body note. No steward
|
|
586
586
|
anywhere leaves the finding unrouted. The queue surfaces (`GET /v1/queue`,
|
|
587
|
-
`
|
|
587
|
+
`show_queue`) carry a `findings` field mirroring `questions`: open findings
|
|
588
588
|
routed to the authenticated identity plus unrouted ones — a finding routed
|
|
589
589
|
to someone else stays out of your view, so gardener output lands with the
|
|
590
590
|
people who steward what it observed instead of everyone.
|
|
@@ -646,7 +646,7 @@ the structural truth:
|
|
|
646
646
|
in TODOs, that's evidence for a post-tool TODO-comment nudge, not more
|
|
647
647
|
prompt.)
|
|
648
648
|
3. **Queue** — ✅ SHIPPED. `rankQueue()` in the core (`lib/queue.js`, also a
|
|
649
|
-
CLI for local mode), `GET /v1/queue`, real `
|
|
649
|
+
CLI for local mode), `GET /v1/queue`, real `show_queue`, `/spor:next`.
|
|
650
650
|
The task and capture-pending seed schemas are `queueable: true`; the §4
|
|
651
651
|
signals (blocking / heat / staleness / age) are computed natively in the
|
|
652
652
|
core for now — per-schema `queueSignals()` attached code joins via the
|
|
@@ -740,5 +740,5 @@ which held: each landed as one commit on top of the registry.
|
|
|
740
740
|
computed item list in the sandbox — accepts `[{id, score}]` or an
|
|
741
741
|
`{id: score}` map; unmentioned items keep their default-blend score.
|
|
742
742
|
Fail-soft: a broken policy annotates the result (`policy: {id, applied,
|
|
743
|
-
error?}` on `/v1/queue` and `
|
|
743
|
+
error?}` on `/v1/queue` and `show_queue`) and the built-in blend stands; a
|
|
744
744
|
proposed policy is inert until approved like any schema node.
|
package/adapters/README.md
CHANGED
|
@@ -18,7 +18,10 @@ The Spor client is a portable core behind per-host adapters
|
|
|
18
18
|
nothing and exits 0.
|
|
19
19
|
- **Universal surfaces**: the server's MCP endpoint (`/mcp`) and REST
|
|
20
20
|
(`/v1/*`) work from any host; `spor-hook agents-md` maintains a managed
|
|
21
|
-
Spor section in `AGENTS.md`
|
|
21
|
+
Spor section in `AGENTS.md` (capture-discipline directive + standing
|
|
22
|
+
briefing) as the floor for hook-less hosts. The committable directive-only
|
|
23
|
+
form of the same block is `spor agents-md` (written by `spor enable` by
|
|
24
|
+
default, refreshed by `spor upgrade`) — see `spor help agents-md`.
|
|
22
25
|
- **Adapters**: a manifest per host mapping its event names onto the
|
|
23
26
|
dispatcher.
|
|
24
27
|
|
package/adapters/codex/README.md
CHANGED
|
@@ -80,7 +80,7 @@ is just a manifest over `bin/spor-hook.js`.
|
|
|
80
80
|
the distill engine short-circuits on it — so a `codex exec` distiller that
|
|
81
81
|
fires its own Stop hook cannot recurse.
|
|
82
82
|
- MCP: add the Spor server to `~/.codex/config.toml` for on-demand graph
|
|
83
|
-
access (`query_graph`, `capture`, `
|
|
83
|
+
access (`query_graph`, `capture`, `show_queue`):
|
|
84
84
|
|
|
85
85
|
```toml
|
|
86
86
|
[mcp_servers.spor]
|
package/bin/spor.js
CHANGED
|
@@ -4859,6 +4859,57 @@ function cmdLink(cfg, { positionals }) {
|
|
|
4859
4859
|
return 0;
|
|
4860
4860
|
}
|
|
4861
4861
|
|
|
4862
|
+
// --- spor agents-md: committed capture-discipline directive ----------------
|
|
4863
|
+
// (task-spor-agents-md-capture-discipline-directive) Write/refresh the managed
|
|
4864
|
+
// AGENTS.md block with the standing user-voice directive to keep the graph
|
|
4865
|
+
// current. The 2026-07 retrospective found front-loaded user-voice directives
|
|
4866
|
+
// were the one condition that reliably produced unprompted capture (~8/8 vs a
|
|
4867
|
+
// ~0-10% baseline), while the hook-injected preamble saying the same thing in
|
|
4868
|
+
// system-reminder voice underperformed — so the directive belongs in a
|
|
4869
|
+
// COMMITTED instructions file, where it reaches every contributor and every
|
|
4870
|
+
// dispatched agent. Default is directive-only: hooked hosts already receive
|
|
4871
|
+
// the briefing at session start, and a committed briefing snapshot stales;
|
|
4872
|
+
// --briefing restores the full hook-less floor (directive + briefing embed).
|
|
4873
|
+
async function cmdAgentsMd(cfg, { values }) {
|
|
4874
|
+
const root = repoRoot();
|
|
4875
|
+
const { writeAgentsBlock } = require(path.join(ROOT, "scripts", "engines", "agents-md.js"));
|
|
4876
|
+
const { file, meta } = await writeAgentsBlock({ cwd: root, briefing: !!values.briefing });
|
|
4877
|
+
out(`updated ${file} (${values.briefing ? meta || "no briefing yet, MCP pointers only" : "capture-discipline directive"})`);
|
|
4878
|
+
// CLAUDE.md rides along via an @AGENTS.md import (Claude Code resolves
|
|
4879
|
+
// @-imports): if the repo has a CLAUDE.md that never mentions AGENTS.md,
|
|
4880
|
+
// Claude Code sessions would miss the directive entirely. Append the import
|
|
4881
|
+
// once; never CREATE a CLAUDE.md (AGENTS.md alone is the portable surface).
|
|
4882
|
+
if (!values["no-claude-md"]) {
|
|
4883
|
+
const claudeMd = path.join(root, "CLAUDE.md");
|
|
4884
|
+
if (fs.existsSync(claudeMd)) {
|
|
4885
|
+
const txt = fs.readFileSync(claudeMd, "utf8");
|
|
4886
|
+
if (!/AGENTS\.md/.test(txt)) {
|
|
4887
|
+
fs.writeFileSync(claudeMd, txt + (txt.endsWith("\n") ? "" : "\n") + "\n@AGENTS.md\n");
|
|
4888
|
+
out(`updated ${claudeMd} (@AGENTS.md import appended)`);
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
out(" commit the file(s) so every contributor and dispatched agent inherits the directive");
|
|
4893
|
+
return 0;
|
|
4894
|
+
}
|
|
4895
|
+
|
|
4896
|
+
// `spor upgrade` rider: the committed directive tracks the packaged wording —
|
|
4897
|
+
// refresh the current repo's managed block IF one exists, preserving whether
|
|
4898
|
+
// it embedded a briefing. A repo that never opted into the block is untouched.
|
|
4899
|
+
async function refreshAgentsBlockIfManaged(root = repoRoot()) {
|
|
4900
|
+
const file = path.join(root, "AGENTS.md");
|
|
4901
|
+
let existing = "";
|
|
4902
|
+
try {
|
|
4903
|
+
existing = fs.readFileSync(file, "utf8");
|
|
4904
|
+
} catch {
|
|
4905
|
+
return;
|
|
4906
|
+
}
|
|
4907
|
+
if (!existing.includes("<!-- spor:begin -->") && !existing.includes("<!-- substrate:begin -->")) return;
|
|
4908
|
+
const { writeAgentsBlock } = require(path.join(ROOT, "scripts", "engines", "agents-md.js"));
|
|
4909
|
+
await writeAgentsBlock({ cwd: root, briefing: /### Standing project briefing/.test(existing) });
|
|
4910
|
+
out(`refreshed ${file} (managed Spor block — commit if the wording changed)`);
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4862
4913
|
// --- spor install / setup: wire spor into a host agent ---------------------
|
|
4863
4914
|
// dec-cc-portable-core-adapters ships a manifest per host under adapters/<host>/
|
|
4864
4915
|
// with a __SPOR_ROOT__ placeholder; installing one resolves the placeholder to
|
|
@@ -5519,6 +5570,10 @@ async function cmdUpgrade(cfg, { values, positionals: pos }) {
|
|
|
5519
5570
|
if (r !== 0) rc = r;
|
|
5520
5571
|
}
|
|
5521
5572
|
if (!dryRun) {
|
|
5573
|
+
// The committed AGENTS.md directive versions with the package — refresh
|
|
5574
|
+
// the current repo's managed block so wording changes actually ship
|
|
5575
|
+
// (task-spor-agents-md-capture-discipline-directive). No-op without one.
|
|
5576
|
+
await refreshAgentsBlockIfManaged();
|
|
5522
5577
|
out("");
|
|
5523
5578
|
out("Restart any running sessions so the refreshed hooks/plugin load.");
|
|
5524
5579
|
// The refresh above closes the loaded-vs-installed gap; this closes the
|
|
@@ -8472,10 +8527,40 @@ const COMMANDS = {
|
|
|
8472
8527
|
run: (cfg) => cmdScope(false),
|
|
8473
8528
|
},
|
|
8474
8529
|
enable: {
|
|
8475
|
-
group: "Repo scoping", parse: "strict", args: "",
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8530
|
+
group: "Repo scoping", parse: "strict", args: "",
|
|
8531
|
+
options: { "no-agents": { type: "boolean", desc: "skip writing the AGENTS.md capture-discipline directive" } },
|
|
8532
|
+
summary: "opt this repo in (.spor.json + AGENTS.md directive)",
|
|
8533
|
+
help: "Set { enabled: true } in this repo's committable .spor.json. Spor is opt-in\nper repo — a repo with no .spor/.spor.json marker is a no-op — so this is how\nyou turn it on (and how you undo a prior 'spor disable'). Also writes the\nAGENTS.md capture-discipline directive (see 'spor help agents-md'; skip with\n--no-agents). Commit the files to share the setting.",
|
|
8534
|
+
run: async (cfg, p) => {
|
|
8535
|
+
const rc = cmdScope(true);
|
|
8536
|
+
// Enabling is the moment this repo's work was decided to belong in the
|
|
8537
|
+
// graph — the standing directive rides along by default.
|
|
8538
|
+
if (rc === 0 && !p.values["no-agents"]) await cmdAgentsMd(cfg, { values: {} });
|
|
8539
|
+
return rc;
|
|
8540
|
+
},
|
|
8541
|
+
},
|
|
8542
|
+
"agents-md": {
|
|
8543
|
+
group: "Repo scoping", parse: "strict", args: "", aliases: ["agents"],
|
|
8544
|
+
summary: "write/refresh the committed AGENTS.md graph-upkeep directive",
|
|
8545
|
+
help:
|
|
8546
|
+
"Write (or idempotently refresh) the managed Spor block in AGENTS.md at the\n" +
|
|
8547
|
+
"repo root: standing user-voice instructions to keep the graph current —\n" +
|
|
8548
|
+
"capture discovered work when it appears, file issues before fixing, prefer\n" +
|
|
8549
|
+
"the graph over private auto-memory for durable facts, resolve with\n" +
|
|
8550
|
+
"artifacts, add Spor: commit trailers. Committed, it reaches every\n" +
|
|
8551
|
+
"contributor and dispatched agent; 'spor upgrade' refreshes the wording.\n" +
|
|
8552
|
+
"If a CLAUDE.md exists that never mentions AGENTS.md, an @AGENTS.md import\n" +
|
|
8553
|
+
"is appended so Claude Code sessions inherit the directive too.\n" +
|
|
8554
|
+
"By default the block carries the directive only (hooked hosts get their\n" +
|
|
8555
|
+
"briefing at session start); --briefing also embeds the standing project\n" +
|
|
8556
|
+
"briefing — the floor for hosts without hooks (same block 'spor-hook\n" +
|
|
8557
|
+
"agents-md' maintains from adapter session-start hooks).",
|
|
8558
|
+
options: {
|
|
8559
|
+
briefing: { type: "boolean", desc: "also embed the standing project briefing (hook-less floor)" },
|
|
8560
|
+
"no-claude-md": { type: "boolean", desc: "don't append the @AGENTS.md import to an existing CLAUDE.md" },
|
|
8561
|
+
},
|
|
8562
|
+
examples: ["spor agents-md", "spor agents-md --briefing"],
|
|
8563
|
+
run: (cfg, p) => cmdAgentsMd(cfg, p),
|
|
8479
8564
|
},
|
|
8480
8565
|
link: {
|
|
8481
8566
|
group: "Repo scoping", parse: "strict", args: "<slug>", options: {},
|
|
@@ -8854,7 +8939,7 @@ async function main() {
|
|
|
8854
8939
|
// Expose the pure helpers for unit tests (the version-check logic has no I/O),
|
|
8855
8940
|
// and only run the CLI when invoked directly — requiring this file must not
|
|
8856
8941
|
// kick off main() and call process.exit under the test runner.
|
|
8857
|
-
module.exports = { nodeFloor, nodeRuntimeCheck, verCmp, sporConnectorBound, COMMANDS, resolveVerb, getNodeJson, gitBlobSha };
|
|
8942
|
+
module.exports = { nodeFloor, nodeRuntimeCheck, verCmp, sporConnectorBound, COMMANDS, resolveVerb, getNodeJson, gitBlobSha, refreshAgentsBlockIfManaged };
|
|
8858
8943
|
|
|
8859
8944
|
if (require.main === module) {
|
|
8860
8945
|
main()
|
package/lib/kernel/graph.js
CHANGED
|
@@ -954,7 +954,7 @@ function compile(graph, opts = {}) {
|
|
|
954
954
|
// The digest/briefing renders used to emit only (type, project, date) +
|
|
955
955
|
// summary, so a done/rejected/resolved node read as live guidance — inviting
|
|
956
956
|
// relitigation of dismissed approaches. Two truth signals get surfaced, the
|
|
957
|
-
// same ones get_node/
|
|
957
|
+
// same ones get_node/show_queue already honor:
|
|
958
958
|
// - a TERMINAL status field (done, rejected, resolved, …) is appended to
|
|
959
959
|
// the parenthetical, so the reader sees the node is not live work.
|
|
960
960
|
// - an inbound resolves/answers edge that retires a still-open node
|
|
@@ -14,7 +14,7 @@ shipped with the plugin as a registry default (QUEUE.md §2). It is the
|
|
|
14
14
|
dec-spor-definition-of-done-org-policy): a resolving artifact (a change/PR)
|
|
15
15
|
or any work node points a `review-requested` edge at each `person` a review
|
|
16
16
|
is wanted from. The server surfaces those nodes into each named reviewer's
|
|
17
|
-
personal queue (`
|
|
17
|
+
personal queue (`show_queue` `reviews`), the same per-person filter questions
|
|
18
18
|
and findings route through.
|
|
19
19
|
|
|
20
20
|
An outcome **flips the edge in place**: an approval rewrites it to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sporhq/spor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.6",
|
|
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",
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
// local briefing sources, same append-or-replace semantics. The session-start
|
|
5
5
|
// floor for hosts without hooks; status goes to stderr (hook hosts treat
|
|
6
6
|
// stdout as the hook's response).
|
|
7
|
+
//
|
|
8
|
+
// Two callers, two shapes (task-spor-agents-md-capture-discipline-directive):
|
|
9
|
+
// - `spor-hook agents-md` (adapter session-start floor): directive +
|
|
10
|
+
// standing-briefing embed — hook-less hosts get their briefing here.
|
|
11
|
+
// - `spor agents-md` (CLI verb, also ridden by `spor enable` / `spor
|
|
12
|
+
// upgrade`): directive only by default — hooked hosts already get the
|
|
13
|
+
// briefing at session start, and the committed block should carry the
|
|
14
|
+
// durable instruction, not a briefing snapshot that stales between
|
|
15
|
+
// refreshes.
|
|
7
16
|
|
|
8
17
|
const fs = require("fs");
|
|
9
18
|
const path = require("path");
|
|
@@ -19,9 +28,34 @@ const LEGACY_END = "<!-- substrate:end -->";
|
|
|
19
28
|
function toolsLine() {
|
|
20
29
|
const server = u.serverBase();
|
|
21
30
|
const mcp = server ? ` It is reachable over MCP at ${server}/mcp (bearer token).` : "";
|
|
22
|
-
return `A team knowledge graph (Spor) holds prior decisions, constraints, dismissed approaches, and deferred work.${mcp} Before designing or deciding anything non-trivial, check it (query_graph).
|
|
31
|
+
return `A team knowledge graph (Spor) holds prior decisions, constraints, dismissed approaches, and deferred work.${mcp} Before designing or deciding anything non-trivial, check it (query_graph). Ask show_queue what to work on next. When a git commit implements a tracked node (a task, decision, or issue), add a 'Spor: <node-id>' trailer to the commit message, in the final trailer block alongside any Co-Authored-By (no blank line between trailers) — git then records which node the commit serves, and the graph records the commit's sha.`;
|
|
23
32
|
}
|
|
24
33
|
|
|
34
|
+
// The capture-discipline directive — user-voice standing instructions that
|
|
35
|
+
// make graph upkeep part of the work instead of an afterthought. One source
|
|
36
|
+
// of truth, versioned with the package: the hook floor, the CLI verb, and
|
|
37
|
+
// `spor enable`/`spor upgrade` all write THIS text, so a wording change ships
|
|
38
|
+
// to every managed block on the next refresh. Each bullet encodes a failure
|
|
39
|
+
// mode observed in the 2026-07-04 capture retrospective
|
|
40
|
+
// (art-cc-capture-discipline-results-2): work discovered but never filed,
|
|
41
|
+
// fix-before-issue, decisions kept only in chat, durable facts leaking to
|
|
42
|
+
// private auto-memory, and bare status flips.
|
|
43
|
+
const DIRECTIVE = `Keep the graph current as you work — do these unprompted:
|
|
44
|
+
|
|
45
|
+
- The moment work is discovered that you won't do right now (an out-of-scope
|
|
46
|
+
bug, a follow-up, a dismissed approach), capture it before moving on:
|
|
47
|
+
/spor:defer (or \`spor add "..."\`) — 2-3 sentences in your own words; the
|
|
48
|
+
server types and links it.
|
|
49
|
+
- Found a defect you ARE about to fix? File it first, fix second — the issue
|
|
50
|
+
node is the lineage the fix resolves.
|
|
51
|
+
- Made a decision worth keeping (approach chosen, alternative ruled out,
|
|
52
|
+
gotcha paid for)? Capture it at the moment it is made, not at session end.
|
|
53
|
+
- Durable, team-relevant facts belong in the graph, never only in private
|
|
54
|
+
auto-memory or scratch notes. If you are about to "remember" something a
|
|
55
|
+
teammate or future session could need, capture it to Spor as well.
|
|
56
|
+
- When tracked work finishes, close the loop: record the resolution (a
|
|
57
|
+
decision or artifact node with a \`resolves\` edge), not a bare status flip.`;
|
|
58
|
+
|
|
25
59
|
// Body after the second '---' line (awk), head -c 7000, $() newline strip.
|
|
26
60
|
function nodeBody(raw) {
|
|
27
61
|
const lines = raw.split("\n");
|
|
@@ -35,59 +69,57 @@ function nodeBody(raw) {
|
|
|
35
69
|
return u.stripTrailingNewlines(u.byteHead(awkOut, 7000));
|
|
36
70
|
}
|
|
37
71
|
|
|
38
|
-
|
|
72
|
+
// Core writer: compose the managed block and splice it into AGENTS.md at the
|
|
73
|
+
// repo root. `briefing: false` skips the standing-briefing fetch/embed
|
|
74
|
+
// entirely (no server round-trip). Returns { file, meta, hadBriefing }.
|
|
75
|
+
async function writeAgentsBlock({ cwd, briefing = true }) {
|
|
39
76
|
const graph = u.graphHome();
|
|
40
|
-
let cwd = "";
|
|
41
|
-
for (let i = 0; i < args.length; i++) {
|
|
42
|
-
if (args[i] === "--cwd") cwd = args[i + 1] ?? "";
|
|
43
|
-
}
|
|
44
|
-
if (!cwd && input && input.cwd) cwd = input.cwd;
|
|
45
|
-
if (!cwd) cwd = process.cwd();
|
|
46
|
-
|
|
47
77
|
const root = u.git(cwd, ["rev-parse", "--show-toplevel"])?.trim() || cwd;
|
|
48
78
|
const slug = u.projectSlug(root);
|
|
49
79
|
|
|
50
80
|
let body = "";
|
|
51
81
|
let meta = "";
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const parsed = JSON.parse(resp.body);
|
|
59
|
-
if (parsed.found === true) {
|
|
60
|
-
// jq -r emits a trailing newline; head -c counts it; $() strips it.
|
|
61
|
-
body = u.stripTrailingNewlines(u.byteHead((parsed.body ?? "") + "\n", 7000));
|
|
62
|
-
const version = parsed.version ?? 1;
|
|
63
|
-
meta = `brief-${slug} v${version} @ ${u.serverHost()}`;
|
|
64
|
-
}
|
|
65
|
-
} catch {}
|
|
66
|
-
} else {
|
|
67
|
-
const brief = path.join(graph, "nodes", `brief-${slug}.md`);
|
|
68
|
-
if (fs.existsSync(brief)) {
|
|
69
|
-
let raw = "";
|
|
82
|
+
if (briefing) {
|
|
83
|
+
if (u.serverBase()) {
|
|
84
|
+
const resp = await u.curl(`${u.serverBase()}/v1/briefing/${slug}`, {
|
|
85
|
+
headers: u.bearer(),
|
|
86
|
+
timeoutMs: 6000,
|
|
87
|
+
});
|
|
70
88
|
try {
|
|
71
|
-
|
|
89
|
+
const parsed = JSON.parse(resp.body);
|
|
90
|
+
if (parsed.found === true) {
|
|
91
|
+
// jq -r emits a trailing newline; head -c counts it; $() strips it.
|
|
92
|
+
body = u.stripTrailingNewlines(u.byteHead((parsed.body ?? "") + "\n", 7000));
|
|
93
|
+
const version = parsed.version ?? 1;
|
|
94
|
+
meta = `brief-${slug} v${version} @ ${u.serverHost()}`;
|
|
95
|
+
}
|
|
72
96
|
} catch {}
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
97
|
+
} else {
|
|
98
|
+
const brief = path.join(graph, "nodes", `brief-${slug}.md`);
|
|
99
|
+
if (fs.existsSync(brief)) {
|
|
100
|
+
let raw = "";
|
|
101
|
+
try {
|
|
102
|
+
raw = fs.readFileSync(brief, "utf8");
|
|
103
|
+
} catch {}
|
|
104
|
+
body = nodeBody(raw);
|
|
105
|
+
const version = raw.match(/^version: *(.*)$/m)?.[1] ?? "";
|
|
106
|
+
meta = `brief-${slug} v${version || "1"} (local)`;
|
|
107
|
+
}
|
|
76
108
|
}
|
|
77
109
|
}
|
|
78
110
|
|
|
79
|
-
const
|
|
80
|
-
const section = body
|
|
81
|
-
? `## Spor team graph
|
|
111
|
+
const directive = `## Spor team graph
|
|
82
112
|
|
|
83
|
-
${
|
|
113
|
+
${toolsLine()}
|
|
84
114
|
|
|
85
|
-
|
|
115
|
+
${DIRECTIVE}`;
|
|
116
|
+
const section = body
|
|
117
|
+
? `${directive}
|
|
86
118
|
|
|
87
|
-
${
|
|
88
|
-
: `## Spor team graph
|
|
119
|
+
### Standing project briefing (${meta}, machine-compiled ${u.localDate()} — do not hand-edit this section; refresh with \`spor agents-md --briefing\`)
|
|
89
120
|
|
|
90
|
-
${
|
|
121
|
+
${body}`
|
|
122
|
+
: directive;
|
|
91
123
|
|
|
92
124
|
const file = path.join(root, "AGENTS.md");
|
|
93
125
|
const block = `${BEGIN}\n${section}\n${END}`;
|
|
@@ -127,8 +159,24 @@ ${tools}`;
|
|
|
127
159
|
const tmp = file + `.spor-tmp-${process.pid}`;
|
|
128
160
|
fs.writeFileSync(tmp, out);
|
|
129
161
|
fs.renameSync(tmp, file);
|
|
130
|
-
|
|
162
|
+
return { file, meta, hadBriefing: !!body };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function agentsMd(input, args = []) {
|
|
166
|
+
let cwd = "";
|
|
167
|
+
let briefing = true;
|
|
168
|
+
for (let i = 0; i < args.length; i++) {
|
|
169
|
+
if (args[i] === "--cwd") cwd = args[i + 1] ?? "";
|
|
170
|
+
if (args[i] === "--directive-only") briefing = false;
|
|
171
|
+
}
|
|
172
|
+
if (!cwd && input && input.cwd) cwd = input.cwd;
|
|
173
|
+
if (!cwd) cwd = process.cwd();
|
|
174
|
+
|
|
175
|
+
const { file, meta } = await writeAgentsBlock({ cwd, briefing });
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
`updated ${file} (${briefing ? meta || "no briefing yet, MCP pointers only" : "directive only"})\n`
|
|
178
|
+
);
|
|
131
179
|
return null;
|
|
132
180
|
}
|
|
133
181
|
|
|
134
|
-
module.exports = { agentsMd };
|
|
182
|
+
module.exports = { agentsMd, writeAgentsBlock, DIRECTIVE };
|
package/skills/next/SKILL.md
CHANGED
|
@@ -26,8 +26,16 @@ to surface (see Presenting). Don't echo `SPOR_SERVER`/`SPOR_TOKEN`/`SPOR_HOME`
|
|
|
26
26
|
or announce which mode is running unless the user asks — it's plumbing; if you
|
|
27
27
|
ever need to confirm the resolved mode/scope, `spor status` reports it.
|
|
28
28
|
|
|
29
|
-
**In Cowork (no shell)**, call the `
|
|
30
|
-
fields (`project`, `types`, `exclude_types` arrays, `limit`/`offset`).
|
|
29
|
+
**In Cowork (no shell)**, call the `show_queue` MCP tool instead, with the same
|
|
30
|
+
fields (`project`, `types`, `exclude_types` arrays, `limit`/`offset`). Omit
|
|
31
|
+
`assignee` for the ordinary "my queue" answer — `assignee: "me"` narrows to
|
|
32
|
+
directly assigned/stewarded work only, a narrower carrying view. In an
|
|
33
|
+
MCP-Apps host that should attach the interactive queue widget, use
|
|
34
|
+
`render_queue` (same fields; `show_queue` stays the data answer).
|
|
35
|
+
|
|
36
|
+
(A different question — "how far along is the whole workstream?", progress
|
|
37
|
+
rather than next actions — is the program view: the `render_program` MCP tool
|
|
38
|
+
or `GET /v1/program/<root-id>`; see /spor:spor.)
|
|
31
39
|
|
|
32
40
|
**Scope and filter flags** (task-cc-queue-filtering-enhancements). The queue
|
|
33
41
|
defaults to this repo's project; three optional, composable levers widen or
|
|
@@ -46,7 +54,7 @@ pending captures"):
|
|
|
46
54
|
(comma-separated, repeatable, exclude wins on overlap). They apply *before*
|
|
47
55
|
ranking, so the aggregate counts describe the filtered queue.
|
|
48
56
|
|
|
49
|
-
The `
|
|
57
|
+
The `show_queue` MCP tool takes the same fields.
|
|
50
58
|
|
|
51
59
|
## In-flight awareness — don't re-pick work an agent is already on (Claude Code only)
|
|
52
60
|
|
|
@@ -62,12 +70,12 @@ boolean — and a `dispatched` summary (`{id, name, state, status, cwd}`) on the
|
|
|
62
70
|
in-flight ones — by matching live background agents to node ids. Add
|
|
63
71
|
`--hide-dispatched` to drop the in-flight items entirely (it reports a
|
|
64
72
|
`hidden_dispatched` count, never silently). This is best-effort and
|
|
65
|
-
Claude-Code-only: the `
|
|
73
|
+
Claude-Code-only: the `show_queue` / server queue can't see local background
|
|
66
74
|
agents, so the flag fails soft (every item reads `in_flight:false`) when the
|
|
67
75
|
`claude` binary is absent (e.g. in Cowork or a plain shell).
|
|
68
76
|
|
|
69
77
|
Prefer reading that flag over shelling out yourself. When you only have the raw
|
|
70
|
-
`
|
|
78
|
+
`show_queue` output (which doesn't carry the flag), an item is **in flight** when
|
|
71
79
|
a `kind: "background"` agent from `claude agents --json` has `name` equal to the
|
|
72
80
|
item's id and `state` is not `"done"`. For those items: badge them "🤖 agent
|
|
73
81
|
dispatched — in progress" and keep them OUT of the top "pick this next"
|
package/skills/spor/SKILL.md
CHANGED
|
@@ -200,13 +200,62 @@ In Cowork (Anthropic's chat workspace) and Claude Code with the connector there
|
|
|
200
200
|
is no shell and no ambient injection — reach the graph through the **Spor MCP
|
|
201
201
|
tools** instead: `query_graph` (ORIENT/TRAVERSE: free-text search, or `root_id`
|
|
202
202
|
to compile one node's neighborhood), `get_node` (raw node + revision),
|
|
203
|
-
`
|
|
204
|
-
|
|
203
|
+
`show_queue` (the ranked queue — omit `assignee` for the ordinary "my queue"
|
|
204
|
+
answer; `assignee: "me"` narrows to directly assigned/stewarded work only),
|
|
205
|
+
`render_queue` (the same queue when an MCP-Apps host should attach the
|
|
206
|
+
interactive widget; `show_queue` stays the data answer), `render_lens` (a saved
|
|
207
|
+
board/table/lineage view; no id lists them), `render_program` (the
|
|
208
|
+
program/progress view for a workstream: given a root node other work `blocks`,
|
|
209
|
+
the gating tree of everything blocking it transitively, with
|
|
210
|
+
resolution-derived progress — "how far along is X?", where `show_queue`
|
|
211
|
+
answers "what's next?"; REST twin `GET /v1/program/{id}`; an empty result
|
|
212
|
+
means nothing carries a `blocks` edge to that root yet — see "Grouping work
|
|
213
|
+
under an umbrella node" below for how to model one), and to COMMIT:
|
|
214
|
+
`capture` (raw prose → server types it — reach
|
|
205
215
|
for this when unsure of the shape), or the precise writes `put_node` /
|
|
206
216
|
`add_edge` / `set_status`. Close loops with edges: answer a question with a node
|
|
207
217
|
carrying an `answers` edge; close work with a `resolves` edge from a
|
|
208
218
|
`decision`/`artifact`.
|
|
209
219
|
|
|
220
|
+
## Grouping work under an umbrella node (programs)
|
|
221
|
+
|
|
222
|
+
When a workstream outgrows a handful of nodes — a milestone, a refactoring
|
|
223
|
+
program, a launch — group it under an **umbrella node** so the program view
|
|
224
|
+
can render it. There is no `program` node type: the umbrella is an ordinary
|
|
225
|
+
node, almost always a `task` (id like `task-<stem>-program`) whose body says
|
|
226
|
+
what the program is and what "done" means. (`type: project` /
|
|
227
|
+
`grouped-under` is the unrelated repo-identity layer — never use it to group
|
|
228
|
+
work.)
|
|
229
|
+
|
|
230
|
+
Membership is **`blocks` topology**: each member carries a `blocks` edge TO
|
|
231
|
+
the umbrella — "the umbrella can't finish until this does":
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
spor edge task-api-rate-limit blocks task-platform-hardening-program
|
|
235
|
+
# MCP: add_edge {id: "task-api-rate-limit", type: "blocks", to: "task-platform-hardening-program"}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
- Write it from the member's perspective (member `blocks` umbrella). The
|
|
239
|
+
inverse spelling (`blocked-by` on the umbrella) is accepted and flipped;
|
|
240
|
+
duplicate edges are a no-op.
|
|
241
|
+
- **Only `blocks` edges count.** The program view walks inbound `blocks`
|
|
242
|
+
edges transitively and nothing else — members joined by `relates-to`,
|
|
243
|
+
`mentions`, or `derived-from` are invisible to it (and to the queue's
|
|
244
|
+
blocked-gating).
|
|
245
|
+
- Nest freely: a workstream hub that `blocks` the program hub hangs its whole
|
|
246
|
+
subtree under it — the walk is transitive, so one edge per level is enough.
|
|
247
|
+
- The gating is real, not cosmetic: the umbrella leaves the actionable queue
|
|
248
|
+
until its members land, and closing it at the end still needs a resolver
|
|
249
|
+
like any task.
|
|
250
|
+
|
|
251
|
+
Read it back with `render_program` (`{id: "<umbrella-id>"}`; REST `GET
|
|
252
|
+
/v1/program/<id>`): the gating tree with resolution-derived progress, where
|
|
253
|
+
**done** = terminal status, supersession, or a live `resolves`/`answers`
|
|
254
|
+
edge — the queue's truth, even while a status field lags. An empty result
|
|
255
|
+
means nothing `blocks` the root yet — add the member edges above.
|
|
256
|
+
`show_queue` answers "what's next?"; `render_program` answers "how far along
|
|
257
|
+
is the whole thing?".
|
|
258
|
+
|
|
210
259
|
## Adding a node or edge type
|
|
211
260
|
|
|
212
261
|
Because schemas are themselves nodes, you extend the ontology by **writing a
|
|
@@ -60,7 +60,7 @@ are same-direction synonyms renamed at write time.
|
|
|
60
60
|
| resolves | 0.9 | this fixes/closes the target | — |
|
|
61
61
|
| triggered-by | 0.7 | this run was triggered by the target | — |
|
|
62
62
|
| performs | 0.8 | this run is an execution of the target workflow | — |
|
|
63
|
-
| blocks | 0.7 | the target can't proceed until this does | inverse `blocked-by` |
|
|
63
|
+
| blocks | 0.7 | the target can't proceed until this does | inverse `blocked-by`; also program membership — member `blocks` umbrella, rendered by `render_program` |
|
|
64
64
|
| answers | 0.7 | this answers the target question | inverse `answered-by` |
|
|
65
65
|
| assigned | 0.5 | work assigned to this person OR agent (explicit routing; an agent target may carry a `profile:` per-assignment override) | — |
|
|
66
66
|
| relates-to | 0.5 | weak association | alias `related-to` |
|
package/skills/triage/SKILL.md
CHANGED
|
@@ -42,8 +42,9 @@ finish the whole queue in one sitting — say what you triaged and what's left.
|
|
|
42
42
|
|
|
43
43
|
## 1. Pull the queue and read the landscape
|
|
44
44
|
|
|
45
|
-
Get it with the queue's own resolver — `spor next --json` (or the `
|
|
46
|
-
MCP tool in Cowork
|
|
45
|
+
Get it with the queue's own resolver — `spor next --json` (or the `show_queue`
|
|
46
|
+
MCP tool in Cowork; omit `assignee` — `assignee: "me"` narrows to directly
|
|
47
|
+
assigned/stewarded work only). Read the **aggregates**, not just the page: `total_count`,
|
|
47
48
|
`counts_by_type`, `counts_by_suggest`, and the side-channels `pending`
|
|
48
49
|
(unprocessed captures), `questions` (routed/unrouted to you), `findings`
|
|
49
50
|
(gardener observations), plus `dormant`/`muted` counts.
|
|
@@ -210,6 +211,12 @@ Record a confirmed one as a `blocks` edge from the prerequisite to the dependent
|
|
|
210
211
|
(mirroring how an issue records its block). The dependent then leaves the
|
|
211
212
|
actionable queue until its blocker resolves — which is the point.
|
|
212
213
|
|
|
214
|
+
Once the topology is recorded you can sanity-check it from the top: the
|
|
215
|
+
program/progress view (`render_program` MCP tool, or `GET
|
|
216
|
+
/v1/program/<root-id>` in a shell) walks a root node's gating tree over
|
|
217
|
+
`blocks` edges with resolution-derived progress — a fast way to confirm the
|
|
218
|
+
edges you just wrote hang the work under the right umbrella.
|
|
219
|
+
|
|
213
220
|
## 7. Open questions → brief the lineage, then answer
|
|
214
221
|
|
|
215
222
|
This is where triage earns its keep: the queue's `questions` are decisions
|