fraim 2.0.286 → 2.0.288
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/dist/src/cli/doctor/checks/agent-cli-health-checks.js +21 -0
- package/dist/src/cli/setup/ide-invocation-surfaces.js +4 -1
- package/dist/src/config/persona-capability-bundles.js +10 -0
- package/dist/src/core/fraim-config-schema.generated.js +35 -1
- package/dist/src/fraim/db-service.js +33 -0
- package/dist/src/local-mcp-server/stdio-server.js +36 -0
- package/dist/src/services/mcp-service.js +36 -0
- package/package.json +1 -1
|
@@ -97,6 +97,27 @@ async function runAgentCliHealthCheck(cli) {
|
|
|
97
97
|
: managedPath === ambientPath
|
|
98
98
|
? ambientVersion
|
|
99
99
|
: probeVersion(managedPath);
|
|
100
|
+
// Issue #1412: a resolved file that fails to execute at all (e.g. an
|
|
101
|
+
// interrupted update left a non-executable fallback launcher) is a harder
|
|
102
|
+
// failure than drift — `getSystemCommandPath()` only proves the file
|
|
103
|
+
// exists, not that it can run. Without this check, a single broken install
|
|
104
|
+
// with nothing else on PATH to disagree with (ambientPath === managedPath,
|
|
105
|
+
// both versions null) falls straight through the mismatch check below and
|
|
106
|
+
// reads as "consistent". Check this before computing npm-global/drift
|
|
107
|
+
// details that don't matter once the CLI can't run at all.
|
|
108
|
+
const brokenPath = ambientPath && !ambientVersion
|
|
109
|
+
? ambientPath
|
|
110
|
+
: managedPath && !managedVersion
|
|
111
|
+
? managedPath
|
|
112
|
+
: null;
|
|
113
|
+
if (brokenPath) {
|
|
114
|
+
return {
|
|
115
|
+
status: 'error',
|
|
116
|
+
message: `${cli.label} is installed at ${brokenPath} but failed to run (\`${cli.command} --version\` produced no output). An interrupted or partial update may have left a non-functional launcher.`,
|
|
117
|
+
suggestion: `Reinstall ${cli.label}, then run "${cli.command} --version" again to confirm it's fixed.`,
|
|
118
|
+
details: { ambientPath, ambientVersion, managedPath, managedVersion },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
100
121
|
const npmGlobalBinDirs = (0, managed_agent_paths_1.resolveNpmGlobalBinDirs)();
|
|
101
122
|
const npmGlobalPath = npmGlobalBinDirs.length > 0
|
|
102
123
|
? (0, command_resolution_1.getSystemCommandPath)(cli.command, npmGlobalBinDirs.join(path_1.default.delimiter))
|
|
@@ -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',
|