fraim 2.0.286 → 2.0.287

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.
@@ -29,7 +29,10 @@ exports.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE = [
29
29
  'If required FRAIM MCP tools remain unavailable after the deferred-tool preload/retry step, stop the FRAIM job.',
30
30
  'Do not continue from memory, local stubs, cached instructions, or prior context.',
31
31
  'In the blocker sentence, state only the work-focused blocker and what outcome is blocked; do not narrate ToolSearch/tool_search, individual tool names, server internals, or retry mechanics.',
32
- 'Always help troubleshoot the unavailable FRAIM MCP connection: tell the user to reconnect or enable the FRAIM MCP server in their IDE/agent, restart or refresh the agent session, retry the FRAIM job, and if it still fails run `fraim doctor --test-mcp` and share the failing check output.'
32
+ 'Always help troubleshoot the unavailable FRAIM MCP connection: tell the user to reconnect or enable the FRAIM MCP server in their IDE/agent, restart or refresh the agent session, retry the FRAIM job, and if it still fails run `fraim doctor --test-mcp` and share the failing check output.',
33
+ 'Exception — troubleshooting FRAIM itself: if the user\'s request is to diagnose or fix a FRAIM issue and FRAIM MCP is unavailable, do NOT stop.',
34
+ 'Instead, read `~/.fraim/docs/TROUBLESHOOTING.md` directly from disk (it is always synced in full by `fraim sync` and is available without MCP) and follow its guidance to diagnose the issue.',
35
+ 'After completing the local diagnosis, prompt the user to re-run `fraim sync` and restart their agent session, then offer to run the `troubleshoot-fraim` FRAIM job for a structured investigation once MCP is restored.'
33
36
  ].join(' ');
34
37
  function buildDeferredToolBootstrapSection(profile) {
35
38
  if (profile === 'none') {
@@ -291,10 +291,20 @@ for (const bundle of Object.values(exports.PERSONA_CAPABILITY_BUNDLES)) {
291
291
  // named specialist persona. They resolve through ownership (never short-circuited
292
292
  // as "free") so the Hub attributes them to FRAIMworker, but they are never
293
293
  // hire-gated because FRAIMworker is not a purchasable persona.
294
+ //
295
+ // Issue #1398: manager-agreements / organization-onboarding / organizational-learning-
296
+ // synthesis are Manager/Company area-level jobs, not tied to a named specialist — they
297
+ // were falling through to DEFAULT_UNASSIGNED_PERSONA_KEY ('mandy'), which misattributed
298
+ // them to an employee who never runs them and left them outside the Manager/Company
299
+ // employee rail's FRAIMworker group.
294
300
  const GENERIC_WORKER_OWNED_JOBS = new Set([
295
301
  'contribute-to-fraim',
296
302
  'file-fraim-issue',
297
303
  'praise-fraim',
304
+ 'troubleshoot-fraim',
305
+ 'manager-agreements',
306
+ 'organization-onboarding',
307
+ 'organizational-learning-synthesis',
298
308
  ]);
299
309
  function getPersonaCapabilityBundle(personaKey) {
300
310
  return exports.PERSONA_CAPABILITY_BUNDLES[personaKey];
@@ -603,6 +603,32 @@ exports.FRAIM_CONFIG_SCHEMA = {
603
603
  }
604
604
  }
605
605
  }
606
+ },
607
+ "artifact_retention_period": {
608
+ "kind": "object",
609
+ "properties": {
610
+ "default": {
611
+ "kind": "number"
612
+ },
613
+ "learning_archive": {
614
+ "kind": "number"
615
+ },
616
+ "retrospectives": {
617
+ "kind": "number"
618
+ },
619
+ "evidence": {
620
+ "kind": "number"
621
+ },
622
+ "feedback": {
623
+ "kind": "number"
624
+ },
625
+ "cleanup_manifests": {
626
+ "kind": "number"
627
+ },
628
+ "learning_usage": {
629
+ "kind": "number"
630
+ }
631
+ }
606
632
  }
607
633
  },
608
634
  "required": true
@@ -734,5 +760,13 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
734
760
  "automation.support.communication.deliveryMode",
735
761
  "automation.support.communication.recipientField",
736
762
  "automation.support.communication.includeTemporaryPassword",
737
- "automation.support.communication.messageTemplate"
763
+ "automation.support.communication.messageTemplate",
764
+ "artifact_retention_period",
765
+ "artifact_retention_period.default",
766
+ "artifact_retention_period.learning_archive",
767
+ "artifact_retention_period.retrospectives",
768
+ "artifact_retention_period.evidence",
769
+ "artifact_retention_period.feedback",
770
+ "artifact_retention_period.cleanup_manifests",
771
+ "artifact_retention_period.learning_usage"
738
772
  ];
@@ -170,6 +170,8 @@ class FraimDbService {
170
170
  // Issue #563 — shared organization context (FRAIM-cloud backend).
171
171
  this.orgArtifactsCollection = this.db.collection('fraim_org_artifacts');
172
172
  this.orgAuditCollection = this.db.collection('fraim_org_audit');
173
+ // Issue #1345 — job execution modes (Coached/Trusted).
174
+ this.jobExecutionModesCollection = this.db.collection('fraim_job_execution_modes');
173
175
  }
174
176
  async initializeIndexes() {
175
177
  if (!this.db)
@@ -199,6 +201,7 @@ class FraimDbService {
199
201
  // requirement — swallow failures like the other Cosmos-sensitive indexes above.
200
202
  await this.orgArtifactsCollection.createIndex({ orgId: 1, relativePath: 1 }, { unique: true }).catch(() => { });
201
203
  await this.orgAuditCollection.createIndex({ orgId: 1, at: -1 }).catch(() => { });
204
+ await this.jobExecutionModesCollection.createIndex({ userId: 1, jobName: 1 }, { unique: true }).catch(() => { });
202
205
  await this.pendingVerificationsCollection.createIndex({ email: 1 });
203
206
  // Compound index covers `findOne({email}, { sort: { createdAt: -1 } })` —
204
207
  // the request-access flow's lookup of the most-recent pending row per
@@ -286,6 +289,36 @@ class FraimDbService {
286
289
  throw new Error('DB not connected');
287
290
  return await this.orgAuditCollection.find({ orgId }).sort({ at: -1 }).toArray();
288
291
  }
292
+ async getJobExecutionMode(userId, jobName) {
293
+ if (!this.jobExecutionModesCollection)
294
+ return null;
295
+ return await this.jobExecutionModesCollection.findOne({ userId, jobName }) ?? null;
296
+ }
297
+ async upsertJobExecutionMode(userId, update) {
298
+ if (!this.jobExecutionModesCollection)
299
+ return;
300
+ const jobName = String(update['jobName'] ?? '');
301
+ if (!jobName)
302
+ return;
303
+ const existing = await this.jobExecutionModesCollection.findOne({ userId, jobName });
304
+ let completedRuns = typeof existing?.completedRuns === 'number' ? existing.completedRuns : 0;
305
+ if (update['incrementRun'] === true)
306
+ completedRuns += 1;
307
+ const record = {
308
+ userId,
309
+ jobName,
310
+ mode: (update['mode'] === 'trusted' || update['mode'] === 'coached')
311
+ ? update['mode']
312
+ : (existing?.mode ?? 'coached'),
313
+ completedRuns,
314
+ trustedSince: typeof update['trustedSince'] === 'string' ? update['trustedSince'] : existing?.trustedSince,
315
+ trustedAtRun: typeof update['trustedAtRun'] === 'number' ? update['trustedAtRun'] : existing?.trustedAtRun,
316
+ lastGraduationOfferRun: typeof update['lastGraduationOfferRun'] === 'number' ? update['lastGraduationOfferRun'] : existing?.lastGraduationOfferRun,
317
+ graduationSuppressedUntilRun: typeof update['graduationSuppressedUntilRun'] === 'number' ? update['graduationSuppressedUntilRun'] : existing?.graduationSuppressedUntilRun,
318
+ updatedAt: new Date(),
319
+ };
320
+ await this.jobExecutionModesCollection.replaceOne({ userId, jobName }, record, { upsert: true });
321
+ }
289
322
  async verifyApiKey(key) {
290
323
  if (!this.keysCollection)
291
324
  throw new Error('DB not connected');
@@ -52,6 +52,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
52
52
  };
53
53
  Object.defineProperty(exports, "__esModule", { value: true });
54
54
  exports.FraimLocalMCPServer = exports.FraimTemplateEngine = void 0;
55
+ exports.parseExecutionModeFromResponse = parseExecutionModeFromResponse;
55
56
  const fs_1 = require("fs");
56
57
  const path_1 = require("path");
57
58
  const os_1 = require("os");
@@ -425,6 +426,29 @@ FraimTemplateEngine.ISSUE_ACTIONS = new Set([
425
426
  'close_issue',
426
427
  'list_issues'
427
428
  ]);
429
+ function parseExecutionModeFromResponse(text) {
430
+ const marker = '**Execution Mode Context:**';
431
+ const idx = text.indexOf(marker);
432
+ if (idx < 0)
433
+ return null;
434
+ const jsonStart = text.indexOf('```json', idx);
435
+ const jsonEnd = text.indexOf('```', jsonStart + 7);
436
+ if (jsonStart < 0 || jsonEnd < 0)
437
+ return null;
438
+ try {
439
+ const parsed = JSON.parse(text.slice(jsonStart + 7, jsonEnd).trim());
440
+ const em = parsed.executionMode;
441
+ if (!em || typeof em.mode !== 'string')
442
+ return null;
443
+ return {
444
+ mode: em.mode === 'trusted' ? 'trusted' : 'coached',
445
+ completedRuns: typeof em.completedRuns === 'number' ? em.completedRuns : 0,
446
+ };
447
+ }
448
+ catch {
449
+ return null;
450
+ }
451
+ }
428
452
  class FraimLocalMCPServer {
429
453
  constructor(writer) {
430
454
  this.config = null;
@@ -1395,6 +1419,18 @@ class FraimLocalMCPServer {
1395
1419
  }
1396
1420
  this.recordUsageOffers(workspaceRoot, userEmail, true, jobDomain, String(args.job ?? 'unknown'), requestId);
1397
1421
  }
1422
+ // Issue #1345: forward executionMode from get_fraim_job response to Hub so
1423
+ // the chip reads from run data (no local file read, no separate endpoint needed).
1424
+ const responseText = finalizedResponse.result?.content?.[0]?.text;
1425
+ if (typeof responseText === 'string' && requestSessionId) {
1426
+ const hubBase = process.env.FRAIM_HUB_BASE_URL;
1427
+ const em = parseExecutionModeFromResponse(responseText);
1428
+ if (hubBase && em) {
1429
+ axios_1.default.post(`${hubBase}/api/ai-hub/runs/by-session/${encodeURIComponent(requestSessionId)}/execution-mode`, em, { headers: { 'Content-Type': 'application/json' } }).catch((err) => {
1430
+ this.log(`[req:${requestId}] execution-mode forward failed: ${err instanceof Error ? err.message : String(err)}`);
1431
+ });
1432
+ }
1433
+ }
1398
1434
  }
1399
1435
  return this.processResponseWithHydration(finalizedResponse, requestSessionId);
1400
1436
  }
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.McpService = exports.REQUIRED_QUALITY_FIELDS = exports.QUALITY_SCORE_JOBS = exports.QUALITY_PRODUCING_JOBS = exports.DEFAULT_LAUNCH_PHRASE_MAPPINGS = void 0;
37
+ exports.buildTrainingModeBlock = buildTrainingModeBlock;
37
38
  const fs_1 = require("fs");
38
39
  const crypto_1 = require("crypto");
39
40
  const job_parser_1 = require("../core/utils/job-parser");
@@ -60,6 +61,11 @@ exports.DEFAULT_LAUNCH_PHRASE_MAPPINGS = {
60
61
  exports.QUALITY_PRODUCING_JOBS = quality_evidence_1.QUALITY_PRODUCING_JOBS;
61
62
  exports.QUALITY_SCORE_JOBS = quality_evidence_1.QUALITY_SCORE_JOBS;
62
63
  exports.REQUIRED_QUALITY_FIELDS = quality_evidence_1.REQUIRED_QUALITY_FIELDS;
64
+ function buildTrainingModeBlock(mode) {
65
+ if (mode === 'trusted')
66
+ return '';
67
+ return `\n\n---\n\n**Training Mode Active (In Training):** Apply these instructions throughout all phases:\n\n1. **Outline checkpoint** (phases with 2 or more output components): Before drafting any content, emit a concise outline listing all components and counts. Wait for manager confirmation before drafting. Do not call \`seekMentoring\` for this pause.\n\n2. **Phase direction check** (all phases): After completing a phase's substantive work, write one sentence summarizing what was done and ask "Does this look right before I move to [next phase]?" Wait for manager approval before calling \`seekMentoring\`.\n\n3. **Training notes**: After each approved phase, append a structured note to the retrospective draft: phase name, what was done, manager preferences observed, questions resolved.`;
68
+ }
63
69
  class McpService {
64
70
  constructor(registryService, sessionManager, aiMentor, dbService, analyticsServiceOrVersion, serverVersion) {
65
71
  this.registryService = registryService;
@@ -381,6 +387,26 @@ class McpService {
381
387
  if (!result.isSimple) {
382
388
  response += `\n\n---\n\n**Job ID:** \`${jobId}\`\n\n**This job has phases.** Use \`seekMentoring\` with the jobId above to get phase-specific instructions.`;
383
389
  }
390
+ // Issue #1345: append executionMode and inject training instructions when coached.
391
+ if (effectiveUserId) {
392
+ try {
393
+ const modeRecord = await this.dbService.getJobExecutionMode(effectiveUserId, jobName);
394
+ const mode = modeRecord?.mode ?? 'coached';
395
+ const executionMode = {
396
+ mode,
397
+ completedRuns: modeRecord?.completedRuns ?? 0,
398
+ };
399
+ if (modeRecord?.lastGraduationOfferRun != null)
400
+ executionMode['lastGraduationOfferRun'] = modeRecord.lastGraduationOfferRun;
401
+ if (modeRecord?.graduationSuppressedUntilRun != null)
402
+ executionMode['graduationSuppressedUntilRun'] = modeRecord.graduationSuppressedUntilRun;
403
+ response += `\n\n---\n\n**Execution Mode Context:** \`\`\`json\n${JSON.stringify({ executionMode })}\n\`\`\``;
404
+ response += buildTrainingModeBlock(mode);
405
+ }
406
+ catch (e) {
407
+ console.error(`❌ McpService: Failed to fetch executionMode for ${jobName}:`, e);
408
+ }
409
+ }
384
410
  return {
385
411
  content: [{
386
412
  type: 'text',
@@ -737,6 +763,16 @@ class McpService {
737
763
  console.error(`❌ McpService: Failed to log mentoring/completion:`, e);
738
764
  }
739
765
  }
766
+ // Issue #1345: persist executionModeUpdate from findings when present.
767
+ const executionModeUpdate = args.findings?.executionModeUpdate;
768
+ if (userId && executionModeUpdate && typeof executionModeUpdate['jobName'] === 'string') {
769
+ try {
770
+ await this.dbService.upsertJobExecutionMode(userId, executionModeUpdate);
771
+ }
772
+ catch (e) {
773
+ console.error(`❌ McpService: Failed to upsert executionMode:`, e);
774
+ }
775
+ }
740
776
  return {
741
777
  content: [{
742
778
  type: 'text',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.286",
3
+ "version": "2.0.287",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {