fraim 2.0.270 → 2.0.272

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/src/cli/commands/add-ide.js +37 -132
  2. package/dist/src/cli/commands/add-provider.js +32 -268
  3. package/dist/src/cli/commands/learning-usage.js +412 -0
  4. package/dist/src/cli/commands/login.js +5 -5
  5. package/dist/src/cli/commands/setup.js +15 -52
  6. package/dist/src/cli/commands/sync.js +111 -80
  7. package/dist/src/cli/fraim.js +1 -42
  8. package/dist/src/cli/mcp/ide-formats.js +1 -1
  9. package/dist/src/cli/mcp/mcp-server-registry.js +3 -3
  10. package/dist/src/cli/providers/local-provider-registry.js +4 -4
  11. package/dist/src/cli/setup/auto-mcp-setup.js +4 -13
  12. package/dist/src/cli/utils/remote-sync.js +41 -25
  13. package/dist/src/core/ai-mentor.js +27 -14
  14. package/dist/src/core/config-loader.js +48 -3
  15. package/dist/src/core/fraim-config-schema.generated.js +18 -0
  16. package/dist/src/core/handoff-contracts.js +37 -1
  17. package/dist/src/core/job-phases.js +2 -14
  18. package/dist/src/core/resolve-phase-edge.js +75 -0
  19. package/dist/src/core/types.js +7 -1
  20. package/dist/src/core/utils/git-utils.js +24 -14
  21. package/dist/src/core/utils/project-fraim-paths.js +16 -1
  22. package/dist/src/first-run/types.js +1 -1
  23. package/dist/src/local-mcp-server/artifact-retention-cleanup.js +8 -0
  24. package/dist/src/local-mcp-server/learning-context-builder.js +448 -95
  25. package/dist/src/local-mcp-server/learning-firing-parser.js +247 -0
  26. package/dist/src/local-mcp-server/learning-usage-analysis.js +347 -0
  27. package/dist/src/local-mcp-server/learning-usage-attestation.js +191 -0
  28. package/dist/src/local-mcp-server/learning-usage-command.js +408 -0
  29. package/dist/src/local-mcp-server/learning-usage-projection.js +79 -0
  30. package/dist/src/local-mcp-server/learning-usage-store.js +417 -0
  31. package/dist/src/local-mcp-server/stdio-server.js +43 -0
  32. package/dist/src/services/provider-service.js +4 -4
  33. package/package.json +1 -1
@@ -0,0 +1,412 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.learningUsageCommand = void 0;
7
+ /**
8
+ * Issue #1103 — `fraim learning-usage <subcommand>`.
9
+ *
10
+ * The read and analysis side of the usage record. The write side of an offer is the
11
+ * proxy (no agent involvement, R2); the write side of a firing is an attestation in
12
+ * a retrospective, which `record-firings` reads back.
13
+ *
14
+ * This is a CLI rather than a new MCP tool deliberately: the store schema then has
15
+ * exactly one implementation, and nothing is added to the MCP tool surface that
16
+ * issues #801, #860 and #861 are working to shrink.
17
+ */
18
+ const commander_1 = require("commander");
19
+ const node_fs_1 = __importDefault(require("node:fs"));
20
+ const node_path_1 = __importDefault(require("node:path"));
21
+ const learning_context_builder_1 = require("../../local-mcp-server/learning-context-builder");
22
+ const learning_usage_analysis_1 = require("../../local-mcp-server/learning-usage-analysis");
23
+ const learning_usage_store_1 = require("../../local-mcp-server/learning-usage-store");
24
+ const learning_usage_attestation_1 = require("../../local-mcp-server/learning-usage-attestation");
25
+ function resolveWorkspaceRoot(options) {
26
+ return node_path_1.default.resolve(options.workspaceRoot || options.root || process.cwd());
27
+ }
28
+ function resolveUser(options) {
29
+ const email = options.user || process.env.FRAIM_ACTIVE_USER_EMAIL || '';
30
+ if (!email.trim()) {
31
+ throw new Error('Missing active user email. Pass --user <email> or set FRAIM_ACTIVE_USER_EMAIL.');
32
+ }
33
+ return email.trim();
34
+ }
35
+ function emit(payload, json, text) {
36
+ process.stdout.write(json ? `${JSON.stringify(payload, null, 2)}\n` : `${text()}\n`);
37
+ }
38
+ function addCommonOptions(command) {
39
+ return command
40
+ .option('--workspace-root <path>', 'Workspace root containing fraim/config.json')
41
+ .option('--root <path>', 'Alias for --workspace-root')
42
+ .option('--user <email>', 'Active user email')
43
+ .option('--json', 'Print JSON');
44
+ }
45
+ // ── offers ───────────────────────────────────────────────────────────────────
46
+ const offersCommand = addCommonOptions(new commander_1.Command('offers'))
47
+ .description('List the learning entries and rule files the runtime delivered, so an attestation is written against the record rather than memory')
48
+ .option('--job <name>', 'The job whose context was loaded', 'unknown')
49
+ .option('--domain <domain>', 'Learning domain the job resolved to')
50
+ .option('--session', 'Use the session frame rather than the job frame')
51
+ .action((options) => {
52
+ const workspaceRoot = resolveWorkspaceRoot(options);
53
+ const user = resolveUser(options);
54
+ const forJob = !options.session;
55
+ // Prefer what the runtime recorded for this job, because that is what was
56
+ // actually delivered. Re-deriving from the files on disk answers a subtly
57
+ // different question — what would be delivered now — and the two diverge if a
58
+ // learning file changed during the job.
59
+ const store = (0, learning_usage_store_1.readUsageStore)();
60
+ const recorded = Object.values(store.entries)
61
+ .filter((entry) => entry.offered.log.some((offer) => offer.job === options.job))
62
+ .map((entry) => {
63
+ const last = [...entry.offered.log].reverse().find((offer) => offer.job === options.job);
64
+ return {
65
+ key: entry.key,
66
+ family: entry.family,
67
+ id: entry.key.startsWith('L-') ? entry.key : null,
68
+ title: entry.titles[entry.titles.length - 1] ?? entry.key,
69
+ fileType: entry.fileType ?? 'unknown',
70
+ level: entry.level ?? 'unknown',
71
+ displayPath: '',
72
+ severity: null,
73
+ aboveThreshold: last.aboveThreshold,
74
+ };
75
+ });
76
+ const source = recorded.length > 0 ? 'offer-record' : 'derived';
77
+ const offered = recorded.length > 0
78
+ ? recorded
79
+ : [
80
+ ...(0, learning_context_builder_1.collectOfferedLearningEntries)(workspaceRoot, user, forJob, options.domain ?? null),
81
+ ...(0, learning_context_builder_1.collectOfferedRuleFiles)(workspaceRoot, forJob),
82
+ ];
83
+ const payload = { job: options.job, forJob, source, offered };
84
+ emit(payload, options.json, () => {
85
+ if (payload.offered.length === 0) {
86
+ return `No offer record and nothing resolves for job ${options.job}. Do not attest any firing for this job.`;
87
+ }
88
+ const rows = payload.offered.map((o) => `| ${o.id ?? '(no id)'} | ${o.title} | ${o.fileType} | ${o.aboveThreshold ? 'yes' : 'no'} |`);
89
+ const header = source === 'offer-record'
90
+ ? `Entries offered for job ${options.job}, from the offer record:`
91
+ : `No offer record exists for job ${options.job} yet, so this is what would be delivered now. Say so when you attest against it.`;
92
+ return [
93
+ header,
94
+ '',
95
+ '| Id | Title | Family | Above threshold |',
96
+ '|---|---|---|---|',
97
+ ...rows,
98
+ ].join('\n');
99
+ });
100
+ });
101
+ // ── record-firings ───────────────────────────────────────────────────────────
102
+ const recordFiringsCommand = addCommonOptions(new commander_1.Command('record-firings'))
103
+ .description('Read the firing section of a retrospective and write the attested firings into the usage record')
104
+ .requiredOption('--retrospective <path>', 'Retrospective file to read')
105
+ .option('--job <name>', 'The job that produced the retrospective')
106
+ .option('--agent <name>', 'Agent that attested')
107
+ .option('--model <model>', 'Model that attested')
108
+ .option('--legacy', 'Also accept the pre-#1103 prose form')
109
+ .action((options) => {
110
+ const workspaceRoot = resolveWorkspaceRoot(options);
111
+ const user = resolveUser(options);
112
+ const filePath = node_path_1.default.resolve(options.retrospective);
113
+ if (!node_fs_1.default.existsSync(filePath)) {
114
+ throw new Error(`Retrospective not found: ${filePath}`);
115
+ }
116
+ const content = node_fs_1.default.readFileSync(filePath, 'utf8');
117
+ const source = node_path_1.default.relative(workspaceRoot, filePath).replace(/\\/g, '/');
118
+ const result = (0, learning_usage_attestation_1.recordAttestationsFromRetrospective)(workspaceRoot, user, {
119
+ content,
120
+ source,
121
+ job: options.job ?? 'unknown',
122
+ agent: options.agent ?? null,
123
+ model: options.model ?? null,
124
+ legacy: Boolean(options.legacy),
125
+ });
126
+ emit(result, options.json, () => {
127
+ const lines = [
128
+ `Section state: ${result.sectionState}`,
129
+ `Items read: ${result.itemsRead}`,
130
+ `Recorded: ${result.applied}`,
131
+ `Already recorded: ${result.alreadyRecorded}`,
132
+ `Unmatched: ${result.unmatched.length}`,
133
+ `Rejected: ${result.rejected.length}`,
134
+ ];
135
+ for (const item of result.unmatched)
136
+ lines.push(` unmatched: ${item.title} — ${item.reason}`);
137
+ for (const item of result.rejected)
138
+ lines.push(` rejected: ${item.reason}`);
139
+ return lines.join('\n');
140
+ });
141
+ });
142
+ // ── classify ─────────────────────────────────────────────────────────────────
143
+ const classifyCommand = addCommonOptions(new commander_1.Command('classify'))
144
+ .description('Say whether a recurrence was ignored (the entry was in context) or not-offered (it never reached the agent), because the two need different fixes')
145
+ .option('--entry <id>', 'The entry id, for an entry that carries one')
146
+ .option('--title <title>', 'The exact entry title, for an entry with no id yet. Use with --family')
147
+ .option('--family <family>', 'The entry family: mistake-patterns, preferences, validated-patterns, or manager-coaching')
148
+ .requiredOption('--job <name>', 'The job the recurrence happened in')
149
+ .requiredOption('--date <YYYY-MM-DD>', 'The date the mistake recurred')
150
+ .option('--window <days>', 'How far back an offer still counts as in-context', '30')
151
+ .action((options) => {
152
+ const workspaceRoot = resolveWorkspaceRoot(options);
153
+ // `--title` plus `--family` exists so a caller never has to build the composite
154
+ // title key by hand. An entry title is corpus content and can contain any
155
+ // character, so an agent assembling `T:<family>:<title>` into a shell command
156
+ // would be interpolating untrusted text into a command line. Passing the title
157
+ // as its own argument keeps it a single argv entry.
158
+ let key;
159
+ if (options.entry) {
160
+ key = String(options.entry);
161
+ }
162
+ else if (options.title && options.family) {
163
+ key = (0, learning_usage_store_1.titleUsageKey)(String(options.family), String(options.title));
164
+ }
165
+ else {
166
+ throw new Error('Pass --entry <id>, or --title <title> together with --family <family>.');
167
+ }
168
+ const store = (0, learning_usage_store_1.readUsageStore)();
169
+ const diagnosis = (0, learning_usage_analysis_1.classifyRecurrence)(store, {
170
+ key,
171
+ job: options.job,
172
+ date: options.date,
173
+ windowDays: Number.parseInt(options.window, 10) || 30,
174
+ });
175
+ void workspaceRoot;
176
+ emit(diagnosis, options.json, () => [
177
+ `Classification: ${diagnosis.classification}`,
178
+ `Diagnosis: ${diagnosis.diagnosis}`,
179
+ `Recommendation: ${diagnosis.recommendation}`,
180
+ `Raise the recurrence count: ${diagnosis.recurrenceBump ? 'yes' : 'no'}`,
181
+ ].join('\n'));
182
+ });
183
+ // ── candidates ───────────────────────────────────────────────────────────────
184
+ const candidatesCommand = addCommonOptions(new commander_1.Command('candidates'))
185
+ .description('List retirement candidates: offered often under the current model with no firing under it, paired with a structural signal')
186
+ .option('--model <model>', 'Override the current model (defaults to the most recently offered-to model)')
187
+ .option('--detector <path>', 'Path to the corpus hygiene detector that provides the structural signal')
188
+ .action((options) => {
189
+ const workspaceRoot = resolveWorkspaceRoot(options);
190
+ const limits = (0, learning_usage_store_1.resolveUsageLimits)(workspaceRoot);
191
+ const result = (0, learning_usage_analysis_1.findRetirementCandidates)((0, learning_usage_store_1.readUsageStore)(), {
192
+ workspaceRoot,
193
+ limits,
194
+ currentModel: options.model ?? undefined,
195
+ structuralSignalPath: options.detector,
196
+ });
197
+ emit(result, options.json, () => {
198
+ if (result.candidates.length === 0) {
199
+ return `No retirement candidates. Current model: ${result.currentModel ?? 'unknown'}; threshold ${result.offerThreshold} offers.`;
200
+ }
201
+ const lines = [`Current model: ${result.currentModel ?? 'unknown'}. Threshold: ${result.offerThreshold} offers.`, ''];
202
+ for (const c of result.candidates) {
203
+ const earlier = c.firedUnderEarlierModels.map((m) => `${m.count} under ${m.model}`).join(', ') || 'none under any earlier model';
204
+ lines.push(`- ${c.title}`, ` Offered ${c.offeredUnderCurrentModel} times under the current model, fired ${c.firedUnderCurrentModel}. Earlier: ${earlier}.`, ` Structural signal: ${c.structural}. ${c.structuralDetail}`, ` Recommendation: ${c.recommendation}`);
205
+ }
206
+ return lines.join('\n');
207
+ });
208
+ });
209
+ // ── standing ─────────────────────────────────────────────────────────────────
210
+ const standingCommand = addCommonOptions(new commander_1.Command('standing'))
211
+ .description('Assess each entry as promoted, standard or demoted from its usage record, and list the ones whose standing should change')
212
+ .option('--changes-only', 'Print only the entries whose standing should change')
213
+ .action((options) => {
214
+ const workspaceRoot = resolveWorkspaceRoot(options);
215
+ const limits = (0, learning_usage_store_1.resolveUsageLimits)(workspaceRoot);
216
+ const report = (0, learning_usage_analysis_1.assessStanding)((0, learning_usage_store_1.readUsageStore)(), { limits });
217
+ const payload = options.changesOnly ? { ...report, assessments: report.changes } : report;
218
+ emit(payload, options.json, () => {
219
+ const rows = (options.changesOnly ? report.changes : report.assessments);
220
+ if (rows.length === 0) {
221
+ return options.changesOnly
222
+ ? 'No standing changes. Every measured entry is already where the record says it belongs.'
223
+ : 'No entry has a usage record yet, so there is nothing to assess.';
224
+ }
225
+ const lines = [];
226
+ if (report.changes.length > 0) {
227
+ lines.push(`${report.changes.length} entr${report.changes.length === 1 ? 'y' : 'ies'} should change standing.`, '');
228
+ }
229
+ for (const a of rows) {
230
+ lines.push(`- ${a.title}`);
231
+ lines.push(` ${a.standing}${a.recommendation ? ` (recommend: ${a.recommendation})` : ''}. ${a.reason}`);
232
+ }
233
+ lines.push('', 'Nothing is applied here. Run sleep-on-learnings to decide.');
234
+ return lines.join('\n');
235
+ });
236
+ });
237
+ // ── report ───────────────────────────────────────────────────────────────────
238
+ const reportCommand = addCommonOptions(new commander_1.Command('report'))
239
+ .description('Answer which learnings are still earning their place, from the record rather than from memory')
240
+ .option('--window <days>', 'Reporting window', '90')
241
+ .action((options) => {
242
+ const workspaceRoot = resolveWorkspaceRoot(options);
243
+ const limits = (0, learning_usage_store_1.resolveUsageLimits)(workspaceRoot);
244
+ const report = (0, learning_usage_analysis_1.buildUsageReport)((0, learning_usage_store_1.readUsageStore)(), {
245
+ workspaceRoot,
246
+ limits,
247
+ windowDays: Number.parseInt(options.window, 10) || 90,
248
+ });
249
+ emit(report, options.json, () => {
250
+ const lines = [
251
+ `${report.totals.entriesOffered} entries were offered. ${report.totals.entriesWithFirings} fired. ${report.totals.entriesNeverFired} have never fired.`,
252
+ ];
253
+ if (report.workingHardest.length) {
254
+ lines.push('', 'Working hardest');
255
+ report.workingHardest.forEach((e, i) => lines.push(`${i + 1}. ${e.title}. Offered ${e.offered} times, fired ${e.fired}. Last fired ${e.lastFired ?? 'never'}.`));
256
+ }
257
+ if (report.neverFired.length) {
258
+ lines.push('', 'Never fired');
259
+ report.neverFired.forEach((e, i) => lines.push(`${i + 1}. ${e.title}. Offered ${e.offered} times, never fired.`));
260
+ }
261
+ if (report.ignoredWhileInContext.length) {
262
+ lines.push('', 'Ignored while in context');
263
+ report.ignoredWhileInContext.forEach((e, i) => lines.push(`${i + 1}. ${e.title}. In context and the work went against it, so the entry needs work rather than another recurrence.`));
264
+ }
265
+ lines.push('', 'Nothing is retired from this view. Run sleep-on-learnings to decide.');
266
+ return lines.join('\n');
267
+ });
268
+ });
269
+ // ── backfill ─────────────────────────────────────────────────────────────────
270
+ const backfillCommand = addCommonOptions(new commander_1.Command('backfill'))
271
+ .description('Read the firings already attested in existing retrospectives, matching only on an id or an exact title, and report what could not be matched')
272
+ .option('--agent <name>', 'Agent to attribute the backfilled records to', 'backfill')
273
+ .option('--model <model>', 'Model to attribute the backfilled records to')
274
+ .action((options) => {
275
+ const workspaceRoot = resolveWorkspaceRoot(options);
276
+ const user = resolveUser(options);
277
+ const report = (0, learning_usage_attestation_1.runBackfill)(workspaceRoot, user, {
278
+ agent: options.agent ?? 'backfill',
279
+ model: options.model ?? null,
280
+ });
281
+ emit(report, options.json, () => [
282
+ `Files scanned: ${report.filesScanned} (${report.filesWithSection} carry the section, ${report.synthesizedFilesRead} already synthesized)`,
283
+ `Items read: ${report.itemsRead}`,
284
+ `Applied: ${report.applied}`,
285
+ `Already recorded: ${report.alreadyRecorded}`,
286
+ `Unmatched: ${report.unmatched.length}`,
287
+ `Rejected: ${report.rejected.length}`,
288
+ '',
289
+ 'Only an id or an exact title resolves. A low match rate is expected on retrospectives written before entries carried ids, and is reported rather than resolved by similarity matching.',
290
+ ].join('\n'));
291
+ });
292
+ /**
293
+ * Add the `**Id**` line to entries that do not have one.
294
+ *
295
+ * This is a format migration, not a change to any lesson: it inserts one
296
+ * identifier line per entry and touches nothing else. R19 forbids a new write path
297
+ * into the corpus for usage data, and this is not one — it writes no usage data,
298
+ * and it is run explicitly by the manager rather than by any automatic path.
299
+ */
300
+ function stampFile(filePath, fileType, apply) {
301
+ const content = node_fs_1.default.readFileSync(filePath, 'utf8');
302
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
303
+ const lines = content.split(/\r?\n/);
304
+ const headingRe = (0, learning_context_builder_1.learningEntryHeadingRegex)();
305
+ const idRe = (0, learning_context_builder_1.learningEntryIdLineRegex)();
306
+ const out = [];
307
+ let stamped = 0;
308
+ let alreadyStamped = 0;
309
+ for (let i = 0; i < lines.length; i++) {
310
+ const line = lines[i];
311
+ out.push(line);
312
+ const header = line.match(headingRe);
313
+ if (!header)
314
+ continue;
315
+ // Look ahead to the next entry heading for an existing id line.
316
+ let hasId = false;
317
+ for (let j = i + 1; j < lines.length; j++) {
318
+ if (headingRe.test(lines[j]))
319
+ break;
320
+ if (idRe.test(lines[j].trim())) {
321
+ hasId = true;
322
+ break;
323
+ }
324
+ }
325
+ if (hasId) {
326
+ alreadyStamped++;
327
+ continue;
328
+ }
329
+ const title = header[2].trim();
330
+ if (!title)
331
+ continue;
332
+ // Insert immediately after the heading, keeping the blank line that usually
333
+ // follows it so the file's shape is unchanged.
334
+ if (lines[i + 1] !== undefined && lines[i + 1].trim() === '') {
335
+ out.push('');
336
+ out.push(`**Id**: ${(0, learning_context_builder_1.generateLearningEntryId)(fileType, title)}`);
337
+ i++;
338
+ }
339
+ else {
340
+ out.push(`**Id**: ${(0, learning_context_builder_1.generateLearningEntryId)(fileType, title)}`);
341
+ }
342
+ stamped++;
343
+ }
344
+ if (apply && stamped > 0) {
345
+ node_fs_1.default.writeFileSync(filePath, out.join(eol), 'utf8');
346
+ }
347
+ return { file: filePath, entriesStamped: stamped, entriesAlreadyStamped: alreadyStamped };
348
+ }
349
+ const FILE_TYPES = ['mistake-patterns', 'preferences', 'validated-patterns', 'manager-coaching'];
350
+ function fileTypeFromName(fileName) {
351
+ for (const type of FILE_TYPES) {
352
+ if (fileName.includes(type))
353
+ return type;
354
+ }
355
+ return null;
356
+ }
357
+ const stampIdsCommand = addCommonOptions(new commander_1.Command('stamp-ids'))
358
+ .description('Add the stable Id line to learning entries that do not have one. A format migration: it inserts one identifier line per entry and changes no lesson text')
359
+ .option('--dir <path>', 'Learning directory to stamp (repeatable)', (value, previous) => [...previous, value], [])
360
+ .option('--apply', 'Write the changes. Without this flag the command reports what it would do')
361
+ .action((options) => {
362
+ const workspaceRoot = resolveWorkspaceRoot(options);
363
+ const dirs = options.dir.length
364
+ ? options.dir.map((d) => node_path_1.default.resolve(d))
365
+ : [node_path_1.default.join(workspaceRoot, 'fraim', 'personalized-employee', 'learnings')];
366
+ const results = [];
367
+ for (const dir of dirs) {
368
+ if (!node_fs_1.default.existsSync(dir))
369
+ continue;
370
+ for (const fileName of node_fs_1.default.readdirSync(dir)) {
371
+ if (!fileName.endsWith('.md'))
372
+ continue;
373
+ const fileType = fileTypeFromName(fileName);
374
+ if (!fileType)
375
+ continue;
376
+ results.push(stampFile(node_path_1.default.join(dir, fileName), fileType, Boolean(options.apply)));
377
+ }
378
+ }
379
+ const payload = {
380
+ apply: Boolean(options.apply),
381
+ filesScanned: results.length,
382
+ entriesStamped: results.reduce((sum, r) => sum + r.entriesStamped, 0),
383
+ entriesAlreadyStamped: results.reduce((sum, r) => sum + r.entriesAlreadyStamped, 0),
384
+ files: results,
385
+ };
386
+ emit(payload, options.json, () => [
387
+ `${payload.apply ? 'Stamped' : 'Would stamp'} ${payload.entriesStamped} entries across ${payload.filesScanned} files.`,
388
+ `${payload.entriesAlreadyStamped} entries already carry an id.`,
389
+ payload.apply ? '' : 'Re-run with --apply to write the changes.',
390
+ ].filter(Boolean).join('\n'));
391
+ });
392
+ // ── prune ────────────────────────────────────────────────────────────────────
393
+ const pruneCommand = addCommonOptions(new commander_1.Command('prune'))
394
+ .description('Apply the configured retention window and log bounds to the usage record. Aggregate counters are never pruned')
395
+ .action((options) => {
396
+ const workspaceRoot = resolveWorkspaceRoot(options);
397
+ const limits = (0, learning_usage_store_1.resolveUsageLimits)(workspaceRoot);
398
+ const result = (0, learning_usage_store_1.pruneUsageStore)(limits);
399
+ emit({ ...result, storePath: (0, learning_usage_store_1.resolveUsageStorePath)(), limits }, options.json, () => `Pruned ${result.prunedOfferRecords} offer records and ${result.prunedFiringRecords} firing records across ${result.entries} entries. Aggregates unchanged.`);
400
+ });
401
+ // ── root ─────────────────────────────────────────────────────────────────────
402
+ exports.learningUsageCommand = new commander_1.Command('learning-usage')
403
+ .description('Read and analyse the learning and rule usage record (issue #1103)')
404
+ .addCommand(offersCommand)
405
+ .addCommand(recordFiringsCommand)
406
+ .addCommand(classifyCommand)
407
+ .addCommand(candidatesCommand)
408
+ .addCommand(standingCommand)
409
+ .addCommand(reportCommand)
410
+ .addCommand(backfillCommand)
411
+ .addCommand(stampIdsCommand)
412
+ .addCommand(pruneCommand);
@@ -10,10 +10,10 @@ exports.loginCommand = new commander_1.Command('login')
10
10
  .description('Login to platforms (GitHub, etc.)');
11
11
  exports.loginCommand
12
12
  .command('github')
13
- .description('Configure GitHub MCP access (redirects to add-provider)')
13
+ .description('Explain agent-managed GitHub MCP setup')
14
14
  .action(async () => {
15
- console.log(chalk_1.default.blue('\n💡 GitHub authentication is now handled natively by your IDE.'));
16
- console.log(chalk_1.default.gray('To configure GitHub MCP access, run:'));
17
- console.log(chalk_1.default.cyan('\n fraim add-provider github\n'));
18
- console.log(chalk_1.default.gray('Your IDE will prompt for GitHub sign-in automatically on first use.'));
15
+ console.log(chalk_1.default.blue('\n💡 GitHub MCP setup is managed by your FRAIM agent.'));
16
+ console.log(chalk_1.default.gray('Ask your agent:'));
17
+ console.log(chalk_1.default.cyan('\n Use the FRAIM connect-mcp skill to connect github.\n'));
18
+ console.log(chalk_1.default.gray('The agent will follow current GitHub and host guidance and verify the connection.'));
19
19
  });
@@ -36,15 +36,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.setupCommandInitialization = exports.setupCommand = exports.runSetup = exports.saveGlobalConfig = void 0;
40
- // Refactored setup.ts - Generic provider system with zero hardcoded provider knowledge
39
+ exports.setupCommand = exports.runSetup = exports.saveGlobalConfig = void 0;
40
+ // Global FRAIM setup. Third-party provider setup is delegated to connect-mcp.
41
41
  const commander_1 = require("commander");
42
42
  const chalk_1 = __importDefault(require("chalk"));
43
43
  const prompts_1 = __importDefault(require("prompts"));
44
44
  const fs_1 = __importDefault(require("fs"));
45
45
  const path_1 = __importDefault(require("path"));
46
46
  const add_ide_1 = require("./add-ide");
47
- const provider_registry_1 = require("../providers/provider-registry");
48
47
  const script_sync_utils_1 = require("../utils/script-sync-utils");
49
48
  function parseModeOption(mode) {
50
49
  if (mode === 'conversational' || mode === 'integrated' || mode === 'split') {
@@ -204,10 +203,10 @@ const runSetup = async (options) => {
204
203
  if (!isKeyUpdate) {
205
204
  console.log(chalk_1.default.gray(' Current configuration:'));
206
205
  console.log(chalk_1.default.gray(` • Mode: ${mode}`));
207
- console.log(chalk_1.default.gray(' • Platforms: managed per-project via fraim add-provider'));
206
+ console.log(chalk_1.default.gray(' • Platforms: connected by your agent through the FRAIM connect-mcp skill'));
208
207
  console.log();
209
208
  if (process.env.FRAIM_NON_INTERACTIVE || process.env.CI) {
210
- console.log(chalk_1.default.gray('Setup already configured. Use "fraim add-provider <provider>" to connect platforms.'));
209
+ console.log(chalk_1.default.gray('Setup already configured. Ask your agent to use the FRAIM connect-mcp skill for platforms.'));
211
210
  return;
212
211
  }
213
212
  const response = await (0, prompts_1.default)({
@@ -252,9 +251,9 @@ const runSetup = async (options) => {
252
251
  console.log(chalk_1.default.blue('\n💾 Saving global configuration...'));
253
252
  (0, exports.saveGlobalConfig)(fraimKey, mode);
254
253
  console.log(chalk_1.default.blue('\n🔌 Configuring MCP servers...'));
255
- await (0, add_ide_1.runAddIDE)({ ide: options.ide, all: !options.ide, skipTokenPrompts: true });
254
+ await (0, add_ide_1.runAddIDE)({ ide: options.ide, all: !options.ide });
256
255
  console.log(chalk_1.default.green('\n🎯 Reconfiguration complete!'));
257
- console.log(chalk_1.default.cyan('\n💡 To connect platforms, run: fraim add-provider <github|gitlab|ado|jira>'));
256
+ console.log(chalk_1.default.cyan('\n💡 To connect a platform, ask your agent to use the FRAIM connect-mcp skill.'));
258
257
  return;
259
258
  }
260
259
  }
@@ -272,12 +271,12 @@ const runSetup = async (options) => {
272
271
  console.log(chalk_1.default.gray(' • Configure FRAIM MCP servers in your IDEs'));
273
272
  console.log(chalk_1.default.gray(' • Sync FRAIM scripts to your system\n'));
274
273
  console.log(chalk_1.default.gray(' Platforms (GitHub, GitLab, etc.) are connected per-project'));
275
- console.log(chalk_1.default.gray(' via "fraim add-provider" no tokens needed here.\n'));
274
+ console.log(chalk_1.default.gray(' by your agent through the FRAIM connect-mcp skill.\n'));
276
275
  fraimKey = options.key || await promptForFraimKey();
277
276
  console.log(chalk_1.default.green('✅ FRAIM key accepted\n'));
278
277
  mode = options.mode ? parseModeOption(options.mode) : await promptForMode();
279
278
  }
280
- // Save global configuration (key + mode only; provider tokens live in IDE MCP configs)
279
+ // Save global configuration (key + mode only; third-party authentication is provider/host-owned)
281
280
  console.log(chalk_1.default.blue('💾 Saving global configuration...'));
282
281
  (0, exports.saveGlobalConfig)(fraimKey, mode);
283
282
  // Configure MCP servers and install IDE surfaces (slash commands, rules)
@@ -285,10 +284,10 @@ const runSetup = async (options) => {
285
284
  console.log(chalk_1.default.blue(isUpdate ? '\n🔄 Updating IDE MCP configurations...' : '\n🔌 Configuring MCP servers...'));
286
285
  if (!isUpdate && mode === 'conversational') {
287
286
  console.log(chalk_1.default.yellow('ℹ️ Conversational mode: Configuring FRAIM MCP server'));
288
- console.log(chalk_1.default.gray(' FRAIM jobs will work; platform-specific features are added via fraim add-provider\n'));
287
+ console.log(chalk_1.default.gray(' FRAIM jobs will work; platform connections are handled through the FRAIM connect-mcp skill\n'));
289
288
  }
290
289
  try {
291
- await (0, add_ide_1.runAddIDE)({ ide: options.ide, all: isUpdate || !options.ide, skipTokenPrompts: true });
290
+ await (0, add_ide_1.runAddIDE)({ ide: options.ide, all: isUpdate || !options.ide });
292
291
  }
293
292
  catch (e) {
294
293
  console.log(chalk_1.default.yellow('⚠️ MCP configuration encountered issues'));
@@ -368,9 +367,9 @@ const runSetup = async (options) => {
368
367
  console.log(chalk_1.default.gray(' This enables project-specific customizations,'));
369
368
  console.log(chalk_1.default.gray(' GitHub workflows, and team learning.'));
370
369
  console.log(chalk_1.default.cyan('\n To connect platforms (GitHub, GitLab, ADO, Jira):'));
371
- console.log(chalk_1.default.white(' fraim add-provider <provider>'));
372
- console.log(chalk_1.default.gray(' Provider tokens are stored directly in IDE MCP configs.'));
373
- console.log(chalk_1.default.gray(' GitHub uses IDE-native OAuth no token needed.'));
370
+ console.log(chalk_1.default.white(' Ask your agent to use the FRAIM connect-mcp skill.'));
371
+ console.log(chalk_1.default.gray(' The agent follows current provider and host guidance for each selected target.'));
372
+ console.log(chalk_1.default.gray(' Authentication and credential entry stay outside model context.'));
374
373
  }
375
374
  }
376
375
  else {
@@ -390,44 +389,8 @@ const runSetup = async (options) => {
390
389
  };
391
390
  exports.runSetup = runSetup;
392
391
  exports.setupCommand = new commander_1.Command('setup')
393
- .description('Complete global FRAIM setup with platform configuration')
392
+ .description('Complete global FRAIM setup')
394
393
  .option('--key <key>', 'FRAIM API key')
395
394
  .option('--mode <mode>', 'Usage mode: integrated | split | conversational')
396
395
  .option('--ide <ides>', 'Configure specific IDEs');
397
- // Track initialization promise for CLI entry point
398
- exports.setupCommandInitialization = null;
399
- // Dynamically add provider options from registry (async initialization)
400
- exports.setupCommandInitialization = (async () => {
401
- try {
402
- const allProviderIds = await (0, provider_registry_1.getAllProviderIds)();
403
- for (const providerId of allProviderIds) {
404
- const provider = await (0, provider_registry_1.getProvider)(providerId);
405
- if (!provider)
406
- continue;
407
- // Add provider flag (e.g., --github)
408
- exports.setupCommand.option(`--${providerId}`, `Add/update ${provider.displayName} integration`);
409
- // Add token option (e.g., --github-token) - primarily for testing/automation
410
- exports.setupCommand.option(`--${providerId}-token <token>`, `${provider.displayName} token (optional - will prompt if not provided)`);
411
- // Add config options if provider requires them
412
- const configReqs = await (0, provider_registry_1.getProviderConfigRequirements)(providerId);
413
- configReqs.forEach(req => {
414
- // Use custom CLI option name if provided, otherwise convert key to kebab-case
415
- const optionSuffix = req.cliOptionName || req.key.replace(/([A-Z])/g, '-$1').toLowerCase();
416
- const optionName = `${providerId}-${optionSuffix}`;
417
- exports.setupCommand.option(`--${optionName} <value>`, `${req.description} (optional - will prompt if not provided)`);
418
- });
419
- }
420
- }
421
- catch (error) {
422
- // If we can't fetch providers (e.g., no config yet), that's okay
423
- // The command will still work, just without dynamic options
424
- }
425
- })();
426
- // Wrap the action to ensure initialization completes first
427
- exports.setupCommand.action(async (options) => {
428
- // Wait for dynamic options to be registered
429
- if (exports.setupCommandInitialization) {
430
- await exports.setupCommandInitialization;
431
- }
432
- return (0, exports.runSetup)(options);
433
- });
396
+ exports.setupCommand.action(exports.runSetup);