fraim 2.0.342 → 2.0.344

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 (30) hide show
  1. package/dist/registry/scripts/artifact-paths.js +108 -0
  2. package/dist/registry/scripts/learning/completion-state.js +205 -5
  3. package/dist/registry/scripts/learning-usage/local-runtime.js +217 -0
  4. package/dist/src/cli/commands/init-project.js +2 -12
  5. package/dist/src/cli/commands/sync.js +36 -162
  6. package/dist/src/cli/setup/ide-invocation-surfaces.js +3 -3
  7. package/dist/src/cli/setup/user-level-sync.js +3 -3
  8. package/dist/src/cli/utils/agent-adapters.js +12 -12
  9. package/dist/src/cli/utils/fraim-gitignore.js +28 -3
  10. package/dist/src/cli/utils/pack-home.js +10 -0
  11. package/dist/src/cli/utils/project-catalog-cleanup.js +62 -0
  12. package/dist/src/cli/utils/remote-sync.js +26 -2
  13. package/dist/src/config/persona-hiring.js +7 -1
  14. package/dist/src/core/ai-mentor.js +42 -11
  15. package/dist/src/core/config-loader.js +4 -0
  16. package/dist/src/core/fraim-config-schema.generated.js +22 -0
  17. package/dist/src/core/handoff-contracts.js +8 -3
  18. package/dist/src/core/utils/local-registry-resolver.js +33 -57
  19. package/dist/src/fraim/db-service.js +127 -118
  20. package/dist/src/local-mcp-server/artifact-retention-cleanup.js +79 -6
  21. package/dist/src/local-mcp-server/learning-context-builder.js +101 -105
  22. package/dist/src/local-mcp-server/learning-source-record.js +122 -0
  23. package/dist/src/local-mcp-server/learning-usage-attestation.js +66 -56
  24. package/dist/src/local-mcp-server/learning-usage-command.js +5 -4
  25. package/dist/src/local-mcp-server/stdio-server.js +10 -7
  26. package/dist/src/local-mcp-server/usage-collector.js +2 -0
  27. package/dist/src/mcp/tool-schemas.js +5 -1
  28. package/dist/src/services/mcp-service.js +6 -4
  29. package/dist/src/services/usage-analytics-service.js +1 -0
  30. package/package.json +3 -1
@@ -2,7 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractLegacyTitle = exports.parseFiringSection = void 0;
4
4
  exports.recordAttestationsFromRetrospective = recordAttestationsFromRetrospective;
5
+ exports.readRetrospectiveForAttestation = readRetrospectiveForAttestation;
5
6
  exports.runBackfill = runBackfill;
7
+ const artifact_paths_1 = require("../../registry/scripts/artifact-paths");
6
8
  /**
7
9
  * Issue #1103 — turning an attestation into a usage record.
8
10
  *
@@ -18,6 +20,7 @@ exports.runBackfill = runBackfill;
18
20
  */
19
21
  const fs_1 = require("fs");
20
22
  const path_1 = require("path");
23
+ const learning_source_record_1 = require("./learning-source-record");
21
24
  const learning_usage_store_1 = require("./learning-usage-store");
22
25
  const learning_firing_parser_1 = require("./learning-firing-parser");
23
26
  const learning_context_builder_1 = require("./learning-context-builder");
@@ -160,69 +163,76 @@ function recordAttestationsFromRetrospective(workspaceRoot, userId, options) {
160
163
  rejected: parsed.rejected,
161
164
  };
162
165
  }
163
- function jobFromFrontmatter(content) {
164
- const match = content.match(/^job:\s*(.+)$/m);
165
- return match ? match[1].trim() : 'unknown';
166
+ // ── R15: backfill ────────────────────────────────────────────────────────────
167
+ /** Resolve live attestation against its recorded home and immutable accounting identity. */
168
+ function readRetrospectiveForAttestation(workspaceRoot, userId, filePath) {
169
+ let reason = 'Retrospective is outside configured learning homes';
170
+ for (const home of (0, learning_context_builder_1.getLearningInputHomes)(workspaceRoot)) {
171
+ const result = (0, learning_context_builder_1.readLearningSourceFile)((0, path_1.resolve)(filePath), home.root, home.contentRoot);
172
+ if (result.status !== 'valid') {
173
+ reason = result.reason;
174
+ continue;
175
+ }
176
+ if (result.record.kind !== 'retrospective' || result.record.author !== userId) {
177
+ throw new Error('Retrospective kind or author does not match this attestation');
178
+ }
179
+ return { content: result.content, record: result.record };
180
+ }
181
+ throw new Error(`Invalid learning retrospective: ${reason}`);
166
182
  }
167
- /**
168
- * Read every retrospective and apply the firings it attests.
169
- *
170
- * Best effort by design (R15): only an id or an exact title resolves, and the
171
- * unmatched count is reported rather than resolved by fuzzy matching. A low match
172
- * rate is an expected outcome to state, not a failure to hide — only a small
173
- * fraction of the existing attestations name a machine-matchable identifier.
174
- *
175
- * A retrospective already stamped `synthesized` is still read: being synthesized
176
- * for learning content is not the same as having contributed usage data.
177
- */
183
+ /** Read only typed future reflections, including archives, with stable accounting identity. */
178
184
  function runBackfill(workspaceRoot, userId, options) {
179
185
  const limits = options.limits ?? (0, learning_usage_store_1.resolveUsageLimits)(workspaceRoot);
180
- const dir = options.retrospectivesDir ?? (0, path_1.join)(workspaceRoot, 'docs', 'retrospectives');
181
186
  const report = {
182
187
  filesScanned: 0, filesWithSection: 0, itemsRead: 0, applied: 0,
183
- alreadyRecorded: 0, unmatched: [], rejected: [], synthesizedFilesRead: 0,
188
+ alreadyRecorded: 0, unmatched: [], rejected: [], synthesizedFilesRead: 0, invalidRecords: [],
184
189
  };
185
- if (!(0, fs_1.existsSync)(dir))
186
- return report;
187
- let files;
188
- try {
189
- files = (0, fs_1.readdirSync)(dir).filter((f) => f.endsWith('.md'));
190
- }
191
- catch {
192
- return report;
193
- }
194
- for (const file of files) {
195
- let content;
196
- try {
197
- content = (0, fs_1.readFileSync)((0, path_1.join)(dir, file), 'utf8');
198
- }
199
- catch {
200
- continue;
190
+ const seen = new Set();
191
+ for (const home of (0, learning_context_builder_1.getLearningInputHomes)(workspaceRoot)) {
192
+ const paths = (0, artifact_paths_1.resolveLearningPaths)(home.root, home.contentRoot);
193
+ for (const dir of [paths.retrospectives, paths.archiveRetrospectives]) {
194
+ if (options.retrospectivesDir && (0, learning_source_record_1.normalizeLearningSourcePath)(dir) !== (0, learning_source_record_1.normalizeLearningSourcePath)(options.retrospectivesDir))
195
+ continue;
196
+ let files;
197
+ try {
198
+ if ((0, fs_1.lstatSync)(dir).isSymbolicLink())
199
+ continue;
200
+ files = (0, fs_1.readdirSync)(dir).filter(f => f.endsWith('.md'));
201
+ }
202
+ catch {
203
+ continue;
204
+ }
205
+ for (const file of files) {
206
+ const filePath = (0, path_1.join)(dir, file);
207
+ const parsed = (0, learning_context_builder_1.readLearningSourceFile)(filePath, home.root, home.contentRoot);
208
+ if (parsed.status !== 'valid') {
209
+ if (parsed.status === 'invalid')
210
+ report.invalidRecords.push({ path: filePath, reason: parsed.reason });
211
+ continue;
212
+ }
213
+ const { record } = parsed;
214
+ if (record.kind !== 'retrospective' || record.author !== userId)
215
+ continue;
216
+ if (seen.has(record.usageSourceRef))
217
+ continue;
218
+ seen.add(record.usageSourceRef);
219
+ report.filesScanned++;
220
+ if (record.synthesized)
221
+ report.synthesizedFilesRead++;
222
+ const result = recordAttestationsFromRetrospective(workspaceRoot, userId, {
223
+ content: parsed.content, source: record.usageSourceRef, job: record.job, date: record.date,
224
+ agent: options.agent, model: options.model, legacy: false,
225
+ limits, now: options.now, storePath: options.storePath, integrityMode: 'flag',
226
+ });
227
+ if (result.sectionState !== 'absent')
228
+ report.filesWithSection++;
229
+ report.itemsRead += result.itemsRead;
230
+ report.applied += result.applied;
231
+ report.alreadyRecorded += result.alreadyRecorded;
232
+ report.unmatched.push(...result.unmatched);
233
+ report.rejected.push(...result.rejected);
234
+ }
201
235
  }
202
- report.filesScanned += 1;
203
- if (/^synthesized:\s*\S/m.test(content))
204
- report.synthesizedFilesRead += 1;
205
- const result = recordAttestationsFromRetrospective(workspaceRoot, userId, {
206
- content,
207
- source: `docs/retrospectives/${file}`,
208
- job: jobFromFrontmatter(content),
209
- agent: options.agent,
210
- model: options.model,
211
- legacy: true,
212
- limits,
213
- now: options.now,
214
- storePath: options.storePath,
215
- // A backfill reconciles hundreds of retrospectives at once, so a single
216
- // stranded-offer entry must not refuse the whole run.
217
- integrityMode: 'flag',
218
- });
219
- if (result.sectionState !== 'absent')
220
- report.filesWithSection += 1;
221
- report.itemsRead += result.itemsRead;
222
- report.applied += result.applied;
223
- report.alreadyRecorded += result.alreadyRecorded;
224
- report.unmatched.push(...result.unmatched);
225
- report.rejected.push(...result.rejected);
226
236
  }
227
237
  return report;
228
238
  }
@@ -134,12 +134,13 @@ const recordFiringsCommand = addCommonOptions(new commander_1.Command('record-fi
134
134
  if (!node_fs_1.default.existsSync(filePath)) {
135
135
  throw new Error(`Retrospective not found: ${filePath}`);
136
136
  }
137
- const content = node_fs_1.default.readFileSync(filePath, 'utf8');
138
- const source = node_path_1.default.relative(workspaceRoot, filePath).replace(/\\/g, '/');
137
+ const { content, record } = (0, learning_usage_attestation_1.readRetrospectiveForAttestation)(workspaceRoot, user, filePath);
138
+ const source = record.usageSourceRef;
139
139
  const result = (0, learning_usage_attestation_1.recordAttestationsFromRetrospective)(workspaceRoot, user, {
140
140
  content,
141
141
  source,
142
- job: options.job ?? 'unknown',
142
+ job: record.job,
143
+ date: record.date,
143
144
  agent: options.agent ?? null,
144
145
  model: options.model ?? null,
145
146
  legacy: Boolean(options.legacy),
@@ -387,7 +388,7 @@ function defaultStampDirs(workspaceRoot) {
387
388
  const dirs = [node_path_1.default.join(workspaceRoot, 'fraim', 'personalized-employee', 'learnings')];
388
389
  for (const level of ['org', 'manager']) {
389
390
  try {
390
- dirs.push(node_path_1.default.join((0, pack_home_1.resolvePackHome)(level).contentRoot, 'learnings'));
391
+ dirs.push(node_path_1.default.join((0, pack_home_1.resolvePackHome)(level, { migrateLegacy: false }).contentRoot, 'learnings'));
391
392
  }
392
393
  catch {
393
394
  // A pack home that does not resolve is simply not stamped. Reported by the
@@ -59,6 +59,7 @@ const path_1 = require("path");
59
59
  const os_1 = require("os");
60
60
  const child_process_1 = require("child_process");
61
61
  const crypto_1 = require("crypto");
62
+ const local_runtime_1 = require("../../registry/scripts/learning-usage/local-runtime");
62
63
  const axios_1 = __importDefault(require("axios"));
63
64
  const provider_utils_1 = require("../core/utils/provider-utils");
64
65
  const object_utils_1 = require("../core/utils/object-utils");
@@ -79,6 +80,7 @@ const learning_context_builder_js_1 = require("./learning-context-builder.js");
79
80
  const learning_usage_store_js_1 = require("./learning-usage-store.js");
80
81
  const learning_domains_js_1 = require("../config/learning-domains.js");
81
82
  const skill_include_dedup_js_1 = require("./skill-include-dedup.js");
83
+ const git_utils_1 = require("../core/utils/git-utils");
82
84
  /**
83
85
  * Handle template substitution logic separately for better testability
84
86
  */
@@ -171,7 +173,7 @@ class FraimTemplateEngine {
171
173
  try {
172
174
  const configPath = (0, path_1.join)(this.projectRoot, 'fraim', 'config.json');
173
175
  if ((0, fs_1.existsSync)(configPath)) {
174
- this.config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
176
+ this.config = (0, local_runtime_1.readJsonIfObject)(configPath, { strict: true });
175
177
  this.configLoadAttempted = true;
176
178
  return this.config;
177
179
  }
@@ -742,8 +744,7 @@ class FraimLocalMCPServer {
742
744
  const configPath = (0, project_fraim_paths_1.getWorkspaceConfigPath)(projectDir);
743
745
  this.log(`🔍 Looking for config at: ${configPath}`);
744
746
  if ((0, fs_1.existsSync)(configPath)) {
745
- const configContent = (0, fs_1.readFileSync)(configPath, 'utf8');
746
- this.config = JSON.parse(configContent);
747
+ this.config = (0, local_runtime_1.readJsonIfObject)(configPath, { strict: true });
747
748
  this.log(`Loaded local ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('config.json')}`);
748
749
  }
749
750
  else {
@@ -1119,7 +1120,7 @@ class FraimLocalMCPServer {
1119
1120
  catch {
1120
1121
  branch = '';
1121
1122
  }
1122
- const defaultBranch = this.config?.repository?.defaultBranch || '';
1123
+ const defaultBranch = this.config?.repository?.defaultBranch || (0, git_utils_1.getDefaultBranch)(projectDir) || '';
1123
1124
  const repoInfo = {
1124
1125
  url: repoUrl,
1125
1126
  name: name || 'unknown',
@@ -1301,7 +1302,9 @@ class FraimLocalMCPServer {
1301
1302
  }
1302
1303
  const timestamp = new Date().toISOString();
1303
1304
  const accepted = acceptedNext !== undefined;
1304
- const currentPhase = accepted
1305
+ const recoveringTerminalReview = previous?.status === 'waiting_review' && previous.currentPhase === null
1306
+ && (args.status === 'starting' || args.status === 'incomplete');
1307
+ const currentPhase = recoveringTerminalReview ? null : accepted
1305
1308
  ? (args.status === 'complete' ? acceptedNext.nextPhase : acceptedNext.nextPhase || args.currentPhase)
1306
1309
  : previous ? previous.currentPhase : args.currentPhase;
1307
1310
  const completedPhases = previous?.completedPhases || [];
@@ -1371,7 +1374,7 @@ class FraimLocalMCPServer {
1371
1374
  updated.phaseInstances = updated.phaseInstances?.filter((instance) => instance.jobId && instance.jobId !== args.jobId);
1372
1375
  }
1373
1376
  this.writeRunStateMarker(sessionId, args.jobName, updated);
1374
- return (inspection.missingStart ? (0, handoff_contracts_1.buildPhaseStartWarning)(args.currentPhase) : '')
1377
+ return (inspection.missingStart ? (0, handoff_contracts_1.buildPhaseStartWarning)(args.currentPhase, nextPhase ?? undefined) : '')
1375
1378
  + (!args.jobId ? `Run-tracking notice: phase ${args.currentPhase} has no jobId; matching used the session, job name and phase.\n\n` : '');
1376
1379
  }
1377
1380
  readCachedTemplateFile(filename) {
@@ -3109,7 +3112,7 @@ class FraimLocalMCPServer {
3109
3112
  const requestId = request.id;
3110
3113
  const cached = this.jobStartCache.get(requestId);
3111
3114
  if (cached) {
3112
- args = { ...args, jobId: cached.jobId, action: 'start' };
3115
+ args = { ...args, jobId: cached.jobId, action: 'start', isReload: false };
3113
3116
  this.jobStartCache.delete(requestId);
3114
3117
  this.log(`📊 Enriched get_fraim_job args with jobId=${cached.jobId}, action=start`);
3115
3118
  }
@@ -147,6 +147,8 @@ class UsageCollector {
147
147
  analyticsArgs.job = args.job;
148
148
  if (args.action)
149
149
  analyticsArgs.action = args.action;
150
+ if (typeof args.isReload === 'boolean')
151
+ analyticsArgs.isReload = args.isReload;
150
152
  break;
151
153
  case 'fraim_connect':
152
154
  if (args.agent?.name)
@@ -343,13 +343,17 @@ Do not use this tool for other repositories or external project issue trackers.`
343
343
  },
344
344
  currentPhase: {
345
345
  type: 'string',
346
- description: 'The phase you are currently in or have just finished (e.g., "implement-scoping"). For initial workflow start, use "starting".'
346
+ description: 'The phase you are currently in or have just finished (e.g., "implement-scoping"). For initial workflow start, pass "starting" as a one-time bootstrap — this is distinct from the per-phase status:"starting" on-arrival call you make at the beginning of every phase.'
347
347
  },
348
348
  status: {
349
349
  type: 'string',
350
350
  description: 'Status of your work in the current phase.',
351
351
  enum: ['starting', 'complete', 'incomplete', 'failure']
352
352
  },
353
+ requestInstructions: {
354
+ type: 'boolean',
355
+ description: 'On a starting call, pass true to restore full phase and skill instructions after context loss. Ordinary arrivals acknowledge the phase without repeating instructions already delivered by the previous response. Initial and recurring entry still deliver instructions.'
356
+ },
353
357
  findings: {
354
358
  type: 'object',
355
359
  description: 'Your findings, summaries, or results from the current phase (required for status="complete")',
@@ -548,9 +548,9 @@ class McpService {
548
548
  '- Use FRAIM when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job.',
549
549
  '- For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.',
550
550
  '- If the user already named a job, call `get_fraim_job({ job: "<job-name>" })` directly.',
551
- '- If routing is active and local FRAIM job stubs are present under `fraim/ai-employee/jobs/` or `fraim/ai-manager/jobs/`, read that directory\'s `INDEX.md` first (name, one-line intent, and category per job) to choose the best job; open an individual stub only to confirm a shortlisted candidate, or fall back to scanning the stubs directly if no `INDEX.md` is present.',
551
+ '- If routing is active and no exact job is named, read `~/.fraim/ai-employee/jobs/INDEX.md` and `~/.fraim/ai-manager/jobs/INDEX.md` first (name, one-line intent, and category per job) to choose the best job; open an individual stub only to confirm a shortlisted candidate, or fall back to scanning stubs in those machine directories if an index is missing or stale. Project baseline catalog directories are not required.',
552
552
  '- Also check `fraim/personalized-employee/jobs/` (and its own `INDEX.md`, if present) for local job overrides or repo-specific jobs when routing is active.',
553
- '- If local stubs are missing, you cannot inspect workspace files, or the user asks for available jobs, call `list_fraim_jobs()` to view the full catalog.',
553
+ '- If machine stubs are missing, you cannot inspect local files, or the user asks for available jobs, call `list_fraim_jobs()` to view the full catalog.',
554
554
  '- If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification.',
555
555
  '- Use `get_fraim_file(...)` only for the specific skills or rules needed for the current task.',
556
556
  '- Once a phased job starts, use `seekMentoring` for phase-specific instructions and transitions.'
@@ -712,7 +712,8 @@ class McpService {
712
712
  currentPhase: args.currentPhase,
713
713
  status: args.status,
714
714
  evidence: args.evidence,
715
- findings: args.findings
715
+ findings: args.findings,
716
+ requestInstructions: args.requestInstructions
716
717
  });
717
718
  // Handoff contract enforcement (Issue #916/#1157/#1276) fall-through
718
719
  // safety net, matching the quality-enforcement note just below.
@@ -824,7 +825,7 @@ class McpService {
824
825
  else if (isPhaseCompletion) {
825
826
  const inspection = (0, handoff_contracts_1.inspectPhaseCompletion)(marker, context);
826
827
  if (inspection.missingStart)
827
- phaseStartWarning = (0, handoff_contracts_1.buildPhaseStartWarning)(args.currentPhase);
828
+ phaseStartWarning = (0, handoff_contracts_1.buildPhaseStartWarning)(args.currentPhase, result.nextPhase ?? undefined);
828
829
  const completed = (0, handoff_contracts_1.commitPhaseCompletion)(marker, inspection, { ...context, timestamp });
829
830
  this.phaseStartMarkers.set(markerKey, completed);
830
831
  if (args.status === 'complete' && result.nextPhase === null) {
@@ -924,6 +925,7 @@ class McpService {
924
925
  status: args.status,
925
926
  evidence: args.evidence,
926
927
  findings: args.findings,
928
+ requestInstructions: args.requestInstructions,
927
929
  skipIncludes: true // ← the key difference: raw, no include resolution
928
930
  });
929
931
  return { content: [{ type: 'text', text: result.message }] };
@@ -254,6 +254,7 @@ class UsageAnalyticsService {
254
254
  async logJobStart(jobId, jobName, userId, sessionId, issueNumber) {
255
255
  const jobStartEvent = UsageAnalyticsService.createUsageEvent('job', jobName, userId, sessionId, true, {
256
256
  action: 'start',
257
+ isReload: false,
257
258
  issueNumber,
258
259
  jobId
259
260
  }, await this.resolveCategory(jobName), jobId);
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.342",
3
+ "version": "2.0.344",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "fraim": "bin/fraim.js"
8
8
  },
9
9
  "files": [
10
+ "dist/registry/scripts/artifact-paths.js",
11
+ "dist/registry/scripts/learning-usage/local-runtime.js",
10
12
  "dist/registry/scripts/learning/learning-domains.json",
11
13
  "dist/registry/scripts/learning/entry-format.js",
12
14
  "dist/registry/scripts/learning/completion-state.js",