fraim 2.0.340 → 2.0.342
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/registry/scripts/learning/completion-state.js +10 -2
- package/dist/src/cli/setup/ide-invocation-surfaces.js +1 -1
- package/dist/src/cli/utils/agent-adapters.js +6 -6
- package/dist/src/cli/utils/remote-sync.js +5 -0
- package/dist/src/core/handoff-contracts.js +122 -21
- package/dist/src/core/proposal-set-contract.js +214 -0
- package/dist/src/core/run-recovery-state.js +9 -0
- package/dist/src/core/utils/capability-index.js +104 -0
- package/dist/src/core/utils/local-registry-resolver.js +33 -6
- package/dist/src/local-mcp-server/learning-context-builder.js +128 -1
- package/dist/src/local-mcp-server/learning-usage-analysis.js +35 -19
- package/dist/src/local-mcp-server/learning-usage-command.js +8 -4
- package/dist/src/local-mcp-server/learning-usage-eligibility.js +59 -0
- package/dist/src/local-mcp-server/learning-usage-projection.js +5 -1
- package/dist/src/local-mcp-server/learning-usage-store.js +163 -38
- package/dist/src/local-mcp-server/learning-usage-verify.js +3 -1
- package/dist/src/local-mcp-server/stdio-server.js +266 -112
- package/dist/src/local-mcp-server/usage-collector.js +5 -29
- package/dist/src/mcp/tool-schemas.js +2 -2
- package/dist/src/routes/analytics.js +10 -3
- package/dist/src/services/mcp-service.js +73 -13
- package/dist/src/services/usage-analytics-service.js +6 -3
- package/package.json +1 -1
|
@@ -46,6 +46,7 @@ const compat_1 = require("../config/compat");
|
|
|
46
46
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
47
47
|
const quality_evidence_1 = require("../core/quality-evidence");
|
|
48
48
|
const handoff_contracts_1 = require("../core/handoff-contracts");
|
|
49
|
+
const test_evidence_contract_1 = require("../core/test-evidence-contract");
|
|
49
50
|
const feature_flags_1 = require("../config/feature-flags");
|
|
50
51
|
const persona_entitlement_service_1 = require("./persona-entitlement-service");
|
|
51
52
|
exports.DEFAULT_LAUNCH_PHRASE_MAPPINGS = {
|
|
@@ -69,6 +70,7 @@ function buildTrainingModeBlock(mode) {
|
|
|
69
70
|
}
|
|
70
71
|
class McpService {
|
|
71
72
|
constructor(registryService, sessionManager, aiMentor, dbService, analyticsServiceOrVersion, serverVersion) {
|
|
73
|
+
this.phaseStartMarkers = new Map();
|
|
72
74
|
this.registryService = registryService;
|
|
73
75
|
this.sessionManager = sessionManager;
|
|
74
76
|
this.aiMentor = aiMentor;
|
|
@@ -226,8 +228,8 @@ class McpService {
|
|
|
226
228
|
throw new Error('Argument "job" is required for get_fraim_job');
|
|
227
229
|
return await this.handleGetJob(args.job, context.apiKey, context.userId);
|
|
228
230
|
case 'seekMentoring':
|
|
229
|
-
if (!args.jobName || !args.issueNumber || !args.currentPhase || !args.status
|
|
230
|
-
throw new Error('Arguments "jobName", "
|
|
231
|
+
if (!args.jobName || !args.issueNumber || !args.currentPhase || !args.status) {
|
|
232
|
+
throw new Error('Arguments "jobName", "issueNumber", "currentPhase", and "status" are required for seekMentoring');
|
|
231
233
|
}
|
|
232
234
|
return await this.handleSeekMentoring(args, context.userId);
|
|
233
235
|
case 'list_fraim_jobs':
|
|
@@ -546,8 +548,8 @@ class McpService {
|
|
|
546
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.',
|
|
547
549
|
'- For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.',
|
|
548
550
|
'- If the user already named a job, call `get_fraim_job({ job: "<job-name>" })` directly.',
|
|
549
|
-
'- If routing is active and local FRAIM job stubs are present under `fraim/ai-employee/jobs/` or `fraim/ai-manager/jobs/`,
|
|
550
|
-
'- Also
|
|
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.',
|
|
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.',
|
|
551
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.',
|
|
552
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.',
|
|
553
555
|
'- Use `get_fraim_file(...)` only for the specific skills or rules needed for the current task.',
|
|
@@ -692,10 +694,15 @@ class McpService {
|
|
|
692
694
|
if (personaLock) {
|
|
693
695
|
return personaLock;
|
|
694
696
|
}
|
|
695
|
-
//
|
|
696
|
-
|
|
697
|
+
// Missing IDs still allow mentoring and session-scoped start matching.
|
|
698
|
+
// Unknown IDs must not produce analytics records for another run.
|
|
699
|
+
let trackingWarning = '';
|
|
700
|
+
let canTrackJob = Boolean(args.jobId && userId && this.analyticsService);
|
|
701
|
+
if (canTrackJob) {
|
|
697
702
|
const isValidJob = await this.analyticsService.validateJobId(args.jobId, userId);
|
|
698
703
|
if (!isValidJob) {
|
|
704
|
+
canTrackJob = false;
|
|
705
|
+
trackingWarning = `Job tracking notice: jobId \`${args.jobId}\` is unrecognized for this user; continuing without job analytics.\n\n`;
|
|
699
706
|
console.warn(`[seekMentoring] Unrecognized jobId: ${args.jobId} for user ${userId} — proceeding without tracking`);
|
|
700
707
|
}
|
|
701
708
|
}
|
|
@@ -744,10 +751,21 @@ class McpService {
|
|
|
744
751
|
: 'enforce';
|
|
745
752
|
if (reviewHandoffApplies && enforcementMode === 'enforce') {
|
|
746
753
|
return {
|
|
747
|
-
content: [{ type: 'text', text: (0, handoff_contracts_1.buildHandoffRejectionMessage)(args.currentPhase, missingField, handoffErrors) }]
|
|
754
|
+
content: [{ type: 'text', text: trackingWarning + (0, handoff_contracts_1.buildHandoffRejectionMessage)(args.currentPhase, missingField, handoffErrors) }]
|
|
748
755
|
};
|
|
749
756
|
}
|
|
750
757
|
}
|
|
758
|
+
const testEvidenceErrors = (0, test_evidence_contract_1.validateTestEvidenceContract)({
|
|
759
|
+
currentPhase: args.currentPhase,
|
|
760
|
+
status: args.status,
|
|
761
|
+
findings: args.findings,
|
|
762
|
+
phases: handoffPhaseMap,
|
|
763
|
+
});
|
|
764
|
+
if (testEvidenceErrors.length > 0) {
|
|
765
|
+
return {
|
|
766
|
+
content: [{ type: 'text', text: trackingWarning + (0, test_evidence_contract_1.buildTestEvidenceRejectionMessage)(args.currentPhase, testEvidenceErrors) }]
|
|
767
|
+
};
|
|
768
|
+
}
|
|
751
769
|
// Quality enforcement (Issue #251): quality-producing jobs MUST emit
|
|
752
770
|
// a valid `evidence.quality` object on their final completion call.
|
|
753
771
|
//
|
|
@@ -771,7 +789,7 @@ class McpService {
|
|
|
771
789
|
];
|
|
772
790
|
if (qualityErrors.length > 0) {
|
|
773
791
|
const rejectionMessage = this.buildQualityRejectionMessage(args.jobName, args.currentPhase, qualityErrors);
|
|
774
|
-
if (userId && this.analyticsService) {
|
|
792
|
+
if (canTrackJob && userId && this.analyticsService) {
|
|
775
793
|
try {
|
|
776
794
|
const sessionId = args.sessionId || 'unknown';
|
|
777
795
|
await this.analyticsService.logMentoringCall(args.jobId, args.jobName, userId, sessionId, args.currentPhase, 'incomplete', args.currentPhase, false);
|
|
@@ -781,17 +799,58 @@ class McpService {
|
|
|
781
799
|
}
|
|
782
800
|
}
|
|
783
801
|
return {
|
|
784
|
-
content: [{ type: 'text', text: rejectionMessage }]
|
|
802
|
+
content: [{ type: 'text', text: trackingWarning + rejectionMessage }]
|
|
785
803
|
};
|
|
786
804
|
}
|
|
787
805
|
}
|
|
806
|
+
// Only accepted calls update the marker. A rejected completion must
|
|
807
|
+
// leave its start available for the corrected completion request.
|
|
808
|
+
let phaseStartWarning = '';
|
|
809
|
+
const sessionId = typeof args.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
810
|
+
const isPhaseCompletion = args.status === 'complete' || args.status === 'failure';
|
|
811
|
+
if (sessionId && sessionId !== 'unknown') {
|
|
812
|
+
const markerKey = JSON.stringify([userId ?? null, sessionId, args.jobName]);
|
|
813
|
+
const context = {
|
|
814
|
+
sessionId,
|
|
815
|
+
jobName: args.jobName,
|
|
816
|
+
phaseId: args.currentPhase,
|
|
817
|
+
...(args.jobId ? { jobId: args.jobId } : {}),
|
|
818
|
+
};
|
|
819
|
+
const marker = this.phaseStartMarkers.get(markerKey);
|
|
820
|
+
const timestamp = new Date().toISOString();
|
|
821
|
+
if (args.status === 'starting') {
|
|
822
|
+
this.phaseStartMarkers.set(markerKey, (0, handoff_contracts_1.recordPhaseStart)(marker, { ...context, timestamp }));
|
|
823
|
+
}
|
|
824
|
+
else if (isPhaseCompletion) {
|
|
825
|
+
const inspection = (0, handoff_contracts_1.inspectPhaseCompletion)(marker, context);
|
|
826
|
+
if (inspection.missingStart)
|
|
827
|
+
phaseStartWarning = (0, handoff_contracts_1.buildPhaseStartWarning)(args.currentPhase);
|
|
828
|
+
const completed = (0, handoff_contracts_1.commitPhaseCompletion)(marker, inspection, { ...context, timestamp });
|
|
829
|
+
this.phaseStartMarkers.set(markerKey, completed);
|
|
830
|
+
if (args.status === 'complete' && result.nextPhase === null) {
|
|
831
|
+
// Concurrent runs can share the session and job name.
|
|
832
|
+
// Clear only this run (including starts before its ID was known).
|
|
833
|
+
const completedJobId = args.jobId || completed.phaseInstances?.find((instance) => instance.instanceId === inspection.instanceId)?.jobId;
|
|
834
|
+
const remaining = completed.phaseInstances?.filter((instance) => instance.jobId && instance.jobId !== completedJobId) || [];
|
|
835
|
+
if (remaining.length > 0) {
|
|
836
|
+
this.phaseStartMarkers.set(markerKey, { phaseInstances: remaining });
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
this.phaseStartMarkers.delete(markerKey);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
else if (isPhaseCompletion) {
|
|
845
|
+
phaseStartWarning = `Phase-start notice: phase \`${args.currentPhase}\` has no identifiable session, so its start cannot be matched to this run. The missing match was flagged, but this completion remains accepted.\n\n`;
|
|
846
|
+
}
|
|
788
847
|
// Update job execution tracking using usage events
|
|
789
|
-
if (userId && this.analyticsService) {
|
|
848
|
+
if (canTrackJob && userId && this.analyticsService) {
|
|
790
849
|
try {
|
|
791
850
|
const sessionId = args.sessionId || 'unknown';
|
|
792
851
|
await this.analyticsService.logMentoringCall(args.jobId, args.jobName, userId, sessionId, args.currentPhase, args.status, result.nextPhase, result.message ? true : false);
|
|
793
852
|
// Log explicit completion event when job finishes
|
|
794
|
-
if (
|
|
853
|
+
if (args.status === 'complete' && result.nextPhase === null) {
|
|
795
854
|
await this.analyticsService.logJobComplete(args.jobId, args.jobName, userId, sessionId, args.currentPhase);
|
|
796
855
|
// Emit quality score for jobs that produce quality assessments (Issue #251).
|
|
797
856
|
// Validation already happened above for quality-producing jobs, so this
|
|
@@ -818,8 +877,9 @@ class McpService {
|
|
|
818
877
|
return {
|
|
819
878
|
content: [{
|
|
820
879
|
type: 'text',
|
|
821
|
-
text: result.message
|
|
822
|
-
}]
|
|
880
|
+
text: trackingWarning + phaseStartWarning + result.message
|
|
881
|
+
}],
|
|
882
|
+
structuredContent: { phaseAccepted: true, nextPhase: result.nextPhase, phaseStartWarning }
|
|
823
883
|
};
|
|
824
884
|
}
|
|
825
885
|
getJobDescription(jobName, fullPath) {
|
|
@@ -63,9 +63,10 @@ class UsageAnalyticsService {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
-
* Log a usage event immediately (bypasses queue)
|
|
66
|
+
* Log a usage event immediately (bypasses queue).
|
|
67
|
+
* Ingestion callers can require persistence; other callers remain best effort.
|
|
67
68
|
*/
|
|
68
|
-
async logUsageImmediate(input) {
|
|
69
|
+
async logUsageImmediate(input, options = {}) {
|
|
69
70
|
try {
|
|
70
71
|
// Enrich with category if missing and type is job
|
|
71
72
|
if (input.type === 'job' && (!input.category || input.category === 'uncategorized')) {
|
|
@@ -75,7 +76,9 @@ class UsageAnalyticsService {
|
|
|
75
76
|
}
|
|
76
77
|
catch (error) {
|
|
77
78
|
console.error('Failed to log usage event:', error);
|
|
78
|
-
|
|
79
|
+
if (options.throwOnError)
|
|
80
|
+
throw error;
|
|
81
|
+
// Analytics failures should not break unrelated main functionality.
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
/**
|