loopctl-mcp-server 2.45.0 → 2.47.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 +25 -2
  2. package/index.js +114 -6
  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 (89)
124
+ ## Tools (95)
125
125
 
126
126
  > Plus **per-tenant generated Context Retriever tools** (`cr_*`) appended
127
127
  > dynamically at runtime — see [Dynamic per-tenant Context Retriever
@@ -139,6 +139,25 @@ autonomous agent) can self-remediate without a human. Full agent-tenant lifecycl
139
139
  | `get_progress` | Get progress summary for a project, including story counts by status. Pass `include_cost=true` for cost data. |
140
140
  | `import_stories` | Import stories into a project from a structured payload (Epic 12 import format). Pass `merge: true` to add stories to epics that already exist (otherwise duplicates return 409). For large payloads, use `payload_path` to read JSON from disk instead of passing it inline. |
141
141
 
142
+ ### KB Scope Tools (agent key)
143
+
144
+ Knowledge-only project scopes (`kind: kb`) partition knowledge articles by repo. Unlike `create_project` (work project, orchestrator+ / human-anchored), these are available to an agent-rooted (KB-tier) tenant on an agent key and carry NO chain-of-custody / work-breakdown surface.
145
+
146
+ | Tool | Description |
147
+ |---|---|
148
+ | `create_kb_scope` | Create a knowledge-only project scope (`kind: kb`) for the current tenant. A kb scope cannot host epics/stories/dispatch/ui-tests — it exists only to partition knowledge articles by repo. Resolve it via `resolve_project` and pass the returned id as `project_id` on article/knowledge writes. Counts toward the tenant's `max_projects` budget. |
149
+ | `archive_kb_scope` | Archive (reversible soft-delete) a kb scope you own. Frees the scope's slot in the tenant's `max_projects` budget; its articles remain readable/writable. Rejects a `kind: work` project (422). Idempotent on an already-archived scope. |
150
+ | `restore_kb_scope` | Restore (re-activate) an archived kb scope you own — the reverse of `archive_kb_scope`. Consumes an active `max_projects` slot, so it is rejected (422) when the tenant is at its cap. Rejects a `kind: work` project (422). |
151
+
152
+ ### Repo Coordination Tools (agent key)
153
+
154
+ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for agents to share working state. A channel IS a `project_id` (a work project or a kb scope); posts are RLS-scoped to the caller's tenant, so this is an agent-role coordination surface, not a chain-of-custody gate.
155
+
156
+ | Tool | Description |
157
+ |---|---|
158
+ | `channel_post` | Post a message to a repo coordination channel. Provide a `key` to upsert your per-session working-state slot (200) instead of appending a new post (201); omit it to append. `host` and `session_id` are proxy-supplied — do NOT pass them. Optional structured `refs` map (`file`, `pr`, `branch`, `commit`). Required: `project_id`, `body`. |
159
+ | `channel_recent` | Read recent posts from a repo coordination channel — RLS returns only your own tenant's channel (oracle-safe read). Use `since` (a full ISO8601 instant) to page forward and `limit` to cap results (default 25, max 100). Required: `project_id`. |
160
+
142
161
  ### Story Tools
143
162
 
144
163
  | Tool | Description |
@@ -188,7 +207,7 @@ autonomous agent) can self-remediate without a human. Full agent-tenant lifecycl
188
207
  | `get_cost_summary` | orch | Get cost/token usage summary for a project, optionally broken down by `agent`, `epic`, or `model`. |
189
208
  | `get_story_token_usage` | orch | Get all token usage records for a single story. |
190
209
  | `get_cost_anomalies` | orch | Get cost anomaly alerts — stories or agents exceeding expected budgets. Optionally filter by project. |
191
- | `get_ingestion_anomalies` | orch | Get ingestion-health anomalies — capture-silence (a source_type stopped producing articles). Check whether knowledge capture is still landing. |
210
+ | `get_ingestion_anomalies` | orch | Get ingestion-health anomalies — capture_silence (a source_type stopped producing articles) and high_reject_rate (writes rejected at high rate, persisting no article row). Check whether knowledge capture is still landing AND being accepted. |
192
211
  | `set_token_budget` | orch | Set a token budget (in millicents) for a project, epic, story, or agent scope. Requires orchestrator role. |
193
212
 
194
213
  ### Knowledge Wiki Tools (agent key)
@@ -323,6 +342,10 @@ Key distribution for the dispatch pattern (Epic 26): per-dispatch ephemeral keys
323
342
  | `dispatch` | Mint an ephemeral, scoped api_key for a sub-agent dispatch, carrying its lineage path. The `raw_key` is returned ONCE — pass it to the sub-agent's launch args, never store it in env vars; it expires after `expires_in_seconds` (default 3600, max 14400). Required: `role` (`agent`/`orchestrator`), `agent_id`. Optional: `parent_dispatch_id`, `story_id`. |
324
343
  | `recover_cap` | Re-mint a capability token for a story you're assigned to, after a session crash lost your cap. Required: `story_id`. Optional: `cap_type` (`start_cap`/`report_cap`, default `start_cap`), `lineage`. |
325
344
  | `get_sth` | Get the latest Signed Tree Head for a tenant's tamper-evident audit chain. Public — no auth required. Required: `tenant_id`. |
345
+ | `request_authenticator_challenge` | **US-26.7.2.** Step 1 of the opt-in WebAuthn trust-tier upgrade ceremony: issues a registration challenge for enrolling a hardware authenticator against an EXISTING agent-rooted (KB-tier) tenant, promoting it to `human_anchored` on success. Requires an interactive WebAuthn client. |
346
+ | `enroll_authenticator` | Step 2 of the WebAuthn trust-tier upgrade ceremony: completes enrollment with the attestation produced by `navigator.credentials.create()` against the challenge from `request_authenticator_challenge`. On a tenant's first enrollment the tenant is promoted to `human_anchored`. |
347
+ | `request_authenticator_revoke_challenge` | Issues a fresh-assertion challenge to authorize revoking one of a tenant's enrolled WebAuthn authenticators. Requires an interactive WebAuthn client + human touch to produce the assertion `revoke_authenticator` needs. |
348
+ | `revoke_authenticator` | Revokes an enrolled authenticator using the assertion from `request_authenticator_revoke_challenge` (`navigator.credentials.get()` against an existing authenticator — human touch required). Refuses (409 `last_authenticator`) when it would leave a human-anchored tenant with no authenticators. |
326
349
 
327
350
  ## Wiki Attribution
328
351
 
package/index.js CHANGED
@@ -437,6 +437,46 @@ async function restoreKbScope({ project_id }) {
437
437
  return toContent(result);
438
438
  }
439
439
 
440
+ async function channelPost({ project_id, body, key, refs }) {
441
+ // Repo Coordination Bus (Epic 39): post a coordination message to a channel
442
+ // (a channel IS a project_id — a work project or a kb scope). Agent-role, RLS
443
+ // tenant-scoped — posting to your own tenant's channel is coordination, NOT
444
+ // self-approval (owner decision #331), so it carries no chain-of-custody authority.
445
+ const payload = { project_id, body };
446
+ if (key) payload.key = key;
447
+ if (refs) payload.refs = refs;
448
+ // host + session_id are proxy-filled (NOT caller args). host from os.hostname();
449
+ // session_id from CLAUDE_SESSION_ID (the SAME id SessionStart sees) so US-39.6
450
+ // self-dedup can skip a session's own echoed posts. Omit session_id entirely when
451
+ // unset — it is client-supplied + informational, never a security dependency.
452
+ payload.host = os.hostname();
453
+ if (process.env.CLAUDE_SESSION_ID) payload.session_id = process.env.CLAUDE_SESSION_ID;
454
+ const result = await apiCall(
455
+ "POST",
456
+ "/api/v1/channel/posts",
457
+ payload,
458
+ process.env.LOOPCTL_AGENT_KEY,
459
+ );
460
+ return toContent(result);
461
+ }
462
+
463
+ async function channelRecent({ project_id, since, limit }) {
464
+ // Read recent coordination posts for a channel (project_id) on the AGENT key.
465
+ // Oracle-safe read: returns the tenant's own channel only (RLS). `since` is a full
466
+ // ISO8601 instant (date-only is ignored server-side); limit defaults to 25, max 100.
467
+ const params = new URLSearchParams();
468
+ if (project_id) params.set("project_id", project_id);
469
+ if (since) params.set("since", since);
470
+ if (limit) params.set("limit", limit);
471
+ const result = await apiCall(
472
+ "GET",
473
+ `/api/v1/channel/posts?${params}`,
474
+ null,
475
+ process.env.LOOPCTL_AGENT_KEY,
476
+ );
477
+ return toContent(result);
478
+ }
479
+
440
480
  async function deleteProject({ project_id }) {
441
481
  const result = await apiCall(
442
482
  "DELETE",
@@ -2207,6 +2247,62 @@ const TOOLS = [
2207
2247
  required: ["project_id"],
2208
2248
  },
2209
2249
  },
2250
+ {
2251
+ name: "channel_post",
2252
+ description:
2253
+ "Post a message to a repo coordination channel (Epic 39 Repo Coordination Bus) on the agent key. A channel IS a project_id (a work project or a kb scope); posts are tenant-isolated by RLS. This is an agent-role COORDINATION surface, not chain-of-custody — posting to your own tenant's channel is not self-approval. host is auto-filled from the proxy's os.hostname() and session_id is auto-filled from the Claude Code session id (both proxy-supplied, informational only — do NOT pass them). Provide a key to upsert your per-session working-state slot (200) instead of appending a new post (201); omit it to append.",
2254
+ inputSchema: {
2255
+ type: "object",
2256
+ properties: {
2257
+ project_id: {
2258
+ type: "string",
2259
+ description: "UUID of the channel (a work project or kb scope) to post to.",
2260
+ },
2261
+ body: { type: "string", description: "The coordination message body." },
2262
+ key: {
2263
+ type: "string",
2264
+ description:
2265
+ "Optional per-session working-state slot key. When given, upserts the caller's slot for that key instead of appending a new post. Requires an active Claude Code session: the upsert is keyed on the auto-filled session_id (from CLAUDE_SESSION_ID), so a keyed post made outside a Claude Code session — where that env var is absent — is rejected with a 422 (session_id can't be blank). Omit key to append a plain post, which needs no session.",
2266
+ },
2267
+ refs: {
2268
+ type: "object",
2269
+ description:
2270
+ "Optional structured references map: { file, pr, branch, commit }.",
2271
+ properties: {
2272
+ file: { type: "string" },
2273
+ pr: { type: "string" },
2274
+ branch: { type: "string" },
2275
+ commit: { type: "string" },
2276
+ },
2277
+ },
2278
+ },
2279
+ required: ["project_id", "body"],
2280
+ },
2281
+ },
2282
+ {
2283
+ name: "channel_recent",
2284
+ description:
2285
+ "Read recent posts from a repo coordination channel (Epic 39 Repo Coordination Bus) on the agent key. A channel IS a project_id; RLS returns only your own tenant's channel, so this is an oracle-safe read. Use since (a full ISO8601 instant) to page forward from a known point and limit to cap results (default 25, max 100).",
2286
+ inputSchema: {
2287
+ type: "object",
2288
+ properties: {
2289
+ project_id: {
2290
+ type: "string",
2291
+ description: "UUID of the channel (a work project or kb scope) to read.",
2292
+ },
2293
+ since: {
2294
+ type: "string",
2295
+ description:
2296
+ "Optional ISO8601 instant; return posts newer than this (date-only is ignored server-side).",
2297
+ },
2298
+ limit: {
2299
+ type: "integer",
2300
+ description: "Optional max posts to return (default 25, max 100).",
2301
+ },
2302
+ },
2303
+ required: ["project_id"],
2304
+ },
2305
+ },
2210
2306
  {
2211
2307
  name: "delete_project",
2212
2308
  description:
@@ -2783,10 +2879,12 @@ const TOOLS = [
2783
2879
  {
2784
2880
  name: "get_ingestion_anomalies",
2785
2881
  description:
2786
- "Get ingestion-health anomalies — capture-silence (a source_type that was producing " +
2787
- "articles has gone silent). Use to check whether knowledge capture is still landing. " +
2788
- "Paginated (page/page_size); advance `page` to enumerate all. Filter by source_type, " +
2789
- "anomaly_type, resolved status, or include archived.",
2882
+ "Get ingestion-health anomalies — capture_silence (a source_type that was producing " +
2883
+ "articles has gone silent) and high_reject_rate (writes attempted but rejected at high " +
2884
+ "rate — 409 title_conflict / validation drops that persist no article row). Use to check " +
2885
+ "whether knowledge capture is still landing AND being accepted. Paginated (page/page_size); " +
2886
+ "advance `page` to enumerate all. Filter by source_type, anomaly_type, resolved status, or " +
2887
+ "include archived.",
2790
2888
  inputSchema: {
2791
2889
  type: "object",
2792
2890
  properties: {
@@ -2796,8 +2894,12 @@ const TOOLS = [
2796
2894
  },
2797
2895
  anomaly_type: {
2798
2896
  type: "string",
2799
- enum: ["capture_silence"],
2800
- description: 'Optional: filter by anomaly type (only "capture_silence" is currently produced).',
2897
+ // Keep in sync with Loopctl.Knowledge.IngestionAnomaly @anomaly_types (the
2898
+ // server-side Ecto.Enum + the ingestion_anomalies_anomaly_type_check DB CHECK).
2899
+ enum: ["capture_silence", "high_reject_rate"],
2900
+ description:
2901
+ 'Optional: filter by anomaly type — "capture_silence" (writes stopped) or ' +
2902
+ '"high_reject_rate" (writes rejected at high rate).',
2801
2903
  },
2802
2904
  resolved: {
2803
2905
  type: "string",
@@ -5011,6 +5113,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5011
5113
  case "restore_kb_scope":
5012
5114
  return await restoreKbScope(args);
5013
5115
 
5116
+ case "channel_post":
5117
+ return await channelPost(args);
5118
+
5119
+ case "channel_recent":
5120
+ return await channelRecent(args);
5121
+
5014
5122
  case "delete_project":
5015
5123
  return await deleteProject(args);
5016
5124
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.45.0",
3
+ "version": "2.47.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",