@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/cli.js ADDED
@@ -0,0 +1,497 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const config_1 = require("./config");
5
+ const node_fs_1 = require("node:fs");
6
+ const store_1 = require("./store");
7
+ const handoff_1 = require("./handoff");
8
+ const dashboard_1 = require("./dashboard");
9
+ const serve_1 = require("./serve");
10
+ const storage_1 = require("./storage");
11
+ const COMMANDS = [
12
+ "init",
13
+ "create",
14
+ "list",
15
+ "show",
16
+ "patterns",
17
+ "begin",
18
+ "resolve",
19
+ "reopen",
20
+ "defer",
21
+ "note",
22
+ "guard",
23
+ "handoff",
24
+ "summary",
25
+ "convergence",
26
+ "guard-gaps",
27
+ "knowledge",
28
+ "knowledge-gaps",
29
+ "related",
30
+ "dashboard",
31
+ "serve",
32
+ "config",
33
+ "mcp",
34
+ "help",
35
+ ];
36
+ function parseArgs(argv) {
37
+ const args = [];
38
+ const options = {};
39
+ for (let i = 0; i < argv.length; i += 1) {
40
+ const token = argv[i];
41
+ if (!token.startsWith("--")) {
42
+ args.push(token);
43
+ continue;
44
+ }
45
+ const split = token.indexOf("=");
46
+ if (split > -1) {
47
+ options[token.substring(2, split)] = token.substring(split + 1);
48
+ continue;
49
+ }
50
+ const next = argv[i + 1];
51
+ if (next && !next.startsWith("--")) {
52
+ options[token.substring(2)] = next;
53
+ i += 1;
54
+ continue;
55
+ }
56
+ options[token.substring(2)] = true;
57
+ }
58
+ return { args, options };
59
+ }
60
+ function toArray(value) {
61
+ if (!value || typeof value !== "string")
62
+ return [];
63
+ return value
64
+ .split(",")
65
+ .map((item) => item.trim())
66
+ .filter(Boolean);
67
+ }
68
+ function printLine(...parts) {
69
+ process.stdout.write(parts.join(" ") + "\n");
70
+ }
71
+ function printJson(value) {
72
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
73
+ }
74
+ function shortSummary(status) {
75
+ return status.toUpperCase().padEnd(8);
76
+ }
77
+ function printHelp() {
78
+ printLine("IntiDev AgentLoops");
79
+ printLine("Commands:");
80
+ printLine(" init initialize a project loop");
81
+ printLine(" create --title ... --summary ... create a new issue/feature/feedback");
82
+ printLine(" list [--status triaged|active|resolved|all] list tickets");
83
+ printLine(" show <id> inspect ticket");
84
+ printLine(" patterns [--status open|active|resolved|all] list patterns");
85
+ printLine(" begin <id> begin a triaged item");
86
+ printLine(" resolve <id> --summary ... resolve a ticket");
87
+ printLine(" reopen <id> --summary ... reopen a resolved/reopen-risk item");
88
+ printLine(" defer <id> [--summary ...] defer a ticket (records an optional reason)");
89
+ printLine(" note <id> --type ... --body ... add a non-resolution note");
90
+ printLine(" guard <id> --guard-status ... set guard decision");
91
+ printLine(" handoff <id> print agent handoff prompt");
92
+ printLine(" summary print loop stats");
93
+ printLine(" convergence [--family ..] [--min-sources N] [--all] patterns spanning multiple sources");
94
+ printLine(" guard-gaps [--family ..] [--include-waived] [--all-kinds] resolved tickets missing a guard");
95
+ printLine(" knowledge [--family ..] [--kind ..] [--query ..] search resolved-ticket fix knowledge");
96
+ printLine(" knowledge-gaps [--family ..] [--severity ..] [--source ..] resolved tickets lacking reusable knowledge");
97
+ printLine(" related <id> [--min-score N] [--limit N] prior-art: tickets related to <id>");
98
+ printLine(" dashboard [--out file.html] [--stdout] write a standalone HTML dashboard");
99
+ printLine(" serve [--port N] serve the dashboard over HTTP (default 4319)");
100
+ printLine(" config print effective config");
101
+ printLine(" mcp [--write] run the MCP server over stdio (read-only unless --write)");
102
+ printLine("");
103
+ printLine("Storage: set DATABASE_URL to run on Postgres; otherwise .agentloops/state.json is used.");
104
+ }
105
+ let openedStore = null;
106
+ let openedSelection = null;
107
+ // Opens (once) the store over the resolved backend — Postgres when DATABASE_URL
108
+ // or config selects it, otherwise the filesystem. Cached so a single process
109
+ // uses one backend (and one Postgres pool).
110
+ async function ensureConfig() {
111
+ if (openedStore)
112
+ return openedStore;
113
+ const cwd = process.cwd();
114
+ const config = await (0, config_1.loadConfig)(cwd);
115
+ openedSelection = await (0, storage_1.resolveBackend)({ cwd, config });
116
+ const store = new store_1.AgentLoopStore(cwd, config, { backend: openedSelection.backend });
117
+ openedStore = { cwd, config, store, kind: openedSelection.kind };
118
+ return openedStore;
119
+ }
120
+ // Release storage resources (closes the Postgres pool so the process can exit).
121
+ async function disposeStorage() {
122
+ if (openedSelection) {
123
+ await openedSelection.dispose();
124
+ openedSelection = null;
125
+ openedStore = null;
126
+ }
127
+ }
128
+ async function cmdInit(argv, options) {
129
+ await (0, config_1.writeDefaultConfig)(process.cwd());
130
+ const { config, store, kind } = await ensureConfig();
131
+ const project = typeof options.project === "string" ? options.project : config.projectName;
132
+ const created = await store.ensureInitialized(project);
133
+ const where = kind === "postgres" ? "Postgres" : ".agentloops/state.json";
134
+ printLine(`Initialized ${created.project} (${where})`);
135
+ }
136
+ async function cmdCreate(argv, options) {
137
+ const { store, config } = await ensureConfig();
138
+ const title = typeof options.title === "string" ? options.title : "";
139
+ const summary = typeof options.summary === "string" ? options.summary : "";
140
+ const family = typeof options.family === "string" ? options.family : config.patterns.defaultFamily;
141
+ const kind = (typeof options.kind === "string" ? options.kind : config.defaultKind);
142
+ const source = typeof options.source === "string" ? options.source : "manual_admin";
143
+ const severity = (typeof options.severity === "string"
144
+ ? options.severity
145
+ : config.ticketKinds.find((entry) => entry.kind === kind)?.defaultSeverity);
146
+ const confidence = (typeof options.confidence === "string"
147
+ ? options.confidence
148
+ : "medium");
149
+ const tags = toArray(options.tags);
150
+ const ticket = await store.createTicket({
151
+ title,
152
+ summary,
153
+ family,
154
+ kind,
155
+ source,
156
+ severity,
157
+ confidence,
158
+ tags,
159
+ handoffText: typeof options.handoff === "string" ? options.handoff : undefined,
160
+ });
161
+ if (options.json) {
162
+ printJson(ticket);
163
+ }
164
+ else {
165
+ printLine(`Created ${ticket.id} (${ticket.aliases.join(", ")})`);
166
+ printLine(`kind=${ticket.kind} family=${ticket.family} status=${ticket.status}`);
167
+ printLine(`title: ${ticket.title}`);
168
+ if (ticket.patternId) {
169
+ printLine(`Pattern: ${ticket.patternId}`);
170
+ }
171
+ }
172
+ }
173
+ async function cmdList(argv, options) {
174
+ const { store } = await ensureConfig();
175
+ const status = options.status ? options.status : "all";
176
+ const rows = await store.listTickets({
177
+ status: status,
178
+ kind: typeof options.kind === "string" ? options.kind : undefined,
179
+ });
180
+ if (options.json) {
181
+ printJson(rows);
182
+ return;
183
+ }
184
+ for (const ticket of rows) {
185
+ printLine(`${shortSummary(ticket.status)} ${ticket.id} ${ticket.aliases[0] ?? ""} ${ticket.kind} [${ticket.family}]`);
186
+ printLine(` ${ticket.title}`);
187
+ }
188
+ if (rows.length === 0) {
189
+ printLine("No tickets found");
190
+ }
191
+ }
192
+ async function cmdShow(argv) {
193
+ const { store } = await ensureConfig();
194
+ const raw = argv[1];
195
+ if (!raw) {
196
+ throw new Error("show requires an id");
197
+ }
198
+ if (raw.startsWith("PATTERN-")) {
199
+ const pattern = await store.getPattern(raw);
200
+ if (!pattern)
201
+ throw new Error(`Pattern not found: ${raw}`);
202
+ printJson(pattern);
203
+ return;
204
+ }
205
+ const canonical = (0, store_1.normalizeTicketInput)(raw, await store.listTickets({ status: "all" }));
206
+ if (canonical.startsWith("PATTERN-")) {
207
+ const pattern = await store.getPattern(canonical);
208
+ if (!pattern)
209
+ throw new Error(`Pattern not found: ${raw}`);
210
+ printJson(pattern);
211
+ return;
212
+ }
213
+ const ticket = await store.showTicket(raw);
214
+ if (!ticket)
215
+ throw new Error(`Not found: ${raw}`);
216
+ printJson(ticket);
217
+ }
218
+ async function cmdPatterns(argv, options) {
219
+ const { store } = await ensureConfig();
220
+ const status = options.status ? options.status : "all";
221
+ const rows = await store.listPatterns({ status });
222
+ if (options.json) {
223
+ printJson(rows);
224
+ return;
225
+ }
226
+ for (const p of rows) {
227
+ printLine(`${p.status.toUpperCase().padEnd(8)} ${p.id} ${p.family} (${p.ticketIds.length} tickets)`);
228
+ printLine(` ${p.title}`);
229
+ }
230
+ if (rows.length === 0) {
231
+ printLine("No patterns found");
232
+ }
233
+ }
234
+ async function cmdBegin(argv) {
235
+ const { store } = await ensureConfig();
236
+ const id = argv[1];
237
+ if (!id)
238
+ throw new Error("begin requires an id");
239
+ const ticket = await store.beginTicket(id);
240
+ printJson(ticket);
241
+ }
242
+ async function cmdResolve(argv, options) {
243
+ const { store } = await ensureConfig();
244
+ const id = argv[1];
245
+ const summary = typeof options.summary === "string" ? options.summary : "";
246
+ if (!id || !summary) {
247
+ throw new Error("resolve requires <id> and --summary");
248
+ }
249
+ const ticket = await store.resolveTicket({
250
+ id,
251
+ summary,
252
+ verification: typeof options.verification === "string" ? options.verification : undefined,
253
+ guardStatus: typeof options["guard-status"] === "string" ? options["guard-status"] : "none",
254
+ guardSummary: typeof options["guard-summary"] === "string" ? options["guard-summary"] : undefined,
255
+ });
256
+ printJson(ticket);
257
+ }
258
+ async function cmdReopen(argv, options) {
259
+ const { store } = await ensureConfig();
260
+ const id = argv[1];
261
+ const reason = typeof options.summary === "string" ? options.summary : "recurrence detected";
262
+ if (!id)
263
+ throw new Error("reopen requires <id>");
264
+ const ticket = await store.reopenTicket(id, reason);
265
+ printJson(ticket);
266
+ }
267
+ async function cmdDefer(argv, options) {
268
+ const { store } = await ensureConfig();
269
+ const id = argv[1];
270
+ if (!id)
271
+ throw new Error("defer requires <id>");
272
+ const reason = typeof options.summary === "string"
273
+ ? options.summary
274
+ : typeof options.reason === "string"
275
+ ? options.reason
276
+ : undefined;
277
+ const ticket = await store.deferTicket(id, reason);
278
+ printJson(ticket);
279
+ }
280
+ async function cmdNote(argv, options) {
281
+ const { store } = await ensureConfig();
282
+ const id = argv[1];
283
+ const body = typeof options.body === "string" ? options.body : "";
284
+ const type = (typeof options.type === "string" ? options.type : "triage");
285
+ if (!id || !body) {
286
+ throw new Error("note requires <id> and --body");
287
+ }
288
+ const ticket = await store.addTicketNote(id, type, body, process.env.USER);
289
+ printJson(ticket);
290
+ }
291
+ async function cmdGuard(argv, options) {
292
+ const { store } = await ensureConfig();
293
+ const id = argv[1];
294
+ const status = typeof options["guard-status"] === "string" ? options["guard-status"] : "guard_deferred";
295
+ const summary = typeof options["guard-summary"] === "string" ? options["guard-summary"] : undefined;
296
+ if (!id)
297
+ throw new Error("guard requires <id>");
298
+ const ticket = await store.setGuard(id, status, summary);
299
+ printJson(ticket);
300
+ }
301
+ async function cmdHandoff(argv) {
302
+ const { store } = await ensureConfig();
303
+ const id = argv[1];
304
+ if (!id)
305
+ throw new Error("handoff requires <id>");
306
+ const ticket = await store.showTicket(id);
307
+ if (!ticket)
308
+ throw new Error(`Not found: ${id}`);
309
+ const prompt = (0, handoff_1.buildHandoffPrompt)(ticket);
310
+ printLine(`Ticket: ${ticket.id}`);
311
+ printLine(`Aliases: ${ticket.aliases.join(", ")}`);
312
+ printLine("Copyable agent handoff:");
313
+ printLine(prompt);
314
+ }
315
+ async function cmdSummary() {
316
+ const { store } = await ensureConfig();
317
+ printJson(await store.summary());
318
+ }
319
+ async function cmdConvergence(options) {
320
+ const { store } = await ensureConfig();
321
+ const family = typeof options.family === "string" ? options.family : undefined;
322
+ const minSources = typeof options["min-sources"] === "string" ? Number(options["min-sources"]) : undefined;
323
+ const includeAll = options.all === true;
324
+ printJson(await store.sourceConvergence({ family, minSources, includeAll }));
325
+ }
326
+ async function cmdGuardGaps(options) {
327
+ const { store } = await ensureConfig();
328
+ const family = typeof options.family === "string" ? options.family : undefined;
329
+ const includeWaived = options["include-waived"] === true;
330
+ const allKinds = options["all-kinds"] === true;
331
+ printJson(await store.guardGaps({ family, includeWaived, allKinds }));
332
+ }
333
+ async function cmdKnowledge(options) {
334
+ const { store } = await ensureConfig();
335
+ const str = (key) => (typeof options[key] === "string" ? options[key] : undefined);
336
+ const limit = typeof options.limit === "string" ? Number(options.limit) : undefined;
337
+ printJson(await store.searchKnowledge({
338
+ family: str("family"),
339
+ kind: str("kind"),
340
+ source: str("source"),
341
+ tag: str("tag"),
342
+ query: str("query"),
343
+ limit,
344
+ }));
345
+ }
346
+ async function cmdKnowledgeGaps(options) {
347
+ const { store } = await ensureConfig();
348
+ const str = (key) => (typeof options[key] === "string" ? options[key] : undefined);
349
+ printJson(await store.knowledgeGaps({
350
+ family: str("family"),
351
+ severity: str("severity"),
352
+ source: str("source"),
353
+ }));
354
+ }
355
+ async function cmdRelated(argv, options) {
356
+ const { store } = await ensureConfig();
357
+ const id = argv[1];
358
+ if (!id)
359
+ throw new Error("related requires an id");
360
+ const minScore = typeof options["min-score"] === "string" ? Number(options["min-score"]) : undefined;
361
+ const limit = typeof options.limit === "string" ? Number(options.limit) : undefined;
362
+ printJson(await store.related(id, { minScore, limit }));
363
+ }
364
+ async function cmdDashboard(options) {
365
+ const { store } = await ensureConfig();
366
+ const html = (0, dashboard_1.renderDashboard)(await (0, dashboard_1.gatherDashboardData)(store));
367
+ if (options.stdout === true) {
368
+ process.stdout.write(html);
369
+ return;
370
+ }
371
+ const out = typeof options.out === "string" ? options.out : "agentloop-dashboard.html";
372
+ await node_fs_1.promises.writeFile(out, html, "utf-8");
373
+ printLine(`Wrote dashboard to ${out}`);
374
+ }
375
+ async function cmdServe(options) {
376
+ const { store, kind } = await ensureConfig();
377
+ const port = typeof options.port === "string" ? Number(options.port) : 4319;
378
+ const server = (0, serve_1.createDashboardServer)(store);
379
+ await new Promise((resolve) => server.listen(port, resolve));
380
+ process.stderr.write(`agentloop dashboard on http://localhost:${port} (${kind})\n`);
381
+ // Run until the process is stopped.
382
+ await new Promise(() => { });
383
+ }
384
+ async function cmdConfig() {
385
+ const { config } = await ensureConfig();
386
+ printJson(config);
387
+ }
388
+ async function cmdMcp(options) {
389
+ const { cwd, config, kind } = await ensureConfig();
390
+ // Writes are opt-in: read-only unless --write (alias --allow-writes) is set.
391
+ const allowWrites = options.write === true || options["allow-writes"] === true;
392
+ // Lazy-load so the MCP SDK is only required when this command runs, and so
393
+ // its dependency never affects startup of the other CLI commands.
394
+ const { startStdioMcpServer } = await import("./mcp.js");
395
+ // stdout is reserved for the JSON-RPC stream; status goes to stderr.
396
+ process.stderr.write(`agentloop MCP server ready on stdio (${allowWrites ? "read-write" : "read-only"}, ${kind})\n`);
397
+ await startStdioMcpServer({ cwd, config, allowWrites, backend: openedSelection?.backend });
398
+ }
399
+ async function main() {
400
+ const argv = process.argv.slice(2);
401
+ const { args, options } = parseArgs(argv);
402
+ const command = args[0] ?? "help";
403
+ if (!COMMANDS.includes(command)) {
404
+ throw new Error(`Unknown command: ${command}`);
405
+ }
406
+ try {
407
+ switch (command) {
408
+ case "help":
409
+ printHelp();
410
+ break;
411
+ case "init":
412
+ await cmdInit(args, options);
413
+ break;
414
+ case "create":
415
+ await cmdCreate(args, options);
416
+ break;
417
+ case "list":
418
+ await cmdList(args, options);
419
+ break;
420
+ case "show":
421
+ await cmdShow(args);
422
+ break;
423
+ case "patterns":
424
+ await cmdPatterns(args, options);
425
+ break;
426
+ case "begin":
427
+ await cmdBegin(args);
428
+ break;
429
+ case "resolve":
430
+ await cmdResolve(args, options);
431
+ break;
432
+ case "reopen":
433
+ await cmdReopen(args, options);
434
+ break;
435
+ case "defer":
436
+ await cmdDefer(args, options);
437
+ break;
438
+ case "note":
439
+ await cmdNote(args, options);
440
+ break;
441
+ case "guard":
442
+ await cmdGuard(args, options);
443
+ break;
444
+ case "handoff":
445
+ await cmdHandoff(args);
446
+ break;
447
+ case "summary":
448
+ await cmdSummary();
449
+ break;
450
+ case "convergence":
451
+ await cmdConvergence(options);
452
+ break;
453
+ case "guard-gaps":
454
+ await cmdGuardGaps(options);
455
+ break;
456
+ case "knowledge":
457
+ await cmdKnowledge(options);
458
+ break;
459
+ case "knowledge-gaps":
460
+ await cmdKnowledgeGaps(options);
461
+ break;
462
+ case "related":
463
+ await cmdRelated(args, options);
464
+ break;
465
+ case "dashboard":
466
+ await cmdDashboard(options);
467
+ break;
468
+ case "serve":
469
+ await cmdServe(options);
470
+ break;
471
+ case "config":
472
+ await cmdConfig();
473
+ break;
474
+ case "mcp":
475
+ await cmdMcp(options);
476
+ break;
477
+ default:
478
+ printHelp();
479
+ }
480
+ }
481
+ finally {
482
+ await disposeStorage();
483
+ }
484
+ }
485
+ // Exit quietly when output is piped to a reader that closed early
486
+ // (e.g. `agentloop list | head`), instead of crashing with EPIPE.
487
+ process.stdout.on("error", (error) => {
488
+ if (error.code === "EPIPE")
489
+ process.exit(0);
490
+ throw error;
491
+ });
492
+ main().catch((error) => {
493
+ const message = error instanceof Error ? error.message : String(error);
494
+ printLine(`Error: ${message}`);
495
+ process.exitCode = 1;
496
+ });
497
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,10 @@
1
+ import { ProjectConfig, TicketKind } from "./types";
2
+ export declare const DEFAULT_CONFIG: ProjectConfig;
3
+ export declare const CONFIG_FILE_NAME = "agentloop.config.json";
4
+ export declare const STATE_DIR = ".agentloops";
5
+ export declare function configPath(cwd: string): string;
6
+ export declare function loadConfig(cwd: string): Promise<ProjectConfig>;
7
+ export declare function writeDefaultConfig(cwd: string): Promise<ProjectConfig>;
8
+ export declare function requiredFields(config: ProjectConfig, kind: TicketKind): string[];
9
+ export declare function ensureKind(config: ProjectConfig, kind: string | undefined): TicketKind;
10
+ export declare function mergeConfig(partial: Partial<ProjectConfig>): ProjectConfig;
package/dist/config.js ADDED
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.STATE_DIR = exports.CONFIG_FILE_NAME = exports.DEFAULT_CONFIG = void 0;
4
+ exports.configPath = configPath;
5
+ exports.loadConfig = loadConfig;
6
+ exports.writeDefaultConfig = writeDefaultConfig;
7
+ exports.requiredFields = requiredFields;
8
+ exports.ensureKind = ensureKind;
9
+ exports.mergeConfig = mergeConfig;
10
+ const fs_1 = require("fs");
11
+ const path_1 = require("path");
12
+ exports.DEFAULT_CONFIG = {
13
+ projectName: "IntiDev AgentLoops",
14
+ description: "Feedback Loops for Agentic Workflows",
15
+ defaultKind: "bug",
16
+ ticketKinds: [
17
+ { kind: "bug", defaultSeverity: "high", requiredFields: ["summary"] },
18
+ { kind: "feature", defaultSeverity: "medium", requiredFields: ["summary"] },
19
+ { kind: "user_feedback", defaultSeverity: "high", requiredFields: ["summary"] },
20
+ { kind: "investigation", defaultSeverity: "medium", requiredFields: ["summary"] },
21
+ { kind: "incident", defaultSeverity: "critical", requiredFields: ["summary"] },
22
+ { kind: "tech_debt", defaultSeverity: "medium", requiredFields: ["summary"] },
23
+ { kind: "task", defaultSeverity: "medium", requiredFields: ["summary"] },
24
+ ],
25
+ queues: [
26
+ { prefix: "USER", kinds: ["user_feedback"], sources: ["user_report"] },
27
+ { prefix: "DEV", kinds: ["feature", "task", "investigation", "tech_debt"] },
28
+ { prefix: "ISSUE", kinds: ["bug", "incident"], default: true },
29
+ ],
30
+ sources: ["user_report", "manual_admin", "agent", "smoke", "ci", "ingestion", "unknown"],
31
+ patterns: {
32
+ autoCreateByFamily: true,
33
+ defaultFamily: "general",
34
+ },
35
+ };
36
+ exports.CONFIG_FILE_NAME = "agentloop.config.json";
37
+ exports.STATE_DIR = ".agentloops";
38
+ function configPath(cwd) {
39
+ return (0, path_1.join)(cwd, exports.CONFIG_FILE_NAME);
40
+ }
41
+ async function loadConfig(cwd) {
42
+ try {
43
+ const text = await fs_1.promises.readFile(configPath(cwd), "utf-8");
44
+ const parsed = JSON.parse(text);
45
+ return mergeConfig(parsed);
46
+ }
47
+ catch {
48
+ return { ...exports.DEFAULT_CONFIG };
49
+ }
50
+ }
51
+ async function writeDefaultConfig(cwd) {
52
+ const path = configPath(cwd);
53
+ await fs_1.promises.writeFile(path, JSON.stringify(exports.DEFAULT_CONFIG, null, 2), "utf-8");
54
+ return { ...exports.DEFAULT_CONFIG };
55
+ }
56
+ function requiredFields(config, kind) {
57
+ const k = config.ticketKinds.find((entry) => entry.kind === kind);
58
+ return k?.requiredFields ?? [];
59
+ }
60
+ function ensureKind(config, kind) {
61
+ const candidate = (kind ?? config.defaultKind).toLowerCase();
62
+ const has = config.ticketKinds.some((entry) => entry.kind === candidate);
63
+ if (!has) {
64
+ return config.defaultKind;
65
+ }
66
+ return candidate;
67
+ }
68
+ function mergeConfig(partial) {
69
+ const mergedKinds = (partial.ticketKinds ?? exports.DEFAULT_CONFIG.ticketKinds).map((kind) => ({ ...kind, defaultSeverity: kind.defaultSeverity ?? "medium" }));
70
+ return {
71
+ ...exports.DEFAULT_CONFIG,
72
+ ...partial,
73
+ ticketKinds: mergedKinds,
74
+ queues: partial.queues ?? exports.DEFAULT_CONFIG.queues,
75
+ patterns: {
76
+ ...exports.DEFAULT_CONFIG.patterns,
77
+ ...(partial.patterns ?? {}),
78
+ },
79
+ };
80
+ }
81
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,56 @@
1
+ import { Pattern, PatternStatus, Ticket, TicketStatus } from "./types";
2
+ export declare const SOURCE_CONVERGENCE_SCHEMA_VERSION: 1;
3
+ export declare const DEFAULT_MIN_SOURCES = 2;
4
+ export interface SourceConvergenceOptions {
5
+ /** Minimum distinct sources for a pattern to count as converged. Default 2. */
6
+ minSources?: number;
7
+ /** Restrict the audit to a single family. */
8
+ family?: string;
9
+ /** Include non-converged patterns in `patterns` too. Default false. */
10
+ includeAll?: boolean;
11
+ }
12
+ export interface ConvergenceTicketRef {
13
+ id: string;
14
+ alias: string;
15
+ source: string;
16
+ kind: string;
17
+ status: TicketStatus;
18
+ }
19
+ export interface ConvergencePattern {
20
+ id: string;
21
+ family: string;
22
+ title: string;
23
+ status: PatternStatus;
24
+ ticketCount: number;
25
+ /** Number of distinct sources among the pattern's tickets. */
26
+ sourceCount: number;
27
+ /** Count of tickets per source. */
28
+ sources: Record<string, number>;
29
+ converged: boolean;
30
+ tickets: ConvergenceTicketRef[];
31
+ }
32
+ export interface SourceConvergenceReport {
33
+ schemaVersion: typeof SOURCE_CONVERGENCE_SCHEMA_VERSION;
34
+ generatedAt: string;
35
+ filters: {
36
+ family: string | null;
37
+ minSources: number;
38
+ };
39
+ summary: {
40
+ totalPatterns: number;
41
+ convergedPatterns: number;
42
+ /** Highest distinct-source count across the analyzed patterns. */
43
+ maxSourceConvergence: number;
44
+ };
45
+ patterns: ConvergencePattern[];
46
+ }
47
+ /**
48
+ * Source-convergence audit (ported concept from Inti's ledger): surface the
49
+ * Patterns whose member tickets were reported through **multiple distinct
50
+ * sources**. A pattern corroborated by, say, a smoke run + a user report + an
51
+ * agent is far higher-signal than a single-source cluster.
52
+ *
53
+ * Pure and deterministic apart from `generatedAt`. Patterns are returned sorted
54
+ * by source diversity (then ticket count, then id) so output is stable.
55
+ */
56
+ export declare function sourceConvergenceReport(tickets: Ticket[], patterns: Pattern[], options?: SourceConvergenceOptions): SourceConvergenceReport;