loopctl-mcp-server 2.74.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 +160 -9
  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",
@@ -4665,7 +4694,12 @@ const TOOLS = [
4665
4694
  "trustworthy answer plus its provenance rather than a ranked list to triage yourself; " +
4666
4695
  "use knowledge_search when you want to browse/enumerate matches. Additive — existing " +
4667
4696
  "knowledge tools are unchanged. If semantic ranking is unavailable it degrades to " +
4668
- "keyword-only (meta.fallback/fallback_reason), same as knowledge_search.",
4697
+ "keyword-only (meta.fallback/fallback_reason), same as knowledge_search.\n\n" +
4698
+ "TODAY THE CURATED BRANCH IS UNREACHABLE ON THIS DEPLOYMENT: a source counts as " +
4699
+ "curated only if its article has curated_at set, and no article does — so every " +
4700
+ "call returns provenance 'retrieved' and a null curated_article_id. Read a " +
4701
+ "'retrieved' verdict as the normal case, not as evidence that a curated answer " +
4702
+ "was considered and rejected. This line comes out when something actually curates.",
4669
4703
  inputSchema: {
4670
4704
  type: "object",
4671
4705
  properties: {
@@ -5656,6 +5690,65 @@ const TOOLS = [
5656
5690
  required: [],
5657
5691
  },
5658
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
+ },
5659
5752
  {
5660
5753
  name: "knowledge_resolve_conflict",
5661
5754
  description:
@@ -5674,6 +5767,13 @@ const TOOLS = [
5674
5767
  "recorded as \"medium\" (see data.requested_confidence and note in the response) and " +
5675
5768
  "the pair STAYS in knowledge_conflicts until an orchestrator+ key records it at high. " +
5676
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. " +
5677
5777
  "Last-write-wins per pair, so re-recording with fresher ground truth overrides. " +
5678
5778
  "Resolve only conflicts material to your current task; adjudicate against the actual " +
5679
5779
  "system, and if you can't tell which is right, LEAVE IT UNRECORDED rather than " +
@@ -6493,7 +6593,40 @@ const TOOLS = [
6493
6593
  "search_follow_through carries two further biases pointing OPPOSITE ways: the 20-row " +
6494
6594
  "recording cap hides opens of results ranked beyond it (DOWN on large pages), while " +
6495
6595
  "one open credits EVERY search in the window that surfaced that article, not just the " +
6496
- "preceding one (UP when an agent refines and re-searches).",
6596
+ "preceding one (UP when an agent refines and re-searches).\n\n" +
6597
+ "Exact attribution (unit: READS — not surfaced results, not calls): attributed_opens " +
6598
+ "/ cross_key_opens / direct_opens count READ rows by how their originating search was " +
6599
+ "established, resolved server-side at write time and never accepted from a caller. " +
6600
+ "Not comparable with followed_through, which counts SURFACED RESULTS later opened. " +
6601
+ "cross_key_opens is the population followed_through cannot see: it correlates on " +
6602
+ "api_key_id, and the injected recall hook searches under a different key from the " +
6603
+ "session that reads, so that channel scores a structural ZERO there — meaning " +
6604
+ "UNMEASURABLE, not unread. Cross-key attribution is circumstantial (two agents in one " +
6605
+ "tenant can reach one article independently), hence labelled rather than folded in. " +
6606
+ "direct_opens is the agent going straight to an article by link or cited id, which " +
6607
+ "used to look identical to 'surfaced and ignored' — close to its opposite.\n\n" +
6608
+ "Disposition (unit: SEARCH CALLS): searches_scored_with_follow_through, " +
6609
+ "searches_reformulated and searches_quiet PARTITION searches_scored — NOT searches. " +
6610
+ "Treating every not-opened search as a failure is wrong — an agent answered by the " +
6611
+ "result snippet correctly opens nothing, and that is a success. A reformulation (the " +
6612
+ "SAME SESSION issuing a later search call with a DIFFERENT QUERY in-window, nothing " +
6613
+ "opened) is the closest thing to an unambiguous failure, so it is split out; what " +
6614
+ "remains is `quiet` and is STILL a mixture of 'snippet sufficed' and 'rows ignored'. " +
6615
+ "This surface does not separate them — do not read quiet as either.\n\n" +
6616
+ "searches_scored is SMALLER than searches and the gap is NOT quiet traffic. A search " +
6617
+ "is scoreable only if it carries a session identity (stamped forward-looking, so a " +
6618
+ "pre-migration row reports searches_scored: 0) and comes from a channel that can " +
6619
+ "react to a result at all — the recall hook and the session-start auto-query emit one " +
6620
+ "distilled query per prompt and never see what came back, so they cannot reformulate " +
6621
+ "by construction. They stay in every other denominator here, precision included. " +
6622
+ "Read searches - searches_scored as n/a, never as zero.\n\n" +
6623
+ "COMPARE ROWS ONLY WITHIN A metric_version. Every row carries the version of the " +
6624
+ "definition set that produced it. Three changes have already altered what a figure here " +
6625
+ "MEANS — searched went from search calls to surfaced results, infrastructure traffic " +
6626
+ "began being excluded, and the disposition trio was rescoped — each forward-looking and " +
6627
+ "each previously leaving no mark on the row, so a series read across one of those " +
6628
+ "boundaries compares definitions rather than days. 0 means the row predates the stamp " +
6629
+ "and its definitions are unknown.",
6497
6630
  inputSchema: {
6498
6631
  type: "object",
6499
6632
  properties: {
@@ -6515,8 +6648,15 @@ const TOOLS = [
6515
6648
  {
6516
6649
  name: "knowledge_analytics_top",
6517
6650
  description:
6518
- "Return the top accessed knowledge articles for the tenant. " +
6519
- "Use to identify which articles agents actually read. Requires orchestrator role.",
6651
+ "Return the top READ knowledge articles for the tenant — articles whose body was " +
6652
+ "actually delivered (get/context/drill). Requires orchestrator role.\n\n" +
6653
+ "access_type DEFAULTS TO READS, not to every event. `search` and `index` rows are " +
6654
+ "IMPRESSIONS the ranker produced — one per surfaced result — and they outnumber reads " +
6655
+ "roughly 50:1, so the old unfiltered default ranked ranker output while claiming to " +
6656
+ "show what agents read. Pass access_type:'all' if you genuinely want impressions " +
6657
+ "counted, or a single type to select one. unique_keys counts distinct API KEYS, not " +
6658
+ "agents: v2 mints one ephemeral key per dispatch, so one agent dispatched N times is " +
6659
+ "N keys.",
6520
6660
  inputSchema: {
6521
6661
  type: "object",
6522
6662
  properties: {
@@ -6549,8 +6689,12 @@ const TOOLS = [
6549
6689
  {
6550
6690
  name: "knowledge_article_stats",
6551
6691
  description:
6552
- "Return per-article usage statistics: total accesses, unique agents, " +
6553
- "by-type breakdown, and the 10 most recent events. Requires orchestrator role.",
6692
+ "Return per-article usage statistics: total_events (impressions included), " +
6693
+ "total_reads (bodies actually delivered get/context/drill), unique_keys, a by-type " +
6694
+ "breakdown, and the 10 most recent events. Requires orchestrator role.\n\n" +
6695
+ "Read total_reads, not total_events, when you want usage: impressions outnumber reads " +
6696
+ "roughly 50:1. unique_keys counts distinct API KEYS rather than agents — v2 mints one " +
6697
+ "ephemeral key per dispatch.",
6554
6698
  inputSchema: {
6555
6699
  type: "object",
6556
6700
  properties: {
@@ -6565,7 +6709,8 @@ const TOOLS = [
6565
6709
  {
6566
6710
  name: "knowledge_agent_usage",
6567
6711
  description:
6568
- "Return knowledge usage for an agent: total reads, unique articles, top read articles. " +
6712
+ "Return knowledge usage for an agent: total_reads (bodies actually delivered), " +
6713
+ "total_events (impressions included), unique articles, top read articles. " +
6569
6714
  "Pass api_key_id (api_keys.id credential) OR agent_id (agents.id logical identity) — not both. " +
6570
6715
  "Requires orchestrator role.",
6571
6716
  inputSchema: {
@@ -6600,8 +6745,11 @@ const TOOLS = [
6600
6745
  {
6601
6746
  name: "knowledge_unused_articles",
6602
6747
  description:
6603
- "Return published articles that have not been accessed in the configured " +
6604
- "time window. Use to identify dead-weight knowledge. Requires orchestrator role.",
6748
+ "Return published articles that have not been READ in the configured time window. " +
6749
+ "Use to identify dead-weight knowledge. Requires orchestrator role.\n\n" +
6750
+ "\"Not read\" means no get/context/drill. It deliberately does NOT mean \"no event\": " +
6751
+ "on that definition an article the ranker surfaces constantly and nobody ever opens " +
6752
+ "counted as USED, which made this blind to the largest class of dead weight there is.",
6605
6753
  inputSchema: {
6606
6754
  type: "object",
6607
6755
  properties: {
@@ -7350,6 +7498,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7350
7498
  case "knowledge_conflicts":
7351
7499
  return await knowledgeConflicts(args);
7352
7500
 
7501
+ case "knowledge_assert_conflict":
7502
+ return await knowledgeAssertConflict(args);
7503
+
7353
7504
  case "knowledge_resolve_conflict":
7354
7505
  return await knowledgeResolveConflict(args);
7355
7506
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.74.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",