loopctl-mcp-server 2.70.0 → 2.72.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 +5 -5
  2. package/index.js +84 -26
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -267,7 +267,7 @@ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for age
267
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
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. Required: `source_article_id`, `target_article_id`, `disposition`. Optional: `authoritative_article_id`, `classification`, `evidence`, `confidence`. |
269
269
  | `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
- | `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 (reversible + audited); 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`. |
270
+ | `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
271
  | `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. |
272
272
  | `knowledge_okf_import` | **Requires `LOOPCTL_USER_KEY`.** Import an OKF v0.1 bundle from a local directory. Creates or (with `merge`) updates articles; tolerates and preserves unknown frontmatter. |
273
273
 
@@ -299,12 +299,12 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
299
299
  | `knowledge_bulk_publish` | **Requires `LOOPCTL_USER_KEY`.** Publish drafts, partial-success style: every valid draft publishes; others are reported per-id as `skipped` (already published — idempotent — or archived/superseded), `not_found`, or `errored`. No 100-id cap (auto-chunked); duplicates ignored; safe to retry. `meta.count` = published; `meta.counts`/`meta.results` give the breakdown. Required: `article_ids` (array). |
300
300
  | `knowledge_unpublish` | **Requires `LOOPCTL_USER_KEY`.** Revert a published article back to draft (hidden from search/context, not deleted). Required: `article_id`. |
301
301
  | `knowledge_bulk_unpublish` | **Requires `LOOPCTL_USER_KEY`.** Revert published articles to draft in bulk, partial-success style (mirror of `knowledge_bulk_publish`): per-id `unpublished`/`skipped` (already draft, or archived/superseded)/`not_found`/`errored`. No 100-id cap (auto-chunked, ≤5000); duplicates ignored; safe to retry. Not deleted (re-publish to restore; `knowledge_bulk_delete` to archive). `meta.count`/`meta.counts`/`meta.results` give the breakdown. Required: `article_ids` (array). |
302
- | `knowledge_archive` | Soft-delete an article (draft or published). Row retained for audit; hidden from all reads (reversible — re-publish/edit it back). Agent role — KB-content curation, visibility-scoped (another agent's private/owner memory 404s). Required: `article_id`. |
303
- | `knowledge_delete` | Alias for `knowledge_archive` — DELETE verb on the REST API archives under the hood (soft delete, reversible, audited). Agent role. (Irreversible HARD delete is `knowledge_bulk_delete hard:true`, which stays `LOOPCTL_USER_KEY`.) Required: `article_id`. |
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). |
302
+ | `knowledge_archive` | Soft-delete an article (draft or published). Row retained for audit; hidden from all reads. **NOT reversible by you** `:archived` is a TERMINAL status (no unarchive call, no outbound transition), so restoring one needs a user-role PATCH with an explicit status. Nothing is destroyed, but do not reach for this as an undoable action: for a retraction you can undo, use `knowledge_unpublish` and `knowledge_publish`. Agent role — KB-content curation, visibility-scoped (another agent's private/owner memory 404s). Required: `article_id`. |
303
+ | `knowledge_delete` | Alias for `knowledge_archive` — DELETE verb on the REST API archives under the hood (soft delete: row retained and audited, but NOT reversible by any call you can make, since `:archived` is terminal — use `knowledge_unpublish` when you need an undoable retraction). Agent role. (Irreversible HARD delete is `knowledge_bulk_delete hard:true`, which stays `LOOPCTL_USER_KEY`.) Required: `article_id`. |
304
+ | `knowledge_bulk_delete` | **Requires `LOOPCTL_USER_KEY`.** Bulk archive (default — non-destructive, but NOT reversible by any call: `:archived` is terminal and restoring needs a user-role PATCH) 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
+ | `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. **This tool** applies nothing and recomputes nothing it returns persisted rows. **The pass** it reports on does write: since #608 the nightly run UNPUBLISHES the losers of each `duplicate_capture` group that two consecutive reports both propose (consecutive meaning the previous report is at most 2 days older, so one skipped nightly run is tolerated and a longer outage is not). That is its only write to `articles`, it is an unpublish and never an archive (archive is terminal for an article), and it still writes no links or conflict resolutions. 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), `generic_title` (a placeholder title that collides on active-title uniqueness and blocks hub creation). Two classes are **RETIRED** (#605) and no longer produced, though the `class` filter still accepts them so historical reports stay readable: `contradiction_candidate` (the nightly lint judges those pairs itself now) and `stale_entry` (age is not a defect signal for stale articles call `knowledge_lint`, which computes them with a caller-chosen `stale_days`). **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` are vestigial — nothing reads them to decide anything, there is no approve/reject surface and there will not be one (#605 supersedes #594); auto-apply is gated on reversibility and two-run agreement. They still reset to pending/null whenever the nightly pass re-derives a proposal, so refreshed machine output never inherits an earlier verdict. Requires orchestrator role. Optional: `day` (ISO8601, default most recent report), `class`, `limit` (default 50, max 500), `offset`. |
308
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`). |
309
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`. |
310
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. |
package/index.js CHANGED
@@ -1786,8 +1786,9 @@ async function knowledgeUnpublish({ article_id }) {
1786
1786
  return toContent(result);
1787
1787
  }
1788
1788
 
1789
- // #331: single-article archive is agent-role KB curation (reversible soft delete,
1790
- // audited, visibility-scoped server-side).
1789
+ // #331: single-article archive is agent-role KB curation (non-destructive soft delete,
1790
+ // audited, visibility-scoped server-side). NOT reversible in code — #606/#605: `:archived`
1791
+ // is a terminal status. The row survives; nothing automated brings it back.
1791
1792
  async function knowledgeArchive({ article_id }) {
1792
1793
  const result = await apiCall(
1793
1794
  "POST",
@@ -1941,12 +1942,16 @@ async function knowledgeConsolidation({ day, class: klass, limit, offset } = {})
1941
1942
  return toContent(result);
1942
1943
  }
1943
1944
 
1944
- async function knowledgeIngest({ url, content, source_type, project_id, publish }) {
1945
+ async function knowledgeIngest({ url, content, source_type, project_id, publish, metadata }) {
1945
1946
  const body = { source_type };
1946
1947
  if (url) body.url = url;
1947
1948
  if (content) body.content = content;
1948
1949
  if (project_id) body.project_id = project_id;
1949
1950
  if (publish) body.publish = true;
1951
+ // Forwarded so `metadata.source_ref` is reachable at all: the server honours it as the
1952
+ // source that article titles are qualified with, and declaring it in the schema above
1953
+ // without forwarding it here would advertise a parameter that silently does nothing.
1954
+ if (metadata && typeof metadata === "object") body.metadata = metadata;
1950
1955
  const result = await apiCall("POST", "/api/v1/knowledge/ingest", body, process.env.LOOPCTL_ORCH_KEY);
1951
1956
  // A keyless tenant gets a 422 (code no_api_key) carrying a remediation — surface it
1952
1957
  // prominently so a first-time agent knows to call set_llm_config before ingesting.
@@ -4817,7 +4822,9 @@ const TOOLS = [
4817
4822
  "project CLAUDE.mds and cross-links). Send only the fields you want to change; every " +
4818
4823
  "field is optional except article_id. `tags` REPLACES the whole array (send the full " +
4819
4824
  "desired set, not a delta). A changed body/tags re-triggers embedding + auto-linking. " +
4820
- "Agent role — this is KB-content curation (reversible + audited). Visibility-scoped: " +
4825
+ "Agent role — this is KB-content curation (non-destructive + audited: the edit is in " +
4826
+ "place and there is no version-restore endpoint, but the prior body is retained in the " +
4827
+ "article.updated audit entry). Visibility-scoped: " +
4821
4828
  "you can only edit an article you can see, so another agent's private/owner memory " +
4822
4829
  "returns 404. `tenant_id` is never accepted. Returns the full updated article. To " +
4823
4830
  "instead retire/replace an article, use knowledge_archive or knowledge_resolve_conflict.",
@@ -5201,11 +5208,15 @@ const TOOLS = [
5201
5208
  {
5202
5209
  name: "knowledge_archive",
5203
5210
  description:
5204
- "Archive an article (soft delete). The article is hidden from search, context, " +
5205
- "and the index but the row is retained for audit/history (reversible re-publish " +
5206
- "or edit it back). Works for drafts and published articles. Agent role KB-content " +
5207
- "curation. Visibility-scoped: you can only archive an article you can see, so " +
5208
- "another agent's private/owner memory returns 404.",
5211
+ "Archive an article (soft delete). The article is hidden from search, context, and " +
5212
+ "the index but the row is retained for audit/history. NOT reversible by you: " +
5213
+ "`:archived` is a TERMINAL article status there is no unarchive call and no " +
5214
+ "{archived -> anything} transition, so restoring one needs a user-role PATCH with an " +
5215
+ "explicit status. Nothing is destroyed, but do not reach for this as an undoable " +
5216
+ "action. If you want a RETRACTION you can undo, use knowledge_unpublish (published " +
5217
+ "-> draft) and knowledge_publish to put it back. Works for drafts and published " +
5218
+ "articles. Agent role — KB-content curation. Visibility-scoped: you can only archive " +
5219
+ "an article you can see, so another agent's private/owner memory returns 404.",
5209
5220
  inputSchema: {
5210
5221
  type: "object",
5211
5222
  properties: {
@@ -5221,10 +5232,13 @@ const TOOLS = [
5221
5232
  name: "knowledge_delete",
5222
5233
  description:
5223
5234
  "Delete an article. Under the hood this performs the same soft-delete (archive) " +
5224
- "as knowledge_archive — use whichever name is clearer at the call site. The row " +
5225
- "is retained for audit; there is no hard delete (that is knowledge_bulk_delete " +
5226
- "hard:true, which stays user-gated). Agent role KB-content curation, reversible + " +
5227
- "audited, visibility-scoped (another agent's private/owner memory 404s).",
5235
+ "as knowledge_archive — use whichever name is clearer at the call site, and note " +
5236
+ "that it inherits archive's terminality: the row is retained for audit, but " +
5237
+ "`:archived` has no outbound transition, so NOTHING you can call restores it. " +
5238
+ "For a retraction you can undo, use knowledge_unpublish instead. There is no hard " +
5239
+ "delete here (that is knowledge_bulk_delete hard:true, which stays user-gated). " +
5240
+ "Agent role — KB-content curation: non-destructive + audited, visibility-scoped " +
5241
+ "(another agent's private/owner memory 404s).",
5228
5242
  inputSchema: {
5229
5243
  type: "object",
5230
5244
  properties: {
@@ -5239,7 +5253,9 @@ const TOOLS = [
5239
5253
  {
5240
5254
  name: "knowledge_bulk_delete",
5241
5255
  description:
5242
- "Bulk archive (default, reversible) or IRREVERSIBLE hard-delete of articles by selector. " +
5256
+ "Bulk archive (default, non-destructive but NOT reversible by any call you can make " +
5257
+ "`:archived` is terminal; restoring needs a user-role PATCH) or IRREVERSIBLE hard-delete " +
5258
+ "of articles by selector. " +
5243
5259
  "REQUIRES LOOPCTL_USER_KEY (user role — orchestrator is NOT sufficient). Provide EXACTLY ONE " +
5244
5260
  "selector: article_ids (explicit list), source_type + source_id (every active article from " +
5245
5261
  "that source), or tag + confirm:true (every active article carrying the tag — high blast " +
@@ -5285,8 +5301,9 @@ const TOOLS = [
5285
5301
  hard: {
5286
5302
  type: "boolean",
5287
5303
  description:
5288
- "IRREVERSIBLE hard delete (vs default reversible archive). Run dry_run first to get a " +
5289
- "token, then pass hard:true + token.",
5304
+ "IRREVERSIBLE hard delete (vs the default soft archive, which is non-destructive but " +
5305
+ "terminal — `:archived` has no outbound transition, so restoring one needs a user-role " +
5306
+ "PATCH). Run dry_run first to get a token, then pass hard:true + token.",
5290
5307
  },
5291
5308
  token: {
5292
5309
  type: "string",
@@ -5477,16 +5494,23 @@ const TOOLS = [
5477
5494
  description:
5478
5495
  "Read the nightly consolidation (\"dream\") report: NUMBERED proposals for reconciling " +
5479
5496
  "the corpus, each naming the articles involved and quoting an excerpt from each as " +
5480
- "evidence. REPORT ONLY the pass writes no articles, links or conflict resolutions, " +
5481
- "every proposal is `pending`, and this tool applies nothing. Requires orchestrator role.\n\n" +
5497
+ "evidence. THIS TOOL applies nothing and recomputes nothing it returns persisted rows. " +
5498
+ "The PASS it reports on does write: since #608 the nightly run UNPUBLISHES the losers of " +
5499
+ "each `duplicate_capture` group that two consecutive reports both propose — consecutive " +
5500
+ "meaning the previous report is at most 2 days older, so ONE skipped nightly run is " +
5501
+ "tolerated and a longer outage is not. That is its " +
5502
+ "only write to articles, it is an unpublish and never an archive (archive is terminal for " +
5503
+ "an article), and it still writes no links or conflict resolutions. Requires orchestrator " +
5504
+ "role.\n\n" +
5482
5505
  "Classes: `duplicate_capture` (titles that collide once case/punctuation normalize away, " +
5483
5506
  "or idempotency keys that collide under the same normalization while differing verbatim — " +
5484
5507
  "capture tag-format drift, which the novelty gate does not catch because novelty scoring " +
5485
- "and idempotency are separate paths); `contradiction_candidate` (a SYSTEM-flagged " +
5486
- "potential_conflict pair of PUBLISHED articles with no recorded verdict record one via " +
5487
- "knowledge_resolve_conflict, which accepts exactly these pairs; this report writes " +
5488
- "none); `generic_title` (a placeholder title that collides on active-title uniqueness and " +
5489
- "blocks hub creation); `stale_entry` (past the lint staleness threshold, never reconciled).\n\n" +
5508
+ "and idempotency are separate paths); `generic_title` (a placeholder title that collides " +
5509
+ "on active-title uniqueness and blocks hub creation). Two classes are RETIRED (#605) and " +
5510
+ "no longer produced, though the `class` filter still accepts them so historical reports " +
5511
+ "stay readable: `contradiction_candidate` (the nightly lint judges those pairs itself now) " +
5512
+ "and `stale_entry` (age is not a defect signal — for stale articles call knowledge_lint, " +
5513
+ "which computes them with a caller-chosen `stale_days`).\n\n" +
5490
5514
  "Denominators: `corpus_size` counts PUBLISHED articles owned by the tenant at scan time, " +
5491
5515
  "not its total article count. `proposal_count` is the TRUE pre-cap count of PROPOSALS, not " +
5492
5516
  "of articles — one duplicate group of three articles is ONE proposal, and one article can " +
@@ -5494,8 +5518,11 @@ const TOOLS = [
5494
5518
  "report carries, lower than `proposal_count` exactly when a class hit `max_per_class` " +
5495
5519
  "(`truncated` flags which). `meta.total_count` counts persisted proposals matching the " +
5496
5520
  "`class` filter, so it is bounded by `persisted_count`, never by `proposal_count`.\n\n" +
5497
- "Review state (`review_status`/`reviewed_by`/`reviewed_at`) RESETS to pending/null whenever " +
5498
- "the nightly pass re-derives a proposal: refreshed machine output never inherits an approval.",
5521
+ "Review state (`review_status`/`reviewed_by`/`reviewed_at`) is VESTIGIAL: nothing reads it " +
5522
+ "to decide anything, there is no approve/reject surface and there will not be one (#605 " +
5523
+ "supersedes #594) — auto-apply is gated on reversibility and two-run agreement, not on an " +
5524
+ "approval. It still RESETS to pending/null whenever the nightly pass re-derives a proposal, " +
5525
+ "so refreshed machine output can never inherit an earlier verdict.",
5499
5526
  inputSchema: {
5500
5527
  type: "object",
5501
5528
  properties: {
@@ -5644,6 +5671,25 @@ const TOOLS = [
5644
5671
  description:
5645
5672
  "Optional: publish extracted articles immediately instead of staging them as drafts (default false).",
5646
5673
  },
5674
+ metadata: {
5675
+ type: "object",
5676
+ description:
5677
+ "Optional metadata map. `source_ref` is the one key with behaviour: it names the " +
5678
+ "SPECIFIC source (a URL, repo, or document name) and is what lets extracted article " +
5679
+ "titles qualify themselves — without it a CHANGELOG file can only become an article " +
5680
+ "titled \"Changelog\", which is indistinguishable from every other document's " +
5681
+ "changelog once it is in the corpus. It overrides the name derived from `url`, and " +
5682
+ "is the ONLY way to name the source of an inline `content` ingest. Its value is " +
5683
+ "included in the extraction prompt POSTed to the tenant's LLM provider (reduced the " +
5684
+ "same way a url is: userinfo and query string stripped, host and path kept). Omit it " +
5685
+ "rather than passing a placeholder — a model will qualify a title WITH it.",
5686
+ properties: {
5687
+ source_ref: {
5688
+ type: "string",
5689
+ description: "The specific source that article titles are qualified with.",
5690
+ },
5691
+ },
5692
+ },
5647
5693
  },
5648
5694
  required: ["source_type"],
5649
5695
  },
@@ -5689,7 +5735,19 @@ const TOOLS = [
5689
5735
  },
5690
5736
  metadata: {
5691
5737
  type: "object",
5692
- description: "Optional metadata map.",
5738
+ description:
5739
+ "Optional metadata map. Set `source_ref` to the SPECIFIC source (URL, repo, " +
5740
+ "or document name) so extracted titles qualify themselves — without it a " +
5741
+ "CHANGELOG becomes an article titled \"Changelog\", indistinguishable from " +
5742
+ "every other document's changelog in the corpus. Overrides the url-derived " +
5743
+ "name, and is the only way to name an inline `content` item. Sent to the " +
5744
+ "LLM provider in the extraction prompt. Omit rather than passing a placeholder.",
5745
+ properties: {
5746
+ source_ref: {
5747
+ type: "string",
5748
+ description: "The specific source that article titles are qualified with.",
5749
+ },
5750
+ },
5693
5751
  },
5694
5752
  },
5695
5753
  required: ["source_type"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.70.0",
3
+ "version": "2.72.0",
4
4
  "description": "MCP server for loopctl \u2014 structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",