@lumenflow/agent 1.0.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.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Feedback Promote Core Logic (WU-1599)
3
+ *
4
+ * Generates draft WU specs from feedback patterns and promotes them to actual WUs.
5
+ * Tracks incident-to-WU mappings in feedback-index.ndjson.
6
+ *
7
+ * @see {@link tools/__tests__/feedback-promote.test.mjs} - Tests
8
+ * @see {@link tools/feedback-promote.mjs} - CLI entry point
9
+ */
10
+ /**
11
+ * Directory for draft WU specs
12
+ */
13
+ export declare const DRAFT_DIRECTORY = ".beacon/feedback-drafts";
14
+ /**
15
+ * Path to feedback index (incident-to-WU mappings)
16
+ */
17
+ export declare const FEEDBACK_INDEX_PATH = ".beacon/feedback-index.ndjson";
18
+ /**
19
+ * Feedback index entry status
20
+ */
21
+ export declare const FEEDBACK_STATUS: {
22
+ PENDING_RESOLUTION: string;
23
+ RESOLVED: string;
24
+ WONT_FIX: string;
25
+ };
26
+ /**
27
+ * Pattern example structure
28
+ */
29
+ interface PatternExample {
30
+ id: string;
31
+ [key: string]: unknown;
32
+ }
33
+ /**
34
+ * Pattern structure from feedback:review
35
+ */
36
+ interface Pattern {
37
+ title: string;
38
+ category?: string;
39
+ frequency?: number;
40
+ score?: number;
41
+ firstSeen?: string;
42
+ lastSeen?: string;
43
+ examples?: PatternExample[];
44
+ }
45
+ /**
46
+ * Draft WU spec structure
47
+ */
48
+ interface DraftSpec {
49
+ title: string;
50
+ lane: string;
51
+ description: string;
52
+ acceptance: string[];
53
+ source_incidents: string[];
54
+ pattern_metadata: {
55
+ frequency: number | undefined;
56
+ category: string | undefined;
57
+ score: number | undefined;
58
+ firstSeen: string | undefined;
59
+ lastSeen: string | undefined;
60
+ };
61
+ filePath?: string;
62
+ }
63
+ /**
64
+ * Options for generating a draft
65
+ */
66
+ interface GenerateDraftOptions {
67
+ writeFile?: boolean;
68
+ }
69
+ /**
70
+ * Generate draft WU spec from a pattern
71
+ *
72
+ * @param baseDir - Base directory
73
+ * @param pattern - Pattern from feedback:review
74
+ * @param options - Options
75
+ * @returns Draft WU spec
76
+ */
77
+ export declare function generateDraft(baseDir: string, pattern: Pattern, options?: GenerateDraftOptions): Promise<DraftSpec>;
78
+ /**
79
+ * Load all draft files from .beacon/feedback-drafts/
80
+ *
81
+ * @param baseDir - Base directory
82
+ * @returns Array of draft objects with filePath
83
+ */
84
+ export declare function loadDrafts(baseDir: string): Promise<DraftSpec[]>;
85
+ /**
86
+ * Options for promoting a draft
87
+ */
88
+ interface PromoteDraftOptions {
89
+ dryRun?: boolean;
90
+ wuIdOverride?: string;
91
+ removeDraft?: boolean;
92
+ }
93
+ /**
94
+ * Result of promoting a draft
95
+ */
96
+ interface PromoteDraftResult {
97
+ success: boolean;
98
+ wuId: string;
99
+ command: string;
100
+ draft?: DraftSpec;
101
+ error?: string;
102
+ draftRemoved?: boolean;
103
+ }
104
+ /**
105
+ * Promote a draft to a WU via wu:create
106
+ *
107
+ * @param baseDir - Base directory
108
+ * @param draft - Draft object
109
+ * @param options - Options
110
+ * @returns Result with success, wuId, command
111
+ */
112
+ export declare function promoteDraft(baseDir: string, draft: DraftSpec, options?: PromoteDraftOptions): Promise<PromoteDraftResult>;
113
+ /**
114
+ * Feedback index entry
115
+ */
116
+ interface FeedbackIndexEntry {
117
+ incident_id: string;
118
+ wu_id: string;
119
+ status: string;
120
+ timestamp: string;
121
+ }
122
+ /**
123
+ * Update feedback index with incident-to-WU mappings
124
+ *
125
+ * @param baseDir - Base directory
126
+ * @param wuId - WU ID
127
+ * @param incidentIds - Array of incident IDs
128
+ */
129
+ export declare function updateFeedbackIndex(baseDir: string, wuId: string, incidentIds: string[]): Promise<void>;
130
+ /**
131
+ * Load feedback index entries
132
+ *
133
+ * @param baseDir - Base directory
134
+ * @returns Array of index entries
135
+ */
136
+ export declare function loadFeedbackIndex(baseDir: string): Promise<FeedbackIndexEntry[]>;
137
+ export {};
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Feedback Promote Core Logic (WU-1599)
3
+ *
4
+ * Generates draft WU specs from feedback patterns and promotes them to actual WUs.
5
+ * Tracks incident-to-WU mappings in feedback-index.ndjson.
6
+ *
7
+ * @see {@link tools/__tests__/feedback-promote.test.mjs} - Tests
8
+ * @see {@link tools/feedback-promote.mjs} - CLI entry point
9
+ */
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import { stringifyYAML, parseYAML } from '@lumenflow/core/lib/wu-yaml.js';
13
+ /**
14
+ * Directory for draft WU specs
15
+ */
16
+ export const DRAFT_DIRECTORY = '.beacon/feedback-drafts';
17
+ /**
18
+ * Path to feedback index (incident-to-WU mappings)
19
+ */
20
+ export const FEEDBACK_INDEX_PATH = '.beacon/feedback-index.ndjson';
21
+ /**
22
+ * Feedback index entry status
23
+ */
24
+ export const FEEDBACK_STATUS = {
25
+ PENDING_RESOLUTION: 'pending_resolution',
26
+ RESOLVED: 'resolved',
27
+ WONT_FIX: 'wont_fix',
28
+ };
29
+ /**
30
+ * Lane constants for DRY compliance
31
+ */
32
+ const LANES = {
33
+ OPERATIONS_TOOLING: 'Operations: Tooling',
34
+ OPERATIONS_DOCUMENTATION: 'Operations: Documentation',
35
+ OPERATIONS_SECURITY: 'Operations: Security',
36
+ OPERATIONS_COMPLIANCE: 'Operations: Compliance',
37
+ OPERATIONS: 'Operations',
38
+ CORE_SYSTEMS: 'Core Systems',
39
+ EXPERIENCE: 'Experience',
40
+ INTELLIGENCE: 'Intelligence',
41
+ };
42
+ /**
43
+ * Lane inference mapping from category to suggested lane
44
+ */
45
+ const CATEGORY_TO_LANE = {
46
+ test: LANES.OPERATIONS_TOOLING,
47
+ tooling: LANES.OPERATIONS_TOOLING,
48
+ docs: LANES.OPERATIONS_DOCUMENTATION,
49
+ documentation: LANES.OPERATIONS_DOCUMENTATION,
50
+ infrastructure: LANES.CORE_SYSTEMS,
51
+ database: LANES.CORE_SYSTEMS,
52
+ api: LANES.CORE_SYSTEMS,
53
+ ui: LANES.EXPERIENCE,
54
+ frontend: LANES.EXPERIENCE,
55
+ ux: LANES.EXPERIENCE,
56
+ llm: LANES.INTELLIGENCE,
57
+ prompt: LANES.INTELLIGENCE,
58
+ ai: LANES.INTELLIGENCE,
59
+ security: LANES.OPERATIONS_SECURITY,
60
+ compliance: LANES.OPERATIONS_COMPLIANCE,
61
+ uncategorized: LANES.OPERATIONS,
62
+ };
63
+ /**
64
+ * Infer lane from pattern category
65
+ *
66
+ * @param category - Pattern category
67
+ * @returns Suggested lane
68
+ */
69
+ function inferLane(category) {
70
+ const normalizedCategory = (category ?? 'uncategorized').toLowerCase();
71
+ // Safe lookup using Object.hasOwn to prevent prototype pollution
72
+ if (Object.hasOwn(CATEGORY_TO_LANE, normalizedCategory)) {
73
+ // eslint-disable-next-line security/detect-object-injection -- Safe: hasOwn validates key exists
74
+ return CATEGORY_TO_LANE[normalizedCategory];
75
+ }
76
+ return CATEGORY_TO_LANE.uncategorized;
77
+ }
78
+ /**
79
+ * Generate description from pattern
80
+ *
81
+ * @param pattern - Pattern object
82
+ * @returns Description with Context/Problem/Solution structure
83
+ */
84
+ function generateDescription(pattern) {
85
+ const frequency = pattern.frequency ?? 1;
86
+ const category = pattern.category ?? 'uncategorized';
87
+ const firstSeen = pattern.firstSeen
88
+ ? new Date(pattern.firstSeen).toISOString().slice(0, 10)
89
+ : 'unknown';
90
+ const lastSeen = pattern.lastSeen
91
+ ? new Date(pattern.lastSeen).toISOString().slice(0, 10)
92
+ : 'unknown';
93
+ return [
94
+ `Context: Pattern detected from ${frequency} incident(s) in category "${category}". First seen: ${firstSeen}, last seen: ${lastSeen}.`,
95
+ '',
96
+ `Problem: ${pattern.title}`,
97
+ '',
98
+ 'Solution: [To be defined by implementer]',
99
+ ].join('\n');
100
+ }
101
+ /**
102
+ * Generate acceptance criteria from pattern
103
+ *
104
+ * @param pattern - Pattern object
105
+ * @returns Acceptance criteria
106
+ */
107
+ function generateAcceptance(pattern) {
108
+ return [
109
+ 'Root cause identified and documented',
110
+ 'Fix implemented',
111
+ 'Tests pass (pnpm gates)',
112
+ `No recurrence of "${pattern.title}" pattern`,
113
+ ];
114
+ }
115
+ /**
116
+ * Generate draft WU spec from a pattern
117
+ *
118
+ * @param baseDir - Base directory
119
+ * @param pattern - Pattern from feedback:review
120
+ * @param options - Options
121
+ * @returns Draft WU spec
122
+ */
123
+ export async function generateDraft(baseDir, pattern, options = {}) {
124
+ const { writeFile: shouldWrite = false } = options;
125
+ const draft = {
126
+ title: pattern.title,
127
+ lane: inferLane(pattern.category),
128
+ description: generateDescription(pattern),
129
+ acceptance: generateAcceptance(pattern),
130
+ source_incidents: (pattern.examples ?? []).map((e) => e.id),
131
+ pattern_metadata: {
132
+ frequency: pattern.frequency,
133
+ category: pattern.category,
134
+ score: pattern.score,
135
+ firstSeen: pattern.firstSeen,
136
+ lastSeen: pattern.lastSeen,
137
+ },
138
+ };
139
+ if (shouldWrite) {
140
+ const draftsDir = path.join(baseDir, DRAFT_DIRECTORY);
141
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
142
+ await fs.mkdir(draftsDir, { recursive: true });
143
+ const timestamp = Date.now();
144
+ const filename = `draft-${timestamp}.yaml`;
145
+ const filePath = path.join(draftsDir, filename);
146
+ const yamlContent = stringifyYAML(draft, { lineWidth: 100 });
147
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes draft file
148
+ await fs.writeFile(filePath, yamlContent, 'utf8');
149
+ draft.filePath = path.join(DRAFT_DIRECTORY, filename);
150
+ }
151
+ return draft;
152
+ }
153
+ /**
154
+ * Load all draft files from .beacon/feedback-drafts/
155
+ *
156
+ * @param baseDir - Base directory
157
+ * @returns Array of draft objects with filePath
158
+ */
159
+ export async function loadDrafts(baseDir) {
160
+ const draftsDir = path.join(baseDir, DRAFT_DIRECTORY);
161
+ let files;
162
+ try {
163
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known directory
164
+ files = await fs.readdir(draftsDir);
165
+ }
166
+ catch (err) {
167
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
168
+ return [];
169
+ }
170
+ throw err;
171
+ }
172
+ const yamlFiles = files.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
173
+ const drafts = [];
174
+ for (const file of yamlFiles) {
175
+ const filePath = path.join(draftsDir, file);
176
+ try {
177
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads draft files
178
+ const content = await fs.readFile(filePath, 'utf8');
179
+ // Parse YAML or JSON content
180
+ let draft;
181
+ try {
182
+ // Try JSON first (for test files that might use JSON.stringify)
183
+ draft = JSON.parse(content);
184
+ }
185
+ catch {
186
+ // Fall back to YAML parsing
187
+ draft = parseYAML(content);
188
+ }
189
+ draft.filePath = path.join(DRAFT_DIRECTORY, file);
190
+ drafts.push(draft);
191
+ }
192
+ catch (err) {
193
+ // Skip malformed files
194
+ const errorMessage = err instanceof Error ? err.message : String(err);
195
+ console.warn(`Warning: Could not load draft ${file}: ${errorMessage}`);
196
+ }
197
+ }
198
+ return drafts;
199
+ }
200
+ /**
201
+ * Promote a draft to a WU via wu:create
202
+ *
203
+ * @param baseDir - Base directory
204
+ * @param draft - Draft object
205
+ * @param options - Options
206
+ * @returns Result with success, wuId, command
207
+ */
208
+ export async function promoteDraft(baseDir, draft, options = {}) {
209
+ const { dryRun = false, wuIdOverride, removeDraft = false } = options;
210
+ // Generate WU ID if not provided
211
+ const wuId = wuIdOverride ?? `WU-${Date.now()}`;
212
+ // Build wu:create command
213
+ const command = buildWuCreateCommand(wuId, draft);
214
+ const result = {
215
+ success: true,
216
+ wuId,
217
+ command,
218
+ draft,
219
+ };
220
+ if (!dryRun) {
221
+ // Execute wu:create command
222
+ const { execSync } = await import('node:child_process');
223
+ try {
224
+ execSync(command, { cwd: baseDir, stdio: 'pipe' });
225
+ }
226
+ catch (err) {
227
+ const errorMessage = err instanceof Error ? err.message : String(err);
228
+ return {
229
+ success: false,
230
+ wuId,
231
+ command,
232
+ error: errorMessage,
233
+ };
234
+ }
235
+ // Update feedback index with incident mappings
236
+ if (draft.source_incidents && draft.source_incidents.length > 0) {
237
+ await updateFeedbackIndex(baseDir, wuId, draft.source_incidents);
238
+ }
239
+ }
240
+ // Remove draft file if requested
241
+ if (removeDraft && draft.filePath) {
242
+ const absolutePath = draft.filePath.startsWith('/')
243
+ ? draft.filePath
244
+ : path.join(baseDir, draft.filePath);
245
+ try {
246
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool removes draft file
247
+ await fs.unlink(absolutePath);
248
+ result.draftRemoved = true;
249
+ }
250
+ catch (err) {
251
+ // Ignore errors if file doesn't exist
252
+ if (err instanceof Error && 'code' in err && err.code !== 'ENOENT') {
253
+ console.warn(`Warning: Could not remove draft ${draft.filePath}: ${err.message}`);
254
+ }
255
+ result.draftRemoved = true; // Mark as removed even if it didn't exist
256
+ }
257
+ }
258
+ return result;
259
+ }
260
+ /**
261
+ * Build wu:create command from draft
262
+ *
263
+ * @param wuId - WU ID
264
+ * @param draft - Draft object
265
+ * @returns Command string
266
+ */
267
+ function buildWuCreateCommand(wuId, draft) {
268
+ const parts = ['pnpm wu:create'];
269
+ parts.push(`--id ${wuId}`);
270
+ parts.push(`--lane "${draft.lane}"`);
271
+ parts.push(`--title "${draft.title.replace(/"/g, '\\"')}"`);
272
+ if (draft.description) {
273
+ parts.push(`--description "${draft.description.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`);
274
+ }
275
+ if (draft.acceptance && draft.acceptance.length > 0) {
276
+ for (const criterion of draft.acceptance) {
277
+ parts.push(`--acceptance "${criterion.replace(/"/g, '\\"')}"`);
278
+ }
279
+ }
280
+ return parts.join(' ');
281
+ }
282
+ /**
283
+ * Update feedback index with incident-to-WU mappings
284
+ *
285
+ * @param baseDir - Base directory
286
+ * @param wuId - WU ID
287
+ * @param incidentIds - Array of incident IDs
288
+ */
289
+ export async function updateFeedbackIndex(baseDir, wuId, incidentIds) {
290
+ const indexPath = path.join(baseDir, FEEDBACK_INDEX_PATH);
291
+ const timestamp = new Date().toISOString();
292
+ // Ensure directory exists
293
+ const indexDir = path.dirname(indexPath);
294
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
295
+ await fs.mkdir(indexDir, { recursive: true });
296
+ // Build NDJSON entries
297
+ const entries = incidentIds.map((incidentId) => ({
298
+ incident_id: incidentId,
299
+ wu_id: wuId,
300
+ status: FEEDBACK_STATUS.PENDING_RESOLUTION,
301
+ timestamp,
302
+ }));
303
+ const content = `${entries.map((e) => JSON.stringify(e)).join('\n')}\n`;
304
+ // Append to index file
305
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes index file
306
+ await fs.appendFile(indexPath, content, 'utf8');
307
+ }
308
+ /**
309
+ * Load feedback index entries
310
+ *
311
+ * @param baseDir - Base directory
312
+ * @returns Array of index entries
313
+ */
314
+ export async function loadFeedbackIndex(baseDir) {
315
+ const indexPath = path.join(baseDir, FEEDBACK_INDEX_PATH);
316
+ try {
317
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads index file
318
+ const content = await fs.readFile(indexPath, 'utf8');
319
+ const lines = content.trim().split('\n').filter(Boolean);
320
+ return lines
321
+ .map((line) => {
322
+ try {
323
+ return JSON.parse(line);
324
+ }
325
+ catch {
326
+ return null;
327
+ }
328
+ })
329
+ .filter((entry) => entry !== null);
330
+ }
331
+ catch (err) {
332
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
333
+ return [];
334
+ }
335
+ throw err;
336
+ }
337
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Feedback Review Core Logic (WU-1598)
3
+ *
4
+ * Aggregates .beacon/incidents/*.ndjson and .beacon/memory/memory.jsonl,
5
+ * clusters by title similarity, scores patterns (frequency x severity x recency),
6
+ * and outputs prioritised patterns for human review.
7
+ *
8
+ * @see {@link tools/__tests__/feedback-review.test.mjs} - Tests
9
+ * @see {@link tools/feedback-review.mjs} - CLI entry point
10
+ */
11
+ import { INCIDENT_SEVERITY } from '@lumenflow/core/lib/wu-constants.js';
12
+ /**
13
+ * Severity weights for scoring
14
+ *
15
+ * Higher severity = higher weight in scoring formula
16
+ */
17
+ export declare const SEVERITY_WEIGHTS: {
18
+ [INCIDENT_SEVERITY.BLOCKER]: number;
19
+ [INCIDENT_SEVERITY.MAJOR]: number;
20
+ [INCIDENT_SEVERITY.MINOR]: number;
21
+ [INCIDENT_SEVERITY.INFO]: number;
22
+ };
23
+ /**
24
+ * Node with ID and optional title/content
25
+ */
26
+ interface NodeWithTitle {
27
+ id: string;
28
+ title?: string;
29
+ content?: string;
30
+ category?: string;
31
+ severity?: string;
32
+ created_at?: string;
33
+ source?: string;
34
+ metadata?: Record<string, unknown>;
35
+ type?: string;
36
+ tags?: string[];
37
+ }
38
+ /**
39
+ * Cluster of nodes grouped by title similarity
40
+ */
41
+ interface Cluster {
42
+ title: string;
43
+ nodes: NodeWithTitle[];
44
+ category: string;
45
+ }
46
+ /**
47
+ * Cluster nodes by title similarity
48
+ *
49
+ * Uses simple greedy clustering with Jaccard similarity.
50
+ *
51
+ * @param nodes - Nodes to cluster
52
+ * @param threshold - Similarity threshold
53
+ * @returns Array of cluster objects
54
+ */
55
+ export declare function clusterByTitle(nodes: NodeWithTitle[], threshold?: number): Cluster[];
56
+ /**
57
+ * Score a pattern cluster
58
+ *
59
+ * Formula: frequency x average_severity x recency_factor
60
+ *
61
+ * @param cluster - Cluster with nodes
62
+ * @returns Score value
63
+ */
64
+ export declare function scorePattern(cluster: Cluster): number;
65
+ /**
66
+ * Options for reviewing feedback
67
+ */
68
+ interface ReviewOptions {
69
+ since?: string;
70
+ minFrequency?: number;
71
+ category?: string;
72
+ json?: boolean;
73
+ }
74
+ /**
75
+ * Pattern example for output
76
+ */
77
+ interface PatternExample {
78
+ id: string;
79
+ severity: string | undefined;
80
+ source: string | undefined;
81
+ }
82
+ /**
83
+ * Pattern in review result
84
+ */
85
+ interface ReviewPattern {
86
+ title: string;
87
+ frequency: number;
88
+ category: string;
89
+ score: number;
90
+ firstSeen: string | undefined;
91
+ lastSeen: string | undefined;
92
+ examples: PatternExample[];
93
+ }
94
+ /**
95
+ * Review result
96
+ */
97
+ interface ReviewResult {
98
+ success: boolean;
99
+ patterns: ReviewPattern[];
100
+ summary: {
101
+ totalNodes: number;
102
+ totalClusters: number;
103
+ topCategory: string | null;
104
+ };
105
+ }
106
+ /**
107
+ * Review feedback from incidents and memory nodes
108
+ *
109
+ * Main entry point for feedback review logic.
110
+ *
111
+ * @param baseDir - Base directory containing .beacon
112
+ * @param options - Review options
113
+ * @returns Review result
114
+ */
115
+ export declare function reviewFeedback(baseDir: string, options?: ReviewOptions): Promise<ReviewResult>;
116
+ export {};