loopctl-mcp-server 2.67.0 → 2.69.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 +2 -1
  2. package/index.js +104 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -304,6 +304,7 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
304
304
  | `knowledge_bulk_delete` | **Requires `LOOPCTL_USER_KEY`.** Bulk archive (default, reversible) or IRREVERSIBLE hard-delete by selector. Provide exactly one selector: `article_ids` (list), `source_type`+`source_id` (every active article from a source), or `tag`+`confirm:true` (every active article with the tag — high blast radius). Default = set-based soft archive (idempotent; `meta.count`=archived, `meta.counts`/`meta.results` give the breakdown; ≤5000). **Dry-run** (`dry_run:true`) mutates nothing, returns `meta.would_affect` (with `hard:true` also a single-use `meta.token`, or `meta.confirm_hash` for oversized selectors). **Hard delete** (irreversible): dry-run with `hard:true` for a token, then call again with `hard:true`+`token` to FK-correctly delete the frozen id-set (links first, access events cascade). |
305
305
  | `knowledge_drafts` | List draft (unpublished) knowledge articles with pagination. Optional: `limit` (default 20, max 1000 — over-max → 400, no silent clamp), `offset` (default 0), `project_id`. Returns `meta.total_count`. |
306
306
  | `knowledge_lint` | Run a lint check on the knowledge wiki to identify stale or low-coverage articles. Optional: `project_id`, `stale_days`, `min_coverage`, `max_per_category` (default 50, max 500). True totals returned in `summary.total_per_category`. |
307
+ | `knowledge_consolidation` | Read the nightly consolidation ("dream") report: NUMBERED proposals for reconciling the corpus, each naming the articles involved and quoting an excerpt from each as evidence. **REPORT ONLY** — the pass writes no articles, links or conflict resolutions; every proposal is `pending` and this tool applies nothing. Classes: `duplicate_capture` (titles that collide once case/punctuation normalize away, or idempotency keys that collide under the same normalization while differing verbatim — capture tag-format drift, which the novelty gate does not catch because novelty scoring and idempotency are separate paths), `contradiction_candidate` (a SYSTEM-flagged `potential_conflict` pair of PUBLISHED articles with no recorded verdict — record one with `knowledge_resolve_conflict`, which accepts exactly these pairs), `generic_title` (a placeholder title that collides on active-title uniqueness and blocks hub creation), `stale_entry`. **Denominators:** `corpus_size` = PUBLISHED articles owned by the tenant at scan time, not its total article count; `proposal_count` = the TRUE pre-cap count of PROPOSALS, not of articles (one duplicate group of three articles is ONE proposal, and one article can appear in proposals of several classes); `persisted_count` = proposal ROWS the report carries, lower than `proposal_count` exactly when a class hit `max_per_class` (`truncated` flags which); `meta.total_count` counts persisted proposals matching the `class` filter, so it is bounded by `persisted_count`, never `proposal_count`. `review_status`/`reviewed_by`/`reviewed_at` reset to pending/null whenever the nightly pass re-derives a proposal — refreshed machine output never inherits an approval. Requires orchestrator role. Optional: `day` (ISO8601, default most recent report), `class`, `limit` (default 50, max 500), `offset`. |
307
308
  | `knowledge_export` | Export all knowledge articles as an OKF v0.1 bundle (gzipped tar archive, unbounded, bounded-memory streaming, fail-closed). Returns a curl command for direct download — **the download requires `LOOPCTL_USER_KEY`** (an orchestrator key would 403). Pass `format=json` for buffered in-memory JSON (convenience tool for file writers; capped at `export_max_buffered_export_articles` — returns 413 if over-cap). Optional: `project_id`, `format` (`tar.gz` default or `json`). |
308
309
  | `knowledge_ingest` | Submit a URL or raw content for knowledge extraction. Enqueues an Oban job. Extracted articles are **drafts by default** (lower-trust LLM output); pass `publish: true` to publish on extraction. **BYO:** runs on the tenant's own Anthropic key — a keyless tenant gets a 422 whose result leads with an `ACTION REQUIRED` notice pointing at `set_llm_config` (see [First-time setup](#first-time-setup--provision-your-byo-llm-keys)). Required: `source_type`. One of: `url` or `content`. Optional: `project_id`, `publish`. |
309
310
  | `knowledge_ingest_batch` | Submit up to 50 ingestion items in a single request. Each item has the same shape as `knowledge_ingest` (incl. `publish`). Returns per-item results. Required: `items`. Optional: batch-level `project_id` / `publish` defaults. |
@@ -326,7 +327,7 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
326
327
  | `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
328
  | `knowledge_unused_articles` | Published articles with zero accesses in the window. Optional: `days_unused` (default 30), `limit` (default 50, max 200). |
328
329
  | `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`. |
330
+ | `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
331
 
331
332
  ### Egress / Privacy Tools (US-41.4)
332
333
 
package/index.js CHANGED
@@ -1927,6 +1927,20 @@ async function knowledgeLint({ project_id, stale_days, min_coverage, max_per_cat
1927
1927
  return toContent(result);
1928
1928
  }
1929
1929
 
1930
+ async function knowledgeConsolidation({ day, class: klass, limit, offset } = {}) {
1931
+ const params = new URLSearchParams();
1932
+ if (day) params.set("day", String(day));
1933
+ if (klass) params.set("class", String(klass));
1934
+ if (limit != null) params.set("limit", String(limit));
1935
+ if (offset != null) params.set("offset", String(offset));
1936
+ const qs = params.toString();
1937
+ const path = qs
1938
+ ? `/api/v1/knowledge/consolidation?${qs}`
1939
+ : "/api/v1/knowledge/consolidation";
1940
+ const result = await apiCall("GET", path, null, process.env.LOOPCTL_ORCH_KEY);
1941
+ return toContent(result);
1942
+ }
1943
+
1930
1944
  async function knowledgeIngest({ url, content, source_type, project_id, publish }) {
1931
1945
  const body = { source_type };
1932
1946
  if (url) body.url = url;
@@ -5447,6 +5461,63 @@ const TOOLS = [
5447
5461
  required: [],
5448
5462
  },
5449
5463
  },
5464
+ {
5465
+ name: "knowledge_consolidation",
5466
+ description:
5467
+ "Read the nightly consolidation (\"dream\") report: NUMBERED proposals for reconciling " +
5468
+ "the corpus, each naming the articles involved and quoting an excerpt from each as " +
5469
+ "evidence. REPORT ONLY — the pass writes no articles, links or conflict resolutions, " +
5470
+ "every proposal is `pending`, and this tool applies nothing. Requires orchestrator role.\n\n" +
5471
+ "Classes: `duplicate_capture` (titles that collide once case/punctuation normalize away, " +
5472
+ "or idempotency keys that collide under the same normalization while differing verbatim — " +
5473
+ "capture tag-format drift, which the novelty gate does not catch because novelty scoring " +
5474
+ "and idempotency are separate paths); `contradiction_candidate` (a SYSTEM-flagged " +
5475
+ "potential_conflict pair of PUBLISHED articles with no recorded verdict — record one via " +
5476
+ "knowledge_resolve_conflict, which accepts exactly these pairs; this report writes " +
5477
+ "none); `generic_title` (a placeholder title that collides on active-title uniqueness and " +
5478
+ "blocks hub creation); `stale_entry` (past the lint staleness threshold, never reconciled).\n\n" +
5479
+ "Denominators: `corpus_size` counts PUBLISHED articles owned by the tenant at scan time, " +
5480
+ "not its total article count. `proposal_count` is the TRUE pre-cap count of PROPOSALS, not " +
5481
+ "of articles — one duplicate group of three articles is ONE proposal, and one article can " +
5482
+ "appear in proposals of several classes. `persisted_count` is how many proposal ROWS the " +
5483
+ "report carries, lower than `proposal_count` exactly when a class hit `max_per_class` " +
5484
+ "(`truncated` flags which). `meta.total_count` counts persisted proposals matching the " +
5485
+ "`class` filter, so it is bounded by `persisted_count`, never by `proposal_count`.\n\n" +
5486
+ "Review state (`review_status`/`reviewed_by`/`reviewed_at`) RESETS to pending/null whenever " +
5487
+ "the nightly pass re-derives a proposal: refreshed machine output never inherits an approval.",
5488
+ inputSchema: {
5489
+ type: "object",
5490
+ properties: {
5491
+ day: {
5492
+ type: "string",
5493
+ description:
5494
+ "Optional ISO8601 date (YYYY-MM-DD, UTC) of the report to read. Defaults to the most recent report.",
5495
+ },
5496
+ class: {
5497
+ type: "string",
5498
+ enum: [
5499
+ "duplicate_capture",
5500
+ "contradiction_candidate",
5501
+ "generic_title",
5502
+ "stale_entry",
5503
+ ],
5504
+ description: "Optional: return only proposals of this class.",
5505
+ },
5506
+ limit: {
5507
+ type: "integer",
5508
+ description: "Proposals per page. Default 50, max 500 (clamped, never rejected).",
5509
+ default: 50,
5510
+ minimum: 1,
5511
+ },
5512
+ offset: {
5513
+ type: "integer",
5514
+ description: "Proposals to skip. Default 0.",
5515
+ minimum: 0,
5516
+ },
5517
+ },
5518
+ required: [],
5519
+ },
5520
+ },
5450
5521
  {
5451
5522
  name: "knowledge_export",
5452
5523
  description:
@@ -6042,10 +6113,36 @@ const TOOLS = [
6042
6113
  name: "knowledge_retrieval_metrics",
6043
6114
  description:
6044
6115
  "Return the daily retrieval-PRECISION time series (agents' KB #3): for each day, the " +
6045
- "share of search results the agent then opened (search → get/context within a window). " +
6046
- "A proxy for whether retrieval is improving — watch it trend up as the corpus is " +
6047
- "de-duplicated, better navigated (MOCs), and conflict-resolved. Most recent day first. " +
6048
- "Requires orchestrator role.",
6116
+ "share of RECORDED surfaced search RESULTS the agent then opened (search → get/context " +
6117
+ "within a window). A proxy for whether retrieval is improving — watch it trend up as " +
6118
+ "the corpus is de-duplicated, better navigated (MOCs), and conflict-resolved. Most " +
6119
+ "recent day first. Requires orchestrator role.\n\n" +
6120
+ // The cap is enforced by Loopctl.Knowledge.Analytics.max_recorded_search_results/0
6121
+ // (Elixir); this JS string cannot interpolate it, so change both together.
6122
+ "Denominators (#582): precision = followed_through / searched, and `searched` counts " +
6123
+ "RECORDED surfaced RESULTS — one row per result put in front of the agent, capped at " +
6124
+ "the first 20 per call — not search calls (`results_recorded` is the same number, " +
6125
+ "named for its unit). Because of that cap precision is precision@20: a call returning " +
6126
+ "more results contributes only 20 to `searched`, and an open of a result ranked beyond " +
6127
+ "the cap is in neither term. The per-CALL rate is separate: `search_follow_through` = " +
6128
+ "searches_with_follow_through / searches — the share of QUERY-BEARING SEARCHES that " +
6129
+ "led to an open. `results_returned` is the true un-truncated result count for those " +
6130
+ "same calls, so it exceeds the rows those calls wrote whenever a page hit the cap.\n\n" +
6131
+ "Call-level population: the four call-level fields are filtered per ROW, not per day " +
6132
+ "— a row counts only if it carries a search identity (nothing recorded before #582 " +
6133
+ "does) and is not a query-less enumeration page (list / list_keyset; browsing is not " +
6134
+ "searching). A day that mixes qualifying and non-qualifying rows reports a PARTIAL " +
6135
+ "searches / results_returned, not 0. Do NOT compare results_returned against " +
6136
+ "searched: different row populations, so results_returned < searched is normal on a " +
6137
+ "legacy-heavy or browse-heavy day.\n\n" +
6138
+ "Caveats: zero-result searches and keyless searches are structurally unrecordable and " +
6139
+ "sit in NO denominator, so both ratios are upper bounds; and both rise if a search " +
6140
+ "simply returns FEWER results, with no better retrieval. Never optimise either alone — " +
6141
+ "read them with the absolute followed_through and the volume fields. " +
6142
+ "search_follow_through carries two further biases pointing OPPOSITE ways: the 20-row " +
6143
+ "recording cap hides opens of results ranked beyond it (DOWN on large pages), while " +
6144
+ "one open credits EVERY search in the window that surfaced that article, not just the " +
6145
+ "preceding one (UP when an agent refines and re-searches).",
6049
6146
  inputSchema: {
6050
6147
  type: "object",
6051
6148
  properties: {
@@ -6873,6 +6970,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6873
6970
  case "knowledge_lint":
6874
6971
  return await knowledgeLint(args);
6875
6972
 
6973
+ case "knowledge_consolidation":
6974
+ return await knowledgeConsolidation(args);
6975
+
6876
6976
  case "knowledge_export":
6877
6977
  return await knowledgeExport(args);
6878
6978
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.67.0",
3
+ "version": "2.69.0",
4
4
  "description": "MCP server for loopctl \u2014 structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",