@sporhq/spor 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/API.md +4 -2
- package/bin/spor.js +124 -32
- package/package.json +2 -1
- package/scripts/engines/session-start.js +8 -1
- package/scripts/engines/util.js +38 -0
- package/skills/spor/SKILL.md +4 -1
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"name": "spor",
|
|
3
3
|
"displayName": "Spor Context Compiler",
|
|
4
4
|
"description": "Maintains a typed, versioned knowledge graph and compiles compact briefings from it: session-start injection, per-prompt relevance digests, capture at discovery, end-of-session distillation, decision queue.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.8.0",
|
|
6
6
|
"author": { "name": "losthammer" }
|
|
7
7
|
}
|
package/API.md
CHANGED
|
@@ -376,8 +376,10 @@ anything with a token.
|
|
|
376
376
|
agent-on-behalf-of-person (§1) — the `person → agent` chain is the audit
|
|
377
377
|
trail. `spor dispatch` mints one per run and injects it into the launched
|
|
378
378
|
background agent (so the agent's own graph writes carry its identity), picking
|
|
379
|
-
the machine's default agent from the `dispatch.agent` client config
|
|
380
|
-
|
|
379
|
+
the machine's default agent from the `dispatch.agent` client config (set with
|
|
380
|
+
`spor agent use <agent-id>`, or `SPOR_DISPATCH_AGENT`), which `spor dispatch
|
|
381
|
+
--as <agent-id>` overrides for a single run. (Not to be confused with `spor
|
|
382
|
+
dispatch --agent`, the unrelated `claude --agent` harness passthrough.)
|
|
381
383
|
- **OAuth 2.1 for MCP connectors** (Cowork/claude.ai, which cannot carry a
|
|
382
384
|
static bearer token): protected-resource metadata discovery (RFC 9728,
|
|
383
385
|
advertised on the `/mcp` 401 via `WWW-Authenticate`), authorization-server
|
package/bin/spor.js
CHANGED
|
@@ -977,10 +977,45 @@ async function cmdAgent(cfg, args) {
|
|
|
977
977
|
if (!sub || sub === "list") {
|
|
978
978
|
return cfg.mode() === "remote" ? cmdAgentListRemote(cfg) : cmdAgentListLocal(cfg);
|
|
979
979
|
}
|
|
980
|
-
|
|
980
|
+
if (sub === "use") {
|
|
981
|
+
return cmdAgentUse(cfg, { id: args[1] });
|
|
982
|
+
}
|
|
983
|
+
err("usage: spor agent create <label> [--owner person-x] [--pubkey <fp>] | spor agent list | spor agent use <agent-id>");
|
|
981
984
|
return 1;
|
|
982
985
|
}
|
|
983
986
|
|
|
987
|
+
// `spor agent use <agent-id>` — make this agent the machine's default dispatch
|
|
988
|
+
// identity by writing `dispatch.agent` to the USER config.json (the same
|
|
989
|
+
// machine-local, never-committed file as the repo map; per-machine, like
|
|
990
|
+
// dispatch.repos). This is the real setter the create/list hints point to;
|
|
991
|
+
// before it, dispatch.agent was settable only via env or by hand-editing the
|
|
992
|
+
// config. `spor agent use --clear` (or an empty id) drops back to person-scoped
|
|
993
|
+
// dispatch. Not a graph write — purely local config, so it works in both modes.
|
|
994
|
+
function cmdAgentUse(cfg, { id }) {
|
|
995
|
+
const clear = id === "--clear" || id === "none" || id === "";
|
|
996
|
+
if (!id) {
|
|
997
|
+
err("usage: spor agent use <agent-id> (or: spor agent use --clear)");
|
|
998
|
+
return 1;
|
|
999
|
+
}
|
|
1000
|
+
if (!clear && !/^[a-z0-9][a-z0-9-]*$/.test(id)) {
|
|
1001
|
+
err(`invalid agent id '${id}' — must match ^[a-z0-9][a-z0-9-]*$ (e.g. agent-your-machine)`);
|
|
1002
|
+
return 1;
|
|
1003
|
+
}
|
|
1004
|
+
const home = cfg.userConfigHome();
|
|
1005
|
+
const wrote = u.setDispatchAgent(home, clear ? null : id);
|
|
1006
|
+
if (clear) {
|
|
1007
|
+
out(wrote ? "cleared dispatch.agent — dispatches run person-scoped again" : "dispatch.agent was already unset");
|
|
1008
|
+
return 0;
|
|
1009
|
+
}
|
|
1010
|
+
if (wrote) {
|
|
1011
|
+
out(`dispatch.agent = ${id} (this machine now dispatches as ${id}; ${path.join(home, "config.json")})`);
|
|
1012
|
+
} else {
|
|
1013
|
+
out(`dispatch.agent already = ${id} (no change)`);
|
|
1014
|
+
}
|
|
1015
|
+
out(" attribution is remote-only; override one dispatch with: spor dispatch --as <agent-id>");
|
|
1016
|
+
return 0;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
984
1019
|
async function cmdAgentCreateRemote(cfg, { label, owner, pubkey }) {
|
|
985
1020
|
const body = { label };
|
|
986
1021
|
if (owner) body.owner = owner;
|
|
@@ -1009,7 +1044,8 @@ async function cmdAgentCreateRemote(cfg, { label, owner, pubkey }) {
|
|
|
1009
1044
|
const j = r.json || {};
|
|
1010
1045
|
out(`created agent ${j.id || `agent-${kebab(label)}`}${j.owner ? ` owned by ${j.owner}` : ""}`);
|
|
1011
1046
|
if (j.spiffe) out(` spiffe: ${j.spiffe}`);
|
|
1012
|
-
out(`
|
|
1047
|
+
out(` make it this machine's default: spor agent use ${j.id || `agent-${kebab(label)}`}`);
|
|
1048
|
+
out(` or dispatch as it once: spor dispatch --as ${j.id || `agent-${kebab(label)}`} …`);
|
|
1013
1049
|
return 0;
|
|
1014
1050
|
}
|
|
1015
1051
|
|
|
@@ -1080,7 +1116,8 @@ async function cmdAgentCreateLocal(cfg, { label, owner, pubkey }) {
|
|
|
1080
1116
|
fs.writeFileSync(path.join(nodesDir, `${id}.md`), md);
|
|
1081
1117
|
out(`created agent ${id} owned by ${ownerId}`);
|
|
1082
1118
|
out(` spiffe: ${spiffe}`);
|
|
1083
|
-
out(`
|
|
1119
|
+
out(` make it this machine's default: spor agent use ${id}`);
|
|
1120
|
+
out(` (note: agent-on-behalf-of attribution applies in remote mode)`);
|
|
1084
1121
|
return 0;
|
|
1085
1122
|
}
|
|
1086
1123
|
|
|
@@ -1633,7 +1670,10 @@ function detectHosts() {
|
|
|
1633
1670
|
// already ensured the claude CLI exists and the marketplace is registered.
|
|
1634
1671
|
function refreshClaudePlugin(cmd, cliScope, before) {
|
|
1635
1672
|
spawnSync(cmd, ["plugin", "marketplace", "update", "spor"], { encoding: "utf8" });
|
|
1636
|
-
|
|
1673
|
+
// Claude Code resolves an installed plugin by its name@marketplace id (the
|
|
1674
|
+
// install side uses 'spor@spor'); the bare 'spor' is unresolvable and fails
|
|
1675
|
+
// with "Plugin 'spor' not found" (issue-spor-upgrade-wrong-plugin-marketplace-id).
|
|
1676
|
+
const upd = spawnSync(cmd, ["plugin", "update", "spor@spor", "--scope", cliScope], { stdio: "inherit" });
|
|
1637
1677
|
if (upd.status !== 0) {
|
|
1638
1678
|
err(`claude plugin update failed (exit ${upd.status == null ? "?" : upd.status})`);
|
|
1639
1679
|
return 1;
|
|
@@ -1818,7 +1858,10 @@ function upgradeClaude(scope, dryRun) {
|
|
|
1818
1858
|
const cliScope = scope === "repo" ? "project" : "user";
|
|
1819
1859
|
const mpAdd = ["plugin", "marketplace", "add", ROOT];
|
|
1820
1860
|
const mpUpd = ["plugin", "marketplace", "update", "spor"];
|
|
1821
|
-
|
|
1861
|
+
// Plugin id is name@marketplace ('spor@spor'); the bare name doesn't resolve
|
|
1862
|
+
// (issue-spor-upgrade-wrong-plugin-marketplace-id). Keep this dry-run preview
|
|
1863
|
+
// in sync with the real call in refreshClaudePlugin().
|
|
1864
|
+
const plUpd = ["plugin", "update", "spor@spor", "--scope", cliScope];
|
|
1822
1865
|
if (dryRun) {
|
|
1823
1866
|
out(`would run: ${cmd} ${mpAdd.join(" ")}`);
|
|
1824
1867
|
out(`would run: ${cmd} ${mpUpd.join(" ")}`);
|
|
@@ -1967,22 +2010,45 @@ async function compileBriefing(cfg, { nodeId, query, full, project }) {
|
|
|
1967
2010
|
return (r.stdout || "").trim();
|
|
1968
2011
|
}
|
|
1969
2012
|
|
|
1970
|
-
// The
|
|
1971
|
-
// fail-soft (null on any
|
|
2013
|
+
// The highest-ranked open queue item for --from-queue — the first that ISN'T
|
|
2014
|
+
// already in flight on THIS machine. Mode-aware, fail-soft (null on any
|
|
2015
|
+
// error/empty). This used to take limit=1 blindly, but the queue's lease filter
|
|
2016
|
+
// is viewer-relative (lib/kernel/queue.js): a lease held by ANOTHER person is
|
|
2017
|
+
// dropped, yet the dispatcher's OWN in-progress claim is kept and floated up by
|
|
2018
|
+
// its `front` signal — so the top item was frequently the caller's own active
|
|
2019
|
+
// work, which the same-machine guard then refused instead of advancing
|
|
2020
|
+
// (task-spor-dispatch-from-queue-skip-in-flight). So pull a page and skip items
|
|
2021
|
+
// with a background agent already running here — dispatchedAgents()/
|
|
2022
|
+
// annotateInFlight, the same NO-LLM, fail-soft cross-reference the same-machine
|
|
2023
|
+
// guard and `spor next --hide-dispatched` use — returning the first not-in-flight
|
|
2024
|
+
// item. If EVERY candidate is in flight, fall back to the top one so the caller's
|
|
2025
|
+
// guard reports it (rather than a misleading "queue empty"). A page (not just the
|
|
2026
|
+
// top) is fetched in BOTH modes; with no agents in flight free[0] is still the
|
|
2027
|
+
// top item, so the prior single-pick behavior is preserved.
|
|
1972
2028
|
async function topQueueItem(cfg, slug) {
|
|
2029
|
+
const LIMIT = 25;
|
|
2030
|
+
let items = [];
|
|
1973
2031
|
if (cfg.mode() === "remote") {
|
|
1974
|
-
const q = slug ? `?project=${encodeURIComponent(slug)}&limit
|
|
2032
|
+
const q = slug ? `?project=${encodeURIComponent(slug)}&limit=${LIMIT}` : `?limit=${LIMIT}`;
|
|
1975
2033
|
const r = await remote.get(cfg, `/v1/queue${q}`, { timeoutMs: 6000 });
|
|
1976
|
-
|
|
2034
|
+
items = r.ok && r.json ? r.json.items || [] : [];
|
|
2035
|
+
} else {
|
|
2036
|
+
try {
|
|
2037
|
+
const g = require(path.join(ROOT, "lib", "graph.js")).loadGraph(cfg.nodesDir());
|
|
2038
|
+
const { rankQueue } = require(path.join(ROOT, "lib", "queue.js"));
|
|
2039
|
+
const r = rankQueue(g, slug ? { project: slug, limit: LIMIT } : { limit: LIMIT });
|
|
2040
|
+
items = r.items || [];
|
|
2041
|
+
} catch {
|
|
2042
|
+
items = [];
|
|
2043
|
+
}
|
|
1977
2044
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
} catch {
|
|
1984
|
-
return null;
|
|
2045
|
+
if (!items.length) return null;
|
|
2046
|
+
// Skip items already in flight on this machine; advance to the first free one.
|
|
2047
|
+
const { items: free, hidden } = annotateInFlight(items, dispatchedAgents(), true);
|
|
2048
|
+
if (hidden && free.length) {
|
|
2049
|
+
err(`from-queue: skipped ${hidden} item(s) already in flight on this machine; picking ${free[0].id}`);
|
|
1985
2050
|
}
|
|
2051
|
+
return free[0] || items[0] || null;
|
|
1986
2052
|
}
|
|
1987
2053
|
|
|
1988
2054
|
// Auto-claim a dispatched node so its lease is established at dispatch time
|
|
@@ -2207,7 +2273,8 @@ async function cmdDispatch(cfg, { values, positionals: pos }) {
|
|
|
2207
2273
|
const dirOpt = values.dir || null;
|
|
2208
2274
|
const model = values.model || null;
|
|
2209
2275
|
const permMode = values["permission-mode"] || null;
|
|
2210
|
-
const agent = values.agent || null;
|
|
2276
|
+
const agent = values.agent || null; // claude --agent (harness agent DEFINITION)
|
|
2277
|
+
const asAgent = values.as || null; // Spor agent IDENTITY override for dispatch.agent
|
|
2211
2278
|
// A user-supplied prompt template (task-spor-dispatch-user-prompt-templates):
|
|
2212
2279
|
// --template wins, else a personal default in the config cascade
|
|
2213
2280
|
// (dispatch.template — an absolute path, like dispatch.repos). Empty until we
|
|
@@ -2335,10 +2402,22 @@ async function cmdDispatch(cfg, { values, positionals: pos }) {
|
|
|
2335
2402
|
// claim can be session-bound at dispatch (the bg agent's post-tool heartbeat
|
|
2336
2403
|
// renews under this same id). SPOR_SESSION_ID pins it for tests/reproducibility.
|
|
2337
2404
|
const session = process.env.SPOR_SESSION_ID || crypto.randomUUID();
|
|
2338
|
-
// This machine's agent node — the WHO a dispatched session runs as.
|
|
2339
|
-
//
|
|
2340
|
-
//
|
|
2341
|
-
|
|
2405
|
+
// This machine's agent node — the WHO a dispatched session runs as. `--as`
|
|
2406
|
+
// overrides the per-machine dispatch.agent default for this one dispatch (its
|
|
2407
|
+
// id format mirrors a node id; a bad/unowned one fails soft at token-mint
|
|
2408
|
+
// below, like the config default). Only meaningful remotely (the server is the
|
|
2409
|
+
// CA that mints the agent token); a local-mode dispatch or an unconfigured
|
|
2410
|
+
// machine simply runs person-scoped.
|
|
2411
|
+
if (asAgent && !/^[a-z0-9][a-z0-9-]*$/.test(asAgent)) {
|
|
2412
|
+
err(`invalid --as agent id '${asAgent}' — must match ^[a-z0-9][a-z0-9-]*$ (e.g. agent-your-machine)`);
|
|
2413
|
+
return 1;
|
|
2414
|
+
}
|
|
2415
|
+
const identityAgent = cfg.mode() === "remote" ? (asAgent || dispatchAgentId(cfg)) : null;
|
|
2416
|
+
// An explicit --as can't take effect in local mode — there is no CA to mint the
|
|
2417
|
+
// agent token. Say so rather than silently dropping it to person-scoped.
|
|
2418
|
+
if (asAgent && cfg.mode() !== "remote") {
|
|
2419
|
+
err(`note: --as ${asAgent} ignored in local mode — agent-on-behalf-of attribution is remote-only`);
|
|
2420
|
+
}
|
|
2342
2421
|
|
|
2343
2422
|
const claudeBin = claudeCmd();
|
|
2344
2423
|
const claudeArgs = ["--bg"];
|
|
@@ -2367,9 +2446,10 @@ async function cmdDispatch(cfg, { values, positionals: pos }) {
|
|
|
2367
2446
|
// unconfigured machine read "person-scoped" — byte-stable but for the new
|
|
2368
2447
|
// session line, which is additive and always present now.
|
|
2369
2448
|
if (identityAgent) {
|
|
2370
|
-
|
|
2449
|
+
const src = asAgent ? " (via --as)" : "";
|
|
2450
|
+
out(`agent: ${identityAgent}${src} (would mint a per-session agent-scoped token + write a 0600 --mcp-config, then add --strict-mcp-config)`);
|
|
2371
2451
|
} else if (cfg.mode() === "remote") {
|
|
2372
|
-
out(`agent: (none configured —
|
|
2452
|
+
out(`agent: (none configured — 'spor agent use agent-<machine>' or --as to attribute as agent-on-behalf-of; dispatching person-scoped)`);
|
|
2373
2453
|
}
|
|
2374
2454
|
// Same-machine guard preview (node mode, any mode): a real dispatch would
|
|
2375
2455
|
// refuse if an agent with this name is already in flight here. Shown only on a
|
|
@@ -2642,7 +2722,7 @@ const COMMANDS = {
|
|
|
2642
2722
|
run: (cfg, args) => cmdToken(cfg, args),
|
|
2643
2723
|
},
|
|
2644
2724
|
agent: {
|
|
2645
|
-
group: "Team admin (remote, admin token)", parse: "raw", args: "create <label> [--owner <id>] [--pubkey <fp>] | list",
|
|
2725
|
+
group: "Team admin (remote, admin token)", parse: "raw", args: "create <label> [--owner <id>] [--pubkey <fp>] | list | use <agent-id>",
|
|
2646
2726
|
summary: "person-owned automation principals (dispatch identity)",
|
|
2647
2727
|
help:
|
|
2648
2728
|
"Create and list agents — first-class `type: agent` nodes owned by a person\n" +
|
|
@@ -2654,10 +2734,15 @@ const COMMANDS = {
|
|
|
2654
2734
|
" defaults to the sole person node, else required)\n" +
|
|
2655
2735
|
" --pubkey <fingerprint> record a public-key fingerprint (forward-compat,\n" +
|
|
2656
2736
|
" unenforced — may be omitted)\n" +
|
|
2657
|
-
" spor agent list list agents and their owners\n
|
|
2658
|
-
"
|
|
2659
|
-
"
|
|
2660
|
-
|
|
2737
|
+
" spor agent list list agents and their owners\n" +
|
|
2738
|
+
" spor agent use <agent-id> make it THIS machine's default dispatch identity\n" +
|
|
2739
|
+
" (writes dispatch.agent to your user config; pass\n" +
|
|
2740
|
+
" --clear to go back to person-scoped dispatch)\n\n" +
|
|
2741
|
+
"'use' is a local config write, not a graph write — it sets which agent\n" +
|
|
2742
|
+
"`spor dispatch` runs as by default (override one dispatch with 'dispatch --as').\n" +
|
|
2743
|
+
"Create/list run remote (POST /v1/admin/agents, admin-gated, the server mints the\n" +
|
|
2744
|
+
"spiffe); local mode writes the node + owned-by edge to the graph home.",
|
|
2745
|
+
examples: ["spor agent create anthony-laptop", "spor agent use agent-anthony-laptop", "spor agent list"],
|
|
2661
2746
|
run: (cfg, args) => cmdAgent(cfg, args),
|
|
2662
2747
|
},
|
|
2663
2748
|
|
|
@@ -2813,7 +2898,8 @@ const COMMANDS = {
|
|
|
2813
2898
|
help:
|
|
2814
2899
|
"Compile a briefing for a task and launch a Claude Code background agent in the\n" +
|
|
2815
2900
|
"right repo. Give free-text, a <node-id>, --node <id>, --from-queue (the top\n" +
|
|
2816
|
-
"ranked item), or --backfill (onboard
|
|
2901
|
+
"ranked item NOT already in flight on this machine), or --backfill (onboard/\n" +
|
|
2902
|
+
"repair a repo). The target dir is the\n" +
|
|
2817
2903
|
"slug->path map ('spor repos'), overridable with --dir.\n\n" +
|
|
2818
2904
|
"In remote mode a node dispatch auto-claims the task — it establishes the\n" +
|
|
2819
2905
|
"heartbeat lease at dispatch time, so concurrent dispatch of the same node is\n" +
|
|
@@ -2822,21 +2908,27 @@ const COMMANDS = {
|
|
|
2822
2908
|
"on THIS machine (each agent is named after its node id) — catches the\n" +
|
|
2823
2909
|
"same-person duplicate the lease's idempotent renew can't. --force overrides.\n\n" +
|
|
2824
2910
|
"--template supplies your own prompt with {{brief}}/{{task}}/{{node}}/{{title}}/\n" +
|
|
2825
|
-
"{{slug}}/{{dir}}/{{default}} placeholders
|
|
2911
|
+
"{{slug}}/{{dir}}/{{default}} placeholders.\n\n" +
|
|
2912
|
+
"Two different 'agent' axes, don't confuse them: --as picks the Spor agent\n" +
|
|
2913
|
+
"IDENTITY the dispatch runs AS (attribution 'agent on behalf of person',\n" +
|
|
2914
|
+
"remote-only; defaults to dispatch.agent — set it with 'spor agent use <id>').\n" +
|
|
2915
|
+
"--agent is the unrelated 'claude --agent' passthrough that picks the harness\n" +
|
|
2916
|
+
"agent DEFINITION (subagent personality/toolset) the background session runs.",
|
|
2826
2917
|
options: {
|
|
2827
2918
|
dir: { type: "string", value: "path", desc: "launch directory (overrides the slug map)" },
|
|
2828
2919
|
node: { type: "string", value: "id", desc: "dispatch a specific node id" },
|
|
2829
2920
|
slug: { type: "string", value: "slug", desc: "target project slug (cross-repo resolution)" },
|
|
2921
|
+
as: { type: "string", value: "agent-id", desc: "Spor agent IDENTITY to run as (overrides dispatch.agent; remote-only)" },
|
|
2830
2922
|
model: { type: "string", value: "M", desc: "claude --model" },
|
|
2831
2923
|
"permission-mode": { type: "string", value: "P", desc: "claude --permission-mode" },
|
|
2832
|
-
agent: { type: "string", value: "A", desc: "claude --agent" },
|
|
2924
|
+
agent: { type: "string", value: "A", desc: "claude --agent (harness agent DEFINITION — NOT the Spor identity; see --as)" },
|
|
2833
2925
|
name: { type: "string", value: "N", desc: "claude --name (session name)" },
|
|
2834
2926
|
template: { type: "string", value: "F", desc: "prompt template file (placeholders above)" },
|
|
2835
2927
|
full: { type: "boolean", desc: "full briefing instead of the digest" },
|
|
2836
2928
|
"no-brief": { type: "boolean", desc: "raw task prompt, no briefing block" },
|
|
2837
2929
|
"no-claim": { type: "boolean", desc: "don't auto-claim the lease (remote node dispatch)" },
|
|
2838
2930
|
force: { type: "boolean", desc: "dispatch even if an agent for this node is already in flight here" },
|
|
2839
|
-
"from-queue": { type: "boolean", desc: "dispatch the top-ranked queue item" },
|
|
2931
|
+
"from-queue": { type: "boolean", desc: "dispatch the top-ranked queue item not already in flight here" },
|
|
2840
2932
|
backfill: { type: "boolean", desc: "onboard/repair this repo (runs /spor:backfill)" },
|
|
2841
2933
|
print: { type: "boolean", desc: "dry run — print the prompt, launch nothing" },
|
|
2842
2934
|
"dry-run": DRYRUN_OPT,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sporhq/spor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Spor — a shared memory substrate for teams and agents. Decisions, their reasons, and the traces they leave. Knowledge-graph context compiler: session-start briefings, per-prompt digests, capture at discovery, end-of-session distillation, decision queue.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Anthony Allen",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
],
|
|
47
47
|
"scripts": {
|
|
48
48
|
"test": "node --test test/*.test.js",
|
|
49
|
+
"test:e2e": "node --test test/e2e-claude.test.js",
|
|
49
50
|
"conformance": "node conformance/runner.js"
|
|
50
51
|
},
|
|
51
52
|
"engines": {
|
|
@@ -9,8 +9,15 @@ const fs = require("fs");
|
|
|
9
9
|
const path = require("path");
|
|
10
10
|
const u = require("./util");
|
|
11
11
|
|
|
12
|
+
// The /spor:spor preload instruction LEADS every usage string (and so every
|
|
13
|
+
// session-start envelope, all modes): Spor's CLI syntax, node/edge format, and
|
|
14
|
+
// MCP/REST surface aren't in the model's training, so any graph operation
|
|
15
|
+
// attempted without first loading the skill tends to invent syntax. Stating it
|
|
16
|
+
// as a standing precondition here is cheaper than the agent rediscovering it
|
|
17
|
+
// per session. Phrased as "before any operation on the graph" to match the
|
|
18
|
+
// skill's own trigger.
|
|
12
19
|
const USAGE =
|
|
13
|
-
"Use /spor:brief <query or node-id> for a task-specific briefing, /spor:correct to fix a bad briefing.";
|
|
20
|
+
"Before any operation on the Spor graph (searching/querying, reading or writing nodes, adding edges, capturing, or running spor CLI/MCP tools), load the /spor:spor skill first — it carries the CLI syntax, node/edge format, and tool surface your training doesn't cover. Use /spor:brief <query or node-id> for a task-specific briefing, /spor:correct to fix a bad briefing.";
|
|
14
21
|
const USAGE_REMOTE =
|
|
15
22
|
USAGE +
|
|
16
23
|
" When you defer discovered work mid-task (out-of-scope fix, follow-up, dismissed approach), capture it the moment you defer it: /spor:defer <2-3 sentences, what + why> — one call, the server types and links it.";
|
package/scripts/engines/util.js
CHANGED
|
@@ -490,6 +490,43 @@ function forgetRepo(graphHomeDir, slug) {
|
|
|
490
490
|
});
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
+
// Record this machine's default dispatch identity (`dispatch.agent`) into the
|
|
494
|
+
// SAME user config.json as the repo map — the per-machine key `spor dispatch`
|
|
495
|
+
// reads to attribute a dispatched session "agent on behalf of person". Scalar
|
|
496
|
+
// sibling of registerRepo: agentId is an `agent-...` node id, or null/"" to
|
|
497
|
+
// clear. Returns true only when it actually wrote; refuses to clobber a
|
|
498
|
+
// present-but-malformed config (same fail-safe as editRepoMap).
|
|
499
|
+
function setDispatchAgent(graphHomeDir, agentId) {
|
|
500
|
+
try {
|
|
501
|
+
const file = userConfigPath(graphHomeDir);
|
|
502
|
+
let raw = null;
|
|
503
|
+
try {
|
|
504
|
+
raw = fs.readFileSync(file, "utf8");
|
|
505
|
+
} catch {
|
|
506
|
+
raw = null; // absent — start fresh
|
|
507
|
+
}
|
|
508
|
+
let data = {};
|
|
509
|
+
if (raw != null) {
|
|
510
|
+
try {
|
|
511
|
+
data = JSON.parse(raw);
|
|
512
|
+
} catch {
|
|
513
|
+
return false; // malformed — do NOT overwrite
|
|
514
|
+
}
|
|
515
|
+
if (data == null || typeof data !== "object" || Array.isArray(data)) data = {};
|
|
516
|
+
}
|
|
517
|
+
if (data.dispatch == null || typeof data.dispatch !== "object" || Array.isArray(data.dispatch)) data.dispatch = {};
|
|
518
|
+
const next = agentId || null;
|
|
519
|
+
if ((data.dispatch.agent || null) === next) return false; // unchanged — skip the write
|
|
520
|
+
if (next == null) delete data.dispatch.agent;
|
|
521
|
+
else data.dispatch.agent = next;
|
|
522
|
+
if (!ensureDir(graphHomeDir)) return false;
|
|
523
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
|
|
524
|
+
return true;
|
|
525
|
+
} catch {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
493
530
|
function appendLine(file, line) {
|
|
494
531
|
try {
|
|
495
532
|
fs.appendFileSync(file, line + "\n");
|
|
@@ -780,6 +817,7 @@ module.exports = {
|
|
|
780
817
|
ensureGraphGitignore,
|
|
781
818
|
registerRepo,
|
|
782
819
|
forgetRepo,
|
|
820
|
+
setDispatchAgent,
|
|
783
821
|
appendLine,
|
|
784
822
|
makeLogger,
|
|
785
823
|
loadGraphCached,
|
package/skills/spor/SKILL.md
CHANGED
|
@@ -86,9 +86,12 @@ spor add "<2-3 sentences>" # capture a node (typed file locally; /v1/capture
|
|
|
86
86
|
# remote (team server) only
|
|
87
87
|
spor lens [<id>] # list saved views, or render one
|
|
88
88
|
spor agent create <label> # create one of your agents — a person-owned principal (`spor agent list`)
|
|
89
|
+
spor agent use <agent-id> # make it THIS machine's default dispatch identity (writes dispatch.agent)
|
|
89
90
|
spor dispatch <id>|"<task>" # run work as a background agent; in team mode its graph writes are
|
|
90
91
|
# attributed "agent on behalf of you" (the machine's default agent =
|
|
91
|
-
# dispatch.agent
|
|
92
|
+
# dispatch.agent, set by `spor agent use`; --as <agent-id> overrides it
|
|
93
|
+
# per dispatch; --agent is the unrelated `claude --agent` passthrough).
|
|
94
|
+
# See API.md §3-§4.
|
|
92
95
|
|
|
93
96
|
# dual-mode (local passthrough / remote dispatch to the server)
|
|
94
97
|
spor compile --query "<text>" # search → compiled neighborhood (--digest for compact)
|