@sporhq/spor 0.18.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/API.md +29 -9
- package/GRAPH.md +41 -0
- package/QUEUE.md +9 -9
- package/README.md +3 -0
- package/adapters/README.md +4 -1
- package/adapters/codex/README.md +1 -1
- package/bin/spor-hook.js +16 -1
- package/bin/spor.js +256 -5
- package/lib/check.js +162 -0
- package/lib/config.js +16 -1
- package/lib/kernel/coupling.js +216 -0
- package/lib/kernel/graph.js +43 -3
- package/lib/seed/schema-edge-review-requested.md +1 -1
- package/lib/seed/schema-question.md +35 -4
- package/package.json +2 -1
- package/scripts/engines/agents-md.js +89 -41
- package/scripts/engines/capture-health.js +82 -0
- package/scripts/engines/doctor.js +29 -0
- package/scripts/engines/enable-hint.js +57 -0
- package/scripts/engines/post-tool.js +198 -4
- package/scripts/engines/prompt-context.js +8 -0
- package/scripts/engines/session-start.js +12 -0
- package/skills/next/SKILL.md +14 -6
- package/skills/spor/SKILL.md +74 -10
- package/skills/spor/references/concepts.md +39 -2
- package/skills/{team-graph/SKILL.md → spor/references/cowork.md} +13 -19
- 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.
|
|
5
|
+
"version": "0.19.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "losthammer"
|
|
8
8
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Maintains a typed, versioned knowledge graph and compiles compact briefings from it: session-start injection, per-prompt relevance digests, capture at discovery, end-of-session distillation, decision queue.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Spor",
|
package/API.md
CHANGED
|
@@ -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
|
|
@@ -345,7 +364,7 @@ gating tasks). Unknown `id` errors with `{ "found": false, "error":
|
|
|
345
364
|
### `recent_changes`
|
|
346
365
|
|
|
347
366
|
The team's recent-activity feed — the temporal entry point the other read
|
|
348
|
-
tools lack (`query_graph` is semantic search, `
|
|
367
|
+
tools lack (`query_graph` is semantic search, `show_queue` is forward-looking
|
|
349
368
|
open work, `render_lens` renders current state). It answers "what changed /
|
|
350
369
|
what was done in the last N hours", "what did the agents write overnight", and
|
|
351
370
|
"what landed since `<commit>`". Input `{ "since"?, "project"?, "limit"? }` →
|
|
@@ -374,7 +393,7 @@ oldest-open bottlenecks. **Completion is a node's status-TRANSITION time** (when
|
|
|
374
393
|
it entered its final terminal run), never `updated_at`, so a later edge append
|
|
375
394
|
can't corrupt the "completed last week" signal (dec-spor-git-derived-timestamps).
|
|
376
395
|
`project` (a repo slug or grouping id) scopes it through the same up-resolution
|
|
377
|
-
as `
|
|
396
|
+
as `show_queue`; `types` restricts node types; `weeks`/`top`/`aging` shape the
|
|
378
397
|
window. The MCP twin of `GET /v1/analytics` (§3) over the same `store.analytics`
|
|
379
398
|
core — for the shell-less Cowork audience that can't run the CLI.
|
|
380
399
|
|
|
@@ -396,7 +415,7 @@ contract from `lib/seed/` files — those miss resident overrides
|
|
|
396
415
|
|
|
397
416
|
### The MCP-app widget (`ui://spor/view-tree.html`)
|
|
398
417
|
|
|
399
|
-
`
|
|
418
|
+
`render_queue`, `render_lens`, and `render_program` declare a UI resource via
|
|
400
419
|
`_meta.ui.resourceUri`: a single trusted interpreter of the view-tree
|
|
401
420
|
component catalog that MCP-apps hosts (Claude, Goose, VS Code) render as an
|
|
402
421
|
interactive iframe — status chips, progress bars, lineage trees, node detail
|
|
@@ -442,6 +461,7 @@ endpoint is the REST twin of a core call:
|
|
|
442
461
|
| `POST /v1/corrections` | /spor:correct | `propose_correction` semantics → 201 `{status, id, revision, warnings}` |
|
|
443
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) |
|
|
444
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`) |
|
|
445
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 |
|
|
446
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 |
|
|
447
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 |
|
package/GRAPH.md
CHANGED
|
@@ -444,6 +444,43 @@ opt-in that turns scoped norms on; set them with `spor repos tag <slug>
|
|
|
444
444
|
keeps the project-scoped behavior above, so a graph using no `applies_to_*` is
|
|
445
445
|
byte-identical (norm-cc-byte-identical-refactor).
|
|
446
446
|
|
|
447
|
+
A norm may also declare **coupling anchors**
|
|
448
|
+
(dec-spor-coupling-norms-declared-first): two flat inline lists, `couples_when:`
|
|
449
|
+
(trigger file globs — "when files matching these change") and `couples_also:`
|
|
450
|
+
(the coupled artifacts that should change in the same edit, or be consciously
|
|
451
|
+
dismissed). A norm carrying both becomes a **coupling norm**: the post-tool
|
|
452
|
+
hook glob-matches every Write/Edit's repo-relative path against the trigger
|
|
453
|
+
sets and, on a hit, injects the targets as an edit-time nudge — once per
|
|
454
|
+
session per norm, deterministic, no LLM (task-spor-coupling-nudge-posttool;
|
|
455
|
+
`SPOR_COUPLING_NUDGE=0` disables). The glob dialect is small: `**` crosses
|
|
456
|
+
path segments, `*` stays within one, `?` is one character, a trailing `/`
|
|
457
|
+
means the whole subtree, and a bare `API.md` anchors at the repo root. An
|
|
458
|
+
entry may be **repo-qualified** as `<slug>:<glob>` (or `repo-<slug>:<glob>`)
|
|
459
|
+
so one norm couples artifacts across repos — a qualified trigger fires only in
|
|
460
|
+
that repo and bypasses the norm's scope (the pin IS the scope), while an
|
|
461
|
+
unqualified trigger applies wherever the norm itself applies: its
|
|
462
|
+
`applies_to_*` selectors when declared, else its `project:` stamp (unstamped =
|
|
463
|
+
every repo — the org-wide case, e.g. `couples_when: [.nvmrc]` /
|
|
464
|
+
`couples_also: [Dockerfile]`). Targets may be qualified the same way for
|
|
465
|
+
display. Either key alone, a scalar value, or the keys on a non-norm type are
|
|
466
|
+
inert (validate warns). The matcher is `lib/kernel/coupling.js`; a graph with
|
|
467
|
+
no coupling norms is byte-identical.
|
|
468
|
+
|
|
469
|
+
The boundary-time consumer of the same anchors is **`spor check`**
|
|
470
|
+
(task-spor-cli-check-coupling-verb): given a change set (uncommitted vs HEAD
|
|
471
|
+
by default; `--staged`, `--range a..b`, or `--files`), it reports each
|
|
472
|
+
coupling norm whose triggers are touched while its same-repo targets are not —
|
|
473
|
+
advisory by default, `--strict` exits 1 for CI/pre-commit; targets pinned to
|
|
474
|
+
another repo surface as reminders, never failures. A coupling norm may
|
|
475
|
+
additionally declare a machine-checkable **value invariant** — two scalar
|
|
476
|
+
keys, `couples_value_a:`/`couples_value_b:`, each `<path>#<regex>` (first
|
|
477
|
+
capture group = the value, e.g. `couples_value_a: .nvmrc#v?(\d+)` /
|
|
478
|
+
`couples_value_b: Dockerfile#FROM node:(\d+)`) — and `spor check` compares the
|
|
479
|
+
two extracted values: "these now disagree" beats "you probably forgot", an
|
|
480
|
+
agreeing invariant suppresses the untouched heuristic, and a disagreement
|
|
481
|
+
reports even when both files were touched. A half-declared or malformed pair
|
|
482
|
+
is inert (validate warns).
|
|
483
|
+
|
|
447
484
|
Because a norm rides along with no relevance gate and the team trust model lets
|
|
448
485
|
every writer author one, the briefing renderer treats norm bodies as an
|
|
449
486
|
**injection surface** (issue-cc-norm-always-on-injection): each is quoted as
|
|
@@ -616,6 +653,10 @@ edges:
|
|
|
616
653
|
the deterministic router walks `stewards` edges from the question's relevance
|
|
617
654
|
neighborhood to the closest steward and writes a `routed-to` edge to that
|
|
618
655
|
person; an unrouted question (no steward matched) surfaces to everyone.
|
|
656
|
+
The question schema's `validate()` door rejects a title/summary/body that is
|
|
657
|
+
ONLY an unfilled template token (`<question>`, `{{text}}`, `[id]`) so a docs
|
|
658
|
+
example run verbatim can't mint an information-free routed ask
|
|
659
|
+
(issue-spor-ask-question-template-placeholder-validation).
|
|
619
660
|
- **`assigned`** points work (task/issue) at a person; per-person queues filter
|
|
620
661
|
on it (the queue's `assignee` parameter — `GET /v1/queue?assignee=<person>`,
|
|
621
662
|
unioned with the person's `stewards` edges; QUEUE.md §5). **`answers`** points
|
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/README.md
CHANGED
|
@@ -6,6 +6,8 @@ It keeps track of the useful things that normally disappear into chat history: d
|
|
|
6
6
|
|
|
7
7
|
The name comes from Norwegian: *spor* means the track something leaves behind.
|
|
8
8
|
|
|
9
|
+
📖 **Full documentation: [docs.sporhq.io](https://docs.sporhq.io)** — concepts, getting started, the CLI, the REST API, the MCP surface, and the hosted product.
|
|
10
|
+
|
|
9
11
|
## Why use Spor?
|
|
10
12
|
|
|
11
13
|
Coding agents are good at working inside a single session. The problem is everything before and after that session.
|
|
@@ -515,6 +517,7 @@ This matters because some hosts cache plugins or hook definitions. Updating the
|
|
|
515
517
|
|
|
516
518
|
## More docs
|
|
517
519
|
|
|
520
|
+
* [docs.sporhq.io](https://docs.sporhq.io) — the full documentation site: concepts, getting started, CLI, REST API, MCP, and the hosted guide
|
|
518
521
|
* `GRAPH.md` — graph format, node types, edges, and schema behaviour
|
|
519
522
|
* `API.md` — REST and MCP server contract
|
|
520
523
|
* `QUEUE.md` — queue, capture, routing, and workflow details
|
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-hook.js
CHANGED
|
@@ -162,7 +162,18 @@ async function main() {
|
|
|
162
162
|
/* logging must never break fail-open */
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
-
if (!cfg.enabled())
|
|
165
|
+
if (!cfg.enabled()) {
|
|
166
|
+
// A repo disabled purely by the opt-in DEFAULT that this machine has prior
|
|
167
|
+
// Spor history for gets a one-time discovery hint instead of silence
|
|
168
|
+
// (issue-spor-opt-in-silent-disable-no-indication) — session-start only,
|
|
169
|
+
// evidence-gated, once per repo; see scripts/engines/enable-hint.js.
|
|
170
|
+
if (event === "session-start") {
|
|
171
|
+
const { enableHint } = require("../scripts/engines/enable-hint");
|
|
172
|
+
const hint = enableHint(payload);
|
|
173
|
+
if (hint) emit(hint, payload, host);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
166
177
|
|
|
167
178
|
// Debounced distill: spool the payload and hand off to a per-session
|
|
168
179
|
// watcher (one at a time — the lock holds the watcher's pid; stale locks
|
|
@@ -204,7 +215,11 @@ async function main() {
|
|
|
204
215
|
const engine = ENGINES[event]();
|
|
205
216
|
const out = await engine(payload);
|
|
206
217
|
if (!out) return;
|
|
218
|
+
emit(out, payload, host);
|
|
219
|
+
}
|
|
207
220
|
|
|
221
|
+
// Write an engine's output envelope to stdout in the host's dialect.
|
|
222
|
+
function emit(out, payload, host) {
|
|
208
223
|
// Cursor speaks a flat snake_case output: {additional_context} only.
|
|
209
224
|
// (jq -c framing: compact JSON with a trailing newline.)
|
|
210
225
|
if (host === "cursor") {
|
package/bin/spor.js
CHANGED
|
@@ -1701,6 +1701,140 @@ async function fetchRemoteExportNodes(cfg, label) {
|
|
|
1701
1701
|
}
|
|
1702
1702
|
}
|
|
1703
1703
|
|
|
1704
|
+
// `spor check` — the coupling-drift report over a diff
|
|
1705
|
+
// (task-spor-cli-check-coupling-verb, dec-spor-coupling-norms-declared-first):
|
|
1706
|
+
// the boundary-time consumer of the same coupling norms the post-tool nudge
|
|
1707
|
+
// serves at edit time (one matcher, lib/kernel/coupling.js). Resolve a change
|
|
1708
|
+
// set via LOCAL git (always — the diff is where you run), load the graph's
|
|
1709
|
+
// coupling norms (local nodes dir, or the team graph via GET /v1/export in
|
|
1710
|
+
// remote mode, the documented graph-wide-sweep path), and report each norm
|
|
1711
|
+
// whose `couples_when` triggers are touched while its `couples_also` targets
|
|
1712
|
+
// are not — plus value-invariant disagreement for norms carrying
|
|
1713
|
+
// couples_value_a/b. Advisory by default (exit 0); --strict exits 1 on
|
|
1714
|
+
// findings for CI/pre-commit enforcement.
|
|
1715
|
+
async function cmdCheck(cfg, args) {
|
|
1716
|
+
const cwd = process.cwd();
|
|
1717
|
+
const topR = git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1718
|
+
const top = topR.status === 0 ? topR.stdout.trim() : "";
|
|
1719
|
+
if (!top) {
|
|
1720
|
+
err("check: not inside a git repository (the change set is a git diff)");
|
|
1721
|
+
return 1;
|
|
1722
|
+
}
|
|
1723
|
+
const slug = u.projectSlug(top);
|
|
1724
|
+
const range = optVal(args, "range");
|
|
1725
|
+
const staged = args.includes("--staged");
|
|
1726
|
+
const strict = args.includes("--strict");
|
|
1727
|
+
const json = args.includes("--json");
|
|
1728
|
+
let files = null;
|
|
1729
|
+
const fi = args.indexOf("--files");
|
|
1730
|
+
if (fi >= 0) {
|
|
1731
|
+
files = [];
|
|
1732
|
+
for (let i = fi + 1; i < args.length && !String(args[i]).startsWith("--"); i++) files.push(args[i]);
|
|
1733
|
+
if (!files.length) {
|
|
1734
|
+
err("check: --files needs at least one path");
|
|
1735
|
+
return 1;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if ((range ? 1 : 0) + (staged ? 1 : 0) + (files ? 1 : 0) > 1) {
|
|
1739
|
+
err("check: --range, --staged, and --files are mutually exclusive");
|
|
1740
|
+
return 1;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// The change set, as repo-relative forward-slash paths (git's own spelling).
|
|
1744
|
+
let changed = [];
|
|
1745
|
+
let rightRev = null; // where value invariants read from (--range reads the range's right side)
|
|
1746
|
+
if (files) {
|
|
1747
|
+
changed = files.map((f) => {
|
|
1748
|
+
const abs = path.isAbsolute(f) ? f : path.resolve(cwd, f);
|
|
1749
|
+
return path.relative(top, abs).split(path.sep).join("/");
|
|
1750
|
+
});
|
|
1751
|
+
} else if (range) {
|
|
1752
|
+
const r = git(top, ["diff", "--name-only", range]);
|
|
1753
|
+
if (r.status !== 0) {
|
|
1754
|
+
err(`check: could not resolve --range '${range}' (${(r.stderr || "").trim().split("\n")[0]})`);
|
|
1755
|
+
return 1;
|
|
1756
|
+
}
|
|
1757
|
+
changed = r.stdout.split("\n").filter(Boolean);
|
|
1758
|
+
const i = range.lastIndexOf("..");
|
|
1759
|
+
rightRev = i > 0 ? range.slice(i + 2) || "HEAD" : null;
|
|
1760
|
+
} else if (staged) {
|
|
1761
|
+
const r = git(top, ["diff", "--name-only", "--cached"]);
|
|
1762
|
+
if (r.status !== 0) {
|
|
1763
|
+
err("check: git diff --cached failed");
|
|
1764
|
+
return 1;
|
|
1765
|
+
}
|
|
1766
|
+
changed = r.stdout.split("\n").filter(Boolean);
|
|
1767
|
+
} else {
|
|
1768
|
+
// Default: everything uncommitted vs HEAD (staged + unstaged) plus
|
|
1769
|
+
// untracked files — the mid-session / pre-commit superset, so an unstaged
|
|
1770
|
+
// edit can't silently pass. (The task sketch said "staged by default";
|
|
1771
|
+
// staged-only reports NOTHING for the common unstaged-working-tree case —
|
|
1772
|
+
// false confidence — so the default is the honest superset and --staged is
|
|
1773
|
+
// the narrow pre-commit view.) A repo with no commits yet falls back to
|
|
1774
|
+
// the index.
|
|
1775
|
+
const d = git(top, ["diff", "--name-only", "HEAD"]);
|
|
1776
|
+
if (d.status === 0) changed = d.stdout.split("\n").filter(Boolean);
|
|
1777
|
+
else {
|
|
1778
|
+
const c = git(top, ["diff", "--name-only", "--cached"]);
|
|
1779
|
+
if (c.status === 0) changed = c.stdout.split("\n").filter(Boolean);
|
|
1780
|
+
}
|
|
1781
|
+
const un = git(top, ["ls-files", "--others", "--exclude-standard"]);
|
|
1782
|
+
if (un.status === 0) changed.push(...un.stdout.split("\n").filter(Boolean));
|
|
1783
|
+
}
|
|
1784
|
+
changed = [...new Set(changed)];
|
|
1785
|
+
|
|
1786
|
+
// The coupling norms: local nodes dir, or the team graph via /v1/export.
|
|
1787
|
+
const couplingLib = require(path.join(ROOT, "lib", "kernel", "coupling.js"));
|
|
1788
|
+
const scanDir = (nodesDir) => {
|
|
1789
|
+
let names = [];
|
|
1790
|
+
try {
|
|
1791
|
+
names = fs.readdirSync(nodesDir).sort();
|
|
1792
|
+
} catch {}
|
|
1793
|
+
return couplingLib.scanCouplingEntries((f) => fs.readFileSync(path.join(nodesDir, f), "utf8"), names);
|
|
1794
|
+
};
|
|
1795
|
+
let scan;
|
|
1796
|
+
let cleanupFetched = null;
|
|
1797
|
+
if (cfg.mode() === "remote" && !namesLocalGraph(args)) {
|
|
1798
|
+
const fetched = await fetchRemoteExportNodes(cfg, "check");
|
|
1799
|
+
if (fetched.error) return 1; // already reported (offline / HTTP error)
|
|
1800
|
+
cleanupFetched = fetched.cleanup;
|
|
1801
|
+
scan = scanDir(fetched.nodesDir);
|
|
1802
|
+
} else {
|
|
1803
|
+
const nodesDir = optVal(args, "nodes") || cfg.nodesDir();
|
|
1804
|
+
if (!fs.existsSync(nodesDir)) {
|
|
1805
|
+
err(`no graph at ${nodesDir} — run 'spor init' first`);
|
|
1806
|
+
return 1;
|
|
1807
|
+
}
|
|
1808
|
+
scan = scanDir(nodesDir);
|
|
1809
|
+
}
|
|
1810
|
+
try {
|
|
1811
|
+
const checkLib = require(path.join(ROOT, "lib", "check.js"));
|
|
1812
|
+
const readFile = (rel) => {
|
|
1813
|
+
try {
|
|
1814
|
+
if (rightRev) {
|
|
1815
|
+
const r = git(top, ["show", `${rightRev}:${rel}`]);
|
|
1816
|
+
return r.status === 0 ? r.stdout : null;
|
|
1817
|
+
}
|
|
1818
|
+
return fs.readFileSync(path.join(top, rel), "utf8");
|
|
1819
|
+
} catch {
|
|
1820
|
+
return null;
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
const { checked, findings, reminders } = checkLib.runCheck({
|
|
1824
|
+
slug,
|
|
1825
|
+
changed,
|
|
1826
|
+
norms: scan.norms,
|
|
1827
|
+
repoTags: scan.repo_tags[slug] ?? [],
|
|
1828
|
+
readFile,
|
|
1829
|
+
});
|
|
1830
|
+
if (json) out(JSON.stringify({ project: slug, changed, checked, findings, reminders, strict }, null, 2));
|
|
1831
|
+
else out(checkLib.renderReport({ slug, changed, checked, findings, reminders }, { strict }));
|
|
1832
|
+
return strict && findings.length ? 1 : 0;
|
|
1833
|
+
} finally {
|
|
1834
|
+
if (cleanupFetched) cleanupFetched();
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1704
1838
|
// analytics folds a graph's git history into created-vs-completed metrics
|
|
1705
1839
|
// (task-spor-work-analytics-consumer). Unlike query/validate (no server twin) it
|
|
1706
1840
|
// is dual-mode: local mode runs the in-repo consumer (lib/analytics.js) over
|
|
@@ -4859,6 +4993,57 @@ function cmdLink(cfg, { positionals }) {
|
|
|
4859
4993
|
return 0;
|
|
4860
4994
|
}
|
|
4861
4995
|
|
|
4996
|
+
// --- spor agents-md: committed capture-discipline directive ----------------
|
|
4997
|
+
// (task-spor-agents-md-capture-discipline-directive) Write/refresh the managed
|
|
4998
|
+
// AGENTS.md block with the standing user-voice directive to keep the graph
|
|
4999
|
+
// current. The 2026-07 retrospective found front-loaded user-voice directives
|
|
5000
|
+
// were the one condition that reliably produced unprompted capture (~8/8 vs a
|
|
5001
|
+
// ~0-10% baseline), while the hook-injected preamble saying the same thing in
|
|
5002
|
+
// system-reminder voice underperformed — so the directive belongs in a
|
|
5003
|
+
// COMMITTED instructions file, where it reaches every contributor and every
|
|
5004
|
+
// dispatched agent. Default is directive-only: hooked hosts already receive
|
|
5005
|
+
// the briefing at session start, and a committed briefing snapshot stales;
|
|
5006
|
+
// --briefing restores the full hook-less floor (directive + briefing embed).
|
|
5007
|
+
async function cmdAgentsMd(cfg, { values }) {
|
|
5008
|
+
const root = repoRoot();
|
|
5009
|
+
const { writeAgentsBlock } = require(path.join(ROOT, "scripts", "engines", "agents-md.js"));
|
|
5010
|
+
const { file, meta } = await writeAgentsBlock({ cwd: root, briefing: !!values.briefing });
|
|
5011
|
+
out(`updated ${file} (${values.briefing ? meta || "no briefing yet, MCP pointers only" : "capture-discipline directive"})`);
|
|
5012
|
+
// CLAUDE.md rides along via an @AGENTS.md import (Claude Code resolves
|
|
5013
|
+
// @-imports): if the repo has a CLAUDE.md that never mentions AGENTS.md,
|
|
5014
|
+
// Claude Code sessions would miss the directive entirely. Append the import
|
|
5015
|
+
// once; never CREATE a CLAUDE.md (AGENTS.md alone is the portable surface).
|
|
5016
|
+
if (!values["no-claude-md"]) {
|
|
5017
|
+
const claudeMd = path.join(root, "CLAUDE.md");
|
|
5018
|
+
if (fs.existsSync(claudeMd)) {
|
|
5019
|
+
const txt = fs.readFileSync(claudeMd, "utf8");
|
|
5020
|
+
if (!/AGENTS\.md/.test(txt)) {
|
|
5021
|
+
fs.writeFileSync(claudeMd, txt + (txt.endsWith("\n") ? "" : "\n") + "\n@AGENTS.md\n");
|
|
5022
|
+
out(`updated ${claudeMd} (@AGENTS.md import appended)`);
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
out(" commit the file(s) so every contributor and dispatched agent inherits the directive");
|
|
5027
|
+
return 0;
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
// `spor upgrade` rider: the committed directive tracks the packaged wording —
|
|
5031
|
+
// refresh the current repo's managed block IF one exists, preserving whether
|
|
5032
|
+
// it embedded a briefing. A repo that never opted into the block is untouched.
|
|
5033
|
+
async function refreshAgentsBlockIfManaged(root = repoRoot()) {
|
|
5034
|
+
const file = path.join(root, "AGENTS.md");
|
|
5035
|
+
let existing = "";
|
|
5036
|
+
try {
|
|
5037
|
+
existing = fs.readFileSync(file, "utf8");
|
|
5038
|
+
} catch {
|
|
5039
|
+
return;
|
|
5040
|
+
}
|
|
5041
|
+
if (!existing.includes("<!-- spor:begin -->") && !existing.includes("<!-- substrate:begin -->")) return;
|
|
5042
|
+
const { writeAgentsBlock } = require(path.join(ROOT, "scripts", "engines", "agents-md.js"));
|
|
5043
|
+
await writeAgentsBlock({ cwd: root, briefing: /### Standing project briefing/.test(existing) });
|
|
5044
|
+
out(`refreshed ${file} (managed Spor block — commit if the wording changed)`);
|
|
5045
|
+
}
|
|
5046
|
+
|
|
4862
5047
|
// --- spor install / setup: wire spor into a host agent ---------------------
|
|
4863
5048
|
// dec-cc-portable-core-adapters ships a manifest per host under adapters/<host>/
|
|
4864
5049
|
// with a __SPOR_ROOT__ placeholder; installing one resolves the placeholder to
|
|
@@ -5519,6 +5704,10 @@ async function cmdUpgrade(cfg, { values, positionals: pos }) {
|
|
|
5519
5704
|
if (r !== 0) rc = r;
|
|
5520
5705
|
}
|
|
5521
5706
|
if (!dryRun) {
|
|
5707
|
+
// The committed AGENTS.md directive versions with the package — refresh
|
|
5708
|
+
// the current repo's managed block so wording changes actually ship
|
|
5709
|
+
// (task-spor-agents-md-capture-discipline-directive). No-op without one.
|
|
5710
|
+
await refreshAgentsBlockIfManaged();
|
|
5522
5711
|
out("");
|
|
5523
5712
|
out("Restart any running sessions so the refreshed hooks/plugin load.");
|
|
5524
5713
|
// The refresh above closes the loaded-vs-installed gap; this closes the
|
|
@@ -8276,6 +8465,38 @@ const COMMANDS = {
|
|
|
8276
8465
|
],
|
|
8277
8466
|
run: (cfg, args) => cmdChanges(cfg, args),
|
|
8278
8467
|
},
|
|
8468
|
+
check: {
|
|
8469
|
+
group: "Graph", parse: "raw", args: "[--staged|--range <a..b>|--files <f...>] [--strict] [--json]",
|
|
8470
|
+
summary: "coupling-drift report over a diff — triggers touched, targets not",
|
|
8471
|
+
help:
|
|
8472
|
+
"Check a change set against the graph's COUPLING NORMS (norm nodes carrying\n" +
|
|
8473
|
+
"couples_when/couples_also file globs — GRAPH.md \"coupling anchors\"): report\n" +
|
|
8474
|
+
"each norm whose trigger set is touched while its target set is not — the\n" +
|
|
8475
|
+
"boundary-time twin of the edit-time post-tool coupling nudge, sharing one\n" +
|
|
8476
|
+
"matcher (lib/kernel/coupling.js). A norm carrying a value invariant\n" +
|
|
8477
|
+
"(couples_value_a/b: <path>#<regex>) has its two extracted values compared:\n" +
|
|
8478
|
+
"\"these now disagree\" beats \"you probably forgot\", and an agreeing invariant\n" +
|
|
8479
|
+
"suppresses the untouched heuristic. Targets pinned to another repo are\n" +
|
|
8480
|
+
"surfaced as reminders, never failures (verify them there).\n" +
|
|
8481
|
+
"\n" +
|
|
8482
|
+
"The change set is always LOCAL git; the norms are mode-aware (local graph, or\n" +
|
|
8483
|
+
"the team graph via GET /v1/export in remote mode).\n" +
|
|
8484
|
+
"\n" +
|
|
8485
|
+
" --staged check the index only (the pre-commit view); default is\n" +
|
|
8486
|
+
" everything uncommitted vs HEAD plus untracked files\n" +
|
|
8487
|
+
" --range <a..b> check a commit range (CI; value invariants read the right side)\n" +
|
|
8488
|
+
" --files <f...> an explicit file list (paths, repo-relative or absolute)\n" +
|
|
8489
|
+
" --strict exit 1 when findings exist (CI / pre-commit enforcement)\n" +
|
|
8490
|
+
" --json machine-readable {project, changed, checked, findings}\n" +
|
|
8491
|
+
" --nodes <dir> read this local graph dir instead of the resolved home",
|
|
8492
|
+
examples: [
|
|
8493
|
+
"spor check",
|
|
8494
|
+
"spor check --staged --strict",
|
|
8495
|
+
"spor check --range origin/main..HEAD --strict",
|
|
8496
|
+
"spor check --files lib/seed/schema-task.md --json",
|
|
8497
|
+
],
|
|
8498
|
+
run: (cfg, args) => cmdCheck(cfg, args),
|
|
8499
|
+
},
|
|
8279
8500
|
export: {
|
|
8280
8501
|
group: "Graph", parse: "strict", args: "[--gzip] [--history|--auth] [--out <file>]",
|
|
8281
8502
|
summary: "the nodes/ tarball, or the --history bundle / --auth restore backup (GET /v1/export)",
|
|
@@ -8472,10 +8693,40 @@ const COMMANDS = {
|
|
|
8472
8693
|
run: (cfg) => cmdScope(false),
|
|
8473
8694
|
},
|
|
8474
8695
|
enable: {
|
|
8475
|
-
group: "Repo scoping", parse: "strict", args: "",
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8696
|
+
group: "Repo scoping", parse: "strict", args: "",
|
|
8697
|
+
options: { "no-agents": { type: "boolean", desc: "skip writing the AGENTS.md capture-discipline directive" } },
|
|
8698
|
+
summary: "opt this repo in (.spor.json + AGENTS.md directive)",
|
|
8699
|
+
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.",
|
|
8700
|
+
run: async (cfg, p) => {
|
|
8701
|
+
const rc = cmdScope(true);
|
|
8702
|
+
// Enabling is the moment this repo's work was decided to belong in the
|
|
8703
|
+
// graph — the standing directive rides along by default.
|
|
8704
|
+
if (rc === 0 && !p.values["no-agents"]) await cmdAgentsMd(cfg, { values: {} });
|
|
8705
|
+
return rc;
|
|
8706
|
+
},
|
|
8707
|
+
},
|
|
8708
|
+
"agents-md": {
|
|
8709
|
+
group: "Repo scoping", parse: "strict", args: "", aliases: ["agents"],
|
|
8710
|
+
summary: "write/refresh the committed AGENTS.md graph-upkeep directive",
|
|
8711
|
+
help:
|
|
8712
|
+
"Write (or idempotently refresh) the managed Spor block in AGENTS.md at the\n" +
|
|
8713
|
+
"repo root: standing user-voice instructions to keep the graph current —\n" +
|
|
8714
|
+
"capture discovered work when it appears, file issues before fixing, prefer\n" +
|
|
8715
|
+
"the graph over private auto-memory for durable facts, resolve with\n" +
|
|
8716
|
+
"artifacts, add Spor: commit trailers. Committed, it reaches every\n" +
|
|
8717
|
+
"contributor and dispatched agent; 'spor upgrade' refreshes the wording.\n" +
|
|
8718
|
+
"If a CLAUDE.md exists that never mentions AGENTS.md, an @AGENTS.md import\n" +
|
|
8719
|
+
"is appended so Claude Code sessions inherit the directive too.\n" +
|
|
8720
|
+
"By default the block carries the directive only (hooked hosts get their\n" +
|
|
8721
|
+
"briefing at session start); --briefing also embeds the standing project\n" +
|
|
8722
|
+
"briefing — the floor for hosts without hooks (same block 'spor-hook\n" +
|
|
8723
|
+
"agents-md' maintains from adapter session-start hooks).",
|
|
8724
|
+
options: {
|
|
8725
|
+
briefing: { type: "boolean", desc: "also embed the standing project briefing (hook-less floor)" },
|
|
8726
|
+
"no-claude-md": { type: "boolean", desc: "don't append the @AGENTS.md import to an existing CLAUDE.md" },
|
|
8727
|
+
},
|
|
8728
|
+
examples: ["spor agents-md", "spor agents-md --briefing"],
|
|
8729
|
+
run: (cfg, p) => cmdAgentsMd(cfg, p),
|
|
8479
8730
|
},
|
|
8480
8731
|
link: {
|
|
8481
8732
|
group: "Repo scoping", parse: "strict", args: "<slug>", options: {},
|
|
@@ -8854,7 +9105,7 @@ async function main() {
|
|
|
8854
9105
|
// Expose the pure helpers for unit tests (the version-check logic has no I/O),
|
|
8855
9106
|
// and only run the CLI when invoked directly — requiring this file must not
|
|
8856
9107
|
// kick off main() and call process.exit under the test runner.
|
|
8857
|
-
module.exports = { nodeFloor, nodeRuntimeCheck, verCmp, sporConnectorBound, COMMANDS, resolveVerb, getNodeJson, gitBlobSha };
|
|
9108
|
+
module.exports = { nodeFloor, nodeRuntimeCheck, verCmp, sporConnectorBound, COMMANDS, resolveVerb, getNodeJson, gitBlobSha, refreshAgentsBlockIfManaged };
|
|
8858
9109
|
|
|
8859
9110
|
if (require.main === module) {
|
|
8860
9111
|
main()
|