@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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/agentloop.config.json.example +31 -0
- package/dist/aliases.d.ts +25 -0
- package/dist/aliases.js +42 -0
- package/dist/backend.d.ts +29 -0
- package/dist/backend.js +40 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +497 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +81 -0
- package/dist/convergence.d.ts +56 -0
- package/dist/convergence.js +64 -0
- package/dist/dashboard.d.ts +23 -0
- package/dist/dashboard.js +234 -0
- package/dist/guards.d.ts +53 -0
- package/dist/guards.js +87 -0
- package/dist/handoff.d.ts +10 -0
- package/dist/handoff.js +23 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +80 -0
- package/dist/knowledge.d.ts +101 -0
- package/dist/knowledge.js +155 -0
- package/dist/mcp.d.ts +130 -0
- package/dist/mcp.js +442 -0
- package/dist/postgres.d.ts +104 -0
- package/dist/postgres.js +364 -0
- package/dist/prior-art.d.ts +65 -0
- package/dist/prior-art.js +114 -0
- package/dist/redaction.d.ts +14 -0
- package/dist/redaction.js +57 -0
- package/dist/serve.d.ts +8 -0
- package/dist/serve.js +42 -0
- package/dist/storage.d.ts +22 -0
- package/dist/storage.js +39 -0
- package/dist/store.d.ts +60 -0
- package/dist/store.js +339 -0
- package/dist/types.d.ts +156 -0
- package/dist/types.js +3 -0
- package/docs/architecture.md +30 -0
- package/docs/config.md +88 -0
- package/docs/mcp.md +88 -0
- package/docs/postgres.md +84 -0
- package/package.json +88 -0
package/dist/storage.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolvePostgresUrl = resolvePostgresUrl;
|
|
4
|
+
exports.resolveBackend = resolveBackend;
|
|
5
|
+
const backend_1 = require("./backend");
|
|
6
|
+
const postgres_1 = require("./postgres");
|
|
7
|
+
/** Connection string precedence: explicit arg → `DATABASE_URL` env → config. */
|
|
8
|
+
function resolvePostgresUrl(options) {
|
|
9
|
+
return options.databaseUrl ?? process.env.DATABASE_URL ?? options.config.storage?.databaseUrl;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Pick a `StateBackend` for the CLI/MCP: Postgres when a connection string is
|
|
13
|
+
* configured, otherwise the filesystem. `pg` is loaded lazily and is an optional
|
|
14
|
+
* peer dependency — filesystem users never need it.
|
|
15
|
+
*/
|
|
16
|
+
async function resolveBackend(options) {
|
|
17
|
+
const url = resolvePostgresUrl(options);
|
|
18
|
+
if (!url) {
|
|
19
|
+
return {
|
|
20
|
+
backend: new backend_1.FilesystemStateBackend(options.cwd),
|
|
21
|
+
kind: "filesystem",
|
|
22
|
+
dispose: async () => { },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const { client, end } = await connectPostgres(url);
|
|
26
|
+
return { backend: new postgres_1.PostgresStateBackend(client), kind: "postgres", dispose: end };
|
|
27
|
+
}
|
|
28
|
+
async function connectPostgres(connectionString) {
|
|
29
|
+
let mod;
|
|
30
|
+
try {
|
|
31
|
+
mod = await import("pg");
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error("Postgres backend selected (DATABASE_URL or storage.databaseUrl is set) but the 'pg' package is not installed. Install it with: npm install pg");
|
|
35
|
+
}
|
|
36
|
+
const pool = new mod.Pool({ connectionString });
|
|
37
|
+
return { client: pool, end: () => pool.end() };
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=storage.js.map
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CreateTicketInput, GuardStatus, LoopState, NoteType, Pattern, PatternStatus, ProjectConfig, ResolveInput, Ticket, TicketRedactor, TicketStatus } from "./types";
|
|
2
|
+
import { StateBackend } from "./backend";
|
|
3
|
+
import { SourceConvergenceOptions, SourceConvergenceReport } from "./convergence";
|
|
4
|
+
import { GuardGapOptions, GuardGapReport } from "./guards";
|
|
5
|
+
import { KnowledgeSearchOptions, ResolutionKnowledgeReport, KnowledgeGapsOptions, KnowledgeGapsReport } from "./knowledge";
|
|
6
|
+
import { PriorArtOptions, PriorArtReport } from "./prior-art";
|
|
7
|
+
export declare class AgentLoopStore {
|
|
8
|
+
private readonly config;
|
|
9
|
+
private state;
|
|
10
|
+
private readonly backend;
|
|
11
|
+
private readonly redactor;
|
|
12
|
+
constructor(cwd: string, config: ProjectConfig, options?: {
|
|
13
|
+
redactor?: TicketRedactor;
|
|
14
|
+
backend?: StateBackend;
|
|
15
|
+
});
|
|
16
|
+
private redact;
|
|
17
|
+
ensureInitialized(project?: string): Promise<LoopState>;
|
|
18
|
+
createTicket(input: CreateTicketInput): Promise<Ticket>;
|
|
19
|
+
listTickets(opts: {
|
|
20
|
+
status?: TicketStatus | "all";
|
|
21
|
+
kind?: string;
|
|
22
|
+
}): Promise<Ticket[]>;
|
|
23
|
+
listPatterns(opts: {
|
|
24
|
+
status?: PatternStatus | "all";
|
|
25
|
+
}): Promise<Pattern[]>;
|
|
26
|
+
getTicketByAnyId(rawId: string): Promise<Ticket | undefined>;
|
|
27
|
+
showTicket(rawId: string): Promise<Ticket | undefined>;
|
|
28
|
+
beginTicket(rawId: string): Promise<Ticket>;
|
|
29
|
+
resolveTicket(input: ResolveInput): Promise<Ticket>;
|
|
30
|
+
reopenTicket(rawId: string, reason: string): Promise<Ticket>;
|
|
31
|
+
deferTicket(rawId: string, reason?: string): Promise<Ticket>;
|
|
32
|
+
addTicketNote(rawId: string, type: NoteType, body: string, author?: string): Promise<Ticket>;
|
|
33
|
+
setGuard(rawId: string, status: GuardStatus, summary?: string): Promise<Ticket>;
|
|
34
|
+
private noteCtx;
|
|
35
|
+
resolvePattern(patternId: string, note: string): Promise<Pattern>;
|
|
36
|
+
summary(): Promise<{
|
|
37
|
+
project: string;
|
|
38
|
+
totalTickets: number;
|
|
39
|
+
activeTickets: number;
|
|
40
|
+
triagedTickets: number;
|
|
41
|
+
resolvedTickets: number;
|
|
42
|
+
reopenedTickets: number;
|
|
43
|
+
deferredTickets: number;
|
|
44
|
+
openPatterns: number;
|
|
45
|
+
stalledPatterns: number;
|
|
46
|
+
resolvedPatterns: number;
|
|
47
|
+
}>;
|
|
48
|
+
getPattern(id: string): Promise<Pattern | undefined>;
|
|
49
|
+
getConfig(): ProjectConfig;
|
|
50
|
+
sourceConvergence(options?: SourceConvergenceOptions): Promise<SourceConvergenceReport>;
|
|
51
|
+
guardGaps(options?: GuardGapOptions): Promise<GuardGapReport>;
|
|
52
|
+
searchKnowledge(options?: KnowledgeSearchOptions): Promise<ResolutionKnowledgeReport>;
|
|
53
|
+
knowledgeGaps(options?: KnowledgeGapsOptions): Promise<KnowledgeGapsReport>;
|
|
54
|
+
related(rawId: string, options?: PriorArtOptions): Promise<PriorArtReport>;
|
|
55
|
+
private transitionTicket;
|
|
56
|
+
private attachPattern;
|
|
57
|
+
private persist;
|
|
58
|
+
private get nowState();
|
|
59
|
+
}
|
|
60
|
+
export declare function normalizeTicketInput(raw: string, tickets: Ticket[]): string;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentLoopStore = void 0;
|
|
4
|
+
exports.normalizeTicketInput = normalizeTicketInput;
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const redaction_1 = require("./redaction");
|
|
7
|
+
const backend_1 = require("./backend");
|
|
8
|
+
const aliases_1 = require("./aliases");
|
|
9
|
+
const convergence_1 = require("./convergence");
|
|
10
|
+
const guards_1 = require("./guards");
|
|
11
|
+
const knowledge_1 = require("./knowledge");
|
|
12
|
+
const prior_art_1 = require("./prior-art");
|
|
13
|
+
const SEQ_PAD = 6;
|
|
14
|
+
function ticketId(seq) {
|
|
15
|
+
return `ISSUE-${String(seq).padStart(SEQ_PAD, "0")}`;
|
|
16
|
+
}
|
|
17
|
+
function patternId(seq) {
|
|
18
|
+
return `PATTERN-${String(seq).padStart(SEQ_PAD, "0")}`;
|
|
19
|
+
}
|
|
20
|
+
function nowIso() {
|
|
21
|
+
return new Date().toISOString();
|
|
22
|
+
}
|
|
23
|
+
class AgentLoopStore {
|
|
24
|
+
config;
|
|
25
|
+
state = null;
|
|
26
|
+
backend;
|
|
27
|
+
redactor;
|
|
28
|
+
constructor(cwd, config, options = {}) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.backend = options.backend ?? new backend_1.FilesystemStateBackend(cwd);
|
|
31
|
+
this.redactor = (0, redaction_1.resolveRedactor)(config, options.redactor);
|
|
32
|
+
}
|
|
33
|
+
redact(value, context) {
|
|
34
|
+
return this.redactor.redactText(value, context);
|
|
35
|
+
}
|
|
36
|
+
async ensureInitialized(project = this.config.projectName) {
|
|
37
|
+
if (this.state) {
|
|
38
|
+
return this.state;
|
|
39
|
+
}
|
|
40
|
+
const loaded = await this.backend.load();
|
|
41
|
+
if (!loaded) {
|
|
42
|
+
this.state = {
|
|
43
|
+
version: 1,
|
|
44
|
+
project,
|
|
45
|
+
createdAt: nowIso(),
|
|
46
|
+
updatedAt: nowIso(),
|
|
47
|
+
nextTicketSeq: 0,
|
|
48
|
+
nextPatternSeq: 0,
|
|
49
|
+
tickets: [],
|
|
50
|
+
patterns: [],
|
|
51
|
+
};
|
|
52
|
+
await this.persist();
|
|
53
|
+
return this.state;
|
|
54
|
+
}
|
|
55
|
+
this.state = loaded;
|
|
56
|
+
if (!this.state.project) {
|
|
57
|
+
this.state.project = project;
|
|
58
|
+
}
|
|
59
|
+
return this.state;
|
|
60
|
+
}
|
|
61
|
+
async createTicket(input) {
|
|
62
|
+
const state = await this.ensureInitialized();
|
|
63
|
+
const { title, summary, family, kind, source, severity, confidence, tags = [], handoffText } = input;
|
|
64
|
+
const missing = (0, config_1.requiredFields)(this.config, kind).filter((field) => !input[field]);
|
|
65
|
+
if (missing.length > 0) {
|
|
66
|
+
throw new Error(`Missing required fields for ${kind}: ${missing.join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
const id = ticketId(++state.nextTicketSeq);
|
|
69
|
+
const defaultKindConfig = this.config.ticketKinds.find((entry) => entry.kind === kind);
|
|
70
|
+
const defaults = defaultKindConfig?.defaultSeverity ?? "medium";
|
|
71
|
+
const c = this.nowState;
|
|
72
|
+
const aliases = (0, aliases_1.deriveAliases)({ kind, source }, state.nextTicketSeq, this.config);
|
|
73
|
+
const ctx = (field) => ({ field, ticketKind: kind, source });
|
|
74
|
+
const ticket = {
|
|
75
|
+
id,
|
|
76
|
+
family,
|
|
77
|
+
kind,
|
|
78
|
+
source,
|
|
79
|
+
title: this.redact(title, ctx("title")),
|
|
80
|
+
summary: this.redact(summary, ctx("summary")),
|
|
81
|
+
severity: severity ?? defaults,
|
|
82
|
+
confidence: confidence ?? "medium",
|
|
83
|
+
status: "triaged",
|
|
84
|
+
createdAt: c,
|
|
85
|
+
updatedAt: c,
|
|
86
|
+
aliases: Array.from(new Set(aliases)),
|
|
87
|
+
tags: Array.from(new Set(tags)),
|
|
88
|
+
notes: [],
|
|
89
|
+
handoffText: handoffText ? this.redact(handoffText, ctx("handoffText")) : undefined,
|
|
90
|
+
reproducible: true,
|
|
91
|
+
};
|
|
92
|
+
ticket.patternId = this.attachPattern(state, family, ticket.id);
|
|
93
|
+
state.tickets.push(ticket);
|
|
94
|
+
state.updatedAt = nowIso();
|
|
95
|
+
await this.persist();
|
|
96
|
+
return ticket;
|
|
97
|
+
}
|
|
98
|
+
async listTickets(opts) {
|
|
99
|
+
const state = await this.ensureInitialized();
|
|
100
|
+
let rows = [...state.tickets];
|
|
101
|
+
if (opts.status && opts.status !== "all") {
|
|
102
|
+
rows = rows.filter((ticket) => ticket.status === opts.status);
|
|
103
|
+
}
|
|
104
|
+
if (opts.kind) {
|
|
105
|
+
rows = rows.filter((ticket) => ticket.kind === opts.kind);
|
|
106
|
+
}
|
|
107
|
+
return rows.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
108
|
+
}
|
|
109
|
+
async listPatterns(opts) {
|
|
110
|
+
const state = await this.ensureInitialized();
|
|
111
|
+
const rows = [...state.patterns];
|
|
112
|
+
if (opts.status && opts.status !== "all") {
|
|
113
|
+
return rows.filter((pattern) => pattern.status === opts.status).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
114
|
+
}
|
|
115
|
+
return rows.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
116
|
+
}
|
|
117
|
+
async getTicketByAnyId(rawId) {
|
|
118
|
+
const state = await this.ensureInitialized();
|
|
119
|
+
const normalized = normalizeTicketInput(rawId, state.tickets);
|
|
120
|
+
return state.tickets.find((ticket) => ticket.id === normalized);
|
|
121
|
+
}
|
|
122
|
+
async showTicket(rawId) {
|
|
123
|
+
return this.getTicketByAnyId(rawId);
|
|
124
|
+
}
|
|
125
|
+
async beginTicket(rawId) {
|
|
126
|
+
const ticket = await this.transitionTicket(rawId, "active");
|
|
127
|
+
ticket.startedAt = nowIso();
|
|
128
|
+
await this.persist();
|
|
129
|
+
return ticket;
|
|
130
|
+
}
|
|
131
|
+
async resolveTicket(input) {
|
|
132
|
+
const ticket = await this.transitionTicket(input.id, "resolved");
|
|
133
|
+
const ctx = (field) => ({
|
|
134
|
+
field,
|
|
135
|
+
ticketKind: ticket.kind,
|
|
136
|
+
source: ticket.source,
|
|
137
|
+
});
|
|
138
|
+
ticket.resolutionSummary = this.redact(input.summary, ctx("resolutionSummary"));
|
|
139
|
+
ticket.verification = input.verification
|
|
140
|
+
? this.redact(input.verification, ctx("verification"))
|
|
141
|
+
: undefined;
|
|
142
|
+
ticket.resolvedAt = nowIso();
|
|
143
|
+
ticket.guardStatus = input.guardStatus ?? "none";
|
|
144
|
+
ticket.guardSummary = input.guardSummary
|
|
145
|
+
? this.redact(input.guardSummary, ctx("guardSummary"))
|
|
146
|
+
: undefined;
|
|
147
|
+
await this.persist();
|
|
148
|
+
return ticket;
|
|
149
|
+
}
|
|
150
|
+
async reopenTicket(rawId, reason) {
|
|
151
|
+
const ticket = await this.transitionTicket(rawId, "reopened");
|
|
152
|
+
ticket.notes.push({
|
|
153
|
+
id: `${ticket.id}-note-${Date.now()}`,
|
|
154
|
+
type: "hypothesis",
|
|
155
|
+
body: `Reopened: ${this.redact(reason, this.noteCtx(ticket))}`,
|
|
156
|
+
createdAt: nowIso(),
|
|
157
|
+
});
|
|
158
|
+
ticket.updatedAt = nowIso();
|
|
159
|
+
await this.persist();
|
|
160
|
+
return ticket;
|
|
161
|
+
}
|
|
162
|
+
async deferTicket(rawId, reason) {
|
|
163
|
+
const ticket = await this.transitionTicket(rawId, "deferred");
|
|
164
|
+
if (reason) {
|
|
165
|
+
ticket.notes.push({
|
|
166
|
+
id: `${ticket.id}-note-${Date.now()}`,
|
|
167
|
+
type: "triage",
|
|
168
|
+
body: `Deferred: ${this.redact(reason, this.noteCtx(ticket))}`,
|
|
169
|
+
createdAt: nowIso(),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
ticket.updatedAt = nowIso();
|
|
173
|
+
await this.persist();
|
|
174
|
+
return ticket;
|
|
175
|
+
}
|
|
176
|
+
async addTicketNote(rawId, type, body, author) {
|
|
177
|
+
const ticket = await this.transitionTicket(rawId, undefined);
|
|
178
|
+
ticket.notes.push({
|
|
179
|
+
id: `${ticket.id}-note-${Date.now()}`,
|
|
180
|
+
type,
|
|
181
|
+
body: this.redact(body, this.noteCtx(ticket)),
|
|
182
|
+
author,
|
|
183
|
+
createdAt: nowIso(),
|
|
184
|
+
});
|
|
185
|
+
ticket.updatedAt = nowIso();
|
|
186
|
+
await this.persist();
|
|
187
|
+
return ticket;
|
|
188
|
+
}
|
|
189
|
+
async setGuard(rawId, status, summary) {
|
|
190
|
+
const ticket = await this.transitionTicket(rawId, undefined);
|
|
191
|
+
ticket.guardStatus = status;
|
|
192
|
+
ticket.guardSummary = summary
|
|
193
|
+
? this.redact(summary, { field: "guardSummary", ticketKind: ticket.kind, source: ticket.source })
|
|
194
|
+
: undefined;
|
|
195
|
+
await this.persist();
|
|
196
|
+
return ticket;
|
|
197
|
+
}
|
|
198
|
+
noteCtx(ticket) {
|
|
199
|
+
return { field: "note", ticketKind: ticket.kind, source: ticket.source };
|
|
200
|
+
}
|
|
201
|
+
async resolvePattern(patternId, note) {
|
|
202
|
+
const state = await this.ensureInitialized();
|
|
203
|
+
const pattern = state.patterns.find((entry) => entry.id === patternId);
|
|
204
|
+
if (!pattern) {
|
|
205
|
+
throw new Error(`Pattern not found: ${patternId}`);
|
|
206
|
+
}
|
|
207
|
+
if (pattern.status !== "resolved") {
|
|
208
|
+
pattern.status = "resolved";
|
|
209
|
+
pattern.updatedAt = nowIso();
|
|
210
|
+
if (note) {
|
|
211
|
+
pattern.title = `${pattern.title} (resolved: ${note})`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
state.updatedAt = nowIso();
|
|
215
|
+
await this.persist();
|
|
216
|
+
return pattern;
|
|
217
|
+
}
|
|
218
|
+
async summary() {
|
|
219
|
+
const state = await this.ensureInitialized();
|
|
220
|
+
return {
|
|
221
|
+
project: state.project,
|
|
222
|
+
totalTickets: state.tickets.length,
|
|
223
|
+
activeTickets: state.tickets.filter((t) => t.status === "active").length,
|
|
224
|
+
triagedTickets: state.tickets.filter((t) => t.status === "triaged").length,
|
|
225
|
+
resolvedTickets: state.tickets.filter((t) => t.status === "resolved").length,
|
|
226
|
+
reopenedTickets: state.tickets.filter((t) => t.status === "reopened").length,
|
|
227
|
+
deferredTickets: state.tickets.filter((t) => t.status === "deferred").length,
|
|
228
|
+
openPatterns: state.patterns.filter((p) => p.status === "active").length,
|
|
229
|
+
stalledPatterns: state.patterns.filter((p) => p.status === "open").length,
|
|
230
|
+
resolvedPatterns: state.patterns.filter((p) => p.status === "resolved").length,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async getPattern(id) {
|
|
234
|
+
const state = await this.ensureInitialized();
|
|
235
|
+
return state.patterns.find((entry) => entry.id === id);
|
|
236
|
+
}
|
|
237
|
+
getConfig() {
|
|
238
|
+
return this.config;
|
|
239
|
+
}
|
|
240
|
+
async sourceConvergence(options = {}) {
|
|
241
|
+
const state = await this.ensureInitialized();
|
|
242
|
+
return (0, convergence_1.sourceConvergenceReport)(state.tickets, state.patterns, options);
|
|
243
|
+
}
|
|
244
|
+
async guardGaps(options = {}) {
|
|
245
|
+
const state = await this.ensureInitialized();
|
|
246
|
+
return (0, guards_1.guardGapReport)(state.tickets, this.config, options);
|
|
247
|
+
}
|
|
248
|
+
async searchKnowledge(options = {}) {
|
|
249
|
+
const state = await this.ensureInitialized();
|
|
250
|
+
return (0, knowledge_1.resolutionKnowledge)(state.tickets, options);
|
|
251
|
+
}
|
|
252
|
+
async knowledgeGaps(options = {}) {
|
|
253
|
+
const state = await this.ensureInitialized();
|
|
254
|
+
return (0, knowledge_1.knowledgeGaps)(state.tickets, options);
|
|
255
|
+
}
|
|
256
|
+
async related(rawId, options = {}) {
|
|
257
|
+
const state = await this.ensureInitialized();
|
|
258
|
+
const targetId = normalizeTicketInput(rawId, state.tickets);
|
|
259
|
+
const configured = this.config.priorArt;
|
|
260
|
+
return (0, prior_art_1.relatedTickets)(targetId, state.tickets, {
|
|
261
|
+
weights: { ...configured?.weights, ...options.weights },
|
|
262
|
+
minScore: options.minScore ?? configured?.minScore,
|
|
263
|
+
limit: options.limit,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// Resolves an id/alias and optionally applies a status change. Does NOT
|
|
267
|
+
// persist — callers mutate further (notes, timestamps, resolution fields) and
|
|
268
|
+
// persist once when done, so those mutations are never lost.
|
|
269
|
+
async transitionTicket(rawId, status) {
|
|
270
|
+
const state = await this.ensureInitialized();
|
|
271
|
+
const targetId = normalizeTicketInput(rawId, state.tickets);
|
|
272
|
+
const ticket = state.tickets.find((entry) => entry.id === targetId);
|
|
273
|
+
if (!ticket) {
|
|
274
|
+
throw new Error(`Ticket not found: ${rawId}`);
|
|
275
|
+
}
|
|
276
|
+
if (status && ticket.status !== status) {
|
|
277
|
+
ticket.status = status;
|
|
278
|
+
ticket.updatedAt = nowIso();
|
|
279
|
+
}
|
|
280
|
+
return ticket;
|
|
281
|
+
}
|
|
282
|
+
attachPattern(state, family, ticketId) {
|
|
283
|
+
if (!this.config.patterns.autoCreateByFamily) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
const normalizedFamily = family || this.config.patterns.defaultFamily;
|
|
287
|
+
let pattern = state.patterns.find((entry) => entry.family === normalizedFamily && entry.status !== "resolved");
|
|
288
|
+
if (!pattern) {
|
|
289
|
+
pattern = {
|
|
290
|
+
id: patternId(++state.nextPatternSeq),
|
|
291
|
+
family: normalizedFamily,
|
|
292
|
+
title: `Recurring ${normalizedFamily} issues`,
|
|
293
|
+
status: "open",
|
|
294
|
+
createdAt: nowIso(),
|
|
295
|
+
updatedAt: nowIso(),
|
|
296
|
+
ticketIds: [],
|
|
297
|
+
};
|
|
298
|
+
state.patterns.push(pattern);
|
|
299
|
+
}
|
|
300
|
+
if (!pattern.ticketIds.includes(ticketId)) {
|
|
301
|
+
pattern.ticketIds.push(ticketId);
|
|
302
|
+
if (pattern.ticketIds.length >= 2) {
|
|
303
|
+
pattern.status = "active";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
pattern.updatedAt = nowIso();
|
|
307
|
+
return pattern.id;
|
|
308
|
+
}
|
|
309
|
+
async persist() {
|
|
310
|
+
if (!this.state) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.state.updatedAt = nowIso();
|
|
314
|
+
await this.backend.save(this.state);
|
|
315
|
+
}
|
|
316
|
+
get nowState() {
|
|
317
|
+
return nowIso();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
exports.AgentLoopStore = AgentLoopStore;
|
|
321
|
+
function normalizeTicketInput(raw, tickets) {
|
|
322
|
+
const normalized = raw.toUpperCase();
|
|
323
|
+
const match = /^([A-Z]+)-(\d{1,})$/.exec(normalized);
|
|
324
|
+
if (!match) {
|
|
325
|
+
return raw.toUpperCase();
|
|
326
|
+
}
|
|
327
|
+
const prefix = match[1];
|
|
328
|
+
const seq = match[2];
|
|
329
|
+
const canonical = `ISSUE-${seq}`;
|
|
330
|
+
if (prefix === "ISSUE") {
|
|
331
|
+
return canonical;
|
|
332
|
+
}
|
|
333
|
+
const found = tickets.find((ticket) => ticket.aliases.includes(`${prefix}-${seq}`));
|
|
334
|
+
if (found) {
|
|
335
|
+
return found.id;
|
|
336
|
+
}
|
|
337
|
+
return canonical;
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=store.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export type TicketKind = "bug" | "feature" | "user_feedback" | "investigation" | "incident" | "tech_debt" | "task";
|
|
2
|
+
export type TicketStatus = "triaged" | "active" | "resolved" | "reopened" | "deferred";
|
|
3
|
+
export type PatternStatus = "open" | "active" | "resolved" | "reopened";
|
|
4
|
+
export type Severity = "low" | "medium" | "high" | "critical";
|
|
5
|
+
export type Confidence = "low" | "medium" | "high";
|
|
6
|
+
export type GuardStatus = "guard_added" | "guard_existing" | "guard_waived" | "guard_deferred" | "none";
|
|
7
|
+
export type NoteType = "hypothesis" | "related_history" | "prior_fix" | "triage" | "investigation";
|
|
8
|
+
export interface KindConfig {
|
|
9
|
+
kind: TicketKind;
|
|
10
|
+
defaultSeverity: Severity;
|
|
11
|
+
requiredFields?: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A queue routes tickets to a single user-facing alias prefix. Queues are
|
|
15
|
+
* evaluated in order (first match wins); a `source` match takes the precedence
|
|
16
|
+
* of the queue it belongs to, so e.g. a `user_report`-sourced bug routes to the
|
|
17
|
+
* USER queue rather than ISSUE.
|
|
18
|
+
*/
|
|
19
|
+
export interface QueueConfig {
|
|
20
|
+
/** Alias prefix, e.g. "USER", "DEV", "ISSUE". */
|
|
21
|
+
prefix: string;
|
|
22
|
+
/** Ticket kinds routed to this queue. */
|
|
23
|
+
kinds?: TicketKind[];
|
|
24
|
+
/** Sources routed to this queue, overriding kind routing. */
|
|
25
|
+
sources?: string[];
|
|
26
|
+
/** Fallback queue when nothing else matches. Exactly one queue should set this. */
|
|
27
|
+
default?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ProjectConfig {
|
|
30
|
+
projectName: string;
|
|
31
|
+
description: string;
|
|
32
|
+
defaultKind: TicketKind;
|
|
33
|
+
ticketKinds: KindConfig[];
|
|
34
|
+
queues: QueueConfig[];
|
|
35
|
+
sources: string[];
|
|
36
|
+
patterns: {
|
|
37
|
+
autoCreateByFamily: boolean;
|
|
38
|
+
defaultFamily: string;
|
|
39
|
+
};
|
|
40
|
+
/** Optional overrides for prior-art relatedness scoring. Core defaults apply when omitted. */
|
|
41
|
+
priorArt?: {
|
|
42
|
+
weights?: Partial<{
|
|
43
|
+
family: number;
|
|
44
|
+
pattern: number;
|
|
45
|
+
tag: number;
|
|
46
|
+
kind: number;
|
|
47
|
+
textOverlap: number;
|
|
48
|
+
}>;
|
|
49
|
+
minScore?: number;
|
|
50
|
+
};
|
|
51
|
+
/** Optional config-driven redaction. Library users can also inject a TicketRedactor directly. */
|
|
52
|
+
redaction?: {
|
|
53
|
+
patterns?: RedactionRule[];
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Optional storage selection for the CLI/MCP. Prefer the `DATABASE_URL`
|
|
57
|
+
* environment variable for the connection string (it takes precedence) so
|
|
58
|
+
* secrets stay out of committed config.
|
|
59
|
+
*/
|
|
60
|
+
storage?: {
|
|
61
|
+
databaseUrl?: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Context passed to a redactor so host implementations can vary behavior by field/ticket. */
|
|
65
|
+
export interface RedactionContext {
|
|
66
|
+
field: string;
|
|
67
|
+
ticketKind?: string;
|
|
68
|
+
source?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Host-pluggable redaction hook. Core ships a no-op default and a config-driven
|
|
72
|
+
* pattern redactor; host apps own real PII/secret scrubbing.
|
|
73
|
+
*/
|
|
74
|
+
export interface TicketRedactor {
|
|
75
|
+
redactText(value: string, context: RedactionContext): string;
|
|
76
|
+
redactJson(value: unknown, context: RedactionContext): unknown;
|
|
77
|
+
}
|
|
78
|
+
/** A single config-driven redaction rule (regex → replacement). */
|
|
79
|
+
export interface RedactionRule {
|
|
80
|
+
name?: string;
|
|
81
|
+
/** Regular-expression source. */
|
|
82
|
+
pattern: string;
|
|
83
|
+
/** Regex flags; defaults to "g". */
|
|
84
|
+
flags?: string;
|
|
85
|
+
/** Replacement string; defaults to "[redacted]". */
|
|
86
|
+
replacement?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface TicketNote {
|
|
89
|
+
id: string;
|
|
90
|
+
type: NoteType;
|
|
91
|
+
body: string;
|
|
92
|
+
author?: string;
|
|
93
|
+
createdAt: string;
|
|
94
|
+
}
|
|
95
|
+
export interface Ticket {
|
|
96
|
+
id: string;
|
|
97
|
+
family: string;
|
|
98
|
+
kind: TicketKind;
|
|
99
|
+
source: string;
|
|
100
|
+
title: string;
|
|
101
|
+
summary: string;
|
|
102
|
+
severity: Severity;
|
|
103
|
+
confidence: Confidence;
|
|
104
|
+
status: TicketStatus;
|
|
105
|
+
createdAt: string;
|
|
106
|
+
updatedAt: string;
|
|
107
|
+
startedAt?: string;
|
|
108
|
+
resolvedAt?: string;
|
|
109
|
+
aliases: string[];
|
|
110
|
+
tags: string[];
|
|
111
|
+
notes: TicketNote[];
|
|
112
|
+
handoffText?: string;
|
|
113
|
+
guardStatus?: GuardStatus;
|
|
114
|
+
guardSummary?: string;
|
|
115
|
+
patternId?: string;
|
|
116
|
+
verification?: string;
|
|
117
|
+
reproducible?: boolean;
|
|
118
|
+
resolutionSummary?: string;
|
|
119
|
+
}
|
|
120
|
+
export interface Pattern {
|
|
121
|
+
id: string;
|
|
122
|
+
family: string;
|
|
123
|
+
title: string;
|
|
124
|
+
status: PatternStatus;
|
|
125
|
+
createdAt: string;
|
|
126
|
+
updatedAt: string;
|
|
127
|
+
ticketIds: string[];
|
|
128
|
+
}
|
|
129
|
+
export interface LoopState {
|
|
130
|
+
version: number;
|
|
131
|
+
project: string;
|
|
132
|
+
createdAt: string;
|
|
133
|
+
updatedAt: string;
|
|
134
|
+
nextTicketSeq: number;
|
|
135
|
+
nextPatternSeq: number;
|
|
136
|
+
tickets: Ticket[];
|
|
137
|
+
patterns: Pattern[];
|
|
138
|
+
}
|
|
139
|
+
export interface CreateTicketInput {
|
|
140
|
+
title: string;
|
|
141
|
+
summary: string;
|
|
142
|
+
family: string;
|
|
143
|
+
kind: TicketKind;
|
|
144
|
+
source: string;
|
|
145
|
+
severity?: Severity;
|
|
146
|
+
confidence?: Confidence;
|
|
147
|
+
tags?: string[];
|
|
148
|
+
handoffText?: string;
|
|
149
|
+
}
|
|
150
|
+
export interface ResolveInput {
|
|
151
|
+
id: string;
|
|
152
|
+
summary: string;
|
|
153
|
+
verification?: string;
|
|
154
|
+
guardStatus?: GuardStatus;
|
|
155
|
+
guardSummary?: string;
|
|
156
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
IntiDev AgentLoops is organized around a tiny local persistence and CLI layer:
|
|
4
|
+
|
|
5
|
+
## Core primitives
|
|
6
|
+
|
|
7
|
+
- `src/store.ts`
|
|
8
|
+
- owns durable state in `.agentloops/state.json`
|
|
9
|
+
- manages create/list/update transitions
|
|
10
|
+
- maintains ticket aliases and simple pattern grouping
|
|
11
|
+
- `src/config.ts`
|
|
12
|
+
- project-level config schema and defaults
|
|
13
|
+
- alias mapping and required fields
|
|
14
|
+
- `src/cli.ts`
|
|
15
|
+
- command parser and user-facing workflows
|
|
16
|
+
- `agentloop.config.json`
|
|
17
|
+
- local configuration created from template
|
|
18
|
+
|
|
19
|
+
## Extensibility points
|
|
20
|
+
|
|
21
|
+
- ticket kinds and aliases can be customized in config
|
|
22
|
+
- custom required fields are represented through `requiredFields` per kind
|
|
23
|
+
- patterns currently group by family; teams can replace that in a fork or add source-level adapters
|
|
24
|
+
|
|
25
|
+
## Planned extraction roadmap
|
|
26
|
+
|
|
27
|
+
1. add MCP read-only tools for dashboard integrations,
|
|
28
|
+
2. add pluggable storage adapters (SQLite/Postgres/HTTP API),
|
|
29
|
+
3. add source adapters (Sentry, GitHub, Linear, Jira),
|
|
30
|
+
4. split CLI/API/SDK packages.
|