loopctl-mcp-server 2.41.0 → 2.42.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 -2
  2. package/index.js +168 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -121,7 +121,7 @@ REST endpoint (`PATCH /api/v1/tenants/me/llm-config`), and the docs — so you (
121
121
  autonomous agent) can self-remediate without a human. Full agent-tenant lifecycle:
122
122
  [`docs/onboarding-agent-tenant.md`](../docs/onboarding-agent-tenant.md).
123
123
 
124
- ## Tools (84)
124
+ ## Tools (89)
125
125
 
126
126
  > Plus **per-tenant generated Context Retriever tools** (`cr_*`) appended
127
127
  > dynamically at runtime — see [Dynamic per-tenant Context Retriever
@@ -133,6 +133,7 @@ autonomous agent) can self-remediate without a human. Full agent-tenant lifecycl
133
133
  |---|---|
134
134
  | `get_tenant` | Get current tenant info. Use to verify connectivity. |
135
135
  | `list_projects` | List all projects in the current tenant. |
136
+ | `resolve_project` | Resolve a repo to its `project_id` in one cheap call — provide any of `slug`, `repo_url` (`git@github.com:owner/repo.git`, `https://github.com/owner/repo`, and bare `owner/repo` all match), or `name`. Precedence `slug > repo_url > name`, first match wins. Use the returned `id` to scope captures/recall (`memory_*`, `recall_context`). `404` `not_found` if nothing matches, `422` `no_identifier` if none supplied, `409` if a fuzzy identifier matches more than one active project. |
136
137
  | `create_project` | Create a new project in the current tenant. |
137
138
  | `delete_project` | **Requires `LOOPCTL_USER_KEY`.** Delete a project and all of its dependent resources (epics, stories, audit entries). Irreversible — orchestrator role is not sufficient. |
138
139
  | `get_progress` | Get progress summary for a project, including story counts by status. Pass `include_cost=true` for cost data. |
@@ -243,7 +244,9 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
243
244
  | `memory_recall` | Semantically recall your own long-term memories most similar to `query`. When embedding generation is unavailable the response degrades to a recent-first text match with `meta.fallback: true` and a stable `meta.reason` (score is `null` on that path) — check `meta.fallback` before treating a short/empty result as a genuinely empty scope. `meta.total_count`/`meta.underfilled` are also returned. Optional: `limit`, `include_superseded`. |
244
245
  | `memory_list` | List your own long-term memories, newest first, paginated with `meta.total_count/limit/offset` (the true scoped count, never silently capped by `limit`). Optional: `limit`, `offset`, `include_superseded`, `all_subjects` (superadmin only; ignored for non-superadmin keys). |
245
246
  | `memory_forget` | Delete one of your own long-term memories by id. A foreign-subject, foreign-tenant, or unknown id returns 404 (no existence leak). Required: `id`. |
246
- | `memory_promote` | Call at session end to compile this session's short-term (`session`-tier) memory into durable `long_term` memory — unlike `memory_remember` (a single explicit write), this compiles the whole session in one shot; fire it once at session end, not per turn. Returns 202 with `{job_id, session_id, status: "enqueued"}` — promotion runs asynchronously, so the resulting memory is recallable via `memory_recall` only after the worker drains. You can only promote your own sessions (scope resolved server-side from your key). Required: `session_id`. |
247
+ | `memory_promote` | Call at session end to compile this session's short-term (`session`-tier) memory into durable `long_term` memory — unlike `memory_remember` (a single explicit write), this compiles the whole session in one shot; fire it once at session end, not per turn. Returns 202 with `{session_id, status: "enqueued"}` — promotion runs asynchronously, so the resulting memory is recallable via `memory_recall` only after the worker drains. You can only promote your own sessions (scope resolved server-side from your key). Required: `session_id`. |
248
+ | `recall_context` | ONE round-trip returning the re-ranked `global ∪ active-project` union of long-term MEMORY **and** KNOWLEDGE for `query` — what you previously assembled by calling `memory_recall` and `knowledge_search` separately. Pass `project_id` (from `resolve_project`) to merge global with that project on both sides; absent → global-only. The knowledge half is combined-search *summaries* (not full bodies — use `knowledge_context` for those). Response carries merged `results` (each tagged `source: memory\|knowledge`) plus the untouched per-source `memory`/`knowledge` envelopes; `meta.degraded?` flags a one-sided degrade (the other side is still returned — never a 500). A blank query, or one over 500 chars, is a `422` up front. Required: `query`. Optional: `project_id`, `limit`. |
249
+ | `memory_graduate` | Graduate ONE of your long-term memories into a durable Knowledge Wiki article — the explicit, on-demand version of the hourly graduation sweep. Use when a private memory has proven valuable enough to become durable knowledge. **Visibility**: the graduated article stays **owner-visible** (`metadata.visibility: "owner"`, keyed to your subject) — discoverable by YOU, NOT peer-readable (graduation does not share a memory to teammates; `re_scope: "global"` widens only the project scope, not visibility). Scope is key-derived (you can only graduate your OWN memory; a foreign/unknown `memory_id` → 404). DEDUPED by the novelty gate: `data.verdict` is `created` (novel → published) or `gated_to_draft` (near-dup → review draft) with a new article (**201**), or `duplicate`/`deduplicated` (already represented → canonical article, nothing created) (**200**). By default the article inherits the memory's project scope; pass `re_scope: "global"` to promote a PROJECT memory to a tenant-wide article — only valid on its FIRST graduation, and only if the hourly sweep hasn't graduated it project-scoped first (`409` `already_graduated` otherwise). An already-graduated global memory re-graduates idempotently (**200**). `503` `gate_unavailable` if the embedding backend is down — retry later. Required: `memory_id`. Optional: `re_scope` (`inherit`\|`global`). |
247
250
 
248
251
  ### Knowledge Management Tools (orchestrator key)
249
252
 
package/index.js CHANGED
@@ -376,6 +376,22 @@ async function listProjects(args = {}) {
376
376
  return toContent(result);
377
377
  }
378
378
 
379
+ async function resolveProject({ slug, repo_url, name } = {}) {
380
+ // Cheap repo -> project_id resolution (loopctl #411 Gap 1). Server tries
381
+ // slug -> repo_url -> name and returns the first match; agent-role read.
382
+ const params = new URLSearchParams();
383
+ if (slug) params.set("slug", slug);
384
+ if (repo_url) params.set("repo_url", repo_url);
385
+ if (name) params.set("name", name);
386
+ const result = await apiCall(
387
+ "GET",
388
+ `/api/v1/projects/resolve?${params}`,
389
+ null,
390
+ process.env.LOOPCTL_AGENT_KEY,
391
+ );
392
+ return toContent(result);
393
+ }
394
+
379
395
  async function createProject({ name, slug, repo_url, description, tech_stack, mission }) {
380
396
  const body = { name, slug };
381
397
  if (repo_url) body.repo_url = repo_url;
@@ -1206,6 +1222,26 @@ async function memoryRecall({ query, limit, include_superseded }) {
1206
1222
  return toContent(result);
1207
1223
  }
1208
1224
 
1225
+ async function recallContext({ query, project_id, limit }) {
1226
+ // Merged recall (#411 Gap 2): ONE round-trip returning the re-ranked
1227
+ // global ∪ active-project union of long-term MEMORY and KNOWLEDGE. Scope
1228
+ // (tenant_id/subject_id) is derived server-side from the agent key; project_id is
1229
+ // the partition key (merges global with that project on BOTH sides).
1230
+ const payload = { query };
1231
+ if (project_id) payload.project_id = project_id;
1232
+ if (limit != null) payload.limit = limit;
1233
+
1234
+ const result = await apiCall(
1235
+ "POST",
1236
+ "/api/v1/recall",
1237
+ payload,
1238
+ process.env.LOOPCTL_AGENT_KEY,
1239
+ );
1240
+ // Surface both per-source metas (memory fallback/underfilled + knowledge degraded)
1241
+ // so the caller can tell a degraded recall from a genuinely empty scope.
1242
+ return toContent(result);
1243
+ }
1244
+
1209
1245
  async function memoryList({ limit, offset, include_superseded, all_subjects }) {
1210
1246
  // all_subjects is superadmin-only server-side; a non-superadmin key sending
1211
1247
  // this is ignored (falls back to its own subject) rather than erroring — the
@@ -1246,6 +1282,24 @@ async function memoryPromote({ session_id }) {
1246
1282
  return toContent(result);
1247
1283
  }
1248
1284
 
1285
+ async function memoryGraduate({ memory_id, re_scope }) {
1286
+ // Explicit memory→knowledge graduation (#411 Gap 3). Scope (tenant_id/subject_id)
1287
+ // is derived server-side from the agent key; memory_id identifies the caller's OWN
1288
+ // memory to graduate. The graduated article stays OWNER-visible (not peer-readable).
1289
+ // re_scope: "global" widens only the project partition (project → tenant-wide), NOT
1290
+ // visibility, and only on the memory's first graduation.
1291
+ const payload = { memory_id };
1292
+ if (re_scope) payload.re_scope = re_scope;
1293
+
1294
+ const result = await apiCall(
1295
+ "POST",
1296
+ "/api/v1/memory/graduate",
1297
+ payload,
1298
+ process.env.LOOPCTL_AGENT_KEY,
1299
+ );
1300
+ return toContent(result);
1301
+ }
1302
+
1249
1303
  // --- Knowledge Management Tools (orch key) ---
1250
1304
 
1251
1305
  async function knowledgePublish({ article_id }) {
@@ -1998,6 +2052,34 @@ const TOOLS = [
1998
2052
  required: [],
1999
2053
  },
2000
2054
  },
2055
+ {
2056
+ name: "resolve_project",
2057
+ description:
2058
+ "Resolve a repo to its project in one cheap call. Provide any of slug, " +
2059
+ "repo_url (git@github.com:owner/repo.git, https://github.com/owner/repo, " +
2060
+ "and bare owner/repo all match), or name. Precedence: slug > repo_url > " +
2061
+ "name; first match wins. Returns the project (use its id to scope " +
2062
+ "captures/recall), 404 not_found if nothing matches, 422 no_identifier " +
2063
+ "if none supplied.",
2064
+ inputSchema: {
2065
+ type: "object",
2066
+ properties: {
2067
+ slug: {
2068
+ type: "string",
2069
+ description: "Exact project slug (often the repo basename).",
2070
+ },
2071
+ repo_url: {
2072
+ type: "string",
2073
+ description: "Git remote URL or bare owner/repo.",
2074
+ },
2075
+ name: {
2076
+ type: "string",
2077
+ description: "Exact project name (case-insensitive).",
2078
+ },
2079
+ },
2080
+ required: [],
2081
+ },
2082
+ },
2001
2083
  {
2002
2084
  name: "create_project",
2003
2085
  description: "Create a new project in the current tenant.",
@@ -3484,6 +3566,45 @@ const TOOLS = [
3484
3566
  required: ["query"],
3485
3567
  },
3486
3568
  },
3569
+ {
3570
+ name: "recall_context",
3571
+ description:
3572
+ "MERGED recall in ONE round-trip: the re-ranked global ∪ active-project union of " +
3573
+ "your long-term MEMORY (private working state) AND the shared KNOWLEDGE wiki for " +
3574
+ "`query` — instead of calling memory_recall and knowledge_context/knowledge_search " +
3575
+ "separately and merging by hand (#411 Gap 2). Both sides merge global with the " +
3576
+ "active project: pass project_id to include that project's rows alongside global " +
3577
+ "ones on BOTH sides (another project's rows are excluded); omit it for global-only. " +
3578
+ "project_id is a PARTITION key, NOT isolation — scope (tenant/subject) is resolved " +
3579
+ "server-side from your key. Returns `data` (merged, each item tagged source: " +
3580
+ "memory|knowledge, sorted by a heuristically-comparable score DESC) PLUS the " +
3581
+ "untouched per-source `memory` and `knowledge` envelopes so you can re-rank. " +
3582
+ "Cross-source scores are heuristic, not calibrated. If the knowledge search " +
3583
+ "degrades (embedding unavailable) or errors, the memory side is still returned and " +
3584
+ "meta.degraded is true — never a hard failure.",
3585
+ inputSchema: {
3586
+ type: "object",
3587
+ properties: {
3588
+ query: {
3589
+ type: "string",
3590
+ description: "Text to embed / match against on BOTH the memory and knowledge sides.",
3591
+ },
3592
+ project_id: {
3593
+ type: "string",
3594
+ format: "uuid",
3595
+ description:
3596
+ "Optional: partition scope. Present → both sides return global ∪ that project; " +
3597
+ "omit → global-only.",
3598
+ },
3599
+ limit: {
3600
+ type: "integer",
3601
+ description:
3602
+ "Optional: overall merged page size, clamped to [1, 50] (default 10).",
3603
+ },
3604
+ },
3605
+ required: ["query"],
3606
+ },
3607
+ },
3487
3608
  {
3488
3609
  name: "memory_list",
3489
3610
  description:
@@ -3562,6 +3683,45 @@ const TOOLS = [
3562
3683
  required: ["session_id"],
3563
3684
  },
3564
3685
  },
3686
+ {
3687
+ name: "memory_graduate",
3688
+ description:
3689
+ "Graduate ONE of your long-term memories into a durable Knowledge Wiki article — the " +
3690
+ "explicit, on-demand version of the hourly graduation sweep. Use when a private memory " +
3691
+ "has proven valuable enough to become durable, curated knowledge. IMPORTANT: the " +
3692
+ "graduated article stays OWNER-VISIBLE (metadata.visibility=owner, keyed to your " +
3693
+ "subject) — discoverable by YOU, NOT peer-readable; graduation does NOT share a memory " +
3694
+ "with teammates, and re_scope only widens project scope, not visibility. Scope " +
3695
+ "(tenant_id/subject_id) is resolved server-side from your API key: you can only " +
3696
+ "graduate your OWN memory; a foreign/unknown memory_id returns 404 (no cross-subject " +
3697
+ "leak). By default the article inherits the memory's project scope; pass " +
3698
+ 're_scope: "global" to promote a PROJECT memory to a tenant-wide (global) article — ' +
3699
+ "only valid on the memory's FIRST graduation (re_scope: global on an already-graduated " +
3700
+ "PROJECT memory returns 409 already_graduated; on an already-graduated GLOBAL memory it " +
3701
+ "is an idempotent no-op → 200). The article is DEDUPED by the novelty gate: the " +
3702
+ 'response `data.verdict` is "created" (novel → published) or "gated_to_draft" ' +
3703
+ '(near-duplicate → review draft) with a new article, or "duplicate"/"deduplicated" ' +
3704
+ "(content already represented → the canonical article, nothing created). Returns 503 " +
3705
+ "gate_unavailable if the embedding backend is down — retry later.",
3706
+ inputSchema: {
3707
+ type: "object",
3708
+ properties: {
3709
+ memory_id: {
3710
+ type: "string",
3711
+ description: "UUID of your own long-term memory to graduate into a knowledge article.",
3712
+ },
3713
+ re_scope: {
3714
+ type: "string",
3715
+ enum: ["inherit", "global"],
3716
+ description:
3717
+ 'Article scope. "inherit" (default) keeps the memory\'s project scope; ' +
3718
+ '"global" promotes a PROJECT memory to a tenant-wide (global) article ' +
3719
+ "(only on its first graduation).",
3720
+ },
3721
+ },
3722
+ required: ["memory_id"],
3723
+ },
3724
+ },
3565
3725
 
3566
3726
  // Knowledge Management Tools (orchestrator key)
3567
3727
  {
@@ -4694,6 +4854,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4694
4854
  case "list_projects":
4695
4855
  return await listProjects(args);
4696
4856
 
4857
+ case "resolve_project":
4858
+ return await resolveProject(args);
4859
+
4697
4860
  case "create_project":
4698
4861
  return await createProject(args);
4699
4862
 
@@ -4833,6 +4996,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4833
4996
 
4834
4997
  case "memory_recall":
4835
4998
  return await memoryRecall(args);
4999
+ case "recall_context":
5000
+ return await recallContext(args);
4836
5001
 
4837
5002
  case "memory_list":
4838
5003
  return await memoryList(args);
@@ -4843,6 +5008,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4843
5008
  case "memory_promote":
4844
5009
  return await memoryPromote(args);
4845
5010
 
5011
+ case "memory_graduate":
5012
+ return await memoryGraduate(args);
5013
+
4846
5014
  // Knowledge Management Tools
4847
5015
  case "knowledge_publish":
4848
5016
  return await knowledgePublish(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.41.0",
3
+ "version": "2.42.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",