@stevenvincentone/intidev-agentloops 0.1.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.
package/dist/mcp.js ADDED
@@ -0,0 +1,442 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MCP_ACTOR_SOURCE = exports.WORKFLOW_STATUSES = exports.GUARD_STATUSES = exports.NOTE_TYPES = exports.CONFIDENCES = exports.SEVERITIES = exports.MCP_SERVER_NAME = exports.MCP_SCHEMA_VERSION = void 0;
4
+ exports.summaryTool = summaryTool;
5
+ exports.listTool = listTool;
6
+ exports.showTool = showTool;
7
+ exports.handoffTool = handoffTool;
8
+ exports.createTicketTool = createTicketTool;
9
+ exports.noteTool = noteTool;
10
+ exports.workflowTool = workflowTool;
11
+ exports.resolveTool = resolveTool;
12
+ exports.guardTool = guardTool;
13
+ exports.createMcpServer = createMcpServer;
14
+ exports.startStdioMcpServer = startStdioMcpServer;
15
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
16
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
17
+ const zod_1 = require("zod");
18
+ const store_1 = require("./store");
19
+ const handoff_1 = require("./handoff");
20
+ /**
21
+ * Schema version for the MCP tool envelopes. Bump only on breaking changes;
22
+ * add fields freely during dogfood (see docs/tickets agent JSON contracts).
23
+ */
24
+ exports.MCP_SCHEMA_VERSION = 1;
25
+ exports.MCP_SERVER_NAME = "agentloop";
26
+ function nowIso() {
27
+ return new Date().toISOString();
28
+ }
29
+ function envelope() {
30
+ return { schemaVersion: exports.MCP_SCHEMA_VERSION, generatedAt: nowIso() };
31
+ }
32
+ /**
33
+ * Pure read-only tool implementations. These are transport-agnostic and
34
+ * deterministic apart from `generatedAt`, so they can be unit-tested directly
35
+ * without standing up an MCP transport.
36
+ */
37
+ async function summaryTool(store) {
38
+ return { ...envelope(), summary: await store.summary() };
39
+ }
40
+ async function listTool(store, args = {}) {
41
+ const status = (args.status ?? "all");
42
+ const tickets = await store.listTickets({ status, kind: args.kind });
43
+ return {
44
+ ...envelope(),
45
+ filters: { status: args.status ?? "all", kind: args.kind ?? null },
46
+ count: tickets.length,
47
+ tickets,
48
+ };
49
+ }
50
+ async function showTool(store, args) {
51
+ const raw = args.id?.trim();
52
+ if (!raw) {
53
+ throw new Error("show requires an id");
54
+ }
55
+ if (/^PATTERN-/i.test(raw)) {
56
+ const pattern = await store.getPattern(raw.toUpperCase());
57
+ if (!pattern) {
58
+ throw new Error(`Pattern not found: ${raw}`);
59
+ }
60
+ return { ...envelope(), kind: "pattern", pattern };
61
+ }
62
+ const ticket = await store.showTicket(raw);
63
+ if (!ticket) {
64
+ throw new Error(`Not found: ${raw}`);
65
+ }
66
+ return { ...envelope(), kind: "ticket", ticket };
67
+ }
68
+ async function handoffTool(store, args) {
69
+ const raw = args.id?.trim();
70
+ if (!raw) {
71
+ throw new Error("handoff requires an id");
72
+ }
73
+ const ticket = await store.showTicket(raw);
74
+ if (!ticket) {
75
+ throw new Error(`Not found: ${raw}`);
76
+ }
77
+ return {
78
+ ...envelope(),
79
+ ticketId: ticket.id,
80
+ aliases: ticket.aliases,
81
+ prompt: (0, handoff_1.buildHandoffPrompt)(ticket),
82
+ };
83
+ }
84
+ exports.SEVERITIES = ["low", "medium", "high", "critical"];
85
+ exports.CONFIDENCES = ["low", "medium", "high"];
86
+ exports.NOTE_TYPES = [
87
+ "hypothesis",
88
+ "related_history",
89
+ "prior_fix",
90
+ "triage",
91
+ "investigation",
92
+ ];
93
+ exports.GUARD_STATUSES = [
94
+ "guard_added",
95
+ "guard_existing",
96
+ "guard_waived",
97
+ "guard_deferred",
98
+ "none",
99
+ ];
100
+ /** Workflow transitions exposed over MCP. `resolved` has its own tool. */
101
+ exports.WORKFLOW_STATUSES = ["active", "reopened", "deferred"];
102
+ /** Source recorded for tickets/notes created by an MCP agent client. */
103
+ exports.MCP_ACTOR_SOURCE = "agent";
104
+ async function createTicketTool(store, args) {
105
+ const config = store.getConfig();
106
+ const kind = (args.kind ?? config.defaultKind);
107
+ if (!config.ticketKinds.some((entry) => entry.kind === kind)) {
108
+ const valid = config.ticketKinds.map((entry) => entry.kind).join(", ");
109
+ throw new Error(`Unknown kind: ${kind} (valid: ${valid})`);
110
+ }
111
+ const ticket = await store.createTicket({
112
+ title: args.title ?? "",
113
+ summary: args.summary,
114
+ family: args.family ?? config.patterns.defaultFamily,
115
+ kind,
116
+ source: args.source ?? exports.MCP_ACTOR_SOURCE,
117
+ severity: args.severity,
118
+ confidence: args.confidence ?? "medium",
119
+ tags: args.tags ?? [],
120
+ handoffText: args.handoff,
121
+ });
122
+ return { ...envelope(), action: "created", ticket };
123
+ }
124
+ async function noteTool(store, args) {
125
+ const ticket = await store.addTicketNote(args.id, args.type ?? "triage", args.body, args.author ?? exports.MCP_ACTOR_SOURCE);
126
+ return { ...envelope(), action: "noted", ticket };
127
+ }
128
+ async function workflowTool(store, args) {
129
+ let ticket;
130
+ if (args.status === "active") {
131
+ ticket = await store.beginTicket(args.id);
132
+ }
133
+ else if (args.status === "reopened") {
134
+ ticket = await store.reopenTicket(args.id, args.reason ?? "recurrence detected");
135
+ }
136
+ else if (args.status === "deferred") {
137
+ ticket = await store.deferTicket(args.id, args.reason);
138
+ }
139
+ else {
140
+ throw new Error(`Unsupported workflow status: ${args.status} (use active|reopened|deferred; resolve via agentloop_resolve)`);
141
+ }
142
+ return { ...envelope(), action: "workflow", ticket };
143
+ }
144
+ async function resolveTool(store, args) {
145
+ const ticket = await store.resolveTicket({
146
+ id: args.id,
147
+ summary: args.summary,
148
+ verification: args.verification,
149
+ guardStatus: args.guardStatus ?? "none",
150
+ guardSummary: args.guardSummary,
151
+ });
152
+ return { ...envelope(), action: "resolved", ticket };
153
+ }
154
+ async function guardTool(store, args) {
155
+ const ticket = await store.setGuard(args.id, args.guardStatus, args.guardSummary);
156
+ return { ...envelope(), action: "guard", ticket };
157
+ }
158
+ function ok(payload) {
159
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
160
+ }
161
+ function fail(error) {
162
+ const message = error instanceof Error ? error.message : String(error);
163
+ return { content: [{ type: "text", text: message }], isError: true };
164
+ }
165
+ /**
166
+ * Build an MCP server over the given store. By default only the read-only tools
167
+ * are exposed; pass `allowWrites: true` to also register the guarded write
168
+ * tools (create / note / workflow / resolve / guard).
169
+ */
170
+ function createMcpServer(store, options = {}) {
171
+ const server = new mcp_js_1.McpServer({
172
+ name: exports.MCP_SERVER_NAME,
173
+ version: options.version ?? "0.1.0",
174
+ });
175
+ const readOnly = { readOnlyHint: true };
176
+ server.registerTool("agentloop_summary", {
177
+ title: "AgentLoop summary",
178
+ description: "Read-only loop health metrics (ticket and pattern counts).",
179
+ inputSchema: {},
180
+ annotations: readOnly,
181
+ }, async () => {
182
+ try {
183
+ return ok(await summaryTool(store));
184
+ }
185
+ catch (error) {
186
+ return fail(error);
187
+ }
188
+ });
189
+ server.registerTool("agentloop_list", {
190
+ title: "List tickets",
191
+ description: "Read-only list of tickets, optionally filtered by status (triaged|active|resolved|reopened|deferred|all) and kind.",
192
+ inputSchema: {
193
+ status: zod_1.z.string().optional(),
194
+ kind: zod_1.z.string().optional(),
195
+ },
196
+ annotations: readOnly,
197
+ }, async (args) => {
198
+ try {
199
+ return ok(await listTool(store, args));
200
+ }
201
+ catch (error) {
202
+ return fail(error);
203
+ }
204
+ });
205
+ server.registerTool("agentloop_show", {
206
+ title: "Show ticket or pattern",
207
+ description: "Read-only details for a ticket (by canonical ISSUE- id or queue alias such as DEV-/USER-) or a PATTERN- id.",
208
+ inputSchema: { id: zod_1.z.string() },
209
+ annotations: readOnly,
210
+ }, async (args) => {
211
+ try {
212
+ return ok(await showTool(store, args));
213
+ }
214
+ catch (error) {
215
+ return fail(error);
216
+ }
217
+ });
218
+ server.registerTool("agentloop_handoff", {
219
+ title: "Ticket handoff prompt",
220
+ description: "Read-only copyable agent handoff prompt for a ticket.",
221
+ inputSchema: { id: zod_1.z.string() },
222
+ annotations: readOnly,
223
+ }, async (args) => {
224
+ try {
225
+ return ok(await handoffTool(store, args));
226
+ }
227
+ catch (error) {
228
+ return fail(error);
229
+ }
230
+ });
231
+ server.registerTool("agentloop_convergence", {
232
+ title: "Source-convergence audit",
233
+ description: "Read-only report of patterns whose tickets span multiple distinct sources (corroboration across intake channels).",
234
+ inputSchema: {
235
+ family: zod_1.z.string().optional(),
236
+ minSources: zod_1.z.number().int().positive().optional(),
237
+ includeAll: zod_1.z.boolean().optional(),
238
+ },
239
+ annotations: readOnly,
240
+ }, async (args) => {
241
+ try {
242
+ return ok(await store.sourceConvergence(args));
243
+ }
244
+ catch (error) {
245
+ return fail(error);
246
+ }
247
+ });
248
+ server.registerTool("agentloop_guard_gaps", {
249
+ title: "Guard-gap report",
250
+ description: "Read-only report of resolved tickets that lack an active regression guard (defects/user reports by default).",
251
+ inputSchema: {
252
+ family: zod_1.z.string().optional(),
253
+ includeWaived: zod_1.z.boolean().optional(),
254
+ allKinds: zod_1.z.boolean().optional(),
255
+ },
256
+ annotations: readOnly,
257
+ }, async (args) => {
258
+ try {
259
+ return ok(await store.guardGaps(args));
260
+ }
261
+ catch (error) {
262
+ return fail(error);
263
+ }
264
+ });
265
+ server.registerTool("agentloop_search_knowledge", {
266
+ title: "Search resolution knowledge",
267
+ description: "Read-only search over resolved-ticket fix knowledge (how prior tickets were resolved), by family/kind/source/tag/free-text query.",
268
+ inputSchema: {
269
+ family: zod_1.z.string().optional(),
270
+ kind: zod_1.z.string().optional(),
271
+ source: zod_1.z.string().optional(),
272
+ tag: zod_1.z.string().optional(),
273
+ query: zod_1.z.string().optional(),
274
+ limit: zod_1.z.number().int().nonnegative().optional(),
275
+ },
276
+ annotations: readOnly,
277
+ }, async (args) => {
278
+ try {
279
+ return ok(await store.searchKnowledge(args));
280
+ }
281
+ catch (error) {
282
+ return fail(error);
283
+ }
284
+ });
285
+ server.registerTool("agentloop_knowledge_gaps", {
286
+ title: "Knowledge-gap report",
287
+ description: "Read-only report of resolved tickets whose reusable knowledge is incomplete (missing resolution or verification).",
288
+ inputSchema: {
289
+ family: zod_1.z.string().optional(),
290
+ severity: zod_1.z.string().optional(),
291
+ source: zod_1.z.string().optional(),
292
+ },
293
+ annotations: readOnly,
294
+ }, async (args) => {
295
+ try {
296
+ return ok(await store.knowledgeGaps(args));
297
+ }
298
+ catch (error) {
299
+ return fail(error);
300
+ }
301
+ });
302
+ server.registerTool("agentloop_related", {
303
+ title: "Related tickets (prior art)",
304
+ description: "Read-only prior-art lookup: tickets most related to the given id by shared family/pattern/tags/kind and title overlap.",
305
+ inputSchema: {
306
+ id: zod_1.z.string(),
307
+ minScore: zod_1.z.number().nonnegative().optional(),
308
+ limit: zod_1.z.number().int().nonnegative().optional(),
309
+ },
310
+ annotations: readOnly,
311
+ }, async (args) => {
312
+ try {
313
+ return ok(await store.related(args.id, { minScore: args.minScore, limit: args.limit }));
314
+ }
315
+ catch (error) {
316
+ return fail(error);
317
+ }
318
+ });
319
+ if (options.allowWrites) {
320
+ registerWriteTools(server, store);
321
+ }
322
+ return server;
323
+ }
324
+ /** Register the mutating tools. Only called when writes are explicitly enabled. */
325
+ function registerWriteTools(server, store) {
326
+ const write = { readOnlyHint: false };
327
+ server.registerTool("agentloop_create", {
328
+ title: "Create ticket",
329
+ description: "Create a ticket. `summary` is required; `kind`/`family`/`source` default from config (source defaults to 'agent').",
330
+ inputSchema: {
331
+ summary: zod_1.z.string().min(1),
332
+ title: zod_1.z.string().optional(),
333
+ family: zod_1.z.string().optional(),
334
+ kind: zod_1.z.string().optional(),
335
+ source: zod_1.z.string().optional(),
336
+ severity: zod_1.z.enum(exports.SEVERITIES).optional(),
337
+ confidence: zod_1.z.enum(exports.CONFIDENCES).optional(),
338
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
339
+ handoff: zod_1.z.string().optional(),
340
+ },
341
+ annotations: write,
342
+ }, async (args) => {
343
+ try {
344
+ return ok(await createTicketTool(store, args));
345
+ }
346
+ catch (error) {
347
+ return fail(error);
348
+ }
349
+ });
350
+ server.registerTool("agentloop_note", {
351
+ title: "Add note",
352
+ description: "Append a non-resolution note to a ticket (by id or alias).",
353
+ inputSchema: {
354
+ id: zod_1.z.string(),
355
+ body: zod_1.z.string().min(1),
356
+ type: zod_1.z.enum(exports.NOTE_TYPES).optional(),
357
+ author: zod_1.z.string().optional(),
358
+ },
359
+ annotations: write,
360
+ }, async (args) => {
361
+ try {
362
+ return ok(await noteTool(store, args));
363
+ }
364
+ catch (error) {
365
+ return fail(error);
366
+ }
367
+ });
368
+ server.registerTool("agentloop_workflow", {
369
+ title: "Workflow transition",
370
+ description: "Transition a ticket: status 'active' begins work, 'reopened' records a recurrence, 'deferred' shelves it (optional reason). Resolve via agentloop_resolve.",
371
+ inputSchema: {
372
+ id: zod_1.z.string(),
373
+ status: zod_1.z.enum(exports.WORKFLOW_STATUSES),
374
+ reason: zod_1.z.string().optional(),
375
+ },
376
+ annotations: write,
377
+ }, async (args) => {
378
+ try {
379
+ return ok(await workflowTool(store, args));
380
+ }
381
+ catch (error) {
382
+ return fail(error);
383
+ }
384
+ });
385
+ server.registerTool("agentloop_resolve", {
386
+ title: "Resolve ticket",
387
+ description: "Resolve a ticket with a required summary; optionally record verification and a guard decision.",
388
+ inputSchema: {
389
+ id: zod_1.z.string(),
390
+ summary: zod_1.z.string().min(1),
391
+ verification: zod_1.z.string().optional(),
392
+ guardStatus: zod_1.z.enum(exports.GUARD_STATUSES).optional(),
393
+ guardSummary: zod_1.z.string().optional(),
394
+ },
395
+ annotations: write,
396
+ }, async (args) => {
397
+ try {
398
+ return ok(await resolveTool(store, args));
399
+ }
400
+ catch (error) {
401
+ return fail(error);
402
+ }
403
+ });
404
+ server.registerTool("agentloop_guard", {
405
+ title: "Record guard decision",
406
+ description: "Set the regression-guard decision for a ticket (by id or alias).",
407
+ inputSchema: {
408
+ id: zod_1.z.string(),
409
+ guardStatus: zod_1.z.enum(exports.GUARD_STATUSES),
410
+ guardSummary: zod_1.z.string().optional(),
411
+ },
412
+ annotations: write,
413
+ }, async (args) => {
414
+ try {
415
+ return ok(await guardTool(store, args));
416
+ }
417
+ catch (error) {
418
+ return fail(error);
419
+ }
420
+ });
421
+ }
422
+ /**
423
+ * Start the AgentLoops MCP server over stdio. Reads/writes JSON-RPC on
424
+ * stdin/stdout, so nothing else may be written to stdout while it runs.
425
+ */
426
+ async function startStdioMcpServer(opts) {
427
+ const store = new store_1.AgentLoopStore(opts.cwd, opts.config, { backend: opts.backend });
428
+ await store.ensureInitialized();
429
+ const server = createMcpServer(store, {
430
+ version: opts.version,
431
+ allowWrites: opts.allowWrites,
432
+ });
433
+ const transport = new stdio_js_1.StdioServerTransport();
434
+ await server.connect(transport);
435
+ // Stay alive until the client closes stdin, so callers can dispose resources
436
+ // (e.g. a Postgres pool) cleanly on shutdown.
437
+ await new Promise((resolve) => {
438
+ process.stdin.once("end", resolve);
439
+ process.stdin.once("close", resolve);
440
+ });
441
+ }
442
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1,104 @@
1
+ import { LoopState } from "./types";
2
+ import { StateBackend } from "./backend";
3
+ /**
4
+ * Public relational schema for the ledger. Faithfully mirrors `LoopState`:
5
+ * timestamps are stored as ISO text to keep round-trips identical to the JSON
6
+ * backend, and ordered child collections carry an `ord` column.
7
+ */
8
+ export declare const TICKET_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS loop_meta (\n id integer PRIMARY KEY DEFAULT 1 CHECK (id = 1),\n project text NOT NULL,\n version integer NOT NULL,\n created_at text NOT NULL,\n updated_at text NOT NULL,\n next_ticket_seq integer NOT NULL,\n next_pattern_seq integer NOT NULL\n);\nCREATE TABLE IF NOT EXISTS ticket_patterns (\n id text PRIMARY KEY,\n family text NOT NULL,\n title text NOT NULL,\n status text NOT NULL,\n created_at text NOT NULL,\n updated_at text NOT NULL\n);\nCREATE TABLE IF NOT EXISTS tickets (\n id text PRIMARY KEY,\n family text NOT NULL,\n kind text NOT NULL,\n source text NOT NULL,\n title text NOT NULL,\n summary text NOT NULL,\n severity text NOT NULL,\n confidence text NOT NULL,\n status text NOT NULL,\n created_at text NOT NULL,\n updated_at text NOT NULL,\n started_at text,\n resolved_at text,\n handoff_text text,\n guard_status text,\n guard_summary text,\n pattern_id text,\n verification text,\n reproducible boolean,\n resolution_summary text\n);\nCREATE TABLE IF NOT EXISTS ticket_aliases (ticket_id text NOT NULL, alias text NOT NULL, ord integer NOT NULL);\nCREATE TABLE IF NOT EXISTS ticket_tags (ticket_id text NOT NULL, tag text NOT NULL, ord integer NOT NULL);\nCREATE TABLE IF NOT EXISTS ticket_notes (\n id text NOT NULL,\n ticket_id text NOT NULL,\n type text NOT NULL,\n body text NOT NULL,\n author text,\n created_at text NOT NULL,\n ord integer NOT NULL\n);\nCREATE TABLE IF NOT EXISTS ticket_pattern_links (pattern_id text NOT NULL, ticket_id text NOT NULL, ord integer NOT NULL);\n";
9
+ interface MetaRows {
10
+ project: string;
11
+ version: number;
12
+ createdAt: string;
13
+ updatedAt: string;
14
+ nextTicketSeq: number;
15
+ nextPatternSeq: number;
16
+ }
17
+ interface PatternRow {
18
+ id: string;
19
+ family: string;
20
+ title: string;
21
+ status: string;
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ }
25
+ interface TicketRow {
26
+ id: string;
27
+ family: string;
28
+ kind: string;
29
+ source: string;
30
+ title: string;
31
+ summary: string;
32
+ severity: string;
33
+ confidence: string;
34
+ status: string;
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ startedAt: string | null;
38
+ resolvedAt: string | null;
39
+ handoffText: string | null;
40
+ guardStatus: string | null;
41
+ guardSummary: string | null;
42
+ patternId: string | null;
43
+ verification: string | null;
44
+ reproducible: boolean | null;
45
+ resolutionSummary: string | null;
46
+ }
47
+ interface AliasRow {
48
+ ticketId: string;
49
+ alias: string;
50
+ ord: number;
51
+ }
52
+ interface TagRow {
53
+ ticketId: string;
54
+ tag: string;
55
+ ord: number;
56
+ }
57
+ interface NoteRow {
58
+ id: string;
59
+ ticketId: string;
60
+ type: string;
61
+ body: string;
62
+ author: string | null;
63
+ createdAt: string;
64
+ ord: number;
65
+ }
66
+ interface LinkRow {
67
+ patternId: string;
68
+ ticketId: string;
69
+ ord: number;
70
+ }
71
+ export interface RelationalRows {
72
+ meta: MetaRows;
73
+ patterns: PatternRow[];
74
+ tickets: TicketRow[];
75
+ aliases: AliasRow[];
76
+ tags: TagRow[];
77
+ notes: NoteRow[];
78
+ links: LinkRow[];
79
+ }
80
+ export declare function serializeState(state: LoopState): RelationalRows;
81
+ export declare function deserializeRows(rows: RelationalRows): LoopState;
82
+ /** Minimal client shape; a `pg.Pool` or `pg.Client` satisfies it. */
83
+ export interface PgClient {
84
+ query<R = unknown>(text: string, params?: unknown[]): Promise<{
85
+ rows: R[];
86
+ }>;
87
+ }
88
+ /**
89
+ * Postgres-backed `StateBackend` over the relational `ticket_*` schema. Brings
90
+ * no `pg` dependency — the host injects a `pg.Pool`/`pg.Client`-shaped client.
91
+ * Saves are transactional (whole-snapshot replace), so a failed write never
92
+ * leaves a partially-updated ledger.
93
+ */
94
+ export declare class PostgresStateBackend implements StateBackend {
95
+ private readonly client;
96
+ private migrated;
97
+ constructor(client: PgClient);
98
+ migrate(): Promise<void>;
99
+ private ensureSchema;
100
+ private withConnection;
101
+ load(): Promise<LoopState | null>;
102
+ save(state: LoopState): Promise<void>;
103
+ }
104
+ export {};