loopctl-mcp-server 2.66.0 → 2.68.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 +4 -4
  2. package/index.js +47 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -254,10 +254,10 @@ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for age
254
254
  | `knowledge_search` | Search the knowledge wiki by topic (keyword, semantic, or combined). Returns snippets. **Ranked, published-only, and LAGS writes by minutes while embeddings index — do NOT use for existence/idempotency/dedup checks (a fresh write false-negatives); use `knowledge_list` for that.** `q` is optional when `tags`/`category` are supplied — that **list mode** returns the complete filtered set paginated via `offset`/`limit` over `meta.total_count`. `meta.total_count` is mode-dependent — read `meta.total_count_scope` (`keyword_matches`/`ranked_corpus`/`merged_candidates`/`filtered_set`) and don't use a relevance-mode count to size the wiki (use `knowledge_list` or `knowledge_stats`). Optional: `project_id`, `story_id` for attribution. |
255
255
  | `knowledge_hybrid_search` | Resolve a topic to a **single best answer with provenance** (US-31.4). Runs combined keyword+semantic over the full ranked pool, then decides whether a governed **curated** source actually answers. `meta.provenance` is `curated` (trust it — the canonical article is first in `data`, `meta.curated_article_id` points at it) or `retrieved` (best fuzzy match, `curated_article_id` null); `meta.confidence` is the winner's absolute score. Prefer over `knowledge_search` when you want one trustworthy answer, not a list to triage. Degrades to keyword-only like `knowledge_search` when embeddings are unavailable. Required: `query`. Optional: `project_id`, `category`, `tags`, `match`, `limit`, `offset`. |
256
256
  | `knowledge_progressive_index` | Progressive disclosure — a **cheap, capped index** of what's relevant to a topic (compact stubs: `id`/`title`/`category`/`summary`, **no bodies**), curated-preferred and hub-enriched, capped at top-K (`meta.truncated` when the pool exceeded it). Survey a topic without flooding context, then open only what you need via `knowledge_progressive_drill`. Required: `topic`. Optional: `category`, `limit`. |
257
- | `knowledge_heat_index` | Browse the corpus with **no query at all** — capped compact stubs (`id`/`title`/`category`/`heat`/`summary`, **no bodies**) ranked by how many **distinct readers** (agents, not key rows — repeat reads by one reader count once, ties broken by the number of distinct days read, never by raw read count) opened each article inside a window. Every other retrieval tool starts from a query and so shares one failure mode: a paraphrase, or material topically central but lexically dissimilar to the question, comes back empty and reads as "the KB has nothing" rather than "I asked badly". Reach for it when a search came back empty or thin, or to survey what the fleet actually reads before you know what to ask. **Ordering is usage, not relevance**, and drilling a listed tenant article (`knowledge_progressive_drill`) does not add heat to what it opened — otherwise being shown would produce the rank that shows it. A `knowledge_get` of the same id still counts, as does a drill of a system canonical, whose body has no other read path. `meta` states `heat_window` (snapped to a UTC day boundary, narrowing only, so the payload is stable between refreshes and safe in a cached prefix), `counted_access_types`, `char_budget`/`chars`, `truncated` and `unresolved`. Open a stub with `knowledge_progressive_drill`**not** `knowledge_get`, which cannot resolve the published system canonicals this index also lists. Optional: `category`, `limit`, `since`. |
258
- | `knowledge_progressive_drill` | Open one stub from `knowledge_progressive_index` or `knowledge_heat_index` — returns the **full article body** for the given id, scope-enforced. Resolves both tenant-owned articles and published system canonicals (the same set those indexes surface). A tenant article opened this way is recorded under an access type `knowledge_heat_index` does not count, so that index cannot rank on the reads it caused. Required: `article_id`. |
257
+ | `knowledge_heat_index` | Browse the corpus with **no query at all** — capped compact stubs (`id`/`title`/`category`/`heat`/`summary`, **no bodies**) ranked by how many **distinct readers** (agents, not key rows — repeat reads by one reader count once, ties broken by the number of distinct days read, never by raw read count) opened each article inside a window. Every other retrieval tool starts from a query and so shares one failure mode: a paraphrase, or material topically central but lexically dissimilar to the question, comes back empty and reads as "the KB has nothing" rather than "I asked badly". Reach for it when a search came back empty or thin, or to survey what the fleet actually reads before you know what to ask. **Ordering is usage, not relevance**, and drilling a listed article (`knowledge_progressive_drill`) does not add heat to what it opened, at any scope — otherwise being shown would produce the rank that shows it. A `knowledge_get` of the same id does count. `meta` states `heat_window` (the default window is snapped to a UTC day boundary so the payload is stable between refreshes and safe in a cached prefix; an explicit `since` is served verbatim), `counted_access_types`, `char_budget`/`chars` (BYTES of the encoded stub array, framing included), `truncated` and `unresolved`. Both read tools open a stub, canonicals includedpick by what the read MEANS: a drill is uncounted, a `get` is a counted vote. Optional: `category`, `limit`, `since`. |
258
+ | `knowledge_progressive_drill` | Open one stub from `knowledge_progressive_index` or `knowledge_heat_index` — returns the **full article body** for the given id, scope-enforced. Resolves both tenant-owned articles and published system canonicals (the same set those indexes surface). Every article opened this way is recorded under an access type `knowledge_heat_index` does not count, whatever its scope, so that index can never rank on the reads it caused. `knowledge_get` reaches the same ids and DOES count — use it when the read is a deliberate vote rather than a step in following a list. Required: `article_id`. |
259
259
  | `knowledge_list` | List articles (`id`, `title`, `category`, `status`, `tags`, `source_type`, `source_id`, `idempotency_key`, timestamps), filtered + paginated. **Body-less summary by default** (safe to page up to `limit=1000`); pass `include_body: true` to also return `body`, in which case the page is bounded by a ~5 MB byte budget — continue via `meta.next_offset` while `meta.has_more`. **Lag-free, all-status** read of the DB of record — unlike `knowledge_search` (ranked, published-only, lags writes) and `knowledge_index` (id/title/category only). The right tool to enumerate/dedup/repair and for idempotency/existence checks: filter by `tags`, `source_type`+`source_id`, or `idempotency_key` and read `meta.total_count` (exact). Single full body → `knowledge_get`; relevant bodies → `knowledge_context`; bulk dump → `knowledge_export`. Optional: `project_id`, `category`, `status`, `tags`, `source_type`, `source_id`, `idempotency_key`, `offset`, `limit`, `include_body`. |
260
- | `knowledge_get` | Get full article content by ID. Use after search to read an article in detail. Each link carries only its FAR side (`article: {id, title}`, plus `similarity` when scored); both arrays are ranked (open conflicts first, then descending similarity, then oldest-first for the unscored) and capped at 25 per direction, with `links_total` / `links_truncated` reporting the truth (`count` returns both, so one cheap call tells you whether the full fetch is capped). Pass `links: "count"` or `"none"` when you only want the text — on a well-linked hub the link block is several times the body. `potential_conflicts` is returned in all three modes, itself capped at 25 with `conflicts_total` / `conflicts_truncated`. Optional: `links`, `project_id`, `story_id` for attribution. |
260
+ | `knowledge_get` | Get full article content by ID. Use after search to read an article in detail. Resolves tenant-owned articles **and published system canonicals**, and records a COUNTED read (it feeds `knowledge_heat_index`) — reach for `knowledge_progressive_drill` instead when you are merely following an index this system just produced. Each link carries only its FAR side (`article: {id, title}`, plus `similarity` when scored); both arrays are ranked (open conflicts first, then descending similarity, then oldest-first for the unscored) and capped at 25 per direction, with `links_total` / `links_truncated` reporting the truth (`count` returns both, so one cheap call tells you whether the full fetch is capped). Pass `links: "count"` or `"none"` when you only want the text — on a well-linked hub the link block is several times the body. `potential_conflicts` is returned in all three modes, itself capped at 25 with `conflicts_total` / `conflicts_truncated`. Optional: `links`, `project_id`, `story_id` for attribution. |
261
261
  | `knowledge_context` | Get relevance-and-recency-ranked full articles for a task query. Best knowledge for your current context. **Agent-memory scoping**: `memory_types` (comma-separated, OR — observation/finding/summary/decision/question/task), `agents` (comma-separated agent_ids, OR), `conversation_id` (exact) filter on article `metadata` (JSONB `@>`). Optional: `project_id`, `story_id` for attribution, `limit`, `recency_weight`. |
262
262
  | `knowledge_graph` | Multi-hop traversal of the published article-link graph from `article_id` (depth 1–3, default 1), **bounded to agent's visible articles**. Agent callers see only their own and `shared` articles. Bidirectional, cycle-safe, bounded to 100 nodes / 500 edges (`truncated` flags a cap). Returns `nodes` (`id`/`title`/`category`/`depth`) + `edges` (`source_article_id`/`target_article_id`/`relationship_type`). Explore typed connections beyond `knowledge_context`'s 1-hop links. Required: `article_id`. Optional: `depth`, `project_id`. |
263
263
  | `knowledge_suggest_links` | Ranked typed-link **candidates** for an article by embedding similarity among **visible articles** — **read-only** (creates nothing). Excludes the article itself + any already-linked article (either direction, any type); only embedded published articles visible to the caller. Agent callers see only their own and `shared` articles. Returns `{id, title, category, similarity_score}` highest-first, to create as a **typed** link (relates_to/derived_from/contradicts/supersedes). Required: `article_id`. Optional: `threshold` (cosine floor 0–1, default 0.5), `limit` (default 5). |
@@ -326,7 +326,7 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
326
326
  | `knowledge_agent_usage` | Per-agent knowledge usage: total reads, unique articles, top read articles. Required: exactly one of `api_key_id` (credential) or `agent_id` (logical identity). Optional: `limit`, `since_days`. See Wiki Attribution section. |
327
327
  | `knowledge_unused_articles` | Published articles with zero accesses in the window. Optional: `days_unused` (default 30), `limit` (default 50, max 200). |
328
328
  | `knowledge_curation_log` | Concise human-readable log of KB CURATION adjustments — novelty-gate decisions (`gate_duplicate`/`gate_draft`/`gate_skip`) and conflict resolutions (`supersede`/`merge`/`dismiss`) — for analyzing the agents'-KB rollout, distinct from the verbose audit log. Each entry: `{at, kind, summary, refs, actor, confidence}`. **RECORDED ONLY when `settings.kb_curation_log` is on** (PATCH `/api/v1/admin/tenants/:id` with `settings:{kb_curation_log:true}`); off by default = no rows. Most recent first. Requires orchestrator role. Optional: `kind`, `since` (ISO8601), `limit` (default 50, max 500), `offset`. |
329
- | `knowledge_retrieval_metrics` | Daily retrieval-PRECISION time series: for each day, the share of search results the agent then opened (search → get/context within a window). A proxy for whether retrieval is improving as the corpus is de-duplicated, better navigated (MOCs), and conflict-resolved. Most recent day first. Requires orchestrator role. Optional: `limit` (default 30, max 365), `offset`. |
329
+ | `knowledge_retrieval_metrics` | Daily retrieval-PRECISION time series: for each day, the share of RECORDED surfaced search RESULTS the agent then opened (search → get/context within a window). A proxy for whether retrieval is improving as the corpus is de-duplicated, better navigated (MOCs), and conflict-resolved. **Denominators (#582):** `precision` = `followed_through`/`searched`, where `searched` counts RECORDED surfaced RESULTS — one row per result put in front of the agent, capped at the first 20 per call (cap enforced by `Loopctl.Knowledge.Analytics.max_recorded_search_results/0`) — not search calls (`results_recorded` is the same number named for its unit). That cap makes `precision` precision@20: a call returning more contributes only 20, and an open of a result ranked beyond the cap is in neither term. The per-CALL rate is the separate `search_follow_through` = `searches_with_follow_through`/`searches` (distinct QUERY-BEARING calls — query-less `list`/`list_keyset` enumeration pages are excluded). `results_returned` is the true un-truncated count for those same calls. The four call-level fields are filtered per ROW, not per day, so a day mixing pre-#582 or enumeration rows with real searches reports a PARTIAL figure rather than 0 — and `results_returned` is NOT comparable to `searched`. Zero-result and keyless searches are unrecordable, so both ratios are upper bounds — and both rise when a search returns FEWER results, so never optimise them without the absolute `followed_through`; `search_follow_through` is additionally biased DOWN by the recording cap (opens beyond rank 20 are invisible) and UP by crediting every search in the window that surfaced the opened article. Most recent day first. Requires orchestrator role. Optional: `limit` (default 30, max 365), `offset`. |
330
330
 
331
331
  ### Egress / Privacy Tools (US-41.4)
332
332
 
package/index.js CHANGED
@@ -4511,10 +4511,11 @@ const TOOLS = [
4511
4511
  "KB has nothing' rather than 'I asked badly'. Reach for this when a search came back " +
4512
4512
  "empty or thin, or to survey what the fleet actually reads before you know what to " +
4513
4513
  "ask. Ordering is usage, NOT relevance to any query. Open a stub with " +
4514
- "knowledge_progressive_drill (not knowledge_get this index also lists published " +
4515
- "system canonicals, which knowledge_get cannot resolve). Drilling a tenant article " +
4516
- "adds NO heat to what you opened, so this index cannot feed the ranking that showed " +
4517
- "you the stub; a knowledge_get of the same id does count.",
4514
+ "knowledge_progressive_drill: drilling adds NO heat to what you opened, whatever its " +
4515
+ "scope, so this index can never feed the ranking that showed you the stub. " +
4516
+ "knowledge_get resolves the same ids (published system canonicals included) and DOES " +
4517
+ "count as a read use it when you are deliberately voting for an article's " +
4518
+ "usefulness, not when you are just following this list.",
4518
4519
  inputSchema: {
4519
4520
  type: "object",
4520
4521
  properties: {
@@ -4531,9 +4532,9 @@ const TOOLS = [
4531
4532
  description:
4532
4533
  "Optional: ISO-8601 timestamp; count only reads at/after it. Defaults to the " +
4533
4534
  "last 90 days, clamped to at most 365 days of lookback and to no later than " +
4534
- "today. Snapped to a UTC day boundary in the NARROWING direction (never wider " +
4535
- "than you asked for; a timestamp inside the current UTC day is used exactly as " +
4536
- "given), and meta.heat_window echoes what you got.",
4535
+ "the start of today. An explicit timestamp is otherwise served VERBATIM; only " +
4536
+ "the default and the ceiling are anchored at the start of today, which is what " +
4537
+ "keeps a default refresh byte-identical. meta.heat_window echoes what you got.",
4537
4538
  },
4538
4539
  },
4539
4540
  required: [],
@@ -4547,8 +4548,12 @@ const TOOLS = [
4547
4548
  "body for the given id, scope-enforced. Resolves both tenant-owned articles and " +
4548
4549
  "published system canonicals (the same set the index surfaces). This is the drill " +
4549
4550
  "half of progressive disclosure: index cheaply, then open only the article(s) you " +
4550
- "need. (knowledge_get works for tenant articles too; use this to also reach the " +
4551
- "system canonicals the progressive index can surface.)",
4551
+ "need.\n\n" +
4552
+ "Pick between this and knowledge_get by what the read MEANS, not by what it can " +
4553
+ "reach — both resolve the same ids now. A drill records an UNCOUNTED read, so " +
4554
+ "following an index never raises the heat of what that index just showed you; " +
4555
+ "knowledge_get records a counted one, which is a vote that the article was worth " +
4556
+ "opening on its own. Following a list is not a vote.",
4552
4557
  inputSchema: {
4553
4558
  type: "object",
4554
4559
  properties: {
@@ -4565,6 +4570,9 @@ const TOOLS = [
4565
4570
  name: "knowledge_get",
4566
4571
  description:
4567
4572
  "Get full article content by ID. Use after search to read an article in detail. " +
4573
+ "Resolves tenant-owned articles AND published system canonicals. Records a COUNTED " +
4574
+ "read (it feeds knowledge_heat_index); use knowledge_progressive_drill instead when " +
4575
+ "you are merely following an index this system just handed you. " +
4568
4576
  "Pass story_id when working on a loopctl story so reads attribute correctly. " +
4569
4577
  "If the response carries a non-empty `potential_conflicts` array AND the conflict is " +
4570
4578
  "material to your current task, act on it: read the peer, judge redundant/complementary/" +
@@ -6034,10 +6042,36 @@ const TOOLS = [
6034
6042
  name: "knowledge_retrieval_metrics",
6035
6043
  description:
6036
6044
  "Return the daily retrieval-PRECISION time series (agents' KB #3): for each day, the " +
6037
- "share of search results the agent then opened (search → get/context within a window). " +
6038
- "A proxy for whether retrieval is improving — watch it trend up as the corpus is " +
6039
- "de-duplicated, better navigated (MOCs), and conflict-resolved. Most recent day first. " +
6040
- "Requires orchestrator role.",
6045
+ "share of RECORDED surfaced search RESULTS the agent then opened (search → get/context " +
6046
+ "within a window). A proxy for whether retrieval is improving — watch it trend up as " +
6047
+ "the corpus is de-duplicated, better navigated (MOCs), and conflict-resolved. Most " +
6048
+ "recent day first. Requires orchestrator role.\n\n" +
6049
+ // The cap is enforced by Loopctl.Knowledge.Analytics.max_recorded_search_results/0
6050
+ // (Elixir); this JS string cannot interpolate it, so change both together.
6051
+ "Denominators (#582): precision = followed_through / searched, and `searched` counts " +
6052
+ "RECORDED surfaced RESULTS — one row per result put in front of the agent, capped at " +
6053
+ "the first 20 per call — not search calls (`results_recorded` is the same number, " +
6054
+ "named for its unit). Because of that cap precision is precision@20: a call returning " +
6055
+ "more results contributes only 20 to `searched`, and an open of a result ranked beyond " +
6056
+ "the cap is in neither term. The per-CALL rate is separate: `search_follow_through` = " +
6057
+ "searches_with_follow_through / searches — the share of QUERY-BEARING SEARCHES that " +
6058
+ "led to an open. `results_returned` is the true un-truncated result count for those " +
6059
+ "same calls, so it exceeds the rows those calls wrote whenever a page hit the cap.\n\n" +
6060
+ "Call-level population: the four call-level fields are filtered per ROW, not per day " +
6061
+ "— a row counts only if it carries a search identity (nothing recorded before #582 " +
6062
+ "does) and is not a query-less enumeration page (list / list_keyset; browsing is not " +
6063
+ "searching). A day that mixes qualifying and non-qualifying rows reports a PARTIAL " +
6064
+ "searches / results_returned, not 0. Do NOT compare results_returned against " +
6065
+ "searched: different row populations, so results_returned < searched is normal on a " +
6066
+ "legacy-heavy or browse-heavy day.\n\n" +
6067
+ "Caveats: zero-result searches and keyless searches are structurally unrecordable and " +
6068
+ "sit in NO denominator, so both ratios are upper bounds; and both rise if a search " +
6069
+ "simply returns FEWER results, with no better retrieval. Never optimise either alone — " +
6070
+ "read them with the absolute followed_through and the volume fields. " +
6071
+ "search_follow_through carries two further biases pointing OPPOSITE ways: the 20-row " +
6072
+ "recording cap hides opens of results ranked beyond it (DOWN on large pages), while " +
6073
+ "one open credits EVERY search in the window that surfaced that article, not just the " +
6074
+ "preceding one (UP when an agent refines and re-searches).",
6041
6075
  inputSchema: {
6042
6076
  type: "object",
6043
6077
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.66.0",
3
+ "version": "2.68.0",
4
4
  "description": "MCP server for loopctl \u2014 structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",