loopctl-mcp-server 2.75.0 → 2.76.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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/index.js +98 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -264,8 +264,9 @@ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for age
264
264
  | `knowledge_distant_pairs` | Distant-but-bridgeable article pairs in the optimal-novelty embedding band (cosine distance, default 0.3–0.7) — the creative sweet spot. Sampled from **agent's visible published articles**; agent callers see only their own and `shared` articles. `bridge_path: true` requires a ≤2-hop link path. Returns `{a, b, distance}` pairs, paginated. Optional: `min_distance`, `max_distance`, `bridge_path`, `limit` (default 20, max 100), `offset`. |
265
265
  | `knowledge_novelty` | Score ideas by novelty: embeds each idea's text, returns `novelty_score` = cosine distance to the nearest **visible** prior proposal (0 = identical, higher = more novel, up to 2.0; `null` when the idea text is blank, no visible priors exist, or embedding fails — see `meta.prior_count`). Agent callers see only their own and `shared` articles as priors. Priors default to articles tagged `proposal`. Provide ideas as `texts` (strings) OR `ideas` (strings or objects), ≤50. Optional: `prior_tag`. |
266
266
  | `knowledge_random_walk` | Random walk through the link graph from `start_id` (no cycles, up to `length` nodes), traversing only **agent's visible published articles**, surfacing unexpected connections. Agent callers see only their own and `shared` articles. Returns `{id, title, category}` in walk order. Required: `start_id`. Optional: `length` (default 4, max 25). |
267
- | `knowledge_conflicts` | List potential-conflict article pairs — published articles flagged "too similar to comfortably coexist" by the auto-linker / nightly lint sweep, highest-overlap first. The KB only FLAGS the pair; it does NOT decide redundancy-vs-contradiction — that's your call with live context. Each entry has both articles (id/title/status/category) + similarity. Then merge (supersede one, `knowledge_create` the merged article, or PATCH) or reconcile if they genuinely disagree. Paginated with `total_count` in meta. Agent role. Optional: `limit` (default 50, max 1000, clamped), `offset`. |
268
- | `knowledge_resolve_conflict` | Record YOUR verdict on a potential-conflict pair (from `knowledge_conflicts`). Dispositions: `dismiss` (false positive, drops from queue), `supersede` (one wins pass `authoritative_article_id`; nightly executor links + retires loser, only at `confidence:"high"`, reversible/audited), `merge` (at high confidence an LLM synthesizes both into ONE new DRAFT, sources preserved, never auto-published). Non-destructive at agent role you record intent, the privileged nightly job executes. Last-write-wins per pair. **`supersede` is the one disposition that retires an article unattended**, so its `confidence` is capped server-side: an agent-role `"high"` is recorded as `"medium"` (`data.requested_confidence` + `note` say so) and the pair stays in `knowledge_conflicts` until an orchestrator+ key records it at high; a `high` supersede also REQUIRES `evidence` (422 without it). `merge` is never capped. Required: `source_article_id`, `target_article_id`, `disposition`. Optional: `authoritative_article_id`, `classification`, `evidence` (required for a high supersede), `confidence`. |
267
+ | `knowledge_conflicts` | List potential-conflict article pairs — published articles flagged "too similar to comfortably coexist" by the auto-linker / nightly lint sweep, highest-overlap first. The KB only FLAGS the pair; it does NOT decide redundancy-vs-contradiction — that's your call with live context. Each entry has both articles (id/title/status/category) + similarity. Then merge (supersede one, `knowledge_create` the merged article, or PATCH) or reconcile if they genuinely disagree. Paginated with `total_count` in meta. Agent role. Optional: `limit` (default 50, max 1000, clamped), `offset`, `origin` (`system` or `asserted` — asserted rows lead the default ordering, so pass `system` to review machine-flagged pairs alone). |
268
+ | `knowledge_assert_conflict` | **ASSERT** a conflict between two articles the system never flagged the way to contest an article you just deliberately refuted. `knowledge_resolve_conflict` only reaches pairs the AUTO-LINKER flagged by similarity, which is exactly wrong for a correction: the pair is minutes old (the nightly linker has not run) and a good correction argues about the CONCLUSION, so it may never be similar enough to be flagged at all. The pair then appears in `knowledge_conflicts` with `origin: "asserted"` and your claim attached, and in both articles' `potential_conflicts`. **It retires, hides and down-ranks nothing**, and does not remove either article from curated answers (that still needs a system flag). **And you cannot judge your own assertion** `knowledge_resolve_conflict` returns `409 self_asserted_conflict` to the asserting key, because you named both ids; another key decides. Idempotent per pair (`created: false` on a re-assert; never overwrites a system flag's provenance). Agent role. Required: `source_article_id`, `target_article_id`, `evidence`. Optional: `classification`, `proposed_authoritative_article_id`. |
269
+ | `knowledge_resolve_conflict` | Record YOUR verdict on a potential-conflict pair (from `knowledge_conflicts`). Dispositions: `dismiss` (false positive, drops from queue), `supersede` (one wins — pass `authoritative_article_id`; nightly executor links + retires loser, only at `confidence:"high"`, reversible/audited), `merge` (at high confidence an LLM synthesizes both into ONE new DRAFT, sources preserved, never auto-published). Non-destructive at agent role — you record intent, the privileged nightly job executes. Only pairs with a real flag are reachable; for a pair that was never flagged, use `knowledge_assert_conflict` first — and note that a DIFFERENT key must then record the verdict. Last-write-wins per pair. **`supersede` is the one disposition that retires an article unattended**, so its `confidence` is capped server-side: an agent-role `"high"` is recorded as `"medium"` (`data.requested_confidence` + `note` say so) and the pair stays in `knowledge_conflicts` until an orchestrator+ key records it at high; a `high` supersede also REQUIRES `evidence` (422 without it). `merge` is never capped. Sent on `LOOPCTL_AGENT_KEY`, so a pair YOU asserted answers `409 self_asserted_conflict` here by design — another principal (a second session, an orchestrator, a human operator) records that verdict. Required: `source_article_id`, `target_article_id`, `disposition`. Optional: `authoritative_article_id`, `classification`, `evidence` (required for a high supersede), `confidence`. |
269
270
  | `knowledge_create` | Create a new knowledge article. File findings, document patterns, or record decisions. **Published immediately by default** (visible per `metadata.visibility` — default `owner` for agent authors, only visible to that agent; `shared` for visibility to all agents) — the response `note` says which outcome occurred. Pass `draft: true` to stage it for later review instead (publish afterwards with `knowledge_publish`). Pass `metadata: {visibility: "shared"}` to make the article visible to other agents; higher roles can set visibility and agent_id explicitly. Pass `idempotency_key` for idempotent capture (re-creating with the same key is a no-op returning the existing article — no partial duplicates). RESERVED TAG NAMESPACE: a tag starting with `idem-` must be `idem-<family>-<digest>` (digest = 12 or 40 lowercase hex chars, e.g. `idem-url-7ebe1ca33431`) or the write is rejected 422 — never silently rewritten; put topics outside that prefix and use `idempotency_key` for idempotent capture. Optional: `category`, `tags`, `project_id`, `draft`, `idempotency_key`, `source_type`, `source_id`, `metadata`. |
270
271
  | `knowledge_update` | Edit an EXISTING article IN PLACE, **preserving its ID** (IDs are load-bearing — cited in project CLAUDE.mds and cross-links). Fold in a new fact, tidy a hub, retag, or reclassify without churning a new row. Send only the fields to change; `tags` REPLACES the whole array. A changed body/tags re-triggers embedding + auto-linking. Agent role — KB-content curation (non-destructive + audited; the in-place edit overwrites the prior body, so it is not reversible either); visibility-scoped, so another agent's private/owner memory 404s. The reserved `idem-` tag namespace applies here too (see `knowledge_create`). Required: `article_id`. Optional: `title`, `body`, `category`, `tags`, `metadata`. |
271
272
  | `knowledge_okf_export` | **Requires `LOOPCTL_USER_KEY`.** Export the wiki as a portable OKF (Open Knowledge Format) v0.1 bundle of markdown files. Writes to `out_dir`, or returns `{files, meta}` inline. |
package/index.js CHANGED
@@ -2062,6 +2062,26 @@ async function knowledgeConflicts({ limit, offset }) {
2062
2062
  return toContent(result);
2063
2063
  }
2064
2064
 
2065
+ async function knowledgeAssertConflict({
2066
+ source_article_id,
2067
+ target_article_id,
2068
+ classification,
2069
+ evidence,
2070
+ proposed_authoritative_article_id,
2071
+ }) {
2072
+ const payload = { source_article_id, target_article_id, evidence };
2073
+ if (classification) payload.classification = classification;
2074
+ if (proposed_authoritative_article_id)
2075
+ payload.proposed_authoritative_article_id = proposed_authoritative_article_id;
2076
+ const result = await apiCall(
2077
+ "POST",
2078
+ "/api/v1/knowledge/conflicts",
2079
+ payload,
2080
+ process.env.LOOPCTL_AGENT_KEY,
2081
+ );
2082
+ return toContent(result);
2083
+ }
2084
+
2065
2085
  async function knowledgeResolveConflict({
2066
2086
  source_article_id,
2067
2087
  target_article_id,
@@ -2076,6 +2096,15 @@ async function knowledgeResolveConflict({
2076
2096
  if (classification) payload.classification = classification;
2077
2097
  if (evidence) payload.evidence = evidence;
2078
2098
  if (confidence) payload.confidence = confidence;
2099
+ // #730: the AGENT key, the same one every other knowledge_* verb sends. It must NOT
2100
+ // reach for LOOPCTL_ORCH_KEY to get past a 409 self_asserted_conflict: that 409 is the
2101
+ // separation working, and clearing it by handing this process a second, higher-privileged
2102
+ // key is the workaround this repo forbids outright ("the MCP server must NEVER hold both
2103
+ // implementer and reviewer keys in the same process"). It would also lift the #331
2104
+ // supersede confidence cap and the agent visibility scope on EVERY verdict, not just on
2105
+ // an asserted pair — an agent-role "high" supersede would stop being capped to "medium"
2106
+ // and the nightly executor would retire the loser unattended. A pair you asserted is
2107
+ // judged by a DIFFERENT key: another session, an orchestrator, or a human operator.
2079
2108
  const result = await apiCall(
2080
2109
  "POST",
2081
2110
  "/api/v1/knowledge/conflicts/resolve",
@@ -5661,6 +5690,65 @@ const TOOLS = [
5661
5690
  required: [],
5662
5691
  },
5663
5692
  },
5693
+ {
5694
+ name: "knowledge_assert_conflict",
5695
+ description:
5696
+ "ASSERT a conflict between two articles the system never flagged — the way to contest " +
5697
+ "an article you have just deliberately refuted. knowledge_resolve_conflict only " +
5698
+ "reaches pairs the AUTO-LINKER flagged by similarity, which is exactly wrong for a " +
5699
+ "correction: your pair is minutes old (the nightly linker has not run), and a good " +
5700
+ "correction argues about the CONCLUSION so it may never be similar enough to be " +
5701
+ "flagged at all. Use this the moment you write an article that contradicts an " +
5702
+ "existing one — do not settle for a 'SUPERSEDED' banner in the loser's body, which " +
5703
+ "changes no ranking and is invisible to any caller reading snippets. " +
5704
+ "`evidence` is REQUIRED: an assertion carries no similarity score, so your argument " +
5705
+ "IS what the reviewer judges. " +
5706
+ "WHAT THIS DOES: the pair appears in knowledge_conflicts with origin \"asserted\" and " +
5707
+ "your claim attached, and in both articles' potential_conflicts. " +
5708
+ "WHAT IT DOES NOT DO: it does not retire, hide, or down-rank either article, and it " +
5709
+ "does not remove either from curated answers (that still needs a system flag). AND " +
5710
+ "YOU CANNOT JUDGE YOUR OWN ASSERTION — knowledge_resolve_conflict returns 409 " +
5711
+ "self_asserted_conflict to the key that asserted the pair, because you named both " +
5712
+ "ids and a party that arranges a pair does not also certify the verdict on it. " +
5713
+ "Another key (a human, an orchestrator, a later session) decides. " +
5714
+ "Idempotent per pair: re-asserting returns the existing flag (created: false) and " +
5715
+ "never overwrites a system flag's provenance. Agent role.",
5716
+ inputSchema: {
5717
+ type: "object",
5718
+ properties: {
5719
+ source_article_id: {
5720
+ type: "string",
5721
+ description: "One article of the pair (UUID). Order does not matter.",
5722
+ },
5723
+ target_article_id: {
5724
+ type: "string",
5725
+ description: "The other article of the pair (UUID).",
5726
+ },
5727
+ classification: {
5728
+ type: "string",
5729
+ enum: ["redundant", "complementary", "contradictory"],
5730
+ description:
5731
+ "What kind of conflict you are asserting: redundant (same claim twice), " +
5732
+ "complementary (same topic, different facets), or contradictory (cannot both " +
5733
+ "be true — the usual reason to assert).",
5734
+ },
5735
+ evidence: {
5736
+ type: "string",
5737
+ description:
5738
+ "REQUIRED. Why these two conflict, ideally the ground truth that settles it " +
5739
+ "(commit, file:line, URL, measurement, observed behavior). This travels with " +
5740
+ "the pair in knowledge_conflicts and is what the deciding key reads.",
5741
+ },
5742
+ proposed_authoritative_article_id: {
5743
+ type: "string",
5744
+ description:
5745
+ "Optional: which of the two you believe should win. Recorded as your CLAIM on " +
5746
+ "the queue row — it applies nothing and is not a verdict.",
5747
+ },
5748
+ },
5749
+ required: ["source_article_id", "target_article_id", "evidence"],
5750
+ },
5751
+ },
5664
5752
  {
5665
5753
  name: "knowledge_resolve_conflict",
5666
5754
  description:
@@ -5679,6 +5767,13 @@ const TOOLS = [
5679
5767
  "recorded as \"medium\" (see data.requested_confidence and note in the response) and " +
5680
5768
  "the pair STAYS in knowledge_conflicts until an orchestrator+ key records it at high. " +
5681
5769
  "'merge' is never capped and executes normally at agent role. " +
5770
+ "Only pairs with a real flag are reachable here — if the pair you want was never " +
5771
+ "flagged (you just wrote an article refuting another), assert it first with " +
5772
+ "knowledge_assert_conflict; a DIFFERENT PRINCIPAL then records the verdict, since the " +
5773
+ "asserter of a pair may not judge it (409 self_asserted_conflict). This tool sends " +
5774
+ "LOOPCTL_AGENT_KEY, so a pair YOU asserted answers 409 here by design — hand it to " +
5775
+ "another session, an orchestrator, or a human operator rather than reaching for a " +
5776
+ "higher-privileged key. " +
5682
5777
  "Last-write-wins per pair, so re-recording with fresher ground truth overrides. " +
5683
5778
  "Resolve only conflicts material to your current task; adjudicate against the actual " +
5684
5779
  "system, and if you can't tell which is right, LEAVE IT UNRECORDED rather than " +
@@ -7403,6 +7498,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7403
7498
  case "knowledge_conflicts":
7404
7499
  return await knowledgeConflicts(args);
7405
7500
 
7501
+ case "knowledge_assert_conflict":
7502
+ return await knowledgeAssertConflict(args);
7503
+
7406
7504
  case "knowledge_resolve_conflict":
7407
7505
  return await knowledgeResolveConflict(args);
7408
7506
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.75.0",
3
+ "version": "2.76.0",
4
4
  "description": "MCP server for loopctl \u2014 structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",