@sporhq/spor 0.18.0 → 0.18.1
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 +1 -1
- package/bin/spor.js +213 -0
- package/package.json +1 -1
- package/skills/spor/SKILL.md +10 -0
- package/skills/triage/SKILL.md +42 -6
|
@@ -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.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "losthammer"
|
|
8
8
|
}
|
package/API.md
CHANGED
|
@@ -402,7 +402,7 @@ endpoint is the REST twin of a core call:
|
|
|
402
402
|
| `GET /v1/nodes/{id}` | /spor:brief | `get_node` semantics; the node's active schema may attach read-time enrichment via a `get(node, ctx)` hook (GRAPH.md) — the seed `question`/`issue`/`task`/`incident` schemas attach `resolution`: a live inbound resolves/answers edge carrying the resolver's `summary`/`title` and a `lagging` flag (set when it contradicts a still-open status, clear when the node is already terminal, e.g. an answered question pointing at its answer). Open gardener findings about the node ride along as `open_findings`, and a node marked stale by an inbound supersedes edge as `superseded_by`. All enrichment is additive top-level keys; ignore unknown ones |
|
|
403
403
|
| `GET /v1/nodes/{id}/history?limit=N` | `spor history <id>`, the `node_history` MCP tool | per-node commit lineage — a `git log` projection over `nodes/{id}.md` → `{id, head, count, history: [{sha, short, actor, actor_name, actor_email, date, message, internal, person}]}`, newest first. Each revision is labeled `internal:true` for a server-internal write (boot reconcile / migration, `server@spor.invalid`) vs. a real actor, and mapped to its `person` node by author email. Deliberately NOT `git log --follow` (node files share heavy frontmatter boilerplate, so similarity-based rename detection crosses node boundaries — dec-spor-node-history-git-log-projection). `limit` defaults to 50, max 200. The frontmatter `author` re-stamps to the LAST editor on every write, so this is the only durable record of the full chain of editors. A node with no commit history (unknown id) is `404`; a bad id is `422`. The `spor history <id>` CLI verb is the shell front-door (remote reads this; local mode runs the same projection over the graph home) |
|
|
404
404
|
| `GET /v1/nodes/{id}/history/{sha}` | `spor history <id> <sha>`, `node_history` (sha mode) | one revision's detail, the expensive half gated behind an explicit per-sha fetch → the history record for that commit plus `{change, patch, content}`: the change type (`A`/`M`/`D`/`R`), the patch this commit introduced to the node file, and the full node content at that revision (`null` when the commit deleted it). The `sha` must be one from the node's own history — a sha that didn't touch the node, or an unresolvable sha, is `404`; a malformed sha is `422` |
|
|
405
|
-
| `POST /v1/nodes` | drain-outbox, mechanical writers | `put_node` semantics, batch: `{nodes: [...], if_exists: "skip"}` (entries may be raw strings or `{node, if_exists, revision}`) → `{results: [...]}`, 207 when any entry failed. Entries are applied **sequentially** and each is fully validated before the next — including the completion-resolver gate that runs on create (GRAPH.md "the resolver gate") — so a born-terminal node (`done` task / `resolved` issue) must have its resolving `decision`/`artifact` EARLIER in the same batch (**resolver-first ordering**; the batch does not defer the gate to end-of-batch, dec-spor-batch-create-gate-resolver-first-ordering). The 207 is partial-success: entries already applied before a later entry's failure are not rolled back |
|
|
405
|
+
| `POST /v1/nodes` | `spor put-node`, drain-outbox, mechanical writers | `put_node` semantics, batch: `{nodes: [...], if_exists: "skip"}` (entries may be raw strings or `{node, if_exists, revision}`) → `{results: [...]}`, 207 when any entry failed. Entries are applied **sequentially** and each is fully validated before the next — including the completion-resolver gate that runs on create (GRAPH.md "the resolver gate") — so a born-terminal node (`done` task / `resolved` issue) must have its resolving `decision`/`artifact` EARLIER in the same batch (**resolver-first ordering**; the batch does not defer the gate to end-of-batch, dec-spor-batch-create-gate-resolver-first-ordering). The 207 is partial-success: entries already applied before a later entry's failure are not rolled back |
|
|
406
406
|
| `POST /v1/nodes/{id}/edges` `{type, to, attrs?}` | scripts, mechanical writers | `add_edge` semantics (§1): normalize/flip, dedupe, append — no revision echo. Optional `attrs` adds trailing flat edge attributes (e.g. a per-assignment `profile:` override); re-adding the same edge with different attrs upserts the set. Adding a review-outcome edge (`reviewed-by`/`changes-requested-by`/`review-requested`) flips a sibling review edge to the same person in place — the one-call submit-review primitive |
|
|
407
407
|
| `DELETE /v1/nodes/{id}/edges` `{type, to}` | scripts, mechanical writers | `remove_edge` semantics (§1): the withdrawal twin of the POST above — drop one typed edge by `{type, to}`, normalize/flip exactly as `add_edge` (an inverse form removes the canonical edge on the *other* node and echoes its id), no revision echo. A missing edge is an idempotent `skipped`. For *withdrawing* a relationship the review flip can't express — a pulled review request, a dismissed review |
|
|
408
408
|
| `POST /v1/nodes/{id}/status` `{status}` | scripts, mechanical writers | `set_status` semantics (§1): one-scalar update through the `transitions()` gate. Setting a work node to an in-progress status also CLAIMS it (same lease as `/claim` below) |
|
package/bin/spor.js
CHANGED
|
@@ -747,6 +747,191 @@ function gitBlobSha(buf) {
|
|
|
747
747
|
return h.digest("hex");
|
|
748
748
|
}
|
|
749
749
|
|
|
750
|
+
// --- spor put-node: full validated node writes -----------------------------
|
|
751
|
+
// The shell twin of MCP put_node / REST POST /v1/nodes: write a complete node
|
|
752
|
+
// markdown file (frontmatter + body) through the same create/update collision
|
|
753
|
+
// policy instead of dropping to raw REST from scripts and skills.
|
|
754
|
+
function readPutNodeInput(input) {
|
|
755
|
+
if (!input || input === "-") {
|
|
756
|
+
try {
|
|
757
|
+
return { raw: fs.readFileSync(0, "utf8"), label: "stdin" };
|
|
758
|
+
} catch (e) {
|
|
759
|
+
return { error: `could not read stdin: ${e.message}` };
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
return { raw: fs.readFileSync(input, "utf8"), label: input };
|
|
764
|
+
} catch (e) {
|
|
765
|
+
return { error: `could not read ${input}: ${e.message}` };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function parsePutNode(raw, label) {
|
|
770
|
+
const graphLib = require(path.join(ROOT, "lib", "graph.js"));
|
|
771
|
+
let first;
|
|
772
|
+
try {
|
|
773
|
+
first = graphLib.parseFrontmatter(raw, label || "incoming.md");
|
|
774
|
+
} catch (e) {
|
|
775
|
+
return { error: `invalid node: ${e.message}` };
|
|
776
|
+
}
|
|
777
|
+
if (!first.id) return { error: "invalid node: missing id" };
|
|
778
|
+
if (!NODE_ID_RE.test(first.id)) return { error: `bad node id '${first.id}' — expected kebab-case` };
|
|
779
|
+
try {
|
|
780
|
+
return { node: graphLib.parseFrontmatter(raw, `${first.id}.md`) };
|
|
781
|
+
} catch (e) {
|
|
782
|
+
return { error: `invalid node: ${e.message}` };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function normalizeIfExists(raw) {
|
|
787
|
+
const value = raw == null ? "error" : String(raw).trim().toLowerCase();
|
|
788
|
+
if (["error", "skip", "update"].includes(value)) return { ok: true, value };
|
|
789
|
+
return { ok: false, error: `--if-exists must be one of: error, skip, update` };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function renderPutNodeResult(res, json) {
|
|
793
|
+
if (json) {
|
|
794
|
+
out(JSON.stringify(res || {}, null, 2));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const status = (res && res.status) || "ok";
|
|
798
|
+
const id = res && res.id ? res.id : "(unknown)";
|
|
799
|
+
const rev = res && res.revision ? ` @ ${res.revision}` : "";
|
|
800
|
+
out(status === "skipped" ? `put-node skipped: ${id}${rev}` : `put-node ${status}: ${id}${rev}`);
|
|
801
|
+
for (const w of (res && res.warnings) || []) err(` warning: ${w}`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function putNodeEntryError(res0, httpStatus, prefix = "put-node") {
|
|
805
|
+
const parts = [];
|
|
806
|
+
if (res0 && res0.message) parts.push(res0.message);
|
|
807
|
+
if (res0 && res0.code && !parts.includes(res0.code)) parts.push(res0.code);
|
|
808
|
+
if (res0 && Array.isArray(res0.details)) parts.push(...res0.details);
|
|
809
|
+
if (res0 && res0.revision) parts.push(`current revision: ${res0.revision}`);
|
|
810
|
+
return `${prefix} error ${httpStatus}${parts.length ? `: ${parts.join("; ")}` : ""}`;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function copyNodeFilesForValidation(srcNodes, dstNodes, targetId, raw) {
|
|
814
|
+
fs.mkdirSync(dstNodes, { recursive: true });
|
|
815
|
+
for (const ent of fs.readdirSync(srcNodes, { withFileTypes: true })) {
|
|
816
|
+
if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
|
|
817
|
+
if (ent.name === `${targetId}.md`) continue;
|
|
818
|
+
fs.copyFileSync(path.join(srcNodes, ent.name), path.join(dstNodes, ent.name));
|
|
819
|
+
}
|
|
820
|
+
fs.writeFileSync(path.join(dstNodes, `${targetId}.md`), raw);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function validatePutNodeLocal(nodesDir, node, raw) {
|
|
824
|
+
const graphLib = require(path.join(ROOT, "lib", "graph.js"));
|
|
825
|
+
let g;
|
|
826
|
+
try {
|
|
827
|
+
g = graphLib.loadGraph(nodesDir);
|
|
828
|
+
} catch (e) {
|
|
829
|
+
return { error: `could not load graph: ${e.message}` };
|
|
830
|
+
}
|
|
831
|
+
const v = graphLib.validateNode(g, node);
|
|
832
|
+
if (!v.ok) return { error: `invalid node:\n ${v.errors.join("\n ")}` };
|
|
833
|
+
|
|
834
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "spor-put-node-"));
|
|
835
|
+
const tmpNodes = path.join(tmp, "nodes");
|
|
836
|
+
try {
|
|
837
|
+
copyNodeFilesForValidation(nodesDir, tmpNodes, node.id, raw);
|
|
838
|
+
const vg = graphLib.validateGraph(tmpNodes);
|
|
839
|
+
if (vg.errors && vg.errors.length) return { error: `invalid graph after put-node:\n ${vg.errors.join("\n ")}` };
|
|
840
|
+
return { ok: true, warnings: vg.warnings || [] };
|
|
841
|
+
} finally {
|
|
842
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async function cmdPutNode(cfg, { values, positionals }) {
|
|
847
|
+
const input = positionals[0] || "-";
|
|
848
|
+
const ifExists = normalizeIfExists(values["if-exists"]);
|
|
849
|
+
if (!ifExists.ok) {
|
|
850
|
+
err(ifExists.error);
|
|
851
|
+
return 1;
|
|
852
|
+
}
|
|
853
|
+
const policy = ifExists.value;
|
|
854
|
+
const revision = values.revision || null;
|
|
855
|
+
if (policy === "update" && !revision) {
|
|
856
|
+
err("put-node update requires --revision from 'spor get <id> --json'");
|
|
857
|
+
return 1;
|
|
858
|
+
}
|
|
859
|
+
if (policy !== "update" && revision) {
|
|
860
|
+
err("--revision is only valid with --if-exists update");
|
|
861
|
+
return 1;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const inputRes = readPutNodeInput(input);
|
|
865
|
+
if (inputRes.error) {
|
|
866
|
+
err(inputRes.error);
|
|
867
|
+
return 1;
|
|
868
|
+
}
|
|
869
|
+
const raw = inputRes.raw;
|
|
870
|
+
const parsed = parsePutNode(raw, inputRes.label);
|
|
871
|
+
if (parsed.error) {
|
|
872
|
+
err(parsed.error);
|
|
873
|
+
return 1;
|
|
874
|
+
}
|
|
875
|
+
const id = parsed.node.id;
|
|
876
|
+
|
|
877
|
+
if (cfg.mode() === "remote") {
|
|
878
|
+
const entry = { node: raw, if_exists: policy };
|
|
879
|
+
if (revision) entry.revision = revision;
|
|
880
|
+
const r = await remote.post(cfg, "/v1/nodes", { nodes: [entry] }, { timeoutMs: 15000 });
|
|
881
|
+
if (r.transport) {
|
|
882
|
+
err(`offline — could not reach server (${r.error})`);
|
|
883
|
+
return 1;
|
|
884
|
+
}
|
|
885
|
+
const res0 = r.json && r.json.results && r.json.results[0];
|
|
886
|
+
if (!(res0 && res0.ok)) {
|
|
887
|
+
const top = r.json && r.json.error;
|
|
888
|
+
if (top && !res0) err(`put-node error ${r.status}${top.message ? `: ${top.message}` : ""}`);
|
|
889
|
+
else err(putNodeEntryError(res0, r.status));
|
|
890
|
+
return 1;
|
|
891
|
+
}
|
|
892
|
+
renderPutNodeResult(res0, !!values.json);
|
|
893
|
+
return 0;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const nodesDir = cfg.nodesDir();
|
|
897
|
+
if (!fs.existsSync(nodesDir)) {
|
|
898
|
+
err(`no graph at ${nodesDir} — run 'spor init' first`);
|
|
899
|
+
return 1;
|
|
900
|
+
}
|
|
901
|
+
const file = path.join(nodesDir, `${id}.md`);
|
|
902
|
+
const exists = fs.existsSync(file);
|
|
903
|
+
if (policy === "error" && exists) {
|
|
904
|
+
err(`node already exists: ${id} (use --if-exists update with --revision, or --if-exists skip)`);
|
|
905
|
+
return 1;
|
|
906
|
+
}
|
|
907
|
+
if (policy === "skip" && exists) {
|
|
908
|
+
const res = { ok: true, status: "skipped", id, revision: gitBlobSha(fs.readFileSync(file)), warnings: [] };
|
|
909
|
+
renderPutNodeResult(res, !!values.json);
|
|
910
|
+
return 0;
|
|
911
|
+
}
|
|
912
|
+
if (policy === "update") {
|
|
913
|
+
if (!exists) {
|
|
914
|
+
err(`no such node: ${id}`);
|
|
915
|
+
return 1;
|
|
916
|
+
}
|
|
917
|
+
const current = gitBlobSha(fs.readFileSync(file));
|
|
918
|
+
if (current !== revision) {
|
|
919
|
+
err(`put-node conflict: stale revision for ${id}; current revision: ${current}`);
|
|
920
|
+
return 1;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const valid = validatePutNodeLocal(nodesDir, parsed.node, raw);
|
|
925
|
+
if (valid.error) {
|
|
926
|
+
err(valid.error);
|
|
927
|
+
return 1;
|
|
928
|
+
}
|
|
929
|
+
fs.writeFileSync(file, raw);
|
|
930
|
+
const res = { ok: true, status: exists ? "updated" : "created", id, revision: gitBlobSha(Buffer.from(raw)), warnings: valid.warnings || [] };
|
|
931
|
+
renderPutNodeResult(res, !!values.json);
|
|
932
|
+
return 0;
|
|
933
|
+
}
|
|
934
|
+
|
|
750
935
|
// --- spor blame / commits: commit-sha -> nodes reverse lookup ---------------
|
|
751
936
|
// (task-spor-blame-commit-lookup-cli-verb) The shell verb over the commit->node
|
|
752
937
|
// reverse index: which decisions/tasks/issues reference a git commit in their
|
|
@@ -7777,6 +7962,34 @@ const COMMANDS = {
|
|
|
7777
7962
|
examples: ["spor get dec-cc-zero-dep-client", "spor get dec-cc-zero-dep-client --json"],
|
|
7778
7963
|
run: (cfg, p) => cmdGet(cfg, p),
|
|
7779
7964
|
},
|
|
7965
|
+
"put-node": {
|
|
7966
|
+
group: "Graph", parse: "strict", args: "[<file>|-]",
|
|
7967
|
+
summary: "write a full node markdown file (local validated write; remote: /v1/nodes)",
|
|
7968
|
+
help:
|
|
7969
|
+
"Create, skip, or update one complete node markdown file (frontmatter + body)\n" +
|
|
7970
|
+
"through the same validated full-node write path as MCP put_node / REST\n" +
|
|
7971
|
+
"POST /v1/nodes. With no file, or with '-', reads the node markdown from stdin.\n\n" +
|
|
7972
|
+
"Collision policy is explicit: --if-exists error (default) rejects an existing\n" +
|
|
7973
|
+
"id, --if-exists skip no-ops on collision, and --if-exists update replaces an\n" +
|
|
7974
|
+
"existing node only when --revision matches the blob SHA you read earlier.\n" +
|
|
7975
|
+
"Get that revision with 'spor get <id> --json'; re-read and retry on conflict.\n\n" +
|
|
7976
|
+
"Remote mode sends one-entry batch put_node to /v1/nodes, so server attribution,\n" +
|
|
7977
|
+
"schema transition gates, edge normalization, and validation all apply. Local\n" +
|
|
7978
|
+
"mode writes nodes/<id>.md after parsing and validating the candidate against a\n" +
|
|
7979
|
+
"temporary graph view, so a malformed full node never lands on disk.",
|
|
7980
|
+
options: {
|
|
7981
|
+
"if-exists": { type: "string", value: "error|skip|update", desc: "collision policy (default: error)" },
|
|
7982
|
+
revision: { type: "string", value: "sha", desc: "required with --if-exists update; from 'spor get <id> --json'" },
|
|
7983
|
+
json: { type: "boolean", desc: "machine-readable result envelope" },
|
|
7984
|
+
},
|
|
7985
|
+
examples: [
|
|
7986
|
+
"spor put-node ./nodes/dec-x.md",
|
|
7987
|
+
"spor get dec-x --json",
|
|
7988
|
+
"spor put-node ./dec-x.md --if-exists update --revision <blob-sha>",
|
|
7989
|
+
"cat ./task-new.md | spor put-node --if-exists error",
|
|
7990
|
+
],
|
|
7991
|
+
run: (cfg, p) => cmdPutNode(cfg, p),
|
|
7992
|
+
},
|
|
7780
7993
|
blame: {
|
|
7781
7994
|
group: "Graph", parse: "strict", args: "<sha> [--repo <slug>]", aliases: ["commits"],
|
|
7782
7995
|
summary: "which nodes reference a commit (local: graph scan; remote: /v1/commits/<sha>)",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sporhq/spor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.1",
|
|
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",
|
package/skills/spor/SKILL.md
CHANGED
|
@@ -80,6 +80,9 @@ rest are mode-specific (`spor status` confirms which mode you're in):
|
|
|
80
80
|
spor status # resolved mode, graph, project, identity, health
|
|
81
81
|
spor next [--project <slug>] # the ranked decision queue — "what's next"
|
|
82
82
|
spor get <id> # one node by id
|
|
83
|
+
spor put-node [<file>|-] --if-exists <error|skip|update> [--revision <sha>]
|
|
84
|
+
# write a full node markdown file through validated put_node semantics;
|
|
85
|
+
# use `spor get <id> --json` first and pass its revision for updates
|
|
83
86
|
spor blame <sha> [--repo <s>] # which nodes reference a git commit (alias: spor commits <sha>)
|
|
84
87
|
spor history <id> [<sha>] # a node's commit lineage (actor/when/what); <sha> = that revision's diff (local git log / GET /v1/nodes/<id>/history)
|
|
85
88
|
spor schema [<type>] # introspect the live registry (types/prefixes/weights/flags/gates,
|
|
@@ -161,6 +164,13 @@ A few of these have enough surface to be worth a sentence:
|
|
|
161
164
|
read from git content history, never `updated_at`
|
|
162
165
|
(dec-spor-git-derived-timestamps). `--project`/`--type`/`--weeks`/`--json`
|
|
163
166
|
scope and shape it.
|
|
167
|
+
- **`spor put-node`** is the shell twin of MCP `put_node` / REST `POST
|
|
168
|
+
/v1/nodes`: pass a complete node markdown file, or `-`/stdin. Default
|
|
169
|
+
`--if-exists error` rejects collisions; `--if-exists skip` no-ops on an
|
|
170
|
+
existing id; `--if-exists update` requires the `revision` from `spor get <id>
|
|
171
|
+
--json` so updates are optimistic-concurrency checked instead of last-writer
|
|
172
|
+
wins. Prefer `spor edge`/`spor set-status` for narrow mutations; use
|
|
173
|
+
`put-node` for full-node artifacts such as briefing versions or body edits.
|
|
164
174
|
- **`spor compile`/`spor brief`** are mode-aware: local runs the in-repo
|
|
165
175
|
compiler, remote dispatches to the server (this is what `/spor:brief` pulls).
|
|
166
176
|
In local mode add `--project <repo-slug>` to scope to a repo — without it
|
package/skills/triage/SKILL.md
CHANGED
|
@@ -148,7 +148,43 @@ referenced prior-art artifact no longer trips this; a genuine non-resolving work
|
|
|
148
148
|
product still does. The one thing triage must not do is leave the flag with no
|
|
149
149
|
decision — it's a question addressed to you.
|
|
150
150
|
|
|
151
|
-
## 5.
|
|
151
|
+
## 5. Stale briefing findings → recompile the briefing
|
|
152
|
+
|
|
153
|
+
Gardener findings about briefing freshness are not ordinary "close?" chores.
|
|
154
|
+
If a finding says a `brief-*` node is stale, missing `derived-from`
|
|
155
|
+
provenance, has an input-fingerprint mismatch, or has source-set drift, the
|
|
156
|
+
repair is a new briefing version with current provenance. Do not merely
|
|
157
|
+
acknowledge the finding or mark it handled while the stale briefing remains in
|
|
158
|
+
place — that leaves session-start serving bad context.
|
|
159
|
+
|
|
160
|
+
Treat the affected briefing as a build artifact and rebuild it:
|
|
161
|
+
|
|
162
|
+
- Read the finding and the affected `brief-*` node (`spor get <finding-id>` and
|
|
163
|
+
`spor get <brief-id>`). Check its `compiled-for`, `derived-from`, and
|
|
164
|
+
`shaped-by` edges so you know what it was meant to brief and which
|
|
165
|
+
corrections must still apply.
|
|
166
|
+
- Compile/research the current source set for the briefing target. For a
|
|
167
|
+
root/node briefing, run `spor brief <target-id>` for the human body; in a local
|
|
168
|
+
graph, `spor compile --root <target-id> --skeleton` can produce the updated
|
|
169
|
+
provenance skeleton. For a standing project briefing, compile the current
|
|
170
|
+
repo/product context and honor every standing correction named by `shaped-by`.
|
|
171
|
+
- Write the affected `brief-*` node as the next `version:` with the refreshed
|
|
172
|
+
body, exact current `derived-from` inputs, preserved `compiled-for`, and any
|
|
173
|
+
relevant `shaped-by` correction edges. Use `spor put-node <file> --if-exists
|
|
174
|
+
update --revision <sha>` (or MCP `put_node`) so the update is validated and
|
|
175
|
+
optimistic-concurrency checked.
|
|
176
|
+
- Verify the condition clears, or is ready for the gardener to auto-resolve:
|
|
177
|
+
re-read the briefing and, when appropriate, run `spor admin gardener --json`
|
|
178
|
+
to confirm the stale-briefing finding is no longer filed.
|
|
179
|
+
- Only after the repaired briefing exists should the finding be considered
|
|
180
|
+
handled. If the finding still fires, keep it open and report what source or
|
|
181
|
+
correction is still unresolved.
|
|
182
|
+
|
|
183
|
+
This is intentionally more work than closing a stale anchor: stale briefing
|
|
184
|
+
findings affect future agent context, so triage must leave a fresh `brief-*`
|
|
185
|
+
artifact behind.
|
|
186
|
+
|
|
187
|
+
## 6. Latent dependencies → record missing `blocks` edges
|
|
152
188
|
|
|
153
189
|
Find tasks that should carry a `blocks`/`blocked-by` edge but don't — work the
|
|
154
190
|
queue surfaces as actionable that actually can't start until something else
|
|
@@ -174,7 +210,7 @@ Record a confirmed one as a `blocks` edge from the prerequisite to the dependent
|
|
|
174
210
|
(mirroring how an issue records its block). The dependent then leaves the
|
|
175
211
|
actionable queue until its blocker resolves — which is the point.
|
|
176
212
|
|
|
177
|
-
##
|
|
213
|
+
## 7. Open questions → brief the lineage, then answer
|
|
178
214
|
|
|
179
215
|
This is where triage earns its keep: the queue's `questions` are decisions
|
|
180
216
|
waiting on a human, and a bare list of them is useless — the reader can't answer
|
|
@@ -200,7 +236,7 @@ decide.** For each open question:
|
|
|
200
236
|
(If `questions` is empty there's nothing to do here — but it's the step most
|
|
201
237
|
often forgotten, so check it every pass.)
|
|
202
238
|
|
|
203
|
-
##
|
|
239
|
+
## 8. Prioritise → unblockers first
|
|
204
240
|
|
|
205
241
|
Once you understand the dependency shape, set explicit `priority:` (`p1`/`p2`/
|
|
206
242
|
`p3`) so the queue sequences correctly. The highest-leverage move is to **bump
|
|
@@ -224,7 +260,7 @@ genuinely ready work, start/claim it or let `front` decay; don't fake a resolver
|
|
|
224
260
|
to undo it. (A bare reference no longer trips this —
|
|
225
261
|
`issue-spor-queue-held-guard-false-positive-referenced-outcome`.)
|
|
226
262
|
|
|
227
|
-
##
|
|
263
|
+
## 9. Present the outcome
|
|
228
264
|
|
|
229
265
|
Talk to a human, in plain language. Lead with what you did and what to pick up
|
|
230
266
|
next, one short reason each. Give a compact before/after (queue size, what
|
|
@@ -246,11 +282,11 @@ the REST endpoint against the resolved server; in Cowork use the MCP tool. See
|
|
|
246
282
|
| Set priority | `spor priority <id> <p1\|p2\|p3\|clear>` (or `POST /v1/nodes/<id>/priority {priority}`) | `set_priority` |
|
|
247
283
|
| Set status (merged/rejected/resolved/…) | `spor set-status <id> <status>` (or `POST /v1/nodes/<id>/status {status}`) | `set_status` |
|
|
248
284
|
| Add edge (supersedes/blocks/relates-to/answers) | `spor edge <id> <type> <to>` (or `POST /v1/nodes/<id>/edges {type, to}`) | `add_edge` |
|
|
249
|
-
| Edit a field (body) | `POST /v1/nodes {nodes:[{node, if_exists:"update", revision}]}` | `put_node` |
|
|
285
|
+
| Edit a field (body) | `spor put-node <file> --if-exists update --revision <sha>` (or `POST /v1/nodes {nodes:[{node, if_exists:"update", revision}]}`) | `put_node` |
|
|
250
286
|
| Compile a question's lineage | `spor brief <id>` | `query_graph root_id=<id>` |
|
|
251
287
|
|
|
252
288
|
Edges normalize/flip/dedupe server-side, so write a `blocks` from the
|
|
253
|
-
prerequisite's perspective and let it record the inverse. `put_node` needs
|
|
289
|
+
prerequisite's perspective and let it record the inverse. `spor put-node` / `put_node` needs
|
|
254
290
|
`if_exists:"update"` **and** the current `revision` — without the explicit mode
|
|
255
291
|
it skips an existing node. Status flips run through the schema `transitions()`
|
|
256
292
|
gate (and terminal closes through the completion-resolver gate), so a denied
|